diff --git a/.browserslistrc b/.browserslistrc index 3dd91b3a97..49060a662d 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,4 +1,4 @@ -last 3 version -> 1% -ie >= 11 -firefox esr \ No newline at end of file +last 1 version +> 30% +not IE 11 +not IE_Mob 11 diff --git a/.docker/nginx/ssl/.gitempty b/.docker/dev/mail/config/dovecot-quotas.cf similarity index 100% rename from .docker/nginx/ssl/.gitempty rename to .docker/dev/mail/config/dovecot-quotas.cf diff --git a/.docker/mail/setup.sh b/.docker/dev/mail/setup.sh old mode 100755 new mode 100644 similarity index 100% rename from .docker/mail/setup.sh rename to .docker/dev/mail/setup.sh diff --git a/.docker/nginx/default.conf b/.docker/dev/nginx/default.conf similarity index 99% rename from .docker/nginx/default.conf rename to .docker/dev/nginx/default.conf index 802d504313..aaf702abcf 100644 --- a/.docker/nginx/default.conf +++ b/.docker/dev/nginx/default.conf @@ -1,4 +1,3 @@ - server { listen 80 default; listen 443 ssl; diff --git a/.docker/nginx/ssl.sh b/.docker/dev/nginx/ssl.sh similarity index 100% rename from .docker/nginx/ssl.sh rename to .docker/dev/nginx/ssl.sh diff --git a/.docker/dev/php/Dockerfile b/.docker/dev/php/Dockerfile new file mode 100644 index 0000000000..74759db6c3 --- /dev/null +++ b/.docker/dev/php/Dockerfile @@ -0,0 +1,31 @@ +FROM php:7.4-fpm + +RUN apt-get update + +RUN apt-get install -y \ + git unzip wget zip curl mlocate \ + libicu-dev libpcre3-dev libicu-dev \ + build-essential chrpath libssl-dev \ + libxft-dev libfreetype6 libfreetype6-dev \ + libpng-dev libjpeg62-turbo-dev \ + libfontconfig1 libfontconfig1-dev libzip-dev libldap2-dev + +RUN pecl install xxtea-1.0.11 && \ + docker-php-ext-enable xxtea + +RUN docker-php-ext-configure intl && \ + docker-php-ext-configure ldap && \ + docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ && \ + docker-php-ext-install opcache pdo_mysql zip intl gd ldap + +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + +RUN curl --location --output /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar && chmod +x /usr/local/bin/phpunit + +RUN apt-get -y autoremove && apt-get clean + +RUN sed -i '/^;catch_workers_output/ccatch_workers_output = yes' '/usr/local/etc/php-fpm.d/www.conf' + +EXPOSE 9000 + +CMD ["php-fpm"] diff --git a/.docker/dev/php/snappymail.ini b/.docker/dev/php/snappymail.ini new file mode 100644 index 0000000000..04fa36f9ba --- /dev/null +++ b/.docker/dev/php/snappymail.ini @@ -0,0 +1,8 @@ +date.timezone = UTC +upload_max_filesize = 24M +post_max_size = 25M + +# log_errors = On +# display_errors = On +# error_reporting = E_ALL +# error_log = /dev/stderr diff --git a/.docker/node/Dockerfile b/.docker/node/Dockerfile deleted file mode 100644 index b03be43a4b..0000000000 --- a/.docker/node/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM node:14-alpine - -RUN apk add --no-cache git -RUN yarn global add gulp - -CMD ["node", "--version"] diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile deleted file mode 100644 index 8dbf83e588..0000000000 --- a/.docker/php/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# FROM php:7.3-fpm -# FROM php:7.4-fpm -FROM php:8.0-fpm - -RUN apt-get update - -RUN apt-get install -y \ - git unzip wget zip curl mlocate \ - libmcrypt-dev libicu-dev libpcre3-dev \ - build-essential chrpath libssl-dev \ - libxft-dev libfreetype6 libfreetype6-dev \ - libpng-dev libjpeg62-turbo-dev \ - libfontconfig1 libfontconfig1-dev libzip-dev - -RUN pecl install mcrypt && \ - docker-php-ext-enable mcrypt - -RUN docker-php-ext-configure intl && \ - docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ && \ - docker-php-ext-install opcache pdo_mysql zip intl gd - -RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer - -RUN curl --location --output /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar && chmod +x /usr/local/bin/phpunit - -RUN apt-get -y autoremove && apt-get clean - -RUN sed -i '/^;catch_workers_output/ccatch_workers_output = yes' '/usr/local/etc/php-fpm.d/www.conf' - -EXPOSE 9000 - -CMD ["php-fpm"] diff --git a/.docker/php/rainloop.ini b/.docker/php/rainloop.ini deleted file mode 100644 index 0e54c98d6c..0000000000 --- a/.docker/php/rainloop.ini +++ /dev/null @@ -1,8 +0,0 @@ -date.timezone = UTC -upload_max_filesize = 1G -post_max_size = 1G - -# log_errors = On -# display_errors = On -# error_reporting = E_ALL -# error_log = /dev/stderr diff --git a/.docker/release/Dockerfile b/.docker/release/Dockerfile new file mode 100644 index 0000000000..d6675462eb --- /dev/null +++ b/.docker/release/Dockerfile @@ -0,0 +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.2-fpm-alpine AS final + +LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine" + +# Install dependencies such as nginx +RUN apk add --no-cache ca-certificates nginx supervisor bash + +# Install PHP extensions +# 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 +# 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; + +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 new file mode 100755 index 0000000000..75e82d7292 --- /dev/null +++ b/.docker/release/files/entrypoint.sh @@ -0,0 +1,80 @@ +#!/bin/sh +set -eu + +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 + +# Secure cookies +if [ "${SECURE_COOKIES}" = 'true' ]; then + echo "[INFO] Secure cookies activated" + { + echo 'session.cookie_httponly = On'; + echo 'session.cookie_secure = On'; + echo 'session.use_only_cookies = On'; + } > /usr/local/etc/php/conf.d/cookies.ini; +fi + +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: $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 +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 +sed 's/^auth_syslog = .*/auth_syslog = Off/' -i $SNAPPYMAIL_CONFIG_FILE + +( + 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 + + wget -T 1 -qO- 'http://127.0.0.1:8888/' > /dev/null + echo "[INFO] Snappymail ready at http://localhost:8888/" +) & + +# RUN ! +exec /usr/bin/supervisord -c /supervisor.conf --pidfile /run/supervisord.pid diff --git a/.docker/release/files/etc/nginx/nginx.conf b/.docker/release/files/etc/nginx/nginx.conf new file mode 100644 index 0000000000..aff3829cf6 --- /dev/null +++ b/.docker/release/files/etc/nginx/nginx.conf @@ -0,0 +1,108 @@ +worker_processes auto; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; + use epoll; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log off; + error_log /dev/stderr error; + + sendfile on; + keepalive_timeout 15; + keepalive_disable msie6; + keepalive_requests 100; + tcp_nopush on; + tcp_nodelay on; + server_tokens off; + + fastcgi_temp_path /tmp/fastcgi 1 2; + client_body_temp_path /tmp/client_body 1 2; + proxy_temp_path /tmp/proxy 1 2; + uwsgi_temp_path /tmp/uwsgi 1 2; + scgi_temp_path /tmp/scgi 1 2; + + gzip on; + gzip_comp_level 5; + gzip_min_length 512; + gzip_buffers 4 8k; + gzip_proxied any; + gzip_vary on; + gzip_disable "msie6"; + gzip_types + text/css + text/javascript + text/xml + text/plain + text/x-component + application/javascript + application/x-javascript + application/json + application/xml + application/rss+xml + application/vnd.ms-fontobject + font/truetype + font/opentype + image/svg+xml; + + server { + listen 8888; + listen [::]:8888; + #listen [::]:8888 ipv6only=off; + root /snappymail; + index index.php; + charset utf-8; + + client_max_body_size ; + + location ^~ /data { + deny all; + } + + location / { + try_files $uri $uri/ index.php; + } + + # Assets cache control + # -------------------------------------- + location ~* \.(?:html|xml|json)$ { + expires -1; + } + + location ~* \.(?:css|js)$ { + expires 7d; + add_header Pragma public; + add_header Cache-Control "public"; + } + + location ~* \.(?:gif|jpe?g|png|ico|otf|eot|svg|ttf|woff|woff2)$ { + expires 30d; + log_not_found off; + add_header Pragma public; + add_header Cache-Control "public"; + } + + # PHP Backend + # -------------------------------------- + location ~* \.php$ { + try_files $uri =404; + include fastcgi_params; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param HTTP_PROXY ""; + fastcgi_index index.php; + 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/listener.php b/.docker/release/files/listener.php new file mode 100644 index 0000000000..0d44ab5a90 --- /dev/null +++ b/.docker/release/files/listener.php @@ -0,0 +1,23 @@ + diff --git a/.docker/release/files/supervisor.conf b/.docker/release/files/supervisor.conf new file mode 100644 index 0000000000..13a12a290c --- /dev/null +++ b/.docker/release/files/supervisor.conf @@ -0,0 +1,40 @@ +[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=root +numprocs=1 +autostart=true +autorestart=false +startsecs=0 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:php-fpm] +command=php-fpm -F +process_name=%(program_name)s_%(process_num)02d +user=root +numprocs=1 +autostart=true +autorestart=false +startsecs=0 +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=root +numprocs=1 +events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL +autostart=true +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 new file mode 100644 index 0000000000..fbee3d4e0e --- /dev/null +++ b/.docker/release/files/usr/local/etc/php-fpm.d/php-fpm.conf @@ -0,0 +1,25 @@ +[global] +daemonize = no +error_log = /dev/stderr +log_buffering = no + +[default] +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 = / +pm.status_path = /status +php_admin_value[log_errors] = On +php_admin_value[expose_php] = Off +php_admin_value[display_errors] = Off +php_admin_value[date.timezone] = UTC +php_admin_value[post_max_size] = +php_admin_value[upload_max_filesize] = +php_admin_value[memory_limit] = diff --git a/.docker/release/test/build_and_test.sh b/.docker/release/test/build_and_test.sh new file mode 100755 index 0000000000..f332ad2c25 --- /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 0000000000..47cf00ea95 --- /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 0000000000..1981cb358b --- /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/.docker/tx/Dockerfile b/.docker/tx/Dockerfile deleted file mode 100644 index 8862c9e574..0000000000 --- a/.docker/tx/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM python:3.6-alpine - -RUN apk add --no-cache git -RUN pip install transifex-client - -CMD ["tx", "--version"] diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..16ef81459e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/.git +/node_modules diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..2e23d96d44 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*.php] +indent_style = tab \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 160540b695..e22cb986d9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,25 +1,53 @@ module.exports = { parser: 'babel-eslint', - extends: ['eslint:recommended', 'plugin:prettier/recommended'], - plugins: ['prettier'], +// extends: ['eslint:recommended', 'plugin:prettier/recommended'], + extends: ['eslint:recommended'], parserOptions: { - ecmaVersion: 6, + ecmaVersion: 11, sourceType: 'module' }, env: { node: true, - commonjs: true, - es6: true + browser: true, + es2020: true }, globals: { - 'RL_ES6': true + // SnappyMail + 'rainloopI18N': "readonly", + 'rainloopTEMPLATES': "readonly", + 'rl': "readonly", + 'shortcuts': "readonly", +// '__APP_BOOT': "readonly", + // deb/boot.js + 'progressJs': "readonly", + // others + 'openpgp': "readonly", + 'CKEDITOR': "readonly", + 'Squire': "readonly", + 'SquireUI': "readonly", + // node_modules/knockout but dev/External/ko.js is used + 'ko': "readonly", + // vendors/routes/ + 'hasher': "readonly", + 'Crossroads': "readonly", + // vendors/jua + 'Jua': "readonly", + // vendors/bootstrap/bootstrap.native.js + 'BSN': "readonly", + // Mailvelope + 'mailvelope': "readonly", + // Punycode + 'IDN': "readonly", + // Turndown + 'TurndownService': "readonly", + // Marked + 'marked': "readonly" }, // http://eslint.org/docs/rules/ rules: { + 'no-cond-assign': 0, // plugins - 'prettier/prettier': 'error', - - 'no-console': 'error', + 'no-mixed-spaces-and-tabs': 'off', 'max-len': [ 'error', 120, @@ -30,6 +58,7 @@ module.exports = { ignoreTrailingComments: true, ignorePattern: '(^\\s*(const|let|var)\\s.+=\\s*require\\s*\\(|^import\\s.+\\sfrom\\s.+;$)' } - ] + ], + 'no-constant-condition': ["error", { "checkLoops": false }] } }; diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..b37d3418e6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: the-djmaze +custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"] diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 81dcc56491..10caf8bbce 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,5 +1,5 @@ -**RainLoop version, browser, OS:** +**SnappyMail version, browser, browser version:** **Expected behavior and actual behavior:** diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..9986929649 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Please complete the following information:** +- Browser: [e.g. chromium 100, firefox 100, safari 14, mobile] +- IMAP daemon: [e.g. courier, dovecot] +- PHP version: +- SnappyMail Version: +- Mode: [e.g. standalone, nextcloud, cyberpanel, docker] + +**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/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..bbcbbe7d61 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml deleted file mode 100644 index 155fd638ca..0000000000 --- a/.github/workflows/builder.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Builder - -on: - push: - tags: - - 'v*' - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 1 - - - uses: actions/setup-node@v1 - with: - node-version: 12.x - - - name: Create Cache - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: os-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - - - run: yarn install --frozen-lockfile --check-files - - run: yarn build - - - name: Move all assets to release folder - run: | - mkdir ./release - cp `ls ./build/dist/releases/**/**/*.zip | xargs` ./release - - - name: Configure GPG Key - run: | - export GPG_TTY=$(tty) - echo "$GPG_SIGNING_KEY" | gpg --batch --import - for ff in `ls ./release/*.zip`; do gpg --detach-sign --batch --pinentry-mode loopback --armor --openpgp --yes -u 87DA4591 --passphrase="$GPG_PASSPHRASE" $ff; done - env: - GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - body: Release ${{ github.ref }} - draft: false - prerelease: false - - - name: Upload Release Assets - uses: shogo82148/actions-upload-release-asset@v1 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./release/* diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml new file mode 100644 index 0000000000..af069cdf61 --- /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 0000000000..b957e2fc8b --- /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/.gitignore b/.gitignore index 4a0faab043..5f49d65d56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,32 @@ -/.idea -/.vscode +*.bak +*.old /api.php -/error.log -/nbproject -/npm-debug.log -/yarn-error.log -/rainloop.sublime-project -/rainloop.sublime-workspace -/rainloop/v/0.0.0/static/* -/rainloop/v/0.0.0/app/localization/moment/* -!/rainloop/v/0.0.0/app/localization/moment/.gitempty -/vendors/.* +/*.log +/snappymail/v/0.0.0/static/* /node_modules /build/local /build/dist /build/tmp /build/docker -/.docker/.cache -/.docker/mail/config -/.docker/nginx/ssl/* -!/.docker/nginx/ssl/.gitempty +/.docker/dev/.cache +/.docker/dev/mail/config +/.docker/dev/nginx/ssl/* +!/.docker/dev/nginx/ssl/.gitempty +/.docker/release/snappymail-*.zip /dist /data -.DS_Store /MULTIPLY /include.php +/issues +.idea/ +.env +/test +/public_html +/vendors/knockout/spec +/vendors/openpgp-5 +!/vendors/openpgp-5/dist +/vendors/squire2 +!/vendors/squire2/dist +/vendors/vanillaqr.js/ +/integrations/nextcloud/rainloop +/integrations/owncloud/rainloop diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..ccf1009a67 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,7 @@ +[submodule "vendors/openpgp-5"] + path = vendors/openpgp-5 + url = git@github.com:the-djmaze/openpgpjs.git +[submodule "vendors/squire2"] + path = vendors/squire2 + url = git@github.com:the-djmaze/Squire.git + branch = snappymail diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000000..71041371a2 --- /dev/null +++ b/.htaccess @@ -0,0 +1,51 @@ + + 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] + + + + ExpiresActive On + ExpiresByType text/css A15768000 + ExpiresByType text/html A15768000 + ExpiresByType application/javascript A15768000 + ExpiresByType image/gif A15768000 + ExpiresByType image/jpeg A15768000 + ExpiresByType image/png A15768000 + ExpiresByType image/svg+xml A15768000 + ExpiresByType image/webp A15768000 + ExpiresByType image/vnd.microsoft.icon A15768000 + ExpiresByType font/woff A15768000 + ExpiresByType font/woff2 A15768000 + + + +# Header set Cache-Control "public, max-age=31536000" +# Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'" +# Header set Referrer-Policy "no-referrer" +# Header set Strict-Transport-Security "max-age=31536000" + Header set imagetoolbar "no" +# Header set X-Content-Type-Options "nosniff" +# Header set X-XSS-Protection "1; mode=block" + Header set Service-Worker-Allowed "/" + + # Google FLoC +# Header set Permissions-Policy "interest-cohort=()" + + +# +# AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript application/javascript +# + + + AddOutputFilterByType DEFLATE text/css text/html text/plain text/xml application/xml text/javascript application/javascript + AddOutputFilterByType DEFLATE font/opentype font/otf font/ttf font/woff + diff --git a/.tx/config b/.tx/config deleted file mode 100644 index e74d66a116..0000000000 --- a/.tx/config +++ /dev/null @@ -1,15 +0,0 @@ -[main] -host = https://www.transifex.com -minimum_perc = 60 -type = YAML - -[rainloop-webmail.rainloop-webmail] -file_filter = rainloop/v/0.0.0/app/localization/webmail/.yml -source_file = rainloop/v/0.0.0/app/localization/webmail/_source.en.yml -source_lang = en - -[rainloop-webmail.rainloop-admin] -file_filter = rainloop/v/0.0.0/app/localization/admin/.yml -source_file = rainloop/v/0.0.0/app/localization/admin/_source.en.yml -source_lang = en - diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..d31a1b7718 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2422 @@ +## 2.38.2 – 2024-10-09 +### Fixed +- error '$index is not defined' in Settings > Accounts + [#1797](https://github.com/the-djmaze/snappymail/issues/1797) + + +## 2.38.1 – 2024-10-08 + +### Added +- Admin - Extensions search filter +- Options to unset current font family and size + [#1726](https://github.com/the-djmaze/snappymail/issues/1726) +- Option to allow insecure cryptkey + [#1746](https://github.com/the-djmaze/snappymail/issues/1746) +- Save vCard FN property + [#1761](https://github.com/the-djmaze/snappymail/issues/1761) +- Docker Nginx should listen on IPv6 addresses in addition to IPv4 + [#1770](https://github.com/the-djmaze/snappymail/issues/1770) +- Full-screen Message View on Double-Click + [#1787](https://github.com/the-djmaze/snappymail/issues/1787) + +### Changed +- Use the custom Squire as submodule vendors/squire2 +- Disallow noembed and noframes HTML elements +- Keep 1 space between HTML elements in compressed templates +- Squire: sanitizeToDOMFragment now uses cleanHTML() +- Squire: improved handling of BR elements + [#1389](https://github.com/the-djmaze/snappymail/issues/1389) +- Nextcloud border-radius by @gnilebein + [#1790](https://github.com/the-djmaze/snappymail/pull/1790) +- Compose window sentFolder handling for + [#1793](https://github.com/the-djmaze/snappymail/pull/1793) +- Update Portuguese by @ner00 +- Update Polish by @tinola +- Update French by @hguilbert +- Update Portuguese (Brazil) by @mstolf + +### Fixed +- Composer dialog scroll got broken in v2.28 +- Composer dialog "from" triangle button wrong position due to font changes +- Admin - Config `search` should be ko.observable() not ko.observableArray() +- Squire: paste images + [#1389](https://github.com/the-djmaze/snappymail/issues/1389) +- Domain whitelist failures on login + [#1706](https://github.com/the-djmaze/snappymail/issues/1706) +- Sieve parse errors +- Sieve support `index` was not optional for + [#1709](https://github.com/the-djmaze/snappymail/issues/1709) +- Pagination problem for large mailbox after 2.36.1 + [#1716](https://github.com/the-djmaze/snappymail/issues/1716) +- Undefined constant "LOG_ERROR" + [#1754](https://github.com/the-djmaze/snappymail/issues/1754) +- Search Filter Capital "B" not working + [#1780](https://github.com/the-djmaze/snappymail/issues/1780) +- PHP 8.4: Implicitly nullable parameter declarations deprecated + + +## 2.38.0 – 2024-09-16 + +### Added +- Remove `tel:` links when converting HTML to plain + [#1724](https://github.com/the-djmaze/snappymail/issues/1724) +- Convert HTML to Markdown instead of plain, by using [Turndown](https://github.com/mixmark-io/turndown) + [#1604](https://github.com/the-djmaze/snappymail/issues/1604) +- Check HTMLInputElement.validity() for + [#1733](https://github.com/the-djmaze/snappymail/issues/1733) + +### Changed +- Use a modified [Squire 2.3.2](https://github.com/the-djmaze/Squire/commits/snappymail/) +- cleanHtml use allowedTags instead of disallowedTags and improved CSS handling +- Update Portuguese by @ner00 + +### Fixed +- mXSS exploit found by SonarSource + [CVE-2024-45800](https://github.com/the-djmaze/snappymail/security/advisories/GHSA-2rq7-79vp-ffxm) +- Call to a member function Email() on null + [#1706](https://github.com/the-djmaze/snappymail/issues/1706) +- IMAP capabilities via IMAP Proxy + [#1725](https://github.com/the-djmaze/snappymail/issues/1725) +- Messages on page setting is not validated against 999 max + [#1733](https://github.com/the-djmaze/snappymail/issues/1733) + + +## 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 and removed `with($context)` scope +- 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 + [#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/issues/1192) + +### 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/issues/1204) +- Convert getPdoAccessData() : array to a RainLoop\Pdo\Settings object instance +- New bidi buttons to Squire editor + [#1200](https://github.com/the-djmaze/snappymail/pull/1200) by @rezaei92 + +### Fixed +- Undefined constant PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT + [#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/issues/1186) + + +## 2.28.3 – 2023-06-22 + +### Added +- Attachments in "new window" view + [#1166](https://github.com/the-djmaze/snappymail/issues/1166) + +### Changed +- Update Portuguese by @ner00 +- Update French by @hguilbert + +### Fixed +- Some emails with inline CSS break the UI + [#1187](https://github.com/the-djmaze/snappymail/issues/1187) +- Remote.get() Promise broken by previous change + [#1185](https://github.com/the-djmaze/snappymail/issues/1185) +- Class "MailSo\Base\Exceptions\InvalidArgumentException" not found + [#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/issues/1180) + + +## 2.28.2 – 2023-06-19 + +### Added +- Detailed error for "Cannot access the repository at the moment" + [#1164](https://github.com/the-djmaze/snappymail/issues/1164) +- Bidi in Squire editor + [#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 +- 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/issues/1170) +- Image max-width now 100% instead of 90vw + +### Fixed +- Cannot modify header information + [#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/issues/1169) +- Invalid CSP report-uri +- Prevent MessageList multiple request at the same time + [#1071](https://github.com/the-djmaze/snappymail/issues/1071) +- Error in Addressbook Sync + [#1179](https://github.com/the-djmaze/snappymail/issues/1179) +- base64_decode() second parameter must be true + + +## 2.28.1 – 2023-06-05 + +### Changed +- Optical issue with input fields for mail and folder search + [#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 +- Undefined SIG constants + [#1147](https://github.com/the-djmaze/snappymail/issues/1147) + + +## 2.28.0 – 2023-05-30 + +### Added +- Threaded view make number orange when unread sub-messages + [#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/issues/1013) +- CSS `--btn-border-radius` + +### Changed +- Improved RTL languages support + [#1056](https://github.com/the-djmaze/snappymail/issues/1056) +- Composer text/attachments as tabs + [#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/issues/1111) +- TLS connection for MYSQL contact db + [#1078](https://github.com/the-djmaze/snappymail/issues/1078) +- Allow empty message body when there are attachments + [#1052](https://github.com/the-djmaze/snappymail/issues/1052) +- PHP inherit logger as Trait +- 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 +- Show messagelist timeout/abort error for + [#1071](https://github.com/the-djmaze/snappymail/issues/1071) +- DesktopNotifications setting not saved + [#1137](https://github.com/the-djmaze/snappymail/issues/1137) +- PHP Deprecation warning for $_openPipes + [#1141](https://github.com/the-djmaze/snappymail/issues/1141) +- Images size wrong + [#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/issues/1130) +- Replace ` +
+

+ + + diff --git a/integrations/nextcloud/snappymail/templates/index.php b/integrations/nextcloud/snappymail/templates/index.php new file mode 100644 index 0000000000..f304654fbb --- /dev/null +++ b/integrations/nextcloud/snappymail/templates/index.php @@ -0,0 +1,3 @@ + + + +
+
+
+ +
+ + +
+ +
+'.$_['BaseAppBootScript'].$_['BaseLanguage'].' +'; diff --git a/integrations/nextcloud/snappymail/templates/personal_settings.php b/integrations/nextcloud/snappymail/templates/personal_settings.php new file mode 100644 index 0000000000..93a86c7444 --- /dev/null +++ b/integrations/nextcloud/snappymail/templates/personal_settings.php @@ -0,0 +1,21 @@ +
+
+ +
+

t('SnappyMail Webmail'); ?>

+

+ t('Enter an email and password to auto-login to SnappyMail.'); ?> +

+

+ + + + + +    +

+
+
+
diff --git a/integrations/owncloud/snappymail/CHANGELOG.md b/integrations/owncloud/snappymail/CHANGELOG.md new file mode 100644 index 0000000000..4d0457f06a --- /dev/null +++ b/integrations/owncloud/snappymail/CHANGELOG.md @@ -0,0 +1,774 @@ +## 2.24.4 – 2022-12-30 + +## Added +- Check PHP_INT_SIZE if SnappyMail runs on 64bit + +## Changed +- A lot of MessageList sorting improvements + [#796](https://github.com/the-djmaze/snappymail/pull/796) +- On upgrade also update plugins in Nextcloud due to many misunderstandings and prevent invalid open issues +- Moved application.ini labs.use_app_debug_* to debug.* + +## Fixed +- Dutch translation for confusing message (threads vs grouped) +- Workaround Nextcloud disallowed .htaccess + [#790](https://github.com/the-djmaze/snappymail/pull/790) +- Searching on Nextcloud search failed + [#787](https://github.com/the-djmaze/snappymail/pull/787) + + +## 2.24.3 – 2022-12-28 + +## Changed +- When sorting on FROM also sort on REVERSE DATE + +## Fixed +- F5 and Ctrl-F5 reload logs out of Snappymail in Chrome. + [#800](https://github.com/the-djmaze/snappymail/pull/800) +- Switching accounts does not work anymore with 2.24.2 + [#802](https://github.com/the-djmaze/snappymail/pull/802) + + +## 2.24.2 – 2022-12-27 + +### Changed +- Disable sorting when viewing message thread + [#445](https://github.com/the-djmaze/snappymail/pull/445) +- Update Chinese translation by @mayswind + [#794](https://github.com/the-djmaze/snappymail/pull/794) +- No need to call IMAP EXAMINE when current folder already SELECT +- Thread view now has tree indentation + +### Fixed +- Nextcloud failed on Integrity check + [#790](https://github.com/the-djmaze/snappymail/pull/790) +- Deleting message fails with message "Cannot move message" on hMailServer + [#793](https://github.com/the-djmaze/snappymail/pull/793) +- List messages per day feature is enabled by default and breaks sorting + [#796](https://github.com/the-djmaze/snappymail/pull/796) +- Custom page login not working for first time due to smctoken security + [#798](https://github.com/the-djmaze/snappymail/pull/798) +- Message list is always empty due to wrong implementation of RFC 8474 + [#799](https://github.com/the-djmaze/snappymail/pull/799) + + +## 2.24.1 – 2022-12-23 + +### Changed +- Intl.DateTimeFormat() into toLocaleString() for iOS < 14 +- Cleanup locale date/time handling +- Make MessageList per day optional + [#737](https://github.com/the-djmaze/snappymail/pull/737) + +### Fixed +- Typed property MailSo\Cache\Drivers\Redis::$sKeyPrefix must not be accessed before initialization #792 + [#792](https://github.com/the-djmaze/snappymail/pull/792) +- Attachments in mails in 2.24 not loading in reply/forward #789 + [#789](https://github.com/the-djmaze/snappymail/pull/789) +- Rollback #280 due to complications + [#280](https://github.com/the-djmaze/snappymail/pull/280) + + +## 2.24.0 – 2022-12-22 + +### Added +- Option to enable additional account unread messages count +- Prevent godaddy click tracking +- Dark theme use `color-scheme: dark;` +- More imapsync.php CLI options and help + +### Changed +- MessageList now grouped/split per day + [#737](https://github.com/the-djmaze/snappymail/pull/737) +- Account switcher still shown when allow_additional_accounts is set to Off + [#280](https://github.com/the-djmaze/snappymail/pull/280) +- PHP classes use typed properties +- Speedup Contacts Suggestions handling +- Check SMTP SIZE + [#779](https://github.com/the-djmaze/snappymail/pull/779) + +### Fixed +- Handle multiple DKIM signatures authentication results +- Reload admin extensions on update +- SieveClient quoted string parsing failed +- Invalid Attachments (PDF) + [#466](https://github.com/the-djmaze/snappymail/pull/466) +- Email HTML images rendering issue + [#564](https://github.com/the-djmaze/snappymail/pull/564) +- "Server message: No supported SASL mechanism found, remote server wants:" in hMailServer + [#780](https://github.com/the-djmaze/snappymail/pull/780) + +### Removed +- Some unused plugin hooks to improve Action handling speed + + +## 2.23.1 – 2022-12-15 + +### Changed +- More JMAP RFC matching including role +- Speedup fetch all Folders/mailboxes +- Disable unused folder_list_limit +- Merge MailSo\Mail\Folder into MailSo\Imap\Folder and speedup process +- SnappyMail\Imap\Sync now matches folders based on JMAP role +- Added the new imapsync.php command line script for + [#744](https://github.com/the-djmaze/snappymail/pull/744) +- Added manual setting for 12/24h clock + [#760](https://github.com/the-djmaze/snappymail/pull/760) +- Add options to mark the message I'm viewing as unread and return to the inbox #766 + +### Fixed +- Extension menu shows only some available extensions #778 +- New solution for [#423](https://github.com/the-djmaze/snappymail/pull/423) due to [#774](https://github.com/the-djmaze/snappymail/pull/774) +- Avatars extension error on smartphone + [#764](https://github.com/the-djmaze/snappymail/pull/764) +- Don't fetch Unread count for main account +- CSS .e-checkbox.material-design invisible on show/hide + + +## 2.23.0 – 2022-12-08 + +### Added +- Show the number of unread mails on all mail addresses/accounts + [#437](https://github.com/the-djmaze/snappymail/pull/437) +- Show OpenSSL version in Admin => About + +### Changed +- Redirect to login page instead of "invalid token" popup + [#752](https://github.com/the-djmaze/snappymail/pull/752) +- Make all dialogs fit in mobile view +- Changed some Plugin hooks for better handling: + * json.action-pre-call => json.before-{actionname} + * json.action-post-call => json.after-{actionname} +- Cleaner accounts list in systemdropdown +- Multiple imapConnect handling for new import mail feature + [#744](https://github.com/the-djmaze/snappymail/pull/744) + +### Fixed +- Loosing HTML signature in account identity under settings + [#750](https://github.com/the-djmaze/snappymail/pull/750) +- Plugin configuration did not load anymore when type was SELECTION by @cm-schl + [#753](https://github.com/the-djmaze/snappymail/pull/753) +- Nextcloud Default theme shows gray text on gray background + [#754](https://github.com/the-djmaze/snappymail/pull/754) +- Only run JSON hooks when $sAction is set + [#755](https://github.com/the-djmaze/snappymail/pull/755) +- Unsupported SASL mechanism OAUTHBEARER + [#756](https://github.com/the-djmaze/snappymail/pull/756) + [#758](https://github.com/the-djmaze/snappymail/pull/758) + [#759](https://github.com/the-djmaze/snappymail/pull/759) +- border-box issue with .buttonCompose + +### Removed +- Deprecate \RainLoop\Account->Login() and \RainLoop\Account->Password() + + +## 2.22.7 – 2022-12-06 + +### Changed +- Scroll bar with the mobile version in "Advanced search" screen + [#712](https://github.com/the-djmaze/snappymail/pull/712) + +### Fixed +- Undefined property: MailSo\Mail\FolderCollection::$capabilities +- PHP 8.2 Creation of dynamic property is deprecated +- Attempt to solve #745 in v2.22.6 failed and resulted in errors #746 and #748 + [#745](https://github.com/the-djmaze/snappymail/pull/745) + [#746](https://github.com/the-djmaze/snappymail/pull/746) + [#748](https://github.com/the-djmaze/snappymail/pull/748) +- Admin domain test undefined matched domain should say email@example matched domain + + +## 2.22.6 – 2022-12-05 + +### Changed +- Narrow MessageList wraps star icon + [#737](https://github.com/the-djmaze/snappymail/pull/737) +- Use UIDVALIDITY when HIGHESTMODSEQ not available, maybe solves + [#745](https://github.com/the-djmaze/snappymail/pull/745) +- No need to generate 1000's of ID's for MessageListByRequestIndexOrUids() +- Update Chinese translation by @mayswind + +### Fixed +- PluginProperty DefaultValue contained array while it should not + [#741](https://github.com/the-djmaze/snappymail/pull/741) + +### Removed +- IMAP SELECT/EXAMINE unset `UNSEEN` because IMAP4rev2 deprecated + + +## 2.22.5 – 2022-12-02 + +### Added +- Support plugin minified .min.js and .min.css +- ZIP Download multiple emails + [#717](https://github.com/the-djmaze/snappymail/pull/717) + +### Changed +- Replaced some data-bind="click: function(){} with object functions to prevent eval() +- Improved plugins hash when there are changes + +### Fixed +- Settings Themes style due to border-box change +- "Remember me" failed due to v2.22.4 Session token change + [#719](https://github.com/the-djmaze/snappymail/pull/719) + [#731](https://github.com/the-djmaze/snappymail/pull/731) + +### Removed +- Vacation filter: Button to add recipients (+) + [#728](https://github.com/the-djmaze/snappymail/pull/728) + + +## 2.22.4 – 2022-11-28 + +### Changed +- Contacts dialog layout using flex +- Session token is related to the user agent string + [#713](https://github.com/the-djmaze/snappymail/pull/713) +- Better browser cache handling for avatars plugin + [#714](https://github.com/the-djmaze/snappymail/pull/714) +- Force HTML editor when set as default when replying to message + [#355](https://github.com/the-djmaze/snappymail/pull/355) + +### Fixed +- Contact Error - object Object + [#716](https://github.com/the-djmaze/snappymail/pull/716) +- Unable to move messages to different folder by drag and drop + [#710](https://github.com/the-djmaze/snappymail/pull/710) +- v2.22.3 unknown error + [#709https://github.com/the-djmaze/snappymail/pull/709) + + +## 2.22.3 – 2022-11-25 + +### Added +- application.ini config logs.path and cache.path to improve custom data structure. + +### Changed +- Improved cPanel integration + [#697](https://github.com/the-djmaze/snappymail/pull/697) +- Update to OpenPGP.js v5.5.0 + +### Fixed +- drag & drop folder expansion + [#707](https://github.com/the-djmaze/snappymail/pull/707) +- Save selected messages as .eml in Nextcloud failed + [#704](https://github.com/the-djmaze/snappymail/pull/704) + + +## 2.22.2 – 2022-11-24 + +### Added +- Support cPanel #697 + + +## 2.22.1 – 2022-11-23 + +### Added +- AddressBookInterface::GetContactByEmail() to support sender image/avatar extension + [#115](https://github.com/the-djmaze/snappymail/pull/115) + +### Changed +- All the attachment zone is not clickable, even if the cursor is a hand + [#691](https://github.com/the-djmaze/snappymail/pull/691) +- Different approach for "update button duplicated in admin panel" + [#677](https://github.com/the-djmaze/snappymail/pull/677) +- Better drag & drop solution for leftPanel + +### Fixed +- The page does not change after batch deletion + [#684](https://github.com/the-djmaze/snappymail/pull/684) +- Prevent domain uppercase issues found in + [#689](https://github.com/the-djmaze/snappymail/pull/689) +- Login invalid response: VXNlcm5hbWU6CG + [#693](https://github.com/the-djmaze/snappymail/pull/693) + + +## 2.21.4 – 2022-11-22 + +### Added +- Added domain matcher test for + [#689](https://github.com/the-djmaze/snappymail/pull/689) +- Download all Attachments of selected Emails + [#361](https://github.com/the-djmaze/snappymail/pull/361) + +### Changed +- Log current shortcuts scope for + [#690](https://github.com/the-djmaze/snappymail/pull/690) +- CSS everything to be box-sizing: border-box; +- Make messageview a bit larger so that it is the same height as the messagelist +- Cleanup and rearrange some fontastic glyphs +- Also show From email address by default + [#683](https://github.com/the-djmaze/snappymail/pull/683) + +### Fixed +- Contact.display() returns [object Object] +- When left panel disabled and drag messages, show it +- Issue with admin domain connection type settings selectbox + [#689](https://github.com/the-djmaze/snappymail/pull/689) +- Mobile View on cellphones: automatic scrolling not working near the visual keyboard + [#686](https://github.com/the-djmaze/snappymail/pull/686) +- Unable to separate runtime from installation + [#685](https://github.com/the-djmaze/snappymail/pull/685) + +### Removed +- Removed inline parameter of checkbox and select components + + +## 2.21.3 – 2022-11-16 + +### Added +- Click on PGP KEY attachment opens "Import key" dialog + +### Changed +- Increase visible reading area for small screens + [#672](https://github.com/the-djmaze/snappymail/pull/672) +- Improved message spam score detailed view +- Improved DAV connection logging + +### Fixed +- Handling attachments MIME type / content-type +- Message responsive resizing width/height of elements + [#678](https://github.com/the-djmaze/snappymail/pull/678) +- Focus on textarea when creating a new plain text email + [#501](https://github.com/the-djmaze/snappymail/pull/501) +- CardDav remove photos of my contacts when synchronizing + [#679](https://github.com/the-djmaze/snappymail/pull/679) + +### Removed +- \MailSo\Mime\Enumerations\MimeType + +### Nextcloud +- Use fontastic in Nextcloud Files selector dialog +- Firefox < 98 dialogs + [#673](https://github.com/the-djmaze/snappymail/pull/673) + +## 2.21.2 – 2022-11-15 + +### Added +- Allow browser Spellchecker + [#574](https://github.com/the-djmaze/snappymail/pull/574) +- Decode MIME charset of .EML attachments + [#662](https://github.com/the-djmaze/snappymail/pull/662) + +### Changed +- Increase message visible text area + [#672](https://github.com/the-djmaze/snappymail/pull/672) +- When copy/paste image use the raw data instead of clipboard HTML + [#654](https://github.com/the-djmaze/snappymail/pull/654) +- When application.ini debug.enable is true, also debug js and css +- JavaScript rl.setWindowTitle() renamed to rl.setTitle() + +### Removed +- Message toggle fullscreen button which was only in mobile view + +### Nextcloud +- Workaround Nextcloud calendar crashes + [#622](https://github.com/the-djmaze/snappymail/pull/622) + [#661](https://github.com/the-djmaze/snappymail/pull/661) +- Added share public/internal file link + [#569](https://github.com/the-djmaze/snappymail/pull/569) + + +## 2.21.1 – 2022-11-13 + +### Fixed +- Crypt crashes when Sodium not installed + [#641](https://github.com/the-djmaze/snappymail/pull/641) + [#657](https://github.com/the-djmaze/snappymail/pull/657) + [#663](https://github.com/the-djmaze/snappymail/pull/663) + [#664](https://github.com/the-djmaze/snappymail/pull/664) + [#668](https://github.com/the-djmaze/snappymail/pull/668) + [#669](https://github.com/the-djmaze/snappymail/pull/669) +- Personalised favicon not working - default Snappymail favicon showing + [#665](https://github.com/the-djmaze/snappymail/pull/665) + +### Nextcloud +- v23 ContentSecurityPolicy versions issue + [#666](https://github.com/the-djmaze/snappymail/pull/666) + + +## 2.21.0 – 2022-11-11 + +### Added +- Put messagelist top bar buttons also in dropdown +- Allow setting additional Sec-Fetch rules, as discussed by + [#585](https://github.com/the-djmaze/snappymail/pull/585) +- Light/Dark favicon.svg + [#643](https://github.com/the-djmaze/snappymail/pull/643) +- Allow an account name/label + [#571](https://github.com/the-djmaze/snappymail/pull/571) + +### Changed +- Moved ServiceRemoteAutoLogin to plugin/extension +- Moved ServiceExternalSso to plugin/extension +- Moved ServiceExternalLogin to plugin/extension +- Renamed ManageSieveClient to SieveClient +- New Net/Imap/Smtp/Sieve Settings object system which allows + setting SSL options per domain and verify_certificate by default +- Update plugins to use new Net/Imap/Smtp/Sieve Settings object +- Removed message double-click to full screen + [#638](https://github.com/the-djmaze/snappymail/pull/638) + +### Fixed +- ldap-identities-plugin by @cm-schl + [#647](https://github.com/the-djmaze/snappymail/pull/647) +- OpenSSL v3 ciphers issue + [#641](https://github.com/the-djmaze/snappymail/pull/641) + +### Nextcloud +- Style PopupsNextcloudFiles view +- Link to internal files in composer + + +## 2.20.6 – 2022-11-08 + +### Fixed +- ?admin login failed + [#642](https://github.com/the-djmaze/snappymail/pull/642) +- Resolve PHP 8.2 Creation of dynamic property is deprecated + + +## 2.20.5 – 2022-11-08 + +### Nextcloud +- Improved workaround for Nextcloud Content-Security-Policy bug + Safari [#631](https://github.com/the-djmaze/snappymail/issues/631) + Edge [#633](https://github.com/the-djmaze/snappymail/issues/633) + Reported [#35013](https://github.com/nextcloud/server/issues/35013) + + +## 2.20.4 – 2022-11-07 + +### Fixed +- Nextcloud no-embed use iframe mode failed + +### Nextcloud +- Workaround Nextcloud Content-Security-Policy bug + Safari [#631](https://github.com/the-djmaze/snappymail/issues/631) + Edge [#633](https://github.com/the-djmaze/snappymail/issues/633) + Reported [#35013](https://github.com/nextcloud/server/issues/35013) + + +## 2.20.3 – 2022-11-07 + +### Added +- Throw decrypt errors + [#632](https://github.com/the-djmaze/snappymail/issues/632) + +### Changed +- Better multiple WYSIWYG registration system (not finished) +- Better handling of admin token cookie + +### Fixed +- Cookie “name” has been rejected because it is already expired. + [#636](https://github.com/the-djmaze/snappymail/issues/636) +- Content-Security-Policy 'strict-dynamic' was missing + +### Nextcloud +- Better handling of Content-Security-Policy + [#631](https://github.com/the-djmaze/snappymail/issues/631) + [#633](https://github.com/the-djmaze/snappymail/issues/633) +- Nextcloud 23 Error Call to undefined method useStrictDynamic() + [#634](https://github.com/the-djmaze/snappymail/issues/634) +- Use snappymail icon as favicon-mask.svg instead default nextcloud logo + [#635](https://github.com/the-djmaze/snappymail/issues/635) + + +## 2.20.2 – 2022-11-05 + +### Added +- Add more search operators (i.e. copy lots of Gmail ones) + [#625](https://github.com/the-djmaze/snappymail/issues/625) + +### Changed +- Some CSS borders to var(--border-color) + +### Fixed +- pgpDecrypt() using MailVelope the decrypt message was not green +- Shift + F in search bar resulted in forwarding message + [#624](https://github.com/the-djmaze/snappymail/issues/624) + +### Nextcloud +- auto login mechanism not working anymore + [#627](https://github.com/the-djmaze/snappymail/issues/627) + + +## 2.20.1 – 2022-11-04 + +### Added +- Added CSS --dialog-border-clr and --dialog-border-radius +- Show lock (lock) glyph in messagelist for encrypted messages + +### Fixed +- Decrypt failed when OpenPGP.js not loaded + +### Nextcloud +- Now integrate with Nextcloud by default, but keep iframe option available +- Better theme integration with Nextcloud +- Use Nextcloud 18+ IEventDispatcher +- Solve Nextcloud 25 CSS issues + [#620](https://github.com/the-djmaze/snappymail/issues/620) +- PutinICS does is not working for all calendar events + [#622](https://github.com/the-djmaze/snappymail/issues/622) +- Update readme by @cm-schl + [#617](https://github.com/the-djmaze/snappymail/issues/617) + + +## 2.20.0 – 2022-11-03 + +### Added +- Strip mailchimp tracking + +### Changed +- Use some PHP typed properties +- Move bootstrap @less variables to CSS var() +- Improved theme styling + +### Fixed +- CSS --dropdown-menu-background-color should be --dropdown-menu-bg-color + +### Nextcloud +- Disable Nextcloud Impersonate check due to login/logout complications + [#561](https://github.com/the-djmaze/snappymail/issues/561) +- Improved theme integration and be compatible with Breeze Dark + + +## 2.19.7 – 2022-11-02 + +### Added +- Make it clear that you are on the admin panel login screen +- Force PHP opcache_invalidate due to upgrade error reports "Missing version directory" + +### Fixed +- Switching user (impersonate plugin) keeps old Email logged in + [#561](https://github.com/the-djmaze/snappymail/issues/561) +- PGP Decryption / Encryption Failures + [#600](https://github.com/the-djmaze/snappymail/issues/600) +- Undefined constant "OCA\SnappyMail\Util\RAINLOOP_APP_LIBRARIES_PATH + [#601](https://github.com/the-djmaze/snappymail/issues/601) +- Cannot access admin panel + [#602](https://github.com/the-djmaze/snappymail/issues/602) +- Wont show my emails + [#604](https://github.com/the-djmaze/snappymail/issues/604) +- Return type of MailSo\Base\StreamFilters\LineEndings::filter + [#610](https://github.com/the-djmaze/snappymail/issues/610) +- Create .pgp directory was missing + +### Security +- Logger leaked some passwords + +## 2.19.6 – 2022-10-31 + +### Added +- Put sign and encrypt options in composer dropdown menu and simplify te two existing buttons with a glyph +- Filter scripts UI let user understand which filter is active + [#590](https://github.com/the-djmaze/snappymail/issues/590) + +### Fixed +- Method 'GetRequest' not found in \MailSo\Base\Http + [#585](https://github.com/the-djmaze/snappymail/issues/585) + +### Changed +- Base Domain setup enhancements +- Cleanup MailSo MailClient using __call() +- Domain settings handling and store as JSON instead of ini +- Some JavaScript changes +- When try to login IMAP/SMTP/SIEVE but STARTTLS is required, force STARTTLS +- Embed admin panel into Nextcloud (with autologin, no need for separate login) +- Don't set default_domain in Nextcloud when already set + +### Removed +- Nextcloud dark mode, it is incomplete + +### Deprecated +- nothing +## 2.21.0 – 2022-11-11 + +### Added +- Put messagelist top bar buttons also in dropdown +- Allow setting additional Sec-Fetch rules, as discussed by #585 +- Light/Dark favicon.svg #643 +- Allow an account name/label #571 + +### Changed +- Moved ServiceRemoteAutoLogin to plugin/extension +- Moved ServiceExternalSso to plugin/extension +- Moved ServiceExternalLogin to plugin/extension +- Renamed ManageSieveClient to SieveClient +- New Net/Imap/Smtp/Sieve Settings object system which allows + setting SSL options per domain and verify_certificate by default +- Update plugins to use new Net/Imap/Smtp/Sieve Settings object +- Removed message double-click to full screen #638 + +### Fixed +- ldap-identities-plugin by @cm-schl + [#647](https://github.com/the-djmaze/snappymail/pull/647) +- OpenSSL v3 ciphers issue #641 + +### Nextcloud +- Style PopupsNextcloudFiles view +- Link to internal files in composer + + +## 2.20.6 – 2022-11-08 + +### Fixed +- ?admin login failed + [#642](https://github.com/the-djmaze/snappymail/pull/642) +- Resolve PHP 8.2 Creation of dynamic property is deprecated + + +## 2.20.5 – 2022-11-08 + +### Nextcloud +- Improved workaround for Nextcloud Content-Security-Policy bug + Safari [#631](https://github.com/the-djmaze/snappymail/issues/631) + Edge [#633](https://github.com/the-djmaze/snappymail/issues/633) + Reported [#35013](https://github.com/nextcloud/server/issues/35013) + + +## 2.20.4 – 2022-11-07 + +### Fixed +- Nextcloud no-embed use iframe mode failed + +### Nextcloud +- Workaround Nextcloud Content-Security-Policy bug + Safari [#631](https://github.com/the-djmaze/snappymail/issues/631) + Edge [#633](https://github.com/the-djmaze/snappymail/issues/633) + Reported [#35013](https://github.com/nextcloud/server/issues/35013) + + +## 2.20.3 – 2022-11-07 + +### Added +- Throw decrypt errors + [#632](https://github.com/the-djmaze/snappymail/issues/632) + +### Changed +- Better multiple WYSIWYG registration system (not finished) +- Better handling of admin token cookie + +### Fixed +- Cookie “name” has been rejected because it is already expired. + [#636](https://github.com/the-djmaze/snappymail/issues/636) +- Content-Security-Policy 'strict-dynamic' was missing + +### Nextcloud +- Better handling of Content-Security-Policy + [#631](https://github.com/the-djmaze/snappymail/issues/631) + [#633](https://github.com/the-djmaze/snappymail/issues/633) +- Nextcloud 23 Error Call to undefined method useStrictDynamic() + [#634](https://github.com/the-djmaze/snappymail/issues/634) +- Use snappymail icon as favicon-mask.svg instead default nextcloud logo + [#635](https://github.com/the-djmaze/snappymail/issues/635) + + +## 2.20.2 – 2022-11-05 + +### Added +- Add more search operators (i.e. copy lots of Gmail ones) + [#625](https://github.com/the-djmaze/snappymail/issues/625) + +### Changed +- Some CSS borders to var(--border-color) + +### Fixed +- pgpDecrypt() using MailVelope the decrypt message was not green +- Shift + F in search bar resulted in forwarding message + [#624](https://github.com/the-djmaze/snappymail/issues/624) + +### Nextcloud +- auto login mechanism not working anymore + [#627](https://github.com/the-djmaze/snappymail/issues/627) + + +## 2.20.1 – 2022-11-04 + +### Added +- Added CSS --dialog-border-clr and --dialog-border-radius +- Show lock (lock) glyph in messagelist for encrypted messages + +### Fixed +- Decrypt failed when OpenPGP.js not loaded + +### Nextcloud +- Now integrate with Nextcloud by default, but keep iframe option available +- Better theme integration with Nextcloud +- Use Nextcloud 18+ IEventDispatcher +- Solve Nextcloud 25 CSS issues + [#620](https://github.com/the-djmaze/snappymail/issues/620) +- PutinICS does is not working for all calendar events + [#622](https://github.com/the-djmaze/snappymail/issues/622) +- Update readme by @cm-schl + [#617](https://github.com/the-djmaze/snappymail/issues/617) + + +## 2.20.0 – 2022-11-03 + +### Added +- Strip mailchimp tracking + +### Changed +- Use some PHP typed properties +- Move bootstrap @less variables to CSS var() +- Improved theme styling + +### Fixed +- CSS --dropdown-menu-background-color should be --dropdown-menu-bg-color + +### Nextcloud +- Disable Nextcloud Impersonate check due to login/logout complications + [#561](https://github.com/the-djmaze/snappymail/issues/561) +- Improved theme integration and be compatible with Breeze Dark + + +## 2.19.7 – 2022-11-02 + +### Added +- Make it clear that you are on the admin panel login screen +- Force PHP opcache_invalidate due to upgrade error reports "Missing version directory" + +### Fixed +- Switching user (impersonate plugin) keeps old Email logged in + [#561](https://github.com/the-djmaze/snappymail/issues/561) +- PGP Decryption / Encryption Failures + [#600](https://github.com/the-djmaze/snappymail/issues/600) +- Undefined constant "OCA\SnappyMail\Util\RAINLOOP_APP_LIBRARIES_PATH + [#601](https://github.com/the-djmaze/snappymail/issues/601) +- Cannot access admin panel + [#602](https://github.com/the-djmaze/snappymail/issues/602) +- Wont show my emails + [#604](https://github.com/the-djmaze/snappymail/issues/604) +- Return type of MailSo\Base\StreamFilters\LineEndings::filter + [#610](https://github.com/the-djmaze/snappymail/issues/610) +- Create .pgp directory was missing + +### Security +- Logger leaked some passwords + +## 2.19.6 – 2022-10-31 + +### Added +- Put sign and encrypt options in composer dropdown menu and simplify te two existing buttons with a glyph +- Filter scripts UI let user understand which filter is active + [#590](https://github.com/the-djmaze/snappymail/issues/590) + +### Fixed +- Method 'GetRequest' not found in \MailSo\Base\Http + [#585](https://github.com/the-djmaze/snappymail/issues/585) + +### Changed +- Base Domain setup enhancements +- Cleanup MailSo MailClient using __call() +- Domain settings handling and store as JSON instead of ini +- Some JavaScript changes +- When try to login IMAP/SMTP/SIEVE but STARTTLS is required, force STARTTLS +- Embed admin panel into Nextcloud (with autologin, no need for separate login) +- Don't set default_domain in Nextcloud when already set + +### Removed +- Nextcloud dark mode, it is incomplete + +### Deprecated +- nothing diff --git a/integrations/owncloud/snappymail/INSTALL b/integrations/owncloud/snappymail/INSTALL new file mode 100644 index 0000000000..e199b596b9 --- /dev/null +++ b/integrations/owncloud/snappymail/INSTALL @@ -0,0 +1,17 @@ +************************************************************************ +* +* ownCloud - SnappyMail Webmail package +* +* @author SnappyMail Team, Nextgen-Networks (@nextgen-networks), Tab Fitts (@tabp0le), Pierre-Alain Bandinelli (@pierre-alain-b) +* +************************************************************************ + +REQUIREMENTS: +- ownCloud version 10 and above + +INSTALL & CONFIGURATION: +Start within ownCloud, and click on the "+ Apps" button in the upper-left corner dropdown menu: +Then, enable the SnappyMail plugin that is in the "Social & communication" section. +After a quick wait, SnappyMail is installed. Even if it is really attractive, it is too soon to click on the newly appeared "Email" icon in the apps list. You should configure SnappyMail before using it (which makes some sense, doesn'it): go to ownCloud admin panel (upper-right corner dropdown menu) and go to "Additionnal settings". There click on the "Go to SnappyMail Webmail admin panel". +In the SnappyMail admin prompt, the default login is "admin" and the default password will be generated in "[nextcloud-data]/app_snappymail/_data_/_default_/admin_password.txt". No need to advise you to change it once in the admin panel! +This is it, you are now free to configure SnappyMail as you wish. One important point is the Domains section when you will set up the IMAP/SMTP parameters that shall be associated with the email adresses of your users. diff --git a/integrations/owncloud/snappymail/README.md b/integrations/owncloud/snappymail/README.md new file mode 100644 index 0000000000..dd64fad967 --- /dev/null +++ b/integrations/owncloud/snappymail/README.md @@ -0,0 +1,97 @@ +# snappymail-owncloud + +snappymail-owncloud is a plugin for ownCloud to use the excellent SnappyMail webmail (https://snappymail.eu/). + +Thank you to all contributors to SnappyMail for ownCloud: +- RainLoop Team, who initiated it +- [pierre-alain-b](https://github.com/pierre-alain-b/rainloop-owncloud) +- Tab Fitts (@tabp0le) +- Nextgen Networks (@nextgen-networks) +- [All testers of issue 96](https://github.com/the-djmaze/snappymail/issues/96) + +## How to Install + +Start within ownCloud as user with administrator rights and click on the "+ Apps" button in the upper-right corner dropdown menu: + +![Image1](https://raw.githubusercontent.com/the-djmaze/snappymail/master/integrations/owncloud/screenshots/help_a1.png) + +Then, enable the SnappyMail plugin that you will find in the "Social & communication" section: + +![Image2](https://raw.githubusercontent.com/the-djmaze/snappymail/master/integrations/owncloud/screenshots/help_a2.png) + +After a quick wait, SnappyMail is installed. Now you should configure it before use: open the ownCloud admin panel (upper-right corner dropdown menu -> Settings) and go to "Additional settings" under the "Administration" section. There, click on the "Go to SnappyMail Webmail admin panel" link. + +![Image3](https://raw.githubusercontent.com/the-djmaze/snappymail/master/integrations/owncloud/screenshots/owncloud-admin.png) + +To enter SnappyMail admin area, you must be ownCloud admin (so you get logged in automatically) or else use the admin login credentials. +The default login is "admin" and the default password will be generated in `[owncloud-data]/app_snappymail/_data_/_default_/admin_password.txt`. Don't forget to change it once in the admin panel! + +From that point, all instance-wide SnappyMail settings can be tweaked as you wish. One important point is the "Domains" section where you should set up the IMAP/SMTP parameters that will be associated with the email adresses of your users. Basically, if a user of the ownCloud instance starts SnappyMail and puts "firstname@domain.tld" as an email address, then SnappyMail should know how to connect to the IMAP & SMTP of domain.tld. You can fill in this information in the "Domains" section of the SnappyMail admin settings. For more information how to configure an automatic login for your ownCloud users see [How to auto-connect to SnappyMail?](#how-to-auto-connect-to-snappymail) + +![grafik](https://user-images.githubusercontent.com/63400209/199767908-fbef0f50-ecb7-47ae-9ac1-771959d4b7f5.png) + +![grafik](https://user-images.githubusercontent.com/63400209/199768097-7bd939a7-56d0-47ba-b481-aeac08776fb4.png) + + +## SnappyMail Settings, Where Are They? + +SnappyMail for ownCloud is highly configurable. But settings are available in multiple places and this can be misleading for first-time users. + +### SnappyMail admin settings +SnappyMail admin settings can be reached only by the ownCloud administrator. Open the ownCloud admin panel ("Admin" in the upper-right corner dropdown menu) and go to "Additionnal settings". There, click on the "Go to SnappyMail Webmail admin panel" link. Alternatively, you may use the following link: https://path.to.owncloud/index.php/apps/snappymail/?admin. + +SnappyMail admin settings include all settings that will apply to all SnappyMail users (default login rules, branding, management of plugins, security rules and domains). + +### SnappyMail user settings +Each user of SnappyMail can also change user-specific behaviors in the SnappyMail user settings. SnappyMail user settings are found within SnappyMail by clicking on the user button (in the upper-right corner of SnappyMail) and then choosing "Settings" in the dropdown menu. + +SnappyMail user settings include management of contacts, of email accounts, of folders, appearance and OpenPGP. + +### The specificity of SnappyMail user accounts +The plugin passes the login information of the user to the SnappyMail app which then creates and manages the user accounts. Accounts in SnappyMail are based soley on the authenticated email accounts, and do not take into account the owncloud user which created them in the first place. If two or more ownCloud users have the same email account in additional settings, they will in fact share the same 'email account' in SnappyMail including any additional email accounts that they may have added subsequently to their main account. +This is to be kept in mind for the use case where multiple users shall have the same email account but may be also tempted to add additionnal acounts to their SnappyMail. + +## How to auto-connect to SnappyMail? + +### Default Domain +As already said SnappyMail uses the domain part (@example.com) to choose the IMAP/SMTP server to use. If in the following settings the username passed to SnappyMail does not contain a domain, the "default domain" is added to this username. In this way SnappyMail can lookup the "Domain" configuration to use (IMAP, SMTP, SIEVE server ecc.). +Example: if the username `john` is passed to SnappyMail, the "default domain" `example.com` would be added to the username basing on your configuration. So SnappyMail would try to login the user with the username `john@example.com`. + +You can configure the "default domain" and connected settings in the SnappyMail Admin Panel under the menu "Login". + +### Auto-connect options +The ownCloud administrator can choose how SnappyMail tries to automatically login when a user clicks on the icon of SnappyMail within ownCloud. There are different options that can be found in the ownCloud "Settings -> Administration -> Additional settings": + +#### Option 1: Users will login manually, or define credentials in their personal settings for automatic logins. +If the user sets his credentials for the mailbox in his personal account under "Settings -> Additional settings", these credentials are used by SnappyMail to login. +If no personal credentials are defined the user is prompted by SnappyMail to insert his credentials every time he tries to open the SnappyMail App within ownCloud. + +#### Option 2: Attempt to automatically login users with their ownCloud username and password, or user-defined credentials, if set. +If the user sets his credentials for the mailbox in his personal account under "Settings -> Additional settings", these credentials are used by SnappyMail to login. +If no personal credentials are defined the ownCloud username and password is used by SnappyMail to login (eventually adding the [default domain](#default-domain)). + +If your IMAP server only accepts usernames without a domain (for example the ldap username of your user) the automatic addition of the "default domain" would block your users from logging in to your IMAP server - but on the other side it is needed by SnappyMail to determine the server settings to use. In such a case you must configure SnappyMail to strip off the domain part before sending the credentials to your IMAP server. This is done by entering to the SnappyMail Admin Panel -> Domains -> clicking on your default domain -> flagging the checkbox "Use short login" under IMAP and SMTP. + +#### Option 3: Attempt to automatically login users with their ownCloud email and password, or user-defined credentials, if set. +If the user sets his credentials for the mailbox in his personal account under "Settings -> Additional settings", these credentials are used by SnappyMail to login. +If no personal credentials are defined the mail address of the ownCloud user and his password are used by SnappyMail to login. SnappyMail will lookup the "Domain" settings for a configuration that meets the domain part of the mail address passed as username. + +### Auto-connection for all ownCloud users +If your ownCloud users base is synchronized with an email system, then it is possible that ownCloud credentials could be used right away to access the centralized email system. In the SnappyMail admin settings, the ownCloud administrator can then tick the "Automatically login with ownCloud/ownCloud user credentials" checkbox. + +Beware, if you tick this box, all ownCloud users will *not* be able to use the override it with the setting below. + +### Auto-connection for one user at a time +Except if the above setting is activated, any ownCloud user can have ownCloud and SnappyMail keep in mind the default email/password to connect to SnappyMail. There, logging in ownCloud is sufficient to then access SnappyMail within ownCloud. + +To fill in the default email address and password to use, each ownCloud user should go in the personal settings: choose "Settings" in the upper-right corner dropdown menu. Under "Personal" select the "Additional settings" section where you can find the "SnappyMail Webmail" settings. You can also use this direct link: https://path.to.owncloud/settings/user/additional. + + +## How to Activate SnappyMail Logging and then Find Logs + +You can activate SnappyMail logging here: `/path/to/owncloud/data/appdata_snappymail/_data_/_default_/configs/application.ini` +``` +[logs] +enable = On +``` +Logs are then available in `/path/to/owncloud/data/appdata_snappymail/_data_/_default_/logs/` diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions/OwnCloudSuggestions.php b/integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/OwnCloudSuggestions.php similarity index 86% rename from rainloop/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions/OwnCloudSuggestions.php rename to integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/OwnCloudSuggestions.php index c27640bd31..a99e9052e3 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions/OwnCloudSuggestions.php +++ b/integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/OwnCloudSuggestions.php @@ -1,7 +1,5 @@ oLogger) { diff --git a/integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/index.php b/integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/index.php new file mode 100644 index 0000000000..f50e99e438 --- /dev/null +++ b/integrations/owncloud/snappymail/app/data/_data_/_default_/plugins/owncloud/index.php @@ -0,0 +1,180 @@ +addHook('main.fabrica', 'MainFabrica'); + $this->addHook('filter.app-data', 'FilterAppData'); + $this->addHook('json.attachments', 'DoAttachmentsActions'); + + $sAppPath = ''; + if (\class_exists('OC_App')) { + $sAppPath = \rtrim(\trim(\OC_App::getAppWebPath('snappymail')), '\\/').'/app/'; + } + if (!$sAppPath) { + $sUrl = \MailSo\Base\Http::SingletonInstance()->GetUrl(); + if ($sUrl && \preg_match('/\/index\.php\/apps\/snappymail/', $sUrl)) { + $sAppPath = \preg_replace('/\/index\.php\/apps\/snappymail.+$/', + '/apps/snappymail/app/', $sUrl); + } + } + $_SERVER['SCRIPT_NAME'] = $sAppPath; + } + } + + public function Supported() : string + { + if (!static::IsOwnCloud()) { + return 'OwnCloud not found to use this plugin'; + } + return ''; + } + + // DoAttachmentsActions + public function DoAttachmentsActions(\SnappyMail\AttachmentsAction $data) + { + if ('owncloud' === $data->action) { + if (static::IsOwnCloudLoggedIn() && \class_exists('OCP\Files')) { + $oFiles = \OCP\Files::getStorage('files'); + if ($oFiles && $data->filesProvider->IsActive() && \method_exists($oFiles, 'file_put_contents')) { + $sSaveFolder = $this->Config()->Get('plugin', 'save_folder', '') ?: 'Attachments'; + $oFiles->is_dir($sSaveFolder) || $oFiles->mkdir($sSaveFolder); + $data->result = true; + foreach ($data->items as $aItem) { + $sSavedFileName = isset($aItem['FileName']) ? $aItem['FileName'] : 'file.dat'; + $sSavedFileHash = !empty($aItem['FileHash']) ? $aItem['FileHash'] : ''; + if (!empty($sSavedFileHash)) { + $fFile = $data->filesProvider->GetFile($data->account, $sSavedFileHash, 'rb'); + if (\is_resource($fFile)) { + $sSavedFileNameFull = \MailSo\Base\Utils::SmartFileExists($sSaveFolder.'/'.$sSavedFileName, function ($sPath) use ($oFiles) { + return $oFiles->file_exists($sPath); + }); + + if (!$oFiles->file_put_contents($sSavedFileNameFull, $fFile)) { + $data->result = false; + } + + if (\is_resource($fFile)) { + \fclose($fFile); + } + } + } + } + } + } + + foreach ($data->items as $aItem) { + $sFileHash = (string) (isset($aItem['FileHash']) ? $aItem['FileHash'] : ''); + if (!empty($sFileHash)) { + $data->filesProvider->Clear($data->account, $sFileHash); + } + } + } + } + + /** + * TODO: create pre-login auth hook + */ + public function ServiceOwnCloudAuth() + { +/* + $this->oHttp->ServerNoCache(); + + if (!static::IsOwnCloud() || + !isset($_ENV['___snappymail_owncloud_email']) || + !isset($_ENV['___snappymail_owncloud_password']) || + empty($_ENV['___snappymail_owncloud_email']) + ) + { + $this->oActions->SetAuthLogoutToken(); + $this->oActions->Location('./'); + return ''; + } + + $bLogout = true; + + $sEmail = $_ENV['___snappymail_owncloud_email']; + $sPassword = $_ENV['___snappymail_owncloud_password']; + + try + { + $oAccount = $this->oActions->LoginProcess($sEmail, $sPassword); + $this->oActions->AuthToken($oAccount); + + $bLogout = !($oAccount instanceof \snappymail\Model\Account); + } + catch (\Exception $oException) + { + $this->oActions->Logger()->WriteException($oException); + } + + if ($bLogout) + { + $this->oActions->SetAuthLogoutToken(); + } + + $this->oActions->Location('./'); + return ''; +*/ + } + + /** + * @return void + */ + public function FilterAppData($bAdmin, &$aResult) + { + if (!$bAdmin && \is_array($aResult) && static::IsOwnCloud()) { + $key = \array_search(\RainLoop\Enumerations\Capa::AUTOLOGOUT, $aResult['Capa']); + if (false !== $key) { + unset($aResult['Capa'][$key]); + } + if (static::IsOwnCloudLoggedIn() && \class_exists('OCP\Files')) { + $aResult['System']['attachmentsActions'][] = 'owncloud'; + } + } + } + + /** + * @param string $sName + * @param mixed $mResult + */ + public function MainFabrica($sName, &$mResult) + { + if ('suggestions' === $sName && static::IsOwnCloud() && $this->Config()->Get('plugin', 'suggestions', true)) { + include_once __DIR__.'/OwnCloudSuggestions.php'; + if (!\is_array($mResult)) { + $mResult = array(); + } + $mResult[] = new OwnCloudSuggestions(); + } + } + + protected function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('save_folder')->SetLabel('Save Folder') + ->SetDefaultValue('Attachments'), + \RainLoop\Plugins\Property::NewInstance('suggestions')->SetLabel('Suggestions') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(true) + ); + } +} diff --git a/integrations/owncloud/snappymail/appinfo/info.xml b/integrations/owncloud/snappymail/appinfo/info.xml new file mode 100644 index 0000000000..84ce9cb7f5 --- /dev/null +++ b/integrations/owncloud/snappymail/appinfo/info.xml @@ -0,0 +1,76 @@ + + + snappymail + SnappyMail + SnappyMail Webmail + 2.24.4 + agpl + SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli + + SnappyMail + + https://github.com/the-djmaze/snappymail/blob/master/integrations/owncloud/snappymail/README.md + https://github.com/the-djmaze/snappymail/wiki + + integration + office + search + social + https://raw.githubusercontent.com/the-djmaze/snappymail/master/integrations/owncloud/screenshots/inbox.jpg + https://snappymail.eu + https://github.com/the-djmaze/snappymail/discussions + https://github.com/the-djmaze/snappymail/tree/master/integrations/owncloud + https://github.com/the-djmaze/snappymail/issues + + + mbstring + zlib + + + + + OCA\SnappyMail\Settings\AdminSettings + OCA\SnappyMail\Settings\PersonalSettings + + + + Email + snappymail.page.index + logo-white-64x64.png + 4 + + + diff --git a/integrations/owncloud/snappymail/appinfo/routes.php b/integrations/owncloud/snappymail/appinfo/routes.php new file mode 100644 index 0000000000..5304f9869b --- /dev/null +++ b/integrations/owncloud/snappymail/appinfo/routes.php @@ -0,0 +1,41 @@ + [ + [ + 'name' => 'page#index', + 'url' => '/', + 'verb' => 'GET' + ], + [ + 'name' => 'page#indexPost', + 'url' => '/', + 'verb' => 'POST' + ], + [ + 'name' => 'page#appGet', + 'url' => '/run/', + 'verb' => 'GET' + ], + [ + 'name' => 'page#appPost', + 'url' => '/run/', + 'verb' => 'POST' + ], + [ + 'name' => 'fetch#setPersonal', + 'url' => '/fetch/personal.php', + 'verb' => 'POST' + ], + [ + 'name' => 'fetch#setAdmin', + 'url' => '/fetch/admin.php', + 'verb' => 'POST' + ], + [ + 'name' => 'fetch#upgrade', + 'url' => '/fetch/upgrade', + 'verb' => 'POST' + ] + ] +]; diff --git a/integrations/owncloud/snappymail/css/embed.css b/integrations/owncloud/snappymail/css/embed.css new file mode 100644 index 0000000000..150a27fe95 --- /dev/null +++ b/integrations/owncloud/snappymail/css/embed.css @@ -0,0 +1,124 @@ +/* +This stylesheet is used when SnappyMail runs in embed mode. +*/ +#content.app-snappymail { + max-height: 100%; +} +#content #rl-app { + position: relative; + min-width: 100%; + min-height: 100%; +} + +.squire-wysiwyg { + min-width: 100%; + min-height: 100%; +} + +#V-AdminPane .btn-logout { + display: none; +} + +#rl-app select, +#rl-app input +{ + min-height: auto; + height: auto; + margin: 0; + padding: 3px; +} +#rl-app .select, #rl-app select { + padding-right: 1.5em !important; +} + +#rl-app button.btn:not(.button-vue) { + min-height: auto; + height: auto; + margin: 0; + padding: 4px 12px; +} + +#rl-app .form-horizontal .control-group > :not(label) +{ + margin-left: 20px; +} + +#rl-app .LoginView .fontastic + input { + padding-left: 30px; +} + +#rl-app .loading::after { + display: none; +} + +#rl-app .squire-plain, #rl-app .squire-wysiwyg { + font-size: 13px; + height: 100%; + line-height: 16px; + min-height: 200px; +} + +#rl-app tbody tr:hover { + background-color: inherit; +} + +#rl-app table { + white-space: inherit; +} + +body > header ul { + margin: 0; +} + +#rl-app h3 { + color: inherit; +} + +.messageListItem * { + cursor: pointer; +} + +/** + * Firefox < 98 + * https://github.com/the-djmaze/snappymail/issues/673 + */ +#rl-app dialog.polyfill { + margin-top: 50px; + max-height: calc(100vh - 60px); +} + +/** + * hampoelz + * https://github.com/the-djmaze/snappymail/issues/96#issuecomment-1279783076 + */ + +.messageList { + margin: 0 5px 0 0; +} + +a.selectable { + margin: 2px; + height: 38px !important; + line-height: 38px !important; + border-radius: var(--border-radius-pill); +} + +a.selectable::after { + display: block; + margin: 0 !important; + top: 50%; + transform: translateY(-50%); +} + +.btn { + border: initial !important; +} + +@media print { + #body-user #header { + display: none; + } + #content { + padding-top: 0; + } +} diff --git a/integrations/owncloud/snappymail/css/style.css b/integrations/owncloud/snappymail/css/style.css new file mode 100644 index 0000000000..88f73b2f73 --- /dev/null +++ b/integrations/owncloud/snappymail/css/style.css @@ -0,0 +1,6 @@ +/* +Empty style sheet! +Only needed to give you the opportunity to theme the owncloud part +of the snappymail app with the theming system integrated in ownCoud +if the snappymail app is activated. +*/ diff --git a/integrations/owncloud/snappymail/img/favicon-touch.png b/integrations/owncloud/snappymail/img/favicon-touch.png new file mode 100644 index 0000000000..78ab30b38c Binary files /dev/null and b/integrations/owncloud/snappymail/img/favicon-touch.png differ diff --git a/integrations/owncloud/snappymail/img/favicon.ico b/integrations/owncloud/snappymail/img/favicon.ico new file mode 100644 index 0000000000..ce6462649a Binary files /dev/null and b/integrations/owncloud/snappymail/img/favicon.ico differ diff --git a/integrations/owncloud/snappymail/img/logo-64x64.png b/integrations/owncloud/snappymail/img/logo-64x64.png new file mode 100644 index 0000000000..78ab30b38c Binary files /dev/null and b/integrations/owncloud/snappymail/img/logo-64x64.png differ diff --git a/integrations/owncloud/snappymail/img/logo-white-64x64.png b/integrations/owncloud/snappymail/img/logo-white-64x64.png new file mode 100644 index 0000000000..1bca3e09a8 Binary files /dev/null and b/integrations/owncloud/snappymail/img/logo-white-64x64.png differ diff --git a/integrations/owncloud/snappymail/js/snappymail.js b/integrations/owncloud/snappymail/js/snappymail.js new file mode 100644 index 0000000000..f3aae6f49c --- /dev/null +++ b/integrations/owncloud/snappymail/js/snappymail.js @@ -0,0 +1,107 @@ +/** + * Nextcloud - SnappyMail mail plugin + * + * @author RainLoop Team, Nextgen-Networks (@nextgen-networks), Tab Fitts (@tabp0le), Pierre-Alain Bandinelli (@pierre-alain-b), SnappyMail + * + * Based initially on https://github.com/RainLoop/rainloop-webmail/tree/master/build/owncloud/rainloop-app + */ + +// Do the following things once the document is fully loaded. +document.onreadystatechange = () => { + if (document.readyState === 'complete') { + watchIFrameTitle(); + let form = document.querySelector('form.snappymail'); + form && SnappyMailFormHelper(form); + } +}; + +// The SnappyMail application is already configured to modify the element +// of its root document with the number of unread messages in the inbox. +// However, its document is the SnappyMail iframe. This function sets up a +// Mutation Observer to watch the <title> element of the iframe for changes in +// the unread message count and propagates that to the parent <title> element, +// allowing the unread message count to be displayed in the NC tab's text when +// the SnappyMail app is selected. +function watchIFrameTitle() { + let iframe = document.getElementById('rliframe'); + if (!iframe) { + return; + } + let target = iframe.contentDocument.getElementsByTagName('title')[0]; + let config = { + characterData: true, + childList: true, + subtree: true + }; + let observer = new MutationObserver(mutations => { + let title = mutations[0].target.innerText; + if (title) { + let matches = title.match(/\(([0-9]+)\)/); + if (matches) { + document.title = '('+ matches[1] + ') ' + t('snappymail', 'Email') + ' - Nextcloud'; + } else { + document.title = t('snappymail', 'Email') + ' - Nextcloud'; + } + } + }); + observer.observe(target, config); +} + +function SnappyMailFormHelper(oForm) +{ + try + { + var + oSubmit = document.getElementById('snappymail-save-button'), + sSubmitValue = oSubmit.textContent, + oDesc = oForm.querySelector('.snappymail-result-desc') + ; + + oForm.addEventListener('submit', oEvent => { + oEvent.preventDefault(); + + oForm.classList.add('snappymail-fetch') + oForm.classList.remove('snappymail-error') + oForm.classList.remove('snappymail-success') + + oDesc.textContent = ''; + oSubmit.textContent = '...'; + + let data = new FormData(oForm); + data.set('appname', 'snappymail'); + + fetch(OC.filePath('snappymail', 'fetch', oForm.getAttribute('action')), { + mode: 'same-origin', + cache: 'no-cache', + redirect: 'error', + referrerPolicy: 'no-referrer', + credentials: 'same-origin', + method: 'POST', + headers: {}, + body: data + }) + .then(response => response.json()) + .then(oData => { + let bResult = 'success' === oData?.status; + oForm.classList.remove('snappymail-fetch'); + oSubmit.textContent = sSubmitValue; + if (oData?.Message) { + oDesc.textContent = t('snappymail', oData.Message); + } + if (bResult) { + oForm.classList.add('snappymail-success'); + } else { + oForm.classList.add('snappymail-error'); + if ('' === oDesc.textContent) { + oDesc.textContent = t('snappymail', 'Error'); + } + } + }); + + return false; + }); + } + catch(e) { + console.error(e); + } +} diff --git a/integrations/owncloud/snappymail/l10n/en_GB.js b/integrations/owncloud/snappymail/l10n/en_GB.js new file mode 100644 index 0000000000..dc856193f3 --- /dev/null +++ b/integrations/owncloud/snappymail/l10n/en_GB.js @@ -0,0 +1,9 @@ +OC.L10N.register( + "snappymail", + { + "Email" : "Email", + "Error" : "Error", + "Save" : "Save", + "Password" : "Password" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/integrations/owncloud/snappymail/l10n/en_GB.json b/integrations/owncloud/snappymail/l10n/en_GB.json new file mode 100644 index 0000000000..98a2adb741 --- /dev/null +++ b/integrations/owncloud/snappymail/l10n/en_GB.json @@ -0,0 +1,7 @@ +{ "translations": { + "Email" : "Email", + "Error" : "Error", + "Save" : "Save", + "Password" : "Password" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +} \ No newline at end of file diff --git a/integrations/owncloud/snappymail/lib/AppInfo/Application.php b/integrations/owncloud/snappymail/lib/AppInfo/Application.php new file mode 100644 index 0000000000..6eb04652c8 --- /dev/null +++ b/integrations/owncloud/snappymail/lib/AppInfo/Application.php @@ -0,0 +1,106 @@ +<?php + +namespace OCA\SnappyMail\AppInfo; + +use OCA\SnappyMail\Util\SnappyMailHelper; +use OCA\SnappyMail\Controller\FetchController; +use OCA\SnappyMail\Controller\PageController; +use OCA\SnappyMail\Search\Provider; + +use OCP\AppFramework\App; +use OCP\IL10N; +use OCP\IUser; +use OCP\User\Events\PostLoginEvent; +use OCP\User\Events\BeforeUserLoggedOutEvent; +use OCP\IContainer; + +class Application extends App +{ + public const APP_ID = 'snappymail'; + + public function __construct(array $urlParams = []) + { + parent::__construct(self::APP_ID, $urlParams); + + $container = $this->getContainer(); + $server = $container->getServer(); + + /** + * Controllers + *//* + $container->registerService( + 'PageController', function(IContainer $c) { + return new PageController( + $c->query('AppName'), + $c->query('Request') + ); + } + ); + + $container->registerService( + 'FetchController', function(IContainer $c) { + return new FetchController( + $c->query('AppName'), + $c->query('Request'), + $c->getServer()->getAppManager(), + $c->query('ServerContainer')->getConfig(), + $c->query(IL10N::class) + ); + } + ); + + /** + * Utils + *//* + $container->registerService( + 'SnappyMailHelper', function(IContainer $c) { + return new SnappyMailHelper(); + } + ); +*/ +// $container->registerSearchProvider(Provider::class); + } +/* + public function boot(IBootContext $context): void + { + if (!\is_dir(\rtrim(\trim(\OC::$server->getSystemConfig()->getValue('datadirectory', '')), '\\/') . '/appdata_snappymail')) { + return; + } + + $dispatcher = $context->getAppContainer()->query('OCP\EventDispatcher\IEventDispatcher'); + $dispatcher->addListener(PostLoginEvent::class, function (PostLoginEvent $Event) { + $config = \OC::$server->getConfig(); + // Only store the user's password in the current session if they have + // enabled auto-login using ownCloud username or email address. + if ($config->getAppValue('snappymail', 'snappymail-autologin', false) + || $config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) { + $sUID = $Event->getUser()->getUID(); + \OC::$server->getSession()['snappymail-nc-uid'] = $sUID; + \OC::$server->getSession()['snappymail-password'] = SnappyMailHelper::encodePassword($Event->getPassword(), $sUID); + } + }); + + $dispatcher->addListener(BeforeUserLoggedOutEvent::class, function (BeforeUserLoggedOutEvent $Event) { + \OC::$server->getSession()['snappymail-password'] = ''; + SnappyMailHelper::loadApp(); + \RainLoop\Api::Actions()->Logout(true); + }); + + // https://github.com/nextcloud/impersonate/issues/179 + // https://github.com/nextcloud/impersonate/pull/180 + $class = 'OCA\Impersonate\Events\BeginImpersonateEvent'; + if (\class_exists($class)) { + $dispatcher->addListener($class, function ($Event) { + \OC::$server->getSession()['snappymail-password'] = ''; + SnappyMailHelper::loadApp(); + \RainLoop\Api::Actions()->Logout(true); + }); + $dispatcher->addListener('OCA\Impersonate\Events\EndImpersonateEvent', function ($Event) { + \OC::$server->getSession()['snappymail-password'] = ''; + SnappyMailHelper::loadApp(); + \RainLoop\Api::Actions()->Logout(true); + }); + } + } +*/ +} diff --git a/integrations/owncloud/snappymail/lib/ContentSecurityPolicy.php b/integrations/owncloud/snappymail/lib/ContentSecurityPolicy.php new file mode 100644 index 0000000000..f144f835c5 --- /dev/null +++ b/integrations/owncloud/snappymail/lib/ContentSecurityPolicy.php @@ -0,0 +1,53 @@ +<?php + +namespace OCA\SnappyMail; + +class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy { + + /** @var bool Whether inline JS snippets are allowed */ + protected $inlineScriptAllowed = false; + /** @var bool Whether eval in JS scripts is allowed */ + protected $evalScriptAllowed = true; + /** @var bool Whether strict-dynamic should be set */ +// protected $strictDynamicAllowed = true; // NC24+ + /** @var bool Whether inline CSS is allowed */ + protected $inlineStyleAllowed = true; + + function __construct() { + $CSP = \RainLoop\Api::getCSP(); + + $this->allowedScriptDomains = \array_unique(\array_merge($this->allowedScriptDomains, $CSP->script)); + $this->allowedScriptDomains = \array_diff($this->allowedScriptDomains, ["'unsafe-inline'", "'unsafe-eval'"]); + + // Nextcloud only sets 'strict-dynamic' when browserSupportsCspV3() ? + \method_exists($this, 'useStrictDynamic') + ? $this->useStrictDynamic(true) // NC24+ + : $this->addAllowedScriptDomain("'strict-dynamic'"); + + $this->allowedImageDomains = \array_unique(\array_merge($this->allowedImageDomains, $CSP->img)); + + $this->allowedStyleDomains = \array_unique(\array_merge($this->allowedStyleDomains, $CSP->style)); + $this->allowedStyleDomains = \array_diff($this->allowedStyleDomains, ["'unsafe-inline'"]); + + $this->allowedFrameDomains = \array_unique(\array_merge($this->allowedFrameDomains, $CSP->frame)); + +// $this->reportTo = \array_unique(\array_merge($this->reportTo, $CSP->report_to)); + } + + public function getSnappyMailNonce() { + static $sNonce; + if (!$sNonce) { +/* + $cspManager = \OC::$server->getContentSecurityPolicyNonceManager(); + $sNonce = $cspManager->getNonce() ?: \SnappyMail\UUID::generate(); + if (\method_exists($cspManager, 'browserSupportsCspV3') && !$cspManager->browserSupportsCspV3()) { + $this->addAllowedScriptDomain("'nonce-{$sNonce}'"); + } +*/ + $sNonce = \SnappyMail\UUID::generate(); + $this->addAllowedScriptDomain("'nonce-{$sNonce}'"); + } + return $sNonce; + } + +} diff --git a/integrations/owncloud/snappymail/lib/Controller/FetchController.php b/integrations/owncloud/snappymail/lib/Controller/FetchController.php new file mode 100644 index 0000000000..df8c8dabe8 --- /dev/null +++ b/integrations/owncloud/snappymail/lib/Controller/FetchController.php @@ -0,0 +1,132 @@ +<?php + +namespace OCA\SnappyMail\Controller; + +use OCA\SnappyMail\Util\SnappyMailHelper; + +use OCP\App\IAppManager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IRequest; + +class FetchController extends Controller { + private $config; + private $appManager; + + public function __construct(string $appName, IRequest $request, IAppManager $appManager, IConfig $config, IL10N $l) { + parent::__construct($appName, $request); + $this->config = $config; + $this->appManager = $appManager; + $this->l = $l; + } + + public function upgrade(): JSONResponse { + $error = 'Upgrade failed'; + try { + SnappyMailHelper::loadApp(); + if (SnappyMail\Upgrade::core()) { + return new JSONResponse([ + 'status' => 'success', + 'Message' => $this->l->t('Upgraded successfully') + ]); + } + } catch (Exception $e) { + $error .= ': ' . $e->getMessage(); + } + return new JSONResponse([ + 'status' => 'error', + 'Message' => $error + ]); + } + + public function setAdmin(): JSONResponse { + try { + $sUrl = ''; + $sPath = ''; + + if (isset($_POST['appname']) && 'snappymail' === $_POST['appname']) { + $this->config->setAppValue('snappymail', 'snappymail-autologin', + isset($_POST['snappymail-autologin']) ? '1' === $_POST['snappymail-autologin'] : false); + $this->config->setAppValue('snappymail', 'snappymail-autologin-with-email', + isset($_POST['snappymail-autologin']) ? '2' === $_POST['snappymail-autologin'] : false); + $this->config->setAppValue('snappymail', 'snappymail-no-embed', isset($_POST['snappymail-no-embed'])); + } else { + return new JSONResponse([ + 'status' => 'error', + 'Message' => $this->l->t('Invalid argument(s)') + ]); + } + + if (!empty($_POST['import-rainloop'])) { + $result = SnappyMailHelper::importRainLoop(); + return new JSONResponse([ + 'status' => 'success', + 'Message' => \implode("\n", $result) + ]); + } + + SnappyMailHelper::loadApp(); + $debug = !empty($_POST['snappymail-debug']); + $oConfig = \RainLoop\Api::Config(); + if ($debug != $oConfig->Get('debug', 'enable', false)) { + $oConfig->Set('debug', 'enable', $debug); + $oConfig->Save(); + } + + return new JSONResponse([ + 'status' => 'success', + 'Message' => $this->l->t('Saved successfully') + ]); + } catch (Exception $e) { + return new JSONResponse([ + 'status' => 'error', + 'Message' => $e->getMessage() + ]); + } + } + + /** + * @NoAdminRequired + */ + public function setPersonal(): JSONResponse { + try { + + if (isset($_POST['appname'], $_POST['snappymail-password'], $_POST['snappymail-email']) && 'snappymail' === $_POST['appname']) { + $sUser = \OC::$server->getUserSession()->getUser()->getUID(); + + $sPostEmail = $_POST['snappymail-email']; + $this->config->setUserValue($sUser, 'snappymail', 'snappymail-email', $sPostEmail); + + $sPass = $_POST['snappymail-password']; + if ('******' !== $sPass) { + require_once $this->appManager->getAppPath('snappymail').'/lib/Util/SnappyMailHelper.php'; + + $this->config->setUserValue($sUser, 'snappymail', 'snappymail-password', + $sPass ? SnappyMailHelper::encodePassword($sPass, \md5($sPostEmail)) : ''); + } + + $sEmail = $this->config->getUserValue($sUser, 'snappymail', 'snappymail-email', ''); + } else { + return new JSONResponse([ + 'status' => 'error', + 'Message' => $this->l->t('Invalid argument(s)'), + 'Email' => $sEmail + ]); + } + + return new JSONResponse([ + 'status' => 'success', + 'Message' => $this->l->t('Saved successfully'), + 'Email' => $sEmail + ]); + } catch (Exception $e) { + return new JSONResponse([ + 'status' => 'error', + 'Message' => $e->getMessage() + ]); + } + } +} + diff --git a/integrations/owncloud/snappymail/lib/Controller/PageController.php b/integrations/owncloud/snappymail/lib/Controller/PageController.php new file mode 100644 index 0000000000..e3617b9c30 --- /dev/null +++ b/integrations/owncloud/snappymail/lib/Controller/PageController.php @@ -0,0 +1,117 @@ +<?php + +namespace OCA\SnappyMail\Controller; + +use OCA\SnappyMail\Util\SnappyMailHelper; +use OCA\SnappyMail\ContentSecurityPolicy; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\TemplateResponse; + +class PageController extends Controller +{ + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function index() + { + $config = \OC::$server->getConfig(); + + $bAdmin = false; + if (!empty($_SERVER['QUERY_STRING'])) { + SnappyMailHelper::loadApp(); + $bAdmin = \RainLoop\Api::Config()->Get('security', 'admin_panel_key', 'admin') == $_SERVER['QUERY_STRING']; + if (!$bAdmin) { + SnappyMailHelper::startApp(true); + } + } + + if (!$bAdmin && $config->getAppValue('snappymail', 'snappymail-no-embed')) { + \OC::$server->getNavigationManager()->setActiveEntry('snappymail'); + \OCP\Util::addScript('snappymail', 'snappymail'); + \OCP\Util::addStyle('snappymail', 'style'); + SnappyMailHelper::startApp(); + $response = new TemplateResponse('snappymail', 'index', [ + 'snappymail-iframe-url' => SnappyMailHelper::normalizeUrl(SnappyMailHelper::getAppUrl()) + . (empty($_GET['target']) ? '' : "#{$_GET['target']}") + ]); + $csp = new ContentSecurityPolicy(); + $csp->addAllowedFrameDomain("'self'"); + $response->setContentSecurityPolicy($csp); + return $response; + } + + \OC::$server->getNavigationManager()->setActiveEntry('snappymail'); + + \OCP\Util::addStyle('snappymail', 'embed'); + + SnappyMailHelper::startApp(); + $oConfig = \RainLoop\Api::Config(); + $oActions = $bAdmin ? new \RainLoop\ActionsAdmin() : \RainLoop\Api::Actions(); + $oHttp = \MailSo\Base\Http::SingletonInstance(); + $oServiceActions = new \RainLoop\ServiceActions($oHttp, $oActions); + $sAppJsMin = $oConfig->Get('debug', 'javascript', false) ? '' : '.min'; + $sAppCssMin = $oConfig->Get('debug', 'css', false) ? '' : '.min'; + $sLanguage = $oActions->GetLanguage(false); + + $csp = new ContentSecurityPolicy(); + $sNonce = $csp->getSnappyMailNonce(); + + $params = [ + 'Admin' => $bAdmin ? 1 : 0, + 'LoadingDescriptionEsc' => \htmlspecialchars($oConfig->Get('webmail', 'loading_description', 'SnappyMail'), ENT_QUOTES|ENT_IGNORE, 'UTF-8'), + 'BaseTemplates' => \RainLoop\Utils::ClearHtmlOutput($oServiceActions->compileTemplates($bAdmin)), + 'BaseAppBootScript' => \file_get_contents(APP_VERSION_ROOT_PATH.'static/js'.($sAppJsMin ? '/min' : '').'/boot'.$sAppJsMin.'.js'), + 'BaseAppBootScriptNonce' => $sNonce, + 'BaseLanguage' => $oActions->compileLanguage($sLanguage, $bAdmin), + 'BaseAppBootCss' => \file_get_contents(APP_VERSION_ROOT_PATH.'static/css/boot'.$sAppCssMin.'.css'), + 'BaseAppThemeCssLink' => $oActions->ThemeLink($bAdmin), + 'BaseAppThemeCss' => \preg_replace( + '/\\s*([:;{},]+)\\s*/s', + '$1', + $oActions->compileCss($oActions->GetTheme($bAdmin), $bAdmin) + ) + ]; + +// \OCP\Util::addScript('snappymail', '../app/snappymail/v/'.APP_VERSION.'/static/js'.($sAppJsMin ? '/min' : '').'/boot'.$sAppJsMin); + + // ownCloud html encodes, so addHeader('style') is not possible +// \OCP\Util::addHeader('style', ['id'=>'app-boot-css'], \file_get_contents(APP_VERSION_ROOT_PATH.'static/css/boot'.$sAppCssMin.'.css')); + \OCP\Util::addHeader('link', ['type'=>'text/css','rel'=>'stylesheet','href'=>\RainLoop\Utils::WebStaticPath('css/'.($bAdmin?'admin':'app').$sAppCssMin.'.css')], ''); +// \OCP\Util::addHeader('style', ['id'=>'app-theme-style','data-href'=>$params['BaseAppThemeCssLink']], $params['BaseAppThemeCss']); + + $response = new TemplateResponse('snappymail', 'index_embed', $params); + + $response->setContentSecurityPolicy($csp); + + return $response; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function appGet() + { + SnappyMailHelper::startApp(true); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function appPost() + { + SnappyMailHelper::startApp(true); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function indexPost() + { + SnappyMailHelper::startApp(true); + } +} diff --git a/integrations/owncloud/snappymail/lib/Search/Provider.php b/integrations/owncloud/snappymail/lib/Search/Provider.php new file mode 100644 index 0000000000..7b9b631576 --- /dev/null +++ b/integrations/owncloud/snappymail/lib/Search/Provider.php @@ -0,0 +1,129 @@ +<?php + +declare(strict_types=1); + +namespace OCA\SnappyMail\Search; + +use OCA\SnappyMail\AppInfo\Application; +use OCA\SnappyMail\Util\SnappyMailHelper; +use OCP\IDateTimeFormatter; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\Search\IProvider; +use OCP\Search\ISearchQuery; +use OCP\Search\SearchResult; +use OCP\Search\SearchResultEntry; + +/** + * https://docs.nextcloud.com/server/latest/developer_manual/digging_deeper/search.html#search-providers + */ +class Provider implements IProvider +{ + /** @var IL10N */ + private $l10n; + + /** @var IURLGenerator */ + private $urlGenerator; + + public function __construct(IL10N $l10n, IURLGenerator $urlGenerator) + { + $this->l10n = $l10n; + $this->urlGenerator = $urlGenerator; + } + + public function getId(): string + { + return Application::APP_ID; + } + + public function getName(): string + { + return 'SnappyMail'; +// return $this->l10n->t('Mails'); + } + + public function getOrder(string $route, array $routeParameters): int + { + if (0 === \strpos($route, Application::APP_ID . '.')) { + // Active app, prefer Mail results + return -1; + } + return 20; + } + + public function search(IUser $user, ISearchQuery $query): SearchResult + { + $result = []; + if (2 > \strlen(\trim($query->getTerm()))) { + return SearchResult::complete($this->getName(), $result); + } + SnappyMailHelper::startApp(); + $oActions = \RainLoop\Api::Actions(); +// $oAccount = $oActions->getMainAccountFromToken(false); // Issue: when account switched, wrong email is shown + $oAccount = $oActions->getAccountFromToken(false); + $iCursor = (int) $query->getCursor(); + $iLimit = $query->getLimit(); + if ($oAccount) { + $oConfig = $oActions->Config(); + + $oParams = new \MailSo\Mail\MessageListParams; + $oParams->sFolderName = 'INBOX'; // or \All ? + $oParams->sSearch = $query->getTerm(); + $oParams->oCacher = ($oConfig->Get('cache', 'enable', true) && $oConfig->Get('cache', 'server_uids', false)) + ? $oActions->Cacher($oAccount) : null; + $oParams->bUseSortIfSupported = !!$oConfig->Get('labs', 'use_imap_sort', true); +// $oParams->bUseThreads = $oConfig->Get('labs', 'use_imap_thread', false); +// $oParams->bHideDeleted = false; +// $oParams->sSort = (string) $aValues['Sort']; +// ISearchQuery::SORT_DATE_DESC == $query->getSortOrder(): int; + $oParams->iOffset = $iCursor; + $oParams->iLimit = $iLimit; +// $oParams->iPrevUidNext = 0, // used to check for new messages +// $oParams->iThreadUid = 0; + + $oMailClient = $oActions->MailClient(); + if (!$oMailClient->IsLoggined()) { + $oAccount->ImapConnectAndLoginHelper($oActions->Plugins(), $oMailClient->ImapClient(), $oConfig); + } + + // instanceof \MailSo\Mail\MessageCollection + $MessageCollection = $oMailClient->MessageList($oParams); + + $baseURL = $this->urlGenerator->linkToRoute('snappymail.page.index'); + $config = \OC::$server->getConfig(); + if ($config->getAppValue('snappymail', 'snappymail-no-embed')) { + $baseURL .= '?target='; + } else { + $baseURL .= '#'; + } + $search = \rawurlencode($oParams->sSearch); + +// $MessageCollection->totalEmails; + foreach ($MessageCollection as $Message) { + // $Message instanceof \MailSo\Mail\Message + $result[] = new SearchResultEntry( + // thumbnailUrl + '', + // title + $Message->Subject(), + // subline + $Message->From()->ToString(), + // resourceUrl /index.php/apps/snappymail/#/mailbox/INBOX/p2/text=an&unseen + $baseURL . '/mailbox/INBOX/m' . $Message->Uid() . '/' . $search, + // icon + 'icon-mail', + // rounded + false + ); + } + } else { + \error_log('SnappyMail not logged in to use unified search'); + } + + if ($iLimit > \count($result)) { + return SearchResult::complete($this->getName(), $result); + } + return SearchResult::paginated($this->getName(), $result, $iCursor + $iLimit); + } +} diff --git a/integrations/owncloud/snappymail/lib/Settings/AdminSettings.php b/integrations/owncloud/snappymail/lib/Settings/AdminSettings.php new file mode 100644 index 0000000000..c55adf0258 --- /dev/null +++ b/integrations/owncloud/snappymail/lib/Settings/AdminSettings.php @@ -0,0 +1,77 @@ +<?php +namespace OCA\SnappyMail\Settings; + +use OCA\SnappyMail\Util\SnappyMailHelper; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IConfig; +use OCP\Settings\ISettings; + +class AdminSettings implements ISettings +{ + private $config; + + public function __construct(IConfig $config) + { + $this->config = $config; + } + + public function getPanel() + { + \OCA\SnappyMail\Util\SnappyMailHelper::loadApp(); + + $keys = [ + 'snappymail-autologin', + 'snappymail-autologin-with-email', + 'snappymail-no-embed' + ]; + $parameters = []; + foreach ($keys as $k) { + $v = $this->config->getAppValue('snappymail', $k); + $parameters[$k] = $v; + } + $uid = \OC::$server->getUserSession()->getUser()->getUID(); + if (\OC_User::isAdminUser($uid)) { +// $parameters['snappymail-admin-panel-link'] = SnappyMailHelper::getAppUrl().'?admin'; + SnappyMailHelper::loadApp(); + $parameters['snappymail-admin-panel-link'] = + \OC::$server->getURLGenerator()->linkToRoute('snappymail.page.index') + . '?' . \RainLoop\Api::Config()->Get('security', 'admin_panel_key', 'admin'); + } + + $oConfig = \RainLoop\Api::Config(); + $passfile = APP_PRIVATE_DATA . 'admin_password.txt'; + $sPassword = ''; + if (\is_file($passfile)) { + $sPassword = \file_get_contents($passfile); + $parameters['snappymail-admin-panel-link'] .= '#/security'; + } + $parameters['snappymail-admin-password'] = $sPassword; + + $parameters['can-import-rainloop'] = $sPassword && \is_dir( + \rtrim(\trim(\OC::$server->getSystemConfig()->getValue('datadirectory', '')), '\\/') + . '/rainloop-storage' + ); + + $parameters['snappymail-debug'] = $oConfig->Get('debug', 'enable', false); + + // Check for owncloud plugin update, if so then update + foreach (\SnappyMail\Repository::getPackagesList()['List'] as $plugin) { + if ('owncloud' == $plugin['id'] && $plugin['canBeUpdated']) { + \SnappyMail\Repository::installPackage('plugin', 'owncloud'); + } + } + + \OCP\Util::addScript('snappymail', 'snappymail'); + return new TemplateResponse('snappymail', 'admin-local', $parameters); + } + + public function getSectionID() + { + return 'additional'; + } + + public function getPriority() + { + return 50; + } +} diff --git a/integrations/owncloud/snappymail/lib/Settings/PersonalSettings.php b/integrations/owncloud/snappymail/lib/Settings/PersonalSettings.php new file mode 100644 index 0000000000..310beb17b0 --- /dev/null +++ b/integrations/owncloud/snappymail/lib/Settings/PersonalSettings.php @@ -0,0 +1,37 @@ +<?php +namespace OCA\SnappyMail\Settings; + +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IConfig; +use OCP\Settings\ISettings; + +class PersonalSettings implements ISettings +{ + private $config; + + public function __construct(IConfig $config) + { + $this->config = $config; + } + + public function getPanel() + { + $uid = \OC::$server->getUserSession()->getUser()->getUID(); + $parameters = [ + 'snappymail-email' => $this->config->getUserValue($uid, 'snappymail', 'snappymail-email'), + 'snappymail-password' => $this->config->getUserValue($uid, 'snappymail', 'snappymail-password') ? '******' : '' + ]; + \OCP\Util::addScript('snappymail', 'snappymail'); + return new TemplateResponse('snappymail', 'personal_settings', $parameters, ''); + } + + public function getSectionID() + { + return 'additional'; + } + + public function getPriority() + { + return 50; + } +} diff --git a/integrations/owncloud/snappymail/lib/Util/SnappyMailHelper.php b/integrations/owncloud/snappymail/lib/Util/SnappyMailHelper.php new file mode 100644 index 0000000000..8835da06ce --- /dev/null +++ b/integrations/owncloud/snappymail/lib/Util/SnappyMailHelper.php @@ -0,0 +1,278 @@ +<?php + +namespace OCA\SnappyMail\Util; + +class SnappyMailHelper +{ + + public static function loadApp() : void + { + if (\class_exists('RainLoop\\Api')) { + return; + } + + // ownCloud the default spl_autoload_register() not working + \spl_autoload_register(function($sClassName){ + $file = SNAPPYMAIL_LIBRARIES_PATH . \strtolower(\strtr($sClassName, '\\', DIRECTORY_SEPARATOR)) . '.php'; + if (\is_file($file)) { + include_once $file; + } + }); + + $_ENV['SNAPPYMAIL_OWNCLOUD'] = true; // Obsolete + $_ENV['SNAPPYMAIL_INCLUDE_AS_API'] = true; + +// define('APP_VERSION', '0.0.0'); +// define('APP_INDEX_ROOT_PATH', __DIR__ . DIRECTORY_SEPARATOR); +// include APP_INDEX_ROOT_PATH.'snappymail/v/'.APP_VERSION.'/include.php'; +// define('APP_DATA_FOLDER_PATH', \rtrim(\trim(\OC::$server->getSystemConfig()->getValue('datadirectory', '')), '\\/').'/appdata_snappymail/'); + + $app_dir = \dirname(\dirname(__DIR__)) . '/app'; + require_once $app_dir . '/index.php'; + + // https://github.com/the-djmaze/snappymail/issues/790#issuecomment-1366527884 + if (!file_exists($app_dir . '/.htaccess') && file_exists($app_dir . '/_htaccess')) { + rename($app_dir . '/_htaccess', $app_dir . '/.htaccess'); + if (!file_exists(APP_VERSION_ROOT_PATH . '/app/.htaccess') && file_exists(APP_VERSION_ROOT_PATH . '/app/_htaccess')) { + rename(APP_VERSION_ROOT_PATH . '/app/_htaccess', APP_VERSION_ROOT_PATH . '/app/.htaccess'); + } + if (!file_exists(APP_VERSION_ROOT_PATH . '/static/.htaccess') && file_exists(APP_VERSION_ROOT_PATH . '/static/_htaccess')) { + rename(APP_VERSION_ROOT_PATH . '/static/_htaccess', APP_VERSION_ROOT_PATH . '/static/.htaccess'); + } + } + + $oConfig = \RainLoop\Api::Config(); + $bSave = false; + + if (!$oConfig->Get('webmail', 'app_path')) { + $oConfig->Set('webmail', 'app_path', \OC::$server->getAppManager()->getAppWebPath('snappymail') . '/app/'); + $bSave = true; + } + + if (!\is_dir(APP_PLUGINS_PATH . 'owncloud')) { + \SnappyMail\Repository::installPackage('plugin', 'owncloud'); + $oConfig->Set('plugins', 'enable', true); + $aList = \SnappyMail\Repository::getEnabledPackagesNames(); + $aList[] = 'owncloud'; + $oConfig->Set('plugins', 'enabled_list', \implode(',', \array_unique($aList))); +// $oConfig->Set('webmail', 'theme', 'ownCloud@custom'); + $bSave = true; + } + + $sPassword = $oConfig->Get('security', 'admin_password'); + if ('12345' == $sPassword || !$sPassword) { + $sPassword = \substr(\base64_encode(\random_bytes(16)), 0, 12); + $oConfig->SetPassword($sPassword); + \RainLoop\Utils::saveFile(APP_PRIVATE_DATA . 'admin_password.txt', $sPassword . "\n"); + $bSave = true; + } + + // Pre-configure domain + $ocConfig = \OC::$server->getConfig(); + if ($ocConfig->getAppValue('snappymail', 'snappymail-autologin', false) + || $ocConfig->getAppValue('snappymail', 'snappymail-autologin-with-email', false) + ) { + $oProvider = \RainLoop\Api::Actions()->DomainProvider(); + $oDomain = $oProvider->Load('owncloud'); + if (!$oDomain) { +// $oDomain = \RainLoop\Model\Domain::fromIniArray('owncloud', []); + $oDomain = new \RainLoop\Model\Domain('owncloud'); + $iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::NONE; + $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'); + $bSave = true; + } + } + } + + $bSave && $oConfig->Save(); + } + + public static function startApp(bool $handle = false) : void + { + static::loadApp(); + + try { + $oActions = \RainLoop\Api::Actions(); + $oConfig = \RainLoop\Api::Config(); + if (isset($_GET[$oConfig->Get('security', 'admin_panel_key', 'admin')])) { + if ($oConfig->Get('security', 'allow_admin_panel', true) + && \OC_User::isAdminUser(\OC::$server->getUserSession()->getUser()->getUID()) + && !$oActions->IsAdminLoggined(false) + ) { + $sRand = \MailSo\Base\Utils::Sha1Rand(); + if ($oActions->Cacher(null, true)->Set(\RainLoop\KeyPathHelper::SessionAdminKey($sRand), \time())) { + $sToken = \RainLoop\Utils::EncodeKeyValuesQ(array('token', $sRand)); +// $oActions->setAdminAuthToken($sToken); + \RainLoop\Utils::SetCookie('smadmin', $sToken); + } + } + } else { + $doLogin = !$oActions->getMainAccountFromToken(false); + $aCredentials = static::getLoginCredentials(); +/* + // NC25+ workaround for Impersonate plugin + // https://github.com/the-djmaze/snappymail/issues/561#issuecomment-1301317723 + // https://github.com/nextcloud/server/issues/34935#issuecomment-1302145157 + require \OC::$SERVERROOT . '/version.php'; +// \OC\SystemConfig +// file_get_contents(\OC::$SERVERROOT . 'config/config.php'); +// $CONFIG['version'] + if (24 < $OC_Version[0]) { + $ocSession = \OC::$server->getSession(); + $ocSession->reopen(); + if (!$doLogin && $ocSession['snappymail-uid'] && $ocSession['snappymail-uid'] != $aCredentials[0]) { + // UID changed, Impersonate plugin probably active + $oActions->Logout(true); + $doLogin = true; + } + $ocSession->set('snappymail-uid', $aCredentials[0]); + } +*/ + if ($doLogin && $aCredentials[1] && $aCredentials[2]) { + $oActions->Logger()->AddSecret($aCredentials[2]); + $oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]); + } + } + } catch (\Throwable $e) { + // Ignore login failure + } + + if ($handle) { + \header_remove('Content-Security-Policy'); + \RainLoop\Service::Handle(); + exit; + } + } + + public static function getLoginCredentials() : array + { + $sEmail = ''; + $sPassword = ''; + $config = \OC::$server->getConfig(); + $sUID = \OC::$server->getUserSession()->getUser()->getUID(); + $ocSession = \OC::$server->getSession(); + // Only use the user's password in the current session if they have + // enabled auto-login using ownCloud username or email address. + if ($ocSession['snappymail-nc-uid'] == $sUID) { + if ($config->getAppValue('snappymail', 'snappymail-autologin', false)) { + $sEmail = $sUID; + $sPassword = $ocSession['snappymail-password']; + } else if ($config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) { + $sEmail = $config->getUserValue($sUID, 'settings', 'email', ''); + $sPassword = $ocSession['snappymail-password']; + } + if ($sPassword) { + $sPassword = static::decodePassword($sPassword, $sUID); + } + } + + // If the user has set credentials for SnappyMail in their personal + // settings, override everything before and use those instead. + $sCustomEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email', ''); + if ($sCustomEmail) { + $sEmail = $sCustomEmail; + $sPassword = $config->getUserValue($sUID, 'snappymail', 'snappymail-password', ''); + if ($sPassword) { + $sPassword = static::decodePassword($sPassword, \md5($sEmail)); + } + } + return [$sUID, $sEmail, $sPassword ?: '']; + } + + public static function getAppUrl() : string + { + return \OC::$server->getURLGenerator()->linkToRoute('snappymail.page.appGet'); + } + + public static function normalizeUrl(string $sUrl) : string + { + $sUrl = \rtrim(\trim($sUrl), '/\\'); + if ('.php' !== \strtolower(\substr($sUrl, -4))) { + $sUrl .= '/'; + } + + return $sUrl; + } + + public static function encodePassword(string $sPassword, string $sSalt) : string + { + static::loadApp(); + return \SnappyMail\Crypt::EncryptUrlSafe($sPassword, $sSalt); + } + + public static function decodePassword(string $sPassword, string $sSalt)/* : mixed */ + { + static::loadApp(); + return \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt); + } + + // Imports data from RainLoop + public static function importRainLoop() : array + { + $result = []; + + $dir = \rtrim(\trim(\OC::$server->getSystemConfig()->getValue('datadirectory', '')), '\\/'); + $dir_snappy = $dir . '/appdata_snappymail/'; + $dir_rainloop = $dir . '/rainloop-storage'; + $rainloop_plugins = []; + if (\is_dir($dir_rainloop)) { + \is_dir($dir_snappy) || \mkdir($dir_snappy, 0755, true); + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir_rainloop, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($iterator as $item) { + $target = $dir_snappy . $iterator->getSubPathname(); + if (\preg_match('@/plugins/([^/])@', $target, $match)) { + $rainloop_plugins[$match[1]] = $match[1]; + } else if (!\strpos($target, '/cache/')) { + if ($item->isDir()) { + \is_dir($target) || \mkdir($target, 0755, true); + } else if (\file_exists($target)) { + $result[] = "skipped: {$target}"; + } else { + \copy($item, $target); + $result[] = "copied : {$target}"; + } + } + } + } + +// $password = APP_PRIVATE_DATA . 'admin_password.txt'; +// \is_file($password) && \unlink($password); + + static::loadApp(); + + // Attempt to install same plugins as RainLoop + if ($rainloop_plugins) { + foreach (\SnappyMail\Repository::getPackagesList()['List'] as $plugin) { + if (\in_array($plugin['id'], $rainloop_plugins)) { + $result[] = "install plugin : {$plugin['id']}"; + \SnappyMail\Repository::installPackage('plugin', $plugin['id']); + unset($rainloop_plugins[$plugin['id']]); + } + } + foreach ($rainloop_plugins as $plugin) { + $result[] = "skipped plugin : {$plugin}"; + } + } + + $oConfig = \RainLoop\Api::Config(); +// $oConfig->Set('webmail', 'theme', 'ownCloud@custom'); + $oConfig->Save(); + + return $result; + } + +} diff --git a/integrations/owncloud/snappymail/templates/admin-local.php b/integrations/owncloud/snappymail/templates/admin-local.php new file mode 100644 index 0000000000..749befd180 --- /dev/null +++ b/integrations/owncloud/snappymail/templates/admin-local.php @@ -0,0 +1,64 @@ +<div class="section"> + <form class="snappymail" action="admin.php" method="post"> + <input type="hidden" name="requesttoken" value="<?php echo $_['requesttoken'] ?>" id="requesttoken"> + <fieldset class="personalblock"> + <h2><?php echo($l->t('SnappyMail Webmail')); ?></h2> + <br /> + <?php if ($_['snappymail-admin-panel-link']) { ?> + <p> + <a href="<?php echo $_['snappymail-admin-panel-link'] ?>" style="text-decoration: underline"> + <?php echo($l->t('Go to SnappyMail Webmail admin panel')); ?> + </a> + <?php if ($_['snappymail-admin-password']) { ?> + <br/> + Username: admin<br/> + Temporary password: <?php echo $_['snappymail-admin-password']; ?> + <?php } ?> + </p> + <br /> + <?php } ?> + <p> + <div style="display: flex;"> + <input type="radio" id="snappymail-noautologin" name="snappymail-autologin" value="0" <?php if (!$_['snappymail-autologin']&&!$_['snappymail-autologin-with-email']) echo 'checked="checked"'; ?> /> + <label style="margin: auto 5px;" for="snappymail-noautologin"> + <?php echo($l->t('Users will login manually, or define credentials in their personal settings for automatic logins.')); ?> + </label> + </div> + <div style="display: flex;"> + <input type="radio" id="snappymail-autologin" name="snappymail-autologin" value="1" <?php if ($_['snappymail-autologin']) echo 'checked="checked"'; ?> /> + <label style="margin: auto 5px;" for="snappymail-autologin"> + <?php echo($l->t('Attempt to automatically login users with their ownCloud username and password, or user-defined credentials, if set.')); ?> + </label> + </div> + <div style="display: flex;"> + <input type="radio" id="snappymail-autologin-with-email" name="snappymail-autologin" value="2" <?php if ($_['snappymail-autologin-with-email']) echo 'checked="checked"'; ?> /> + <label style="margin: auto 5px;" for="snappymail-autologin-with-email"> + <?php echo($l->t('Attempt to automatically login users with their ownCloud email and password, or user-defined credentials, if set.')); ?> + </label> + </div> + </p> + <br /> + <p> + <input id="snappymail-no-embed" name="snappymail-no-embed" type="checkbox" class="checkbox" <?php if ($_['snappymail-no-embed']) echo 'checked="checked"'; ?>> + <label for="snappymail-no-embed">Don't fully integrate in ownCloud, use in iframe</label> + </p> + <br /> + <p> + <input id="snappymail-debug" name="snappymail-debug" type="checkbox" class="checkbox" <?php if ($_['snappymail-debug']) echo 'checked="checked"'; ?>> + <label for="snappymail-debug">Debug</label> + </p> + <br /> + <?php if ($_['can-import-rainloop']) { ?> + <p> + <input id="import-rainloop" name="import-rainloop" type="checkbox" class="checkbox"> + <label for="import-rainloop">Import RainLoop data</label> + </p> + <br /> + <?php } ?> + <p> + <button id="snappymail-save-button" name="snappymail-save-button"><?php echo($l->t('Save')); ?></button> + <div class="snappymail-result-desc" style="white-space: pre"></div> + </p> + </fieldset> + </form> +</div> diff --git a/integrations/owncloud/snappymail/templates/index.php b/integrations/owncloud/snappymail/templates/index.php new file mode 100644 index 0000000000..f304654fbb --- /dev/null +++ b/integrations/owncloud/snappymail/templates/index.php @@ -0,0 +1,3 @@ +<iframe id="rliframe" style="border: none; width: 100%; min-height: 100%; position: relative;" tabindex="-1" frameborder="0" src="<?php echo $_['snappymail-iframe-url']; ?>"></iframe> +<?php +// OCP\Util::addScript('snappymail', 'resize'); diff --git a/integrations/owncloud/snappymail/templates/index_embed.php b/integrations/owncloud/snappymail/templates/index_embed.php new file mode 100644 index 0000000000..c51f8e355c --- /dev/null +++ b/integrations/owncloud/snappymail/templates/index_embed.php @@ -0,0 +1,19 @@ +<style id="app-boot-css"><?php echo $_['BaseAppBootCss']; ?></style> +<style id="app-theme-style" data-href="<?php echo $_['BaseAppThemeCssLink']; ?>"><?php echo $_['BaseAppThemeCss']; ?></style> +<div id="rl-app" data-admin="<?php echo $_['Admin']; ?>" spellcheck="false"> + <div id="rl-loading"> + <div id="rl-loading-desc"><?php echo $_['LoadingDescriptionEsc']; ?></div> + <i class="icon-spinner"></i> + </div> + <div id="rl-loading-error" hidden="">An error occurred.<br>Please refresh the page and try again.</div> + <div id="rl-content" hidden=""> + <div id="rl-left"></div> + <div id="rl-right"></div> + </div> + <div id="rl-popups"></div> + <?php echo $_['BaseTemplates']; ?> +</div> +<?php +echo ' + <script nonce="'.$_['BaseAppBootScriptNonce'].'" type="text/javascript">'.$_['BaseAppBootScript'].$_['BaseLanguage'].'</script> +'; diff --git a/integrations/owncloud/snappymail/templates/personal_settings.php b/integrations/owncloud/snappymail/templates/personal_settings.php new file mode 100644 index 0000000000..93a86c7444 --- /dev/null +++ b/integrations/owncloud/snappymail/templates/personal_settings.php @@ -0,0 +1,21 @@ +<div class="section"> + <form class="snappymail" action="personal.php" method="post"> + <input type="hidden" name="requesttoken" value="<?php echo $_['requesttoken'] ?>" id="requesttoken"> + <fieldset class="personalblock"> + <h2><?php echo $l->t('SnappyMail Webmail'); ?></h2> + <p> + <?php echo $l->t('Enter an email and password to auto-login to SnappyMail.'); ?> + </p> + <p> + <input type="text" id="snappymail-email" name="snappymail-email" + value="<?php echo $_['snappymail-email']; ?>" placeholder="<?php echo($l->t('Email')); ?>" /> + + <input type="password" id="snappymail-password" name="snappymail-password" + value="<?php echo $_['snappymail-password']; ?>" placeholder="<?php echo($l->t('Password')); ?>" /> + + <button id="snappymail-save-button" name="snappymail-save-button"><?php echo($l->t('Save')); ?></button> +   <span class="snappymail-result-desc"></span> + </p> + </fieldset> + </form> +</div> diff --git a/integrations/virtualmin/snappymail.pl b/integrations/virtualmin/snappymail.pl new file mode 100644 index 0000000000..c3fcc7ab53 --- /dev/null +++ b/integrations/virtualmin/snappymail.pl @@ -0,0 +1,405 @@ +# This script is probably broken and not tested! +# https://forum.virtualmin.com/t/add-snappymail-to-install-scripts/112560 + +# script_snappymail_desc() +sub script_snappymail_desc +{ +return "SnappyMail"; +} + +sub script_snappymail_uses +{ +return ( "php" ); +} + +sub script_snappymail_longdesc +{ +return "SnappyMail Webmail is a browser-based multilingual IMAP client with an application-like user interface"; +} + +# script_snappymail_versions() +sub script_snappymail_versions +{ +return ( "2.38.2" ); +} + +sub script_snappymail_version_desc +{ +local ($ver) = @_; +return &compare_versions($ver, "2.7") >= 0 ? "$ver" : "$ver (Un-supported)"; +} + +sub script_snappymail_category +{ +return "Email"; +} + +sub script_snappymail_php_modules +{ +local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2); +local @modules = ( "zlib", "mbstring" ); +push(@modules, $dbtype eq "mysql" ? "pdo_mysql" : $dbtype eq "sqlite" ? "pdo_sqlite" : "pdo_pgsql"); +return @modules; +} + +sub script_snappymail_php_optional_modules +{ +return ( "gd", "openssl", "sockets", "xxtea", "curl", "intl", "ldap", "zip", "gmagick", "imagick" ); +} + +sub script_snappymail_dbs +{ +return ("mysql", "postgres", "sqlite"); +} + +# script_snappymail_php_vars(&domain) +# Returns an array of extra PHP variables needed for this script +sub script_snappymail_php_vars +{ +return ([ 'memory_limit', '64M', '+' ], + [ 'max_execution_time', 300, '+' ], + [ 'file_uploads', 'On' ], + [ 'upload_max_filesize', '25M', '+' ], + [ 'post_max_size', '25M', '+' ], + [ 'session.auto_start', 'Off' ], + [ 'mbstring.func_overload', 'Off' ]); +} + + +sub script_snappymail_php_vers +{ +return ( 7 ); +} + +sub script_snappymail_release +{ +return 3; # For folders path fix +} + +sub script_snappymail_php_fullver +{ +return "7.4"; +} + +# script_snappymail_params(&domain, version, &upgrade-info) +# Returns HTML for table rows for options for installing PHP-NUKE +sub script_snappymail_params +{ +local ($d, $ver, $upgrade) = @_; +local $rv; +local $hdir = &public_html_dir($d, 1); +if ($upgrade) { + # Options are fixed when upgrading + local ($dbtype, $dbname) = split(/_/, $upgrade->{'opts'}->{'db'}, 2); + $rv .= &ui_table_row("Database for SnappyMail preferences", $dbname); + local $dir = $upgrade->{'opts'}->{'dir'}; + $dir =~ s/^$d->{'home'}\///; + $rv .= &ui_table_row("Install directory", $dir); + } +else { + # Show editable install options + local @dbs = &domain_databases($d, [ "mysql", "postgres", "sqlite" ]); + $rv .= &ui_table_row("Database for SnappyMail preferences", + &ui_database_select("db", undef, \@dbs, $d, "snappymail")); + $rv .= &ui_table_row("Install sub-directory under <tt>$hdir</tt>", + &ui_opt_textbox("dir", &substitute_scriptname_template("snappymail", $d), 30, "At top level")); + } +return $rv; +} + +# script_snappymail_parse(&domain, version, &in, &upgrade-info) +# Returns either a hash ref of parsed options, or an error string +sub script_snappymail_parse +{ +local ($d, $ver, $in, $upgrade) = @_; +if ($upgrade) { + # Options are always the same + return $upgrade->{'opts'}; + } +else { + local $hdir = &public_html_dir($d, 0); + $in{'dir_def'} || $in{'dir'} =~ /\S/ && $in{'dir'} !~ /\.\./ || + return "Missing or invalid installation directory"; + local $dir = $in{'dir_def'} ? $hdir : "$hdir/$in{'dir'}"; + local ($newdb) = ($in->{'db'} =~ s/^\*//); + return { 'db' => $in->{'db'}, + 'newdb' => $newdb, + 'dir' => $dir, + 'path' => $in{'dir_def'} ? "/" : "/$in{'dir'}", }; + } +} + +# script_snappymail_check(&domain, version, &opts, &upgrade-info) +# Returns an error message if a required option is missing or invalid +sub script_snappymail_check +{ +local ($d, $ver, $opts, $upgrade) = @_; +$opts->{'dir'} =~ /^\// || return "Missing or invalid install directory"; +$opts->{'db'} || return "Missing database"; +if (-r "$opts->{'dir'}/config/db.inc.php") { + return "SnappyMail appears to be already installed in the selected directory"; + } +local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2); +local $clash = &find_database_table($dbtype, $dbname, "system|filestore|contacts|users"); +$clash && return "SnappyMail appears to be already using the selected database (table $clash)"; +return undef; +} + +# script_snappymail_files(&domain, version, &opts, &upgrade-info) +# Returns a list of files needed by SnappyMail, each of which is a hash ref +# containing a name, filename and URL +sub script_snappymail_files +{ +local ($d, $ver, $opts, $upgrade) = @_; +local @files = ( { 'name' => "source", + 'file' => "snappymail-$ver.tar.gz", + 'url' => "https://github.com/the-djmaze/snappymail/releases/download/v${ver}/snappymail-${ver}.tar.gz" }, + ); +return @files; +} + +sub script_snappymail_commands +{ +return ("tar", "gunzip"); +} + +# script_snappymail_install(&domain, version, &opts, &files, &upgrade-info) +# Actually installs SnappyMail, and returns either 1 and an informational +# message, or 0 and an error +sub script_snappymail_install +{ +local ($d, $version, $opts, $files, $upgrade) = @_; +local ($out, $ex); + +# Create and get DB +if ($opts->{'newdb'} && !$upgrade) { + local $err = &create_script_database($d, $opts->{'db'}); + return (0, "Database creation failed : $err") if ($err); + } +local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2); +local $dbuser = $dbtype eq "mysql" ? &mysql_user($d) : &postgres_user($d); +local $dbpass = $dbtype eq "mysql" ? &mysql_pass($d) : &postgres_pass($d, 1); +local $dbphptype = $dbtype eq "mysql" ? "mysql" : "psql"; +local $dbhost = &get_database_host($dbtype, $d); +local $dberr = &check_script_db_connection($dbtype, $dbname, $dbuser, $dbpass); +return (0, "Database connection failed : $dberr") if ($dberr); + +# Extract tar file to temp dir and copy to target +local $temp = &transname(); +local $verdir = $ver; +$verdir =~ s/-complete$//; +local $err = &extract_script_archive($files->{'source'}, $temp, $d, + $opts->{'dir'}, "snappymail-$verdir"); +$err && return (0, "Failed to extract source : $err"); + +if (!$upgrade) { + # Fix up the DB config file + local $dbcfileorig = "$opts->{'dir'}/config/db.inc.php.dist"; + local $dbcfile = "$opts->{'dir'}/config/db.inc.php"; + if (-r $dbcfileorig) { + ©_source_dest_as_domain_user($d, $dbcfileorig, $dbcfile); + local $lref = &read_file_lines_as_domain_user($d, $dbcfile); + foreach my $l (@$lref) { + if ($l =~ /^\$rcmail_config\['db_dsnw'\]\s+=/) { + $l = "\$rcmail_config['db_dsnw'] = 'mysql://$dbuser:". + &php_quotemeta($dbpass, 1). + "\@$dbhost/$dbname';"; + } + elsif ($l =~ /^\$rcmail_config\['db_backend'\]\s+=/) { + $l = "\$rcmail_config['db_backend'] = 'db';"; + } + } + &flush_file_lines_as_domain_user($d, $dbcfile); + } + + # Figure out folder names + local %fmap; + $fmap{'drafts'} = $config{'drafts_folder'} || 'drafts'; + $fmap{'sent'} = $config{'sent_folder'} || 'sent'; + $fmap{'trash'} = $config{'trash_folder'} || 'sent'; + local ($sdmode, $sdpath) = &get_domain_spam_delivery($d); + if (($sdmode == 6 || $sdmode == 4) && $sdpath) { + $fmap{'junk'} = $sdpath; + } + elsif ($sdmode == 1 && $sdpath =~ /^Maildir\/\.?(\S+)\/$/) { + $fmap{'junk'} = $1; + } + + # Fix up the main config file + local $mcfileorig = "$opts->{'dir'}/config/main.inc.php.dist"; + local $mcfile = "$opts->{'dir'}/config/main.inc.php"; + if (!-r $mcfileorig) { + $mcfileorig = "$opts->{'dir'}/config/config.inc.php.sample"; + $mcfile = "$opts->{'dir'}/config/config.inc.php"; + } + ©_source_dest_as_domain_user($d, $mcfileorig, $mcfile); + local $lref = &read_file_lines_as_domain_user($d, $mcfile); + local $vuf = &get_mail_virtusertable(); + local $added_vuf = 0; + foreach my $l (@$lref) { + if ($l =~ /^\$(rcmail_config|config)\['enable_caching'\]\s+=/) { + $l = "\$${1}['enable_caching'] = FALSE;"; + } + if ($l =~ /^\$(rcmail_config|config)\['default_host'\]\s+=/) { + $l = "\$${1}['default_host'] = 'localhost';"; + } + if ($l =~ /^\$(rcmail_config|config)\['default_port'\]\s+=/) { + $l = "\$${1}['default_port'] = 143;"; + } + if ($l =~ /^\$(rcmail_config|config)\['smtp_server'\]\s+=/) { + $l = "\$${1}['smtp_server'] = 'localhost';"; + } + if ($l =~ /^\$(rcmail_config|config)\['smtp_port'\]\s+=/) { + $l = "\$${1}['smtp_port'] = 25;"; + } + if ($l =~ /^\$(rcmail_config|config)\['smtp_user'\]\s+=/) { + $l = "\$${1}['smtp_user'] = '%u';"; + } + if ($l =~ /^\$(rcmail_config|config)\['smtp_pass'\]\s+=/) { + $l = "\$${1}['smtp_pass'] = '%p';"; + } + if ($l =~ /^\$(rcmail_config|config)\['mail_domain'\]\s+=/) { + $l = "\$${1}['mail_domain'] = '$d->{'dom'}';"; + } + if ($l =~ /^\$(rcmail_config|config)\['virtuser_file'\]\s+=/ && $vuf) { + $added_vuf = 1; + $l = "\$${1}['virtuser_file'] = '$vuf';"; + } + if ($l =~ /^\$(rcmail_config|config)\['plugins'\]\s+=\s+array\(\s*$/) { + $l = "\$${1}['plugins'] = array('virtuser_file',"; + } + elsif ($l =~ /^\$(rcmail_config|config)\['plugins'\]\s+=/) { + $l = "\$${1}['plugins'] = array('virtuser_file');"; + } + if ($l =~ /^\$(rcmail_config|config)\['db_dsnw'\]\s+=/) { + $l = "\$${1}['db_dsnw'] = 'mysql://$dbuser:". + &php_quotemeta($dbpass, 1)."\@$dbhost/$dbname';"; + } + if ($l =~ /^\$(rcmail_config|config)\['(\S+)_mbox'\]\s+=/ && + $fmap{$2} && $fmap{$2} ne "*") { + $l = "\$${1}['${2}_mbox'] = '$fmap{$2}';"; + } + } + if (!$added_vuf && $vuf) { + # Need to add virtuser_file directive, as no default exists + push(@$lref, "\$rcmail_config['virtuser_file'] = '$vuf';"); + push(@$lref, "\$config['virtuser_file'] = '$vuf';"); + } + &flush_file_lines_as_domain_user($d, $mcfile); + + # Run SQL setup script + &require_mysql(); + local $sqlfile; + if ($dbtype eq "mysql") { + $sqlfile = "$opts->{'dir'}/SQL/mysql.initial.sql"; + } + else { + $sqlfile = "$opts->{'dir'}/SQL/postgres.initial.sql"; + } + local ($ex, $out) = &mysql::execute_sql_file($dbname, $sqlfile, + $dbuser, $dbpass); + $ex && return (-1, "Failed to run database setup script : ". + "<tt>$out</tt>."); + } +else { + # Create script of upgrade SQL to run, by extracting SQL from the old + # version onwards from mysql.update.sql + &require_mysql(); + local $sqltemp = &transname(); + &open_tempfile(SQLTEMP, ">$sqltemp", 0, 1); + open(SQLIN, "<$opts->{'dir'}/SQL/mysql.update.sql"); + local $foundver = 0; + while(<SQLIN>) { + if (/Updates\s+from\s+version\s+(\S+)/ && + &compare_versions("$1", $upgrade->{'version'}) >= 0) { + $foundver = 1; + } + if ($foundver) { + &print_tempfile(SQLTEMP, $_); + } + } + close(SQLIN); + &close_tempfile(SQLTEMP); + if ($foundver) { + local ($ex, $out) = &mysql::execute_sql_file($dbname, $sqltemp, + $dbuser, $dbpass); + $ex && return (-1, "Failed to run database date script : ". + "<tt>$out</tt>."); + } + &unlink_file($sqltemp); + } + +# Return a URL for the user +local $url = &script_path_url($d, $opts); +local $rp = $opts->{'dir'}; +$rp =~ s/^$d->{'home'}\///; +return (1, "SnappyMail installation complete. It can be accessed at <a target=_blank href='$url'>$url</a>.", "Under $rp using $dbphptype database $dbname", $url); +} + +# script_wordpress_db_conn_desc() +# Returns a list of options for config file to update +sub script_snappymail_db_conn_desc +{ +my $conn_desc = + { + 'replace' => [ '\$(rcmail_config|config)\[[\'"]db_dsnw[\'"]\]\s*=\s*' => + '\'$$sdbtype://$$sdbuser:$$sdbpass@$$sdbhost/$$sdbname\';' ], + 'func' => 'php_quotemeta', + 'func_params' => 1, + 'multi' => 1, + }; +my $db_conn_desc = + { 'config/config.inc.php' => + { + 'dbtype' => $conn_desc, + 'dbuser' => $conn_desc, + 'dbpass' => $conn_desc, + 'dbhost' => $conn_desc, + 'dbname' => $conn_desc, + } + }; +return $db_conn_desc; +} + +# script_snappymail_uninstall(&domain, version, &opts) +# Un-installs a SnappyMail installation, by deleting the directory and database. +# Returns 1 on success and a message, or 0 on failure and an error +sub script_snappymail_uninstall +{ +local ($d, $version, $opts) = @_; + +# Remove snappymail tables from the database +&cleanup_script_database($d, $opts->{'db'}, "(.*)"); + +# Take out the DB +if ($opts->{'newdb'}) { + &delete_script_database($d, $opts->{'db'}); + } + +# Remove the contents of the target directory +local $derr = &delete_script_install_directory($d, $opts); +return (0, $derr) if ($derr); + +return (1, "SnappyMail directory and tables deleted."); +} + +# script_snappymail_latest(version) +# Returns a URL and regular expression or callback func to get the version +sub script_snappymail_latest +{ +local ($ver) = @_; +return ( "https://snappymail.eu/download/", + "snappymail-([0-9\\.]+).tar.gz" ); +} + +sub script_snappymail_site +{ +return 'https://snappymail.eu/'; +} + +sub script_snappymail_gpl +{ +return 1; +} + +1; diff --git a/jsconfig.json b/jsconfig.json index 869ced3972..d9824fd006 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -16,7 +16,7 @@ "build", "data", "vendors", - "rainloop", + "snappymail", "plugins", "node_modules", "bower_components", diff --git a/makedeb.sh b/makedeb.sh deleted file mode 100755 index 1b293ed1ed..0000000000 --- a/makedeb.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -#Build RainLoop Webmail Debian Package -PACKAGEVERSION="0"; - -#Build rainloop -gulp build; -cd build/dist/releases/webmail; -cd $(ls -t); #Most recent build folder - -#Working directory -mkdir rainloop-deb-build; -cd rainloop-deb-build; - -#Prepare zip file -unzip ../rainloop-legacy-latest.zip; -find . -type d -exec chmod 755 {} \; -find . -type f -exec chmod 644 {} \; - -#Set up package directory -VERSION=$(cat data/VERSION); -DIR="rainloop_$VERSION-$PACKAGEVERSION"; -mkdir -m 0755 -p $DIR; -mkdir -m 0755 -p $DIR/usr; -mkdir -m 0755 -p $DIR/usr/share; -mkdir -m 0755 -p $DIR/usr/share/rainloop; -mkdir -m 0755 -p $DIR/var; -mkdir -m 0755 -p $DIR/var/lib; - -#Move files into package directory -mv rainloop $DIR/usr/share/rainloop/rainloop; -mv index.php $DIR/usr/share/rainloop/index.php; -mv data $DIR/var/lib/rainloop; - -#Update settings for Debian package -sed -i "s/\$sCustomDataPath = '';/\$sCustomDataPath = '\/var\/lib\/rainloop';/" $DIR/usr/share/rainloop/rainloop/v/$VERSION/include.php - -#Set up Debian packaging tools -cd $DIR; -mkdir -m 0755 DEBIAN; - -#Create Debian packging control file -cat >> DEBIAN/control <<-EOF - Package: rainloop - Version: $VERSION - Section: web - Priority: optional - Architecture: all - Depends: php5-fpm (>= 5.4), php5-curl, php5-json - Maintainer: Rainloop <support@rainloop.net> - Installed-Size: 20330 - Description: Rainloop Webmail - A modern PHP webmail client. -EOF - -#Create Debian packaging post-install script -cat >> DEBIAN/postinst <<-EOF - #!/bin/sh - chown -R www-data:www-data /var/lib/rainloop -EOF -chmod +x DEBIAN/postinst; - -#Build Debian package -cd ..; -fakeroot dpkg-deb -b $DIR; - -#Clean up -mv $DIR.deb ..; -cd ..; -rm -rf rainloop-deb-build; diff --git a/package.json b/package.json index 405a46395c..3e3ff929dc 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,31 @@ { - "name": "rainloop", - "title": "RainLoop Webmail", + "name": "snappymail", + "title": "SnappyMail", "description": "Simple, modern & fast web-based email client", "private": true, - "version": "1.17.0", - "ownCloudVersion": "5.5.0", - "homepage": "https://www.rainloop.net", + "version": "2.38.2", + "homepage": "https://snappymail.eu", "author": { - "name": "RainLoop Team", - "email": "support@rainloop.net", - "web": "https://www.rainloop.net" + "name": "DJ Maze", + "email": "support@snappymail.eu", + "web": "https://snappymail.eu" }, "repository": { "type": "git", - "url": "git://github.com/RainLoop/rainloop-webmail.git" + "url": "git://github.com/the-djmaze/snappymail.git" }, "scripts": { - "watch-css": "gulp watchCss", - "watch-js": "webpack --color --watch", - "build": "gulp all" + "watch-css": "gulp watchCss" }, "license": "SEE LICENSE IN LICENSE", "licenses": [ { - "type": "MIT", - "ulr": "https://choosealicense.com/licenses/mit/" + "type": "AGPL 3.0", + "ulr": "http://www.gnu.org/licenses/agpl-3.0.html" } ], "bugs": { - "url": "https://github.com/RainLoop/rainloop-webmail/issues" + "url": "https://github.com/the-djmaze/snappymail/issues" }, "keywords": [ "webmail", @@ -43,78 +40,31 @@ "openpgp" ], "readmeFilename": "README.md", - "devDependencies": { - "@babel/core": "7.14.0", - "@babel/plugin-proposal-class-properties": "7.13.0", - "@babel/plugin-proposal-decorators": "7.13.15", - "@babel/plugin-transform-runtime": "7.13.15", - "@babel/preset-env": "7.14.1", - "@babel/runtime-corejs3": "7.14.0", - "autolinker": "3.14.3", - "babel-eslint": "10.1.0", - "babel-loader": "8.2.2", - "classnames": "2.3.1", - "copy-webpack-plugin": "5.1.1", - "core-js": "3.12.0", - "css-loader": "3.4.2", - "element-dataset": "2.2.6", - "emailjs-addressparser": "2.0.2", - "eslint": "6.8.0", - "eslint-config-prettier": "6.10.0", - "eslint-plugin-prettier": "3.1.2", - "gulp": "4.0.2", - "gulp-autoprefixer": "7.0.1", - "gulp-cached": "1.1.1", - "gulp-chmod": "3.0.0", - "gulp-clean-css": "4.3.0", - "gulp-concat-util": "0.5.5", - "gulp-eol": "0.2.0", - "gulp-eslint": "6.0.0", - "gulp-expect-file": "1.0.2", - "gulp-filter": "7.0.0", - "gulp-header": "2.0.9", - "gulp-if": "3.0.0", - "gulp-less": "4.0.1", - "gulp-livereload": "4.0.2", - "gulp-plumber": "1.2.1", - "gulp-rename": "2.0.0", - "gulp-replace": "1.1.3", - "gulp-rimraf": "1.0.0", - "gulp-size": "4.0.0", - "gulp-sourcemaps": "3.0.0", - "gulp-stripbom": "1.0.5", - "gulp-terser": "2.0.1", - "gulp-through": "0.4.0", - "gulp-util": "3.0.8", - "gulp-zip": "5.1.0", - "ifvisible.js": "1.0.6", - "intersection-observer": "0.12.0", - "jquery": "3.6.0", - "jquery-backstretch": "2.1.18", - "jquery-migrate": "3.3.2", - "jquery-mousewheel": "3.1.13", - "jquery-scrollstop": "1.2.0", - "js-cookie": "2.2.1", - "json-loader": "0.5.7", - "json3": "3.3.3", - "knockout": "3.4.2", - "knockout-sortable": "1.2.0", - "lozad": "1.16.0", - "matchmedia-polyfill": "0.3.2", - "moment": "2.29.1", - "node-fs": "0.1.7", - "normalize.css": "8.0.1", - "openpgp": "2.6.2", - "opentip": "2.4.3", - "pikaday": "1.8.2", - "prettier": "1.19.1", - "raf": "3.4.1", - "raw-loader": "4.0.2", - "rimraf": "3.0.2", - "simplestatemanager": "4.1.1", - "style-loader": "1.1.3", - "webpack": "4.42.0", - "webpack-cli": "3.3.11", - "underscore": "1.13.1" + "dependencies": { + "@rollup/plugin-node-resolve": "^13.1.3", + "@rollup/plugin-replace": "^3.0.1", + "babel-eslint": "^10.1.0", + "del": "^6.0.0", + "eslint": "^7.32.0", + "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", + "gulp-eol": "^0.2.0", + "gulp-eslint": "^6.0.0", + "gulp-expect-file": "^2.0.0", + "gulp-filter": "^6.0.0", + "gulp-group-css-media-queries": "^1.2.2", + "gulp-less": "^5.0.0", + "gulp-rename": "^2.0.0", + "gulp-replace": "^1.1.3", + "gulp-rollup-2": "^1.2.1", + "gulp-size": "^3.0.0", + "gulp-terser": "^2.1.0", + "rollup": "^2.56.3", + "rollup-plugin-external-globals": "^0.6.1", + "rollup-plugin-includepaths": "^0.2.4", + "rollup-plugin-terser": "^7.0.2" } } diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000000..7e318c186d --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,583 @@ +Also see https://github.com/the-djmaze/snappymail/tree/master/plugins/example + +PHP +```php +class Plugin extends \RainLoop\Plugins\AbstractPlugin +{ + public function __construct(); + + /** Returns static::NAME */ + public function Name(): string; + + /** Returns /README file contents or static::DESCRIPTION */ + public function Description(): string; + + /** When $bLangs is boolean it sets the value, else returns current value */ + public function UseLangs(?bool $bLangs = null): bool; + + /** When true the result is empty string, else the error message */ + public function Supported(): string; + + /** Initialize settings */ + public function Init(): void; + + public function FilterAppDataPluginSection(bool $bAdmin, bool $bAuth, array &$aConfig): void; + + /** Returns array of all plugin Property options for use in Admin -> Extensions -> Plugin cog wheel */ + protected function configMapping(): array; + + /** With this function you hook to an event + * $sHookName see chapter "Hooks" below for available names + * $sFunctionName the name of a function in this class + */ + final protected function addHook(string $sHookName, string $sFunctionName): self; + + final protected function addCss(string $sFile, bool $bAdminScope = false): self; + + final protected function addJs(string $sFile, bool $bAdminScope = false): self; + + final protected function addTemplate(string $sFile, bool $bAdminScope = false): self; + + final protected function addJsonHook(string $sActionName, string $sFunctionName): self; + + /** + * You may register your own service actions. + * Url is like /?{actionname}/etc. + * Predefined actions of \RainLoop\ServiceActions that can't be registered are: + * - admin + * - AdminAppData + * - AppData + * - Append + * - Backup + * - BadBrowser + * - CspReport + * - Css + * - Json + * - Lang + * - Mailto + * - NoCookie + * - NoScript + * - Ping + * - Plugins + * - ProxyExternal + * - Raw + * - Sso + * - Upload + * - UploadBackground + * - UploadContacts + */ + final protected function addPartHook(string $sActionName, string $sFunctionName): self + + final public function Config(): \RainLoop\Config\Plugin; + final public function Manager(): \RainLoop\Plugins\Manager; + final public function Path(): string; + final public function ConfigMap(bool $flatten = false): array; + + /** + * Returns result of Actions->DefaultResponse($sFunctionName, $mData) or json_encode($mData) + */ + final protected function jsonResponse(string $sFunctionName, $mData): mixed; + + final public function jsonParam(string $sKey, $mDefault = null): mixed; + + final public function getUserSettings(): array; + + final public function saveUserSettings(array $aSettings): bool; +} +``` + +JavaScript +```javascript +class PluginPopupView extends rl.pluginPopupView +{ + // Happens when DOM is created + onBuild(dom) {} + + // Happens before showModal() + beforeShow(...params) {} + // Happens after showModal() + onShow(...params) {} + // Happens after showModal() animation transitionend + afterShow() {} + + // Happens when user hits Escape or Close key + // return false to prevent closing, use close() manually + onClose() {} + // Happens before animation transitionend + onHide() {} + // Happens after animation transitionend + afterHide() {} +} + +PluginPopupView.showModal(); +``` + +# Hooks + +```php +$Plugin->addHook('hook.name', 'functionName'); +``` + +## Login + +### login.credentials.step-1 + params: + string &$sEmail + + Happens in resolveLoginCredentials($sEmail) BEFORE resolving domain name. + This is the pure text from the login screen (DoLogin) or the SSO feature (ServiceSso) received by LoginProcess(). + - DoLogin() -> LoginProcess() -> resolveLoginCredentials($sEmail) + - ServiceSso() -> LoginProcess() -> resolveLoginCredentials($sEmail) + So $sEmail can just have the value `test` without a domain. + +### login.credentials.step-2 + params: + string &$sEmail + string &$sPassword + + Happens in resolveLoginCredentials($sEmail) AFTER resolving domain name. + So $sEmail always has a domain (for example `test` is now `test@example.com`). + +### login.credentials + params: + string &$sEmail + string &$sImapUser + string &$sPassword + string &$sSmtpUser + + $sEmail is the domain imap->fixUsername() without shortening. + $sImapUser is the domain imap->fixUsername() for login into IMAP. + $sSmtpUser is the domain smtp->fixUsername() for login into SMTP. + +### login.success + params: + \RainLoop\Model\MainAccount $oAccount + +## IMAP + +### imap.before-connect + params: + \RainLoop\Model\Account $oAccount + \MailSo\Imap\ImapClient $oImapClient + \MailSo\Imap\Settings $oSettings + +### imap.after-connect + params: + \RainLoop\Model\Account $oAccount + \MailSo\Imap\ImapClient $oImapClient + \MailSo\Imap\Settings $oSettings + +### imap.before-login + params: + \RainLoop\Model\Account $oAccount + \MailSo\Imap\ImapClient $oImapClient + \MailSo\Imap\Settings $oSettings + +### imap.after-login + params: + \RainLoop\Model\Account $oAccount + \MailSo\Imap\ImapClient $oImapClient + 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 + params: + \RainLoop\Model\Account $oAccount + \MailSo\Sieve\SieveClient $oSieveClient + \MailSo\Sieve\Settings $oSettings + +### sieve.after-connect + params: + \RainLoop\Model\Account $oAccount + \MailSo\Sieve\SieveClient $oSieveClient + \MailSo\Sieve\Settings $oSettings + +### sieve.before-login + params: + \RainLoop\Model\Account $oAccount + \MailSo\Sieve\SieveClient $oSieveClient + \MailSo\Sieve\Settings $oSettings + +### sieve.after-login + params: + \RainLoop\Model\Account $oAccount + \MailSo\Sieve\SieveClient $oSieveClient + bool $bSuccess + \MailSo\Sieve\Settings $oSettings + +## SMTP + +### smtp.before-connect + params: + \RainLoop\Model\Account $oAccount + \MailSo\Smtp\SmtpClient $oSmtpClient + \MailSo\Smtp\Settings $oSettings + +### smtp.after-connect + params: + \RainLoop\Model\Account $oAccount + \MailSo\Smtp\SmtpClient $oSmtpClient + \MailSo\Smtp\Settings $oSettings + +### smtp.before-login + params: + \RainLoop\Model\Account $oAccount + \MailSo\Smtp\SmtpClient $oSmtpClient + \MailSo\Smtp\Settings $oSettings + +### smtp.after-login + params: + \RainLoop\Model\Account $oAccount + \MailSo\Smtp\SmtpClient $oSmtpClient + bool $bSuccess + \MailSo\Smtp\Settings $oSettings + +## Json service actions +Called by RainLoop\ServiceActions::ServiceJson() +{actionname} is one of the RainLoop\Actions::Do{ActionName}(), +or an extension action as "Plugin{ActionName}" added with Plugin::addJsonHook() +and called in JavaScript using rl.pluginRemoteRequest(). + +### json.before-{actionname} + params: none + +### json.after-{actionname} + params: + array &$aResponse + +### json.action-post-call + Obsolete, use json.after-{actionname} + +### json.action-pre-call + Obsolete, use json.before-{actionname} + +### filter.json-response + Obsolete, use json.after-{actionname} + +## Others + +### filter.account + params: + \RainLoop\Model\Account $oAccount + +### filter.action-params + params: + string $sMethodName + array &$aCurrentActionParams + +### filter.app-data + params: + bool $bAdmin + array &$aAppData + +### filter.application-config + params: + \RainLoop\Config\Application $oConfig + +### filter.build-message + params: + \MailSo\Mime\Message $oMessage + + Happens before send/save message + +### filter.build-read-receipt-message + params: + \MailSo\Mime\Message $oMessage + \RainLoop\Model\Account $oAccount + +### filter.domain + params: + \RainLoop\Model\Domain $oDomain + +### filter.fabrica + params: + string $sName + mixed &$mResult + \RainLoop\Model\Account $oAccount + +### filter.http-paths + 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 + \MailSo\Mime\Message $oMessage + string &$sTextConverted + + Happens before send/save message + +### filter.message-plain + params: + \RainLoop\Model\Account $oAccount + \MailSo\Mime\Message $oMessage + string &$sTextConverted + + Happens before send/save message + +### filter.message-rcpt + Called by DoSendMessage and DoSendReadReceiptMessage + params: + \RainLoop\Model\Account $oAccount + \MailSo\Mime\EmailCollection $oRcpt + +### filter.read-receipt-message-plain + params: + \RainLoop\Model\Account $oAccount + \MailSo\Mime\Message $oMessage + string &$sText + +### filter.result-message + params: + \MailSo\Mime\Message $oMessage + + Happens when reading message + +### filter.save-message + params: + \MailSo\Mime\Message $oMessage + + Happens before save message + +### filter.send-message + params: + \MailSo\Mime\Message $oMessage + + Happens before send message + +### filter.send-message-stream + params: + \RainLoop\Model\Account $oAccount + resource &$rMessageStream + int &$iMessageStreamSize + +### filter.send-read-receipt-message + params: + \MailSo\Mime\Message $oMessage + \RainLoop\Model\Account $oAccount + +### filter.smtp-from + params: + \RainLoop\Model\Account $oAccount + \MailSo\Mime\Message $oMessage + string &$sFrom + +### filter.smtp-hidden-rcpt + params: + \RainLoop\Model\Account $oAccount + \MailSo\Mime\Message $oMessage + array &$aHiddenRcpt + +### filter.smtp-message-stream + Called by DoSendMessage and DoSendReadReceiptMessage + params: + \RainLoop\Model\Account $oAccount + resource &$rMessageStream + int &$iMessageStreamSize + +### filter.upload-response + params: + array &$aResponse + +### json.attachments + params: + \SnappyMail\AttachmentsAction $oData + +### json.suggestions-input-parameters + params: + string &$sQuery + int &$iLimit + \RainLoop\Model\Account $oAccount + +### main.content-security-policy + params: + \SnappyMail\HTTP\CSP $oCSP + + Allows you to edit the policy, like: + `$oCSP->script[] = "'strict-dynamic'";` + +### main.default-response + Obsolete, use json.after-{actionname} + +### main.default-response-data + Obsolete, use json.after-{actionname} + +### main.default-response-error-data + Obsolete, use json.after-{actionname} + +### main.fabrica + params: + string $sName + mixed &$mResult + +# 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 +### 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. + 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() +### sm-admin-login-response + event.detail = { error: int, data: {JSON response} } +### sm-user-login + event.detail = FormData + cancelable using preventDefault() +### sm-user-login-response + event.detail = { error: int, data: {JSON response} } + +### sm-show-screen + event.detail = 'screenname' + cancelable using preventDefault() + +### squire-toolbar + event.detail = { squire: SquireUI, actions: object } + `actions` is the toolbar structure. + ```javascript + block-of-buttons: { + button-name: { + select: ['selectbox options'], + html: 'button text', + cmd: () => `command to execute`, + key: 'keyboard shortcut', + matches: 'HTML elements that match' + } + } + ``` + See [SquireUI.js](https://github.com/the-djmaze/snappymail/blob/master/dev/External/SquireUI.js) + for all default toolbar actions. + +# JavaScript `rl` object + +## rl.Enums.StorageResultType +### rl.Enums.StorageResultType.Abort +### rl.Enums.StorageResultType.Error +### rl.Enums.StorageResultType.Success + +## rl.​Utils.htmlToPlain(html) +Converts HTML to text + +## rl.Utils.plainToHtml(plain) +Converts text to HTML + +## rl.addSettingsViewModel(SettingsViewModelClass, template, labelName, route)​ +Examples in +* ./change-password/js/ChangePasswordUserSettings.js +* ./example/js/ExampleUserSettings.js +* ./kolab/js/settings.js +* ./two-factor-auth/js/TwoFactorAuthSettings.js + +## rl.addSettingsViewModelForAdmin(SettingsViewModelClass, template, labelName, route)​ +Examples in +* ./example/js/ExampleAdminSettings.js:34: rl.addSettingsViewModelForAdmin(ExampleAdminSettings, 'ExampleAdminSettingsTab', + +## rl.adminArea()​ +Returns true or false when in '?admin' area + +## rl.app.Remote.abort(action)​​​​​ + +## rl.app.Remote.get(action, url)​​​​​ + +## rl.app.Remote.getPublicKey(fCallback)​​​​​ + +## rl.app.Remote.post(action, fTrigger, params, timeOut)​​​​​ + +## rl.app.Remote.request(action, fCallback, params, iTimeout, sGetAdd)​​​​​ + +## rl.app.Remote.setTrigger(trigger, value)​​​​​ + +## rl.app.Remote.streamPerLine(fCallback, sGetAdd, postData) + +## rl.app.folderList +A knockout observable array of all folders/mailboxes + +## rl.fetch(resource, init, postData)​ + +## rl.fetchJSON(resource, init, postData)​ + +## rl.i18n(key, valueList, defaulValue)​ + +## rl.loadScript(src)​ + +## rl.logoutReload(url)​ + +## rl.pluginPopupView +class AbstractViewPopup + +## rl.pluginRemoteRequest(callback, action, parameters, timeout)​ + +## rl.pluginSettingsGet(pluginSection, name)​ + +## rl.registerWYSIWYG(name, construct)​ + +## rl.route.root() + +## rl.route.reload() + +## rl.route.off() + +## rl.setTitle(title)​ + +## rl.settings.get(name) + +## rl.settings.set(name, value) + +## rl.settings.app(name) diff --git a/plugins/_depricated/convert-headers-styles/CssToInlineStyles.php b/plugins/_depricated/convert-headers-styles/CssToInlineStyles.php deleted file mode 100644 index 1d19bca354..0000000000 --- a/plugins/_depricated/convert-headers-styles/CssToInlineStyles.php +++ /dev/null @@ -1,700 +0,0 @@ -<?php - -namespace TijsVerkoyen\CssToInlineStyles; - -/** - * CSS to Inline Styles class - * - * @author Tijs Verkoyen <php-css-to-inline-styles@verkoyen.eu> - * @version 1.1.0 - * @copyright Copyright (c), Tijs Verkoyen. All rights reserved. - * @license BSD License - */ -class CssToInlineStyles -{ - /** - * The CSS to use - * - * @var string - */ - private $css; - - /** - * The processed CSS rules - * - * @var array - */ - private $cssRules; - - /** - * Should the generated HTML be cleaned - * - * @var bool - */ - private $cleanup = false; - - /** - * The encoding to use. - * - * @var string - */ - private $encoding = 'UTF-8'; - - /** - * The HTML to process - * - * @var string - */ - private $html; - - /** - * Use inline-styles block as CSS - * - * @var bool - */ - private $useInlineStylesBlock = false; - - /* - * Strip original style tags - * - * @var bool - */ - private $stripOriginalStyleTags = false; - - /** - * Creates an instance, you could set the HTML and CSS here, or load it - * later. - * - * @return void - * @param string[optional] $html The HTML to process. - * @param string[optional] $css The CSS to use. - */ - public function __construct($html = null, $css = null) - { - if($html !== null) $this->setHTML($html); - if($css !== null) $this->setCSS($css); - } - - /** - * Convert a CSS-selector into an xPath-query - * - * @return string - * @param string $selector The CSS-selector. - */ - private function buildXPathQuery($selector) - { - // redefine - $selector = (string) $selector; - - // the CSS selector - $cssSelector = array( - // E F, Matches any F element that is a descendant of an E element - '/(\w)\s+(\w)/', - // E > F, Matches any F element that is a child of an element E - '/(\w)\s*>\s*(\w)/', - // E:first-child, Matches element E when E is the first child of its parent - '/(\w):first-child/', - // E + F, Matches any F element immediately preceded by an element - '/(\w)\s*\+\s*(\w)/', - // E[foo], Matches any E element with the "foo" attribute set (whatever the value) - '/(\w)\[([\w\-]+)]/', - // E[foo="warning"], Matches any E element whose "foo" attribute value is exactly equal to "warning" - '/(\w)\[([\w\-]+)\=\"(.*)\"]/', - // div.warning, HTML only. The same as DIV[class~="warning"] - '/(\w+|\*)+\.([\w\-]+)+/', - // .warning, HTML only. The same as *[class~="warning"] - '/\.([\w\-]+)/', - // E#myid, Matches any E element with id-attribute equal to "myid" - '/(\w+)+\#([\w\-]+)/', - // #myid, Matches any element with id-attribute equal to "myid" - '/\#([\w\-]+)/' - ); - - // the xPath-equivalent - $xPathQuery = array( - // E F, Matches any F element that is a descendant of an E element - '\1//\2', - // E > F, Matches any F element that is a child of an element E - '\1/\2', - // E:first-child, Matches element E when E is the first child of its parent - '*[1]/self::\1', - // E + F, Matches any F element immediately preceded by an element - '\1/following-sibling::*[1]/self::\2', - // E[foo], Matches any E element with the "foo" attribute set (whatever the value) - '\1 [ @\2 ]', - // E[foo="warning"], Matches any E element whose "foo" attribute value is exactly equal to "warning" - '\1[ contains( concat( " ", @\2, " " ), concat( " ", "\3", " " ) ) ]', - // div.warning, HTML only. The same as DIV[class~="warning"] - '\1[ contains( concat( " ", @class, " " ), concat( " ", "\2", " " ) ) ]', - // .warning, HTML only. The same as *[class~="warning"] - '*[ contains( concat( " ", @class, " " ), concat( " ", "\1", " " ) ) ]', - // E#myid, Matches any E element with id-attribute equal to "myid" - '\1[ @id = "\2" ]', - // #myid, Matches any element with id-attribute equal to "myid" - '*[ @id = "\1" ]' - ); - - // return - $xPath = (string) '//' . preg_replace($cssSelector, $xPathQuery, $selector); - - return str_replace('] *', ']//*', $xPath); - } - - /** - * Calculate the specifity for the CSS-selector - * - * @return int - * @param string $selector The selector to calculate the specifity for. - */ - private function calculateCSSSpecifity($selector) - { - // cleanup selector - $selector = str_replace(array('>', '+'), array(' > ', ' + '), $selector); - - // init var - $specifity = 0; - - // split the selector into chunks based on spaces - $chunks = explode(' ', $selector); - - // loop chunks - foreach ($chunks as $chunk) { - // an ID is important, so give it a high specifity - if(strstr($chunk, '#') !== false) $specifity += 100; - - // classes are more important than a tag, but less important then an ID - elseif(strstr($chunk, '.')) $specifity += 10; - - // anything else isn't that important - else $specifity += 1; - } - - // return - return $specifity; - } - - - /** - * Cleanup the generated HTML - * - * @return string - * @param string $html The HTML to cleanup. - */ - private function cleanupHTML($html) - { - // remove classes - $html = preg_replace('/(\s)+class="(.*)"(\s)+/U', ' ', $html); - - // remove IDs - $html = preg_replace('/(\s)+id="(.*)"(\s)+/U', ' ', $html); - - // return - return $html; - } - - - /** - * Converts the loaded HTML into an HTML-string with inline styles based on the loaded CSS - * - * @return string - * @param bool[optional] $outputXHTML Should we output valid XHTML? - */ - public function convert($outputXHTML = false) - { - // redefine - $outputXHTML = (bool) $outputXHTML; - - // validate - if($this->html == null) throw new Exception('No HTML provided.'); - - // should we use inline style-block - if ($this->useInlineStylesBlock) { - // init var - $matches = array(); - - // match the style blocks - preg_match_all('|<style(.*)>(.*)</style>|isU', $this->html, $matches); - - // any style-blocks found? - if (!empty($matches[2])) { - // add - foreach($matches[2] as $match) $this->css .= trim($match) ."\n"; - } - } - - // process css - $this->processCSS(); - - // create new DOMDocument - $document = new \DOMDocument('1.0', $this->getEncoding()); - - // set error level - libxml_use_internal_errors(true); - - // load HTML -// $document->loadHTML($this->html); - $document->loadHTML('<'.'?xml version="1.0" encoding="'.$this->getEncoding().'"?'.'><head><meta http-equiv="Content-Type" content="text/html; charset='.$this->getEncoding().'"></head>'.$this->html); - - // create new XPath - $xPath = new \DOMXPath($document); - - // any rules? - if (!empty($this->cssRules)) { - // loop rules - foreach ($this->cssRules as $rule) { - // init var - $query = $this->buildXPathQuery($rule['selector']); - - // validate query - if($query === false) continue; - - // search elements - $elements = $xPath->query($query); - - // validate elements - if($elements === false) continue; - - // loop found elements - foreach ($elements as $element) { - // no styles stored? - if ($element->attributes->getNamedItem( - 'data-css-to-inline-styles-original-styles' - ) == null) { - // init var - $originalStyle = ''; - if ($element->attributes->getNamedItem('style') !== null) { - $originalStyle = $element->attributes->getNamedItem('style')->value; - } - - // store original styles - $element->setAttribute( - 'data-css-to-inline-styles-original-styles', - $originalStyle - ); - - // clear the styles - $element->setAttribute('style', ''); - } - - // init var - $properties = array(); - - // get current styles - $stylesAttribute = $element->attributes->getNamedItem('style'); - - // any styles defined before? - if ($stylesAttribute !== null) { - // get value for the styles attribute - $definedStyles = (string) $stylesAttribute->value; - - // split into properties - $definedProperties = (array) explode(';', $definedStyles); - - // loop properties - foreach ($definedProperties as $property) { - // validate property - if($property == '') continue; - - // split into chunks - $chunks = (array) explode(':', trim($property), 2); - - // validate - if(!isset($chunks[1])) continue; - - // loop chunks - $properties[$chunks[0]] = trim($chunks[1]); - } - } - - // add new properties into the list - foreach ($rule['properties'] as $key => $value) { - $properties[$key] = $value; - } - - // build string - $propertyChunks = array(); - - // build chunks - foreach ($properties as $key => $values) { - foreach ((array) $values as $value) { - $propertyChunks[] = $key . ': ' . $value . ';'; - } - } - - // build properties string - $propertiesString = implode(' ', $propertyChunks); - - // set attribute - if ($propertiesString != '') { - $element->setAttribute('style', $propertiesString); - } - } - } - - // reapply original styles - $query = $this->buildXPathQuery( - '*[@data-css-to-inline-styles-original-styles]' - ); - - // validate query - if($query === false) return; - - // search elements - $elements = $xPath->query($query); - - // loop found elements - foreach ($elements as $element) { - // get the original styles - $originalStyle = $element->attributes->getNamedItem( - 'data-css-to-inline-styles-original-styles' - )->value; - - if ($originalStyle != '') { - $originalProperties = array(); - $originalStyles = (array) explode(';', $originalStyle); - - foreach ($originalStyles as $property) { - // validate property - if($property == '') continue; - - // split into chunks - $chunks = (array) explode(':', trim($property), 2); - - // validate - if(!isset($chunks[1])) continue; - - // loop chunks - $originalProperties[$chunks[0]] = trim($chunks[1]); - } - - // get current styles - $stylesAttribute = $element->attributes->getNamedItem('style'); - $properties = array(); - - // any styles defined before? - if ($stylesAttribute !== null) { - // get value for the styles attribute - $definedStyles = (string) $stylesAttribute->value; - - // split into properties - $definedProperties = (array) explode(';', $definedStyles); - - // loop properties - foreach ($definedProperties as $property) { - // validate property - if($property == '') continue; - - // split into chunks - $chunks = (array) explode(':', trim($property), 2); - - // validate - if(!isset($chunks[1])) continue; - - // loop chunks - $properties[$chunks[0]] = trim($chunks[1]); - } - } - - // add new properties into the list - foreach ($originalProperties as $key => $value) { - $properties[$key] = $value; - } - - // build string - $propertyChunks = array(); - - // build chunks - foreach ($properties as $key => $values) { - foreach ((array) $values as $value) { - $propertyChunks[] = $key . ': ' . $value . ';'; - } - } - - // build properties string - $propertiesString = implode(' ', $propertyChunks); - - // set attribute - if($propertiesString != '') $element->setAttribute( - 'style', $propertiesString - ); - } - - // remove placeholder - $element->removeAttribute( - 'data-css-to-inline-styles-original-styles' - ); - } - } - - // should we output XHTML? - if ($outputXHTML) { - // set formating - $document->formatOutput = true; - - // get the HTML as XML - $html = $document->saveXML(null, LIBXML_NOEMPTYTAG); - - // get start of the XML-declaration - $startPosition = strpos($html, '<?xml'); - - // valid start position? - if ($startPosition !== false) { - // get end of the xml-declaration - $endPosition = strpos($html, '?>', $startPosition); - - // remove the XML-header - $html = ltrim(substr($html, $endPosition + 1)); - } - } - - // just regular HTML 4.01 as it should be used in newsletters - else { - // get the HTML - $html = $document->saveHTML(); - } - - // cleanup the HTML if we need to - if($this->cleanup) $html = $this->cleanupHTML($html); - - // strip original style tags if we need to - if ($this->stripOriginalStyleTags) { - $html = $this->stripOriginalStyleTags($html); - } - - // return - return $html; - } - - - /** - * Get the encoding to use - * - * @return string - */ - private function getEncoding() - { - return $this->encoding; - } - - - /** - * Process the loaded CSS - * - * @return void - */ - private function processCSS() - { - // init vars - $css = (string) $this->css; - - // remove newlines - $css = str_replace(array("\r", "\n"), '', $css); - - // replace double quotes by single quotes - $css = str_replace('"', '\'', $css); - - // remove comments - $css = preg_replace('|/\*.*?\*/|', '', $css); - - // remove spaces - $css = preg_replace('/\s\s+/', ' ', $css); - - // rules are splitted by } - $rules = (array) explode('}', $css); - - // init var - $i = 1; - - // loop rules - foreach ($rules as $rule) { - // split into chunks - $chunks = explode('{', $rule); - - // invalid rule? - if(!isset($chunks[1])) continue; - - // set the selectors - $selectors = trim($chunks[0]); - - // get cssProperties - $cssProperties = trim($chunks[1]); - - // split multiple selectors - $selectors = (array) explode(',', $selectors); - - // loop selectors - foreach ($selectors as $selector) { - // cleanup - $selector = trim($selector); - - // build an array for each selector - $ruleSet = array(); - - // store selector - $ruleSet['selector'] = $selector; - - // process the properties - $ruleSet['properties'] = $this->processCSSProperties( - $cssProperties - ); - - // calculate specifity - $ruleSet['specifity'] = $this->calculateCSSSpecifity( - $selector - ) + $i; - - // add into global rules - $this->cssRules[] = $ruleSet; - } - - // increment - $i++; - } - - // sort based on specifity - if (!empty($this->cssRules)) { - usort($this->cssRules, array(__CLASS__, 'sortOnSpecifity')); - } - } - - /** - * Process the CSS-properties - * - * @return array - * @param string $propertyString The CSS-properties. - */ - private function processCSSProperties($propertyString) - { - // split into chunks - $properties = (array) explode(';', $propertyString); - - // init var - $pairs = array(); - - // loop properties - foreach ($properties as $property) { - // split into chunks - $chunks = (array) explode(':', $property, 2); - - // validate - if(!isset($chunks[1])) continue; - - // cleanup - $chunks[0] = trim($chunks[0]); - $chunks[1] = trim($chunks[1]); - - // add to pairs array - if(!isset($pairs[$chunks[0]]) || - !in_array($chunks[1], $pairs[$chunks[0]])) { - $pairs[$chunks[0]][] = $chunks[1]; - } - } - - // sort the pairs - ksort($pairs); - - // return - return $pairs; - } - - /** - * Should the IDs and classes be removed? - * - * @return void - * @param bool[optional] $on Should we enable cleanup? - */ - public function setCleanup($on = true) - { - $this->cleanup = (bool) $on; - } - - /** - * Set CSS to use - * - * @return void - * @param string $css The CSS to use. - */ - public function setCSS($css) - { - $this->css = (string) $css; - } - - /** - * Set the encoding to use with the DOMDocument - * - * @return void - * @param string $encoding The encoding to use. - */ - public function setEncoding($encoding) - { - $this->encoding = (string) $encoding; - } - - /** - * Set HTML to process - * - * @return void - * @param string $html The HTML to process. - */ - public function setHTML($html) - { - $this->html = (string) $html; - } - - /** - * Set use of inline styles block - * If this is enabled the class will use the style-block in the HTML. - * - * @return void - * @param bool[optional] $on Should we process inline styles? - */ - public function setUseInlineStylesBlock($on = true) - { - $this->useInlineStylesBlock = (bool) $on; - } - - /** - * Set strip original style tags - * If this is enabled the class will remove all style tags in the HTML. - * - * @return void - * @param bool[optional] $onShould we process inline styles? - */ - public function setStripOriginalStyleTags($on = true) - { - $this->stripOriginalStyleTags = (bool) $on; - } - - /** - * Strip style tags into the generated HTML - * - * @return string - * @param string $html The HTML to strip style tags. - */ - private function stripOriginalStyleTags($html) - { - return preg_replace('|<style(.*)>(.*)</style>|isU', '', $html); - } - - /** - * Sort an array on the specifity element - * - * @return int - * @param array $e1 The first element. - * @param array $e2 The second element. - */ - private static function sortOnSpecifity($e1, $e2) - { - // validate - if(!isset($e1['specifity']) || !isset($e2['specifity'])) return 0; - - // lower - if($e1['specifity'] < $e2['specifity']) return -1; - - // higher - if($e1['specifity'] > $e2['specifity']) return 1; - - // fallback - return 0; - } -} diff --git a/plugins/_depricated/convert-headers-styles/README b/plugins/_depricated/convert-headers-styles/README deleted file mode 100644 index 746409f68d..0000000000 --- a/plugins/_depricated/convert-headers-styles/README +++ /dev/null @@ -1 +0,0 @@ -Plugin used for processing complex embedded styles in mail messages. In some cases, it can improve rendering HTML mails. \ No newline at end of file diff --git a/plugins/_depricated/convert-headers-styles/VERSION b/plugins/_depricated/convert-headers-styles/VERSION deleted file mode 100644 index a58941b07a..0000000000 --- a/plugins/_depricated/convert-headers-styles/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.3 \ No newline at end of file diff --git a/plugins/_depricated/convert-headers-styles/index.php b/plugins/_depricated/convert-headers-styles/index.php deleted file mode 100644 index 249291d12f..0000000000 --- a/plugins/_depricated/convert-headers-styles/index.php +++ /dev/null @@ -1,29 +0,0 @@ -<?php - -class ConvertHeadersStylesPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('filter.result-message', 'FilterResultMessage'); - } - - /** - * @param \MailSo\Mail\Message &$oMessage - */ - public function FilterResultMessage(&$oMessage) - { - if ($oMessage) - { - $sHtml = $oMessage->Html(); - if ($sHtml && 0 < strlen($sHtml)) - { - include_once __DIR__.'/CssToInlineStyles.php'; - - $oCSSToInlineStyles = new \TijsVerkoyen\CssToInlineStyles\CssToInlineStyles($sHtml); - $oCSSToInlineStyles->setEncoding('utf-8'); - $oCSSToInlineStyles->setUseInlineStylesBlock(true); - $oMessage->SetHtml($oCSSToInlineStyles->convert().'<!-- convert-headers-styles-plugin -->'); - } - } - } -} diff --git a/plugins/_depricated/recaptcha/LICENSE b/plugins/_depricated/recaptcha/LICENSE deleted file mode 100644 index 271342337d..0000000000 --- a/plugins/_depricated/recaptcha/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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 -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/_depricated/recaptcha/README b/plugins/_depricated/recaptcha/README deleted file mode 100644 index 17e4ab032d..0000000000 --- a/plugins/_depricated/recaptcha/README +++ /dev/null @@ -1,3 +0,0 @@ -A CAPTCHA is a program that can generate and grade tests that humans can pass but current computer programs cannot. -For example, humans can read distorted text as the one shown below, but current computer programs can't. -More info at http://www.google.com/recaptcha \ No newline at end of file diff --git a/plugins/_depricated/recaptcha/VERSION b/plugins/_depricated/recaptcha/VERSION deleted file mode 100644 index 872765e5f2..0000000000 --- a/plugins/_depricated/recaptcha/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.9 \ No newline at end of file diff --git a/plugins/_depricated/recaptcha/index.php b/plugins/_depricated/recaptcha/index.php deleted file mode 100644 index 2875839f71..0000000000 --- a/plugins/_depricated/recaptcha/index.php +++ /dev/null @@ -1,139 +0,0 @@ -<?php - -class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - /** - * @return void - */ - public function Init() - { - $this->UseLangs(true); - - $this->addJs('js/recaptcha.js'); - - $this->addHook('ajax.action-pre-call', 'AjaxActionPreCall'); - $this->addHook('filter.ajax-response', 'FilterAjaxResponse'); - - $this->addTemplate('templates/PluginLoginReCaptchaGroup.html'); - $this->addTemplateHook('Login', 'BottomControlGroup', 'PluginLoginReCaptchaGroup'); - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('public_key')->SetLabel('Public Key') - ->SetAllowedInJs(true) - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('private_key')->SetLabel('Private Key') - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('error_limit')->SetLabel('Limit') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) - ->SetDefaultValue(array(0, 1, 2, 3, 4, 5)) - ->SetDescription('') - ); - } - - /** - * @return string - */ - private function getCaptchaCacherKey() - { - return 'Captcha/Login/'.\RainLoop\Utils::GetConnectionToken(); - } - - /** - * @return int - */ - private function getLimit() - { - $iConfigLimit = $this->Config()->Get('plugin', 'error_limit', 0); - if (0 < $iConfigLimit) - { - $oCacher = $this->Manager()->Actions()->Cacher(); - $sLimit = $oCacher && $oCacher->IsInited() ? $oCacher->Get($this->getCaptchaCacherKey()) : '0'; - - if (0 < strlen($sLimit) && is_numeric($sLimit)) - { - $iConfigLimit -= (int) $sLimit; - } - } - - return $iConfigLimit; - } - - /** - * @return void - */ - public function FilterAppDataPluginSection($bAdmin, $bAuth, &$aData) - { - if (!$bAdmin && !$bAuth && is_array($aData)) - { - $aData['show_captcha_on_login'] = 1 > $this->getLimit(); - } - } - - /** - * @param string $sAction - */ - public function AjaxActionPreCall($sAction) - { - if ('Login' === $sAction && 0 >= $this->getLimit()) - { - require_once __DIR__.'/recaptchalib.php'; - - $oResp = recaptcha_check_answer( - $this->Config()->Get('plugin', 'private_key', ''), - isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '', - $this->Manager()->Actions()->GetActionParam('RecaptchaChallenge', ''), - $this->Manager()->Actions()->GetActionParam('RecaptchaResponse', '') - ); - - if (!$oResp || !isset($oResp->is_valid) || !$oResp->is_valid) - { - $this->Manager()->Actions()->Logger()->WriteDump($oResp); - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::CaptchaError); - } - } - } - - /** - * @param string $sAction - * @param array $aResponseItem - */ - public function FilterAjaxResponse($sAction, &$aResponseItem) - { - if ('Login' === $sAction && $aResponseItem && isset($aResponseItem['Result'])) - { - $oCacher = $this->Manager()->Actions()->Cacher(); - $iConfigLimit = (int) $this->Config()->Get('plugin', 'error_limit', 0); - $sKey = $this->getCaptchaCacherKey(); - - if (0 < $iConfigLimit && $oCacher && $oCacher->IsInited()) - { - if (false === $aResponseItem['Result']) - { - $iLimit = 0; - $sLimut = $oCacher->Get($sKey); - if (0 < strlen($sLimut) && is_numeric($sLimut)) - { - $iLimit = (int) $sLimut; - } - - $oCacher->Set($sKey, ++$iLimit); - - if ($iConfigLimit <= $iLimit) - { - $aResponseItem['Captcha'] = true; - } - } - else - { - $oCacher->Delete($sKey); - } - } - } - } -} diff --git a/plugins/_depricated/recaptcha/js/recaptcha.js b/plugins/_depricated/recaptcha/js/recaptcha.js deleted file mode 100644 index cbda2d2166..0000000000 --- a/plugins/_depricated/recaptcha/js/recaptcha.js +++ /dev/null @@ -1,78 +0,0 @@ - -$(function () { - - var - bStarted = false, - bShown = false - ; - - function ShowRecaptcha() - { - if (window.Recaptcha) - { - if (bShown) - { - window.Recaptcha.reload(); - } - else - { - window.Recaptcha.create(window.rl.pluginSettingsGet('recaptcha', 'public_key'), 'recaptcha-place', { - 'theme': 'custom', - 'lang': window.rl.settingsGet('Language') - }); - } - - bShown = true; - } - } - - function StartRecaptcha() - { - if (!window.Recaptcha) - { - $.getScript('//www.google.com/recaptcha/api/js/recaptcha_ajax.js', ShowRecaptcha); - } - else - { - ShowRecaptcha(); - } - } - - if (window.rl) - { - window.rl.addHook('view-model-on-show', function (sName, oViewModel) { - if (!bStarted && oViewModel && - ('View:RainLoop:Login' === sName || 'View/App/Login' === sName || 'LoginViewModel' === sName || 'LoginAppView' === sName) && - window.rl.pluginSettingsGet('recaptcha', 'show_captcha_on_login')) - { - bStarted = true; - StartRecaptcha(); - } - }); - - window.rl.addHook('ajax-default-request', function (sAction, oParameters) { - if ('Login' === sAction && oParameters && bShown && window.Recaptcha) - { - oParameters['RecaptchaChallenge'] = window.Recaptcha.get_challenge(); - oParameters['RecaptchaResponse'] = window.Recaptcha.get_response(); - } - }); - - window.rl.addHook('ajax-default-response', function (sAction, oData, sType) { - if ('Login' === sAction) - { - if (!oData || 'success' !== sType || !oData['Result']) - { - if (bShown && window.Recaptcha) - { - window.Recaptcha.reload(); - } - else if (oData && oData['Captcha']) - { - StartRecaptcha(); - } - } - } - }); - } -}); \ No newline at end of file diff --git a/plugins/_depricated/recaptcha/langs/en.ini b/plugins/_depricated/recaptcha/langs/en.ini deleted file mode 100644 index 87ed5f76cd..0000000000 --- a/plugins/_depricated/recaptcha/langs/en.ini +++ /dev/null @@ -1,2 +0,0 @@ -[PLUGIN] -LABEL_ENTER_THE_WORDS_ABOVE = "Enter the words above" \ No newline at end of file diff --git a/plugins/_depricated/recaptcha/langs/ru.ini b/plugins/_depricated/recaptcha/langs/ru.ini deleted file mode 100644 index 77329095cf..0000000000 --- a/plugins/_depricated/recaptcha/langs/ru.ini +++ /dev/null @@ -1,2 +0,0 @@ -[PLUGIN] -LABEL_ENTER_THE_WORDS_ABOVE = "Введите слова с изображения" \ No newline at end of file diff --git a/plugins/_depricated/recaptcha/recaptchalib.php b/plugins/_depricated/recaptcha/recaptchalib.php deleted file mode 100644 index 337fbc795e..0000000000 --- a/plugins/_depricated/recaptcha/recaptchalib.php +++ /dev/null @@ -1,280 +0,0 @@ -<?php -/* - * This is a PHP library that handles calling reCAPTCHA. - * - Documentation and latest version - * http://recaptcha.net/plugins/php/ - * - Get a reCAPTCHA API Key - * https://www.google.com/recaptcha/admin/create - * - Discussion group - * http://groups.google.com/group/recaptcha - * - * Copyright (c) 2007 reCAPTCHA -- http://recaptcha.net - * AUTHORS: - * Mike Crawford - * Ben Maurer - * - * 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. - */ - -/** - * The reCAPTCHA server URL's - */ -define("RECAPTCHA_API_SERVER", "http://www.google.com/recaptcha/api"); -define("RECAPTCHA_API_SECURE_SERVER", "https://www.google.com/recaptcha/api"); -define("RECAPTCHA_VERIFY_SERVER", "www.google.com"); - -/** - * Encodes the given data into a query string format - * @param $data - array of string elements to be encoded - * @return string - encoded request - */ -function _recaptcha_qsencode ($data) { - $req = ""; - foreach ( $data as $key => $value ) - $req .= $key . '=' . urlencode( stripslashes($value) ) . '&'; - - // Cut the last '&' - $req=substr($req,0,strlen($req)-1); - return $req; -} - - - -/** - * Submits an HTTP POST to a reCAPTCHA server - * @param string $host - * @param string $path - * @param array $data - * @param int port - * @return array response - */ -function _recaptcha_http_post($host, $path, $data, $port = 80) { - - $req = _recaptcha_qsencode ($data); - - $http_request = "POST $path HTTP/1.0\r\n"; - $http_request .= "Host: $host\r\n"; - $http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n"; - $http_request .= "Content-Length: " . strlen($req) . "\r\n"; - $http_request .= "User-Agent: reCAPTCHA/PHP\r\n"; - $http_request .= "\r\n"; - $http_request .= $req; - - $response = ''; - if( false == ( $fs = @fsockopen($host, $port, $errno, $errstr, 10) ) ) { - die ('Could not open socket'); - } - - fwrite($fs, $http_request); - - while ( !feof($fs) ) - $response .= fgets($fs, 1160); // One TCP-IP packet - fclose($fs); - $response = explode("\r\n\r\n", $response, 2); - - return $response; -} - - - -/** - * Gets the challenge HTML (javascript and non-javascript version). - * This is called from the browser, and the resulting reCAPTCHA HTML widget - * is embedded within the HTML form it was called from. - * @param string $pubkey A public key for reCAPTCHA - * @param string $error The error given by reCAPTCHA (optional, default is null) - * @param boolean $use_ssl Should the request be made over ssl? (optional, default is false) - - * @return string - The HTML to be embedded in the user's form. - */ -function recaptcha_get_html ($pubkey, $error = null, $use_ssl = false) -{ - if ($pubkey == null || $pubkey == '') { - die ("To use reCAPTCHA you must get an API key from <a href='https://www.google.com/recaptcha/admin/create'>https://www.google.com/recaptcha/admin/create</a>"); - } - - if ($use_ssl) { - $server = RECAPTCHA_API_SECURE_SERVER; - } else { - $server = RECAPTCHA_API_SERVER; - } - - $errorpart = ""; - if ($error) { - $errorpart = "&error=" . $error; - } - return '<script type="text/javascript" data-cfasync="false" src="'. $server . '/challenge?k=' . $pubkey . $errorpart . '"></script> - - <noscript> - <iframe src="'. $server . '/noscript?k=' . $pubkey . $errorpart . '" height="300" width="500" frameborder="0"></iframe><br/> - <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea> - <input type="hidden" name="recaptcha_response_field" value="manual_challenge"/> - </noscript>'; -} - - - - -/** - * A ReCaptchaResponse is returned from recaptcha_check_answer() - */ -class ReCaptchaResponse { - var $is_valid; - var $error; -} - - -/** - * Calls an HTTP POST function to verify if the user's guess was correct - * @param string $privkey - * @param string $remoteip - * @param string $challenge - * @param string $response - * @param array $extra_params an array of extra variables to post to the server - * @return ReCaptchaResponse - */ -function recaptcha_check_answer ($privkey, $remoteip, $challenge, $response, $extra_params = array()) -{ - if ($privkey == null || $privkey == '') { - $recaptcha_response = new ReCaptchaResponse(); - $recaptcha_response->is_valid = false; - $recaptcha_response->error = 'To use reCAPTCHA you must get an API key from <a href=\'https://www.google.com/recaptcha/admin/create\'>https://www.google.com/recaptcha/admin/create</a>'; - return $recaptcha_response; - } - - if ($remoteip == null || $remoteip == '') { - $recaptcha_response = new ReCaptchaResponse(); - $recaptcha_response->is_valid = false; - $recaptcha_response->error = 'For security reasons, you must pass the remote ip to reCAPTCHA'; - return $recaptcha_response; - } - - //discard spam submissions - if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0) { - $recaptcha_response = new ReCaptchaResponse(); - $recaptcha_response->is_valid = false; - $recaptcha_response->error = 'incorrect-captcha-sol'; - return $recaptcha_response; - } - - $response = _recaptcha_http_post (RECAPTCHA_VERIFY_SERVER, "/recaptcha/api/verify", - array ( - 'privatekey' => $privkey, - 'remoteip' => $remoteip, - 'challenge' => $challenge, - 'response' => $response - ) + $extra_params - ); - - $answers = explode ("\n", $response [1]); - $recaptcha_response = new ReCaptchaResponse(); - - if (trim ($answers [0]) == 'true') { - $recaptcha_response->is_valid = true; - } - else { - $recaptcha_response->is_valid = false; - $recaptcha_response->error = $answers [1]; - } - return $recaptcha_response; - -} - -/** - * gets a URL where the user can sign up for reCAPTCHA. If your application - * has a configuration page where you enter a key, you should provide a link - * using this function. - * @param string $domain The domain where the page is hosted - * @param string $appname The name of your application - */ -function recaptcha_get_signup_url ($domain = null, $appname = null) { - return "https://www.google.com/recaptcha/admin/create?" . _recaptcha_qsencode (array ('domains' => $domain, 'app' => $appname)); -} - -function _recaptcha_aes_pad($val) { - $block_size = 16; - $numpad = $block_size - (strlen ($val) % $block_size); - return str_pad($val, strlen ($val) + $numpad, chr($numpad)); -} - -/* Mailhide related code */ - -function _recaptcha_aes_encrypt($val,$ky) { - if (! function_exists ("mcrypt_encrypt")) { - die ("To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed."); - } - $mode=MCRYPT_MODE_CBC; - $enc=MCRYPT_RIJNDAEL_128; - $val=_recaptcha_aes_pad($val); - return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); -} - - -function _recaptcha_mailhide_urlbase64 ($x) { - return strtr(base64_encode ($x), '+/', '-_'); -} - -/* gets the reCAPTCHA Mailhide url for a given email, public key and private key */ -function recaptcha_mailhide_url($pubkey, $privkey, $email) { - if ($pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) { - die ("To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " . - "you can do so at <a href='http://www.google.com/recaptcha/mailhide/apikey'>http://www.google.com/recaptcha/mailhide/apikey</a>"); - } - - - $ky = pack('H*', $privkey); - $cryptmail = _recaptcha_aes_encrypt ($email, $ky); - - return "http://www.google.com/recaptcha/mailhide/d?k=" . $pubkey . "&c=" . _recaptcha_mailhide_urlbase64 ($cryptmail); -} - -/** - * gets the parts of the email to expose to the user. - * eg, given johndoe@example,com return ["john", "example.com"]. - * the email is then displayed as john...@example.com - */ -function _recaptcha_mailhide_email_parts ($email) { - $arr = preg_split("/@/", $email ); - - if (strlen ($arr[0]) <= 4) { - $arr[0] = substr ($arr[0], 0, 1); - } else if (strlen ($arr[0]) <= 6) { - $arr[0] = substr ($arr[0], 0, 3); - } else { - $arr[0] = substr ($arr[0], 0, 4); - } - return $arr; -} - -/** - * Gets html to display an email address given a public an private key. - * to get a key, go to: - * - * http://www.google.com/recaptcha/mailhide/apikey - */ -function recaptcha_mailhide_html($pubkey, $privkey, $email) { - $emailparts = _recaptcha_mailhide_email_parts ($email); - $url = recaptcha_mailhide_url ($pubkey, $privkey, $email); - - return htmlentities($emailparts[0]) . "<a href='" . htmlentities ($url) . - "' onclick=\"window.open('" . htmlentities ($url) . "', '', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=500,height=300'); return false;\" title=\"Reveal this e-mail address\">...</a>@" . htmlentities ($emailparts [1]); - -} - - diff --git a/plugins/_depricated/recaptcha/templates/PluginLoginReCaptchaGroup.html b/plugins/_depricated/recaptcha/templates/PluginLoginReCaptchaGroup.html deleted file mode 100644 index 9af9b8aec4..0000000000 --- a/plugins/_depricated/recaptcha/templates/PluginLoginReCaptchaGroup.html +++ /dev/null @@ -1,18 +0,0 @@ -<div class="recaptcha-control-group" id="recaptcha-place" style="display: none"> - <div class="controls"> - <div id="recaptcha_image" style="border-radius: 3px"></div> - </div> - <div class="controls"> - <div class="input-append"> - - <input class="i18n input-block-level inputLoginForm inputCAPTCHA" type="text" autocomplete="off" - id="recaptcha_response_field" data-i18n-placeholder="PLUGIN/LABEL_ENTER_THE_WORDS_ABOVE" - data-i18n="[placeholder]PLUGIN/LABEL_ENTER_THE_WORDS_ABOVE" /> - - <span class="add-on"> - <i class="icon-repeat" onclick="Recaptcha.reload()" style="cursor: pointer"></i> - </span> - - </div> - </div> -</div> \ No newline at end of file diff --git a/plugins/add-x-originating-ip-header/README b/plugins/add-x-originating-ip-header/README deleted file mode 100644 index 4dfe7fc729..0000000000 --- a/plugins/add-x-originating-ip-header/README +++ /dev/null @@ -1 +0,0 @@ -Adds X-Originating-IP header to outgoing message, containing sender's IP address. \ No newline at end of file diff --git a/plugins/add-x-originating-ip-header/VERSION b/plugins/add-x-originating-ip-header/VERSION deleted file mode 100644 index ea710abb95..0000000000 --- a/plugins/add-x-originating-ip-header/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.2 \ No newline at end of file diff --git a/plugins/add-x-originating-ip-header/index.php b/plugins/add-x-originating-ip-header/index.php index 813d47f58c..4ae52229e8 100644 --- a/plugins/add-x-originating-ip-header/index.php +++ b/plugins/add-x-originating-ip-header/index.php @@ -1,40 +1,45 @@ -<?php - -class AddXOriginatingIpHeaderPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('filter.build-message', 'FilterBuildMessage'); - } - - /** - * @param \MailSo\Mime\Message $oMessage - */ - public function FilterBuildMessage(&$oMessage) - { - if ($oMessage instanceof \MailSo\Mime\Message) - { - $sIP = $this->Manager()->Actions()->Http()->GetClientIp( - !!$this->Config()->Get('plugin', 'check_proxy', false)); - - $oMessage->SetCustomHeader( - \MailSo\Mime\Enumerations\Header::X_ORIGINATING_IP, - $this->Manager()->Actions()->Http()->IsLocalhost($sIP) ? '127.0.0.1' : $sIP - ); - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('check_proxy') - ->SetLabel('Сheck User Proxy') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) - ->SetDescription('Enable, if you need to check proxy header') - ->SetDefaultValue(false) - ); - } -} \ No newline at end of file +<?php + +class AddXOriginatingIpHeaderPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'X-Originating-IP', + VERSION = '2.0', + DESCRIPTION = 'Adds X-Originating-IP header to outgoing message, containing sender\'s IP address.'; + + public function Init() : void + { + $this->addHook('filter.build-message', 'FilterBuildMessage'); + } + + /** + * @param \MailSo\Mime\Message $oMessage + */ + public function FilterBuildMessage(&$oMessage) + { + if ($oMessage instanceof \MailSo\Mime\Message) + { + $sIP = $this->Manager()->Actions()->Http()->GetClientIp( + !!$this->Config()->Get('plugin', 'check_proxy', false)); + + $oMessage->SetCustomHeader( + \MailSo\Mime\Enumerations\Header::X_ORIGINATING_IP, + $this->Manager()->Actions()->Http()->IsLocalhost($sIP) ? '127.0.0.1' : $sIP + ); + } + } + + /** + * @return array + */ + protected function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('check_proxy') + ->SetLabel('Сheck User Proxy') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDescription('Enable, if you need to check proxy header') + ->SetDefaultValue(false) + ); + } +} diff --git a/plugins/attachments-force-open/extension.js b/plugins/attachments-force-open/extension.js new file mode 100644 index 0000000000..8020ca6fa3 --- /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 0000000000..4110b78945 --- /dev/null +++ b/plugins/attachments-force-open/index.php @@ -0,0 +1,20 @@ +<?php + +class AttachmentsForceOpenPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Attachments force open', + AUTHOR = 'SnappyMail', + URL = 'https://github.com/the-djmaze/snappymail/pull/1489', + VERSION = '0.1', + RELEASE = '2024-03-15', + REQUIRED = '2.14.0', + CATEGORY = 'General', + LICENSE = 'MIT', + DESCRIPTION = ''; + + public function Init() : void + { + $this->addJs('extension.js'); // add js file + } +} diff --git a/plugins/auto-domain-grab/LICENSE b/plugins/auto-domain-grab/LICENSE index 325ab989b8..8a36e2657c 100644 --- a/plugins/auto-domain-grab/LICENSE +++ b/plugins/auto-domain-grab/LICENSE @@ -1,4 +1,4 @@ -This plugin, written by https://github.com/jas8522 +This extension, written by https://github.com/jas8522 and optimised by https://github.com/rhyswilliamsza https://github.com/rhysit diff --git a/plugins/auto-domain-grab/README b/plugins/auto-domain-grab/README index ad56f159e7..4d7e0f4959 100644 --- a/plugins/auto-domain-grab/README +++ b/plugins/auto-domain-grab/README @@ -1,14 +1,21 @@ What is it? -In essense, this plugin allows for multiple users across multiple servers to use one RainLoop installation, -all without the administrator needing to create each domain separately. This plugin detects the required -hostname from the inputted email address (for example, it would detect "example.com" as the hostname if -"info@example.com" is inputted). This hostname is then used for IMAP and SMTP communication. +In essense, this extension allows multiple users across multiple servers to +use one Snappymail installation without the administrator having to create +and configure individual domans. + +This extension detects the required hostname from a given email address (for +example, it would use "example.com" as the hostname if a user would +log in as "info@example.com"). + +This hostname is then used for IMAP and SMTP communication. How to Use: -1) Activate plugin -2) Visit Settings, and add a new wildcard domain "*" -3) Set your ports/ssl requirements as needed. These will not be changed by the plugin. -4) In the SMTP and/or IMAP host fields, place the single word "auto". This will be replaced when the plugin is active. -5) That's it! The plugin should now work! \ No newline at end of file +1) Activate extension +2) Go to settings, and add a new wildcard domain "*" +3) Set your ports/TLS/SSL configuration as needed. These settings will not be + altered by the extension. +4) Set the SMTP and/or IMAP host fields to "auto", this field will then be + auto-configured. +5) That's it! diff --git a/plugins/auto-domain-grab/VERSION b/plugins/auto-domain-grab/VERSION deleted file mode 100644 index b123147e2a..0000000000 --- a/plugins/auto-domain-grab/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 \ No newline at end of file diff --git a/plugins/auto-domain-grab/index.php b/plugins/auto-domain-grab/index.php index a2174926d2..ee24589484 100644 --- a/plugins/auto-domain-grab/index.php +++ b/plugins/auto-domain-grab/index.php @@ -1,77 +1,73 @@ <?php /** - * This plug-in automatically detects the IMAP and SMTP settings by extracting them from the email address itself. - * For example, user inputs: "info@example.com" - * This plugin sets the IMAP and SMTP host to "example.com" upon login, and then connects to it. + * This extension automatically detects the IMAP and SMTP settings by + * extracting them from the email address itself. For example, if the user + * attemps to login as 'info@example.com', then the IMAP and SMTP host would + * be set to to 'example.com'. * * Based on: - * https://github.com/RainLoop/rainloop-webmail/blob/master/plugins/override-smtp-credentials/index.php - * + * https://github.com/the-djmaze/snappymail/blob/master/plugins/override-smtp-credentials/index.php + * */ class AutoDomainGrabPlugin extends \RainLoop\Plugins\AbstractPlugin { - - private $imap_prefix = "mail."; - private $smtp_prefix = "mail."; - - public function Init() + const + NAME = 'Auto Domain Selection', + VERSION = '2.9', + RELEASE = '2022-11-11', + REQUIRED = '2.21.0', + CATEGORY = 'General', + DESCRIPTION = 'Sets the IMAP/SMTP host based on the user\'s login'; + + private $imap_prefix = 'mail.'; + private $smtp_prefix = 'mail.'; + + public function Init() : void { - $this->addHook('filter.smtp-credentials', 'FilterSmtpCredentials'); - $this->addHook('filter.imap-credentials', 'FilterImapCredentials'); + $this->addHook('smtp.before-connect', 'FilterSmtpCredentials'); + $this->addHook('imap.before-connect', 'FilterImapCredentials'); } /** - * This function detects the IMAP Host, and if it is set to "auto", replaces it with the MX or email domain. - * - * @param \RainLoop\Model\Account $oAccount - * @param array $aImapCredentials + * This function detects the IMAP Host, and if it is set to 'auto', replaces it with the MX or email domain. */ - public function FilterImapCredentials($oAccount, &$aImapCredentials) + public function FilterImapCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Imap\ImapClient $oImapClient, \MailSo\Imap\Settings $oSettings) { - if ($oAccount instanceof \RainLoop\Model\Account && \is_array($aImapCredentials)) + // Check for mail.$DOMAIN as entered value in RL settings + if ('auto' === $oSettings->host) { - // Check for mail.$DOMAIN as entered value in RL settings - if (!empty($aImapCredentials['Host']) && 'auto' === $aImapCredentials['Host']) + $domain = \substr(\strrchr($oAccount->Email(), '@'), 1); + $mxhosts = array(); + if (\getmxrr($domain, $mxhosts) && $mxhosts) { - $domain = substr(strrchr($oAccount->Email(), "@"), 1); - $mxhosts = array(); - if(getmxrr($domain, $mxhosts) && sizeof($mxhosts) > 0) - { - $aImapCredentials['Host'] = $mxhosts[0]; - } - else - { - $aImapCredentials['Host'] = $this->imap_prefix.$domain; - } + $oSettings->host = $mxhosts[0]; + } + else + { + $oSettings->host = $this->imap_prefix.$domain; } } } /** - * This function detects the SMTP Host, and if it is set to "auto", replaces it with the MX or email domain. - * - * @param \RainLoop\Model\Account $oAccount - * @param array $aSmtpCredentials + * This function detects the SMTP Host, and if it is set to 'auto', replaces it with the MX or email domain. */ - public function FilterSmtpCredentials($oAccount, &$aSmtpCredentials) + public function FilterSmtpCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, \MailSo\Smtp\Settings $oSettings) { - if ($oAccount instanceof \RainLoop\Model\Account && \is_array($aSmtpCredentials)) + // Check for mail.$DOMAIN as entered value in RL settings + if ('auto' === $oSettings->host) { - // Check for mail.$DOMAIN as entered value in RL settings - if (!empty($aSmtpCredentials['Host']) && 'auto' === $aSmtpCredentials['Host']) + $domain = \substr(\strrchr($oAccount->Email(), '@'), 1); + $mxhosts = array(); + if (\getmxrr($domain, $mxhosts) && $mxhosts) + { + $oSettings->host = $mxhosts[0]; + } + else { - $domain = substr(strrchr($oAccount->Email(), "@"), 1); - $mxhosts = array(); - if(getmxrr($domain, $mxhosts) && sizeof($mxhosts) > 0) - { - $aSmtpCredentials['Host'] = $mxhosts[0]; - } - else - { - $aSmtpCredentials['Host'] = $this->smtp_prefix.$domain; - } + $oSettings->host = $this->smtp_prefix . $domain; } } } diff --git a/plugins/custom-admin-settings-tab/LICENSE b/plugins/avatars/LICENSE similarity index 100% rename from plugins/custom-admin-settings-tab/LICENSE rename to plugins/avatars/LICENSE diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js new file mode 100644 index 0000000000..0c09bcbd63 --- /dev/null +++ b/plugins/avatars/avatars.js @@ -0,0 +1,252 @@ +(rl => { + + window.identiconSvg = (hash, txt, font) => { + // color defaults to last 7 chars as hue at 70% saturation, 50% brightness + // hsl2rgb adapted from: https://gist.github.com/aemkei/1325937 + let h = (parseInt(hash.substr(-7), 16) / 0xfffffff) * 6, s = 0.7, l = 0.5, + v = [ + l += s *= l < .5 ? l : 1 - l, + l - h % 1 * s * 2, + l -= s *= 2, + l, + l + h % 1 * s, + l + s + ], + m = txt ? 128 : 200, + color = 'rgb(' + [ + v[ ~~h % 6 ] * m, // red + v[ (h | 16) % 6 ] * m, // green + v[ (h | 8) % 6 ] * m // blue + ].map(Math.round).join(',') + ')'; + + if (txt) { + return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" version="1.1"> + <circle fill="${color}" width="${size}" height="${size}" cx="${size/2}" cy="${size/2}" r="${size/2}"/> + <text x="${size}%" y="${size}%" style="color:#FFF" alignment-baseline="middle" text-anchor="middle" + font-weight="bold" font-size="${Math.round(size*0.5)}" font-family="${font.replace(/"/g, "'")}" + dy=".1em" dominant-baseline="middle" fill="#FFF">${txt}</text> + </svg>`; + } + return `<svg version="1.1" width="412" height="412" viewBox="0 0 412 412" xmlns="http://www.w3.org/2000/svg"> + <path fill="${color}" d="m 404.4267,343.325 c -5.439,-16.32 -15.298,-32.782 -29.839,-42.362 -27.979,-18.572 -60.578,-28.479 -92.099,-39.085 -7.604,-2.664 -15.33,-5.568 -22.279,-9.7 -6.204,-3.686 -8.533,-11.246 -9.974,-17.886 -0.636,-3.512 -1.026,-7.116 -1.228,-10.661 22.857,-31.267 38.019,-82.295 38.019,-124.136 0,-65.298 -36.896,-83.495 -82.402,-83.495 -45.515,0 -82.403,18.17 -82.403,83.468 0,43.338 16.255,96.5 40.489,127.383 -0.221,2.438 -0.511,4.876 -0.95,7.303 -1.444,6.639 -3.77,14.058 -9.97,17.743 -6.957,4.133 -14.682,6.756 -22.287,9.42 -31.520996,10.605 -64.118996,19.957 -92.090996,38.529 -14.549,9.58 -24.403,27.159 -29.838,43.479 -5.597,16.938 -7.88600003,37.917 -7.54100003,54.917 H 205.9917 411.9657 c 0.348,-16.999 -1.946,-37.978 -7.539,-54.917 z"/> + </svg>`; + }; + + const + size = 50, + getEl = id => document.getElementById(id), + queue = [], + avatars = new Map, + ncAvatars = new Map, + identicons = new Map, + templateId = 'MailMessageView', + b64 = data => btoa(unescape(encodeURIComponent(data))), + b64url = data => b64(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), + 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), + email = msg => msg.from[0].email.toLowerCase(), + getAvatarUid = msg => `${getBimiId(msg)}/${email(msg)}`, + getAvatar = msg => ncAvatars.get(email(msg)) || avatars.get(getAvatarUid(msg)), + getAvatarUri = msg => { + if ('remote' === msg.avatar) { + msg.avatar = `?Avatar/${getBimiId(msg)}/${b64url(email(msg))}`; + } +/* + else if (!msg.avatar.startsWith('data:')) { + msg.avatar = null; + } +*/ + return msg.avatar; + }, + + hash = async txt => { + if (/^[0-9a-f]{15,}$/i.test(txt)) { + return txt; + } + const hashArray = Array.from(new Uint8Array( +// await crypto.subtle.digest('SHA-256', (new TextEncoder()).encode(txt.toLowerCase())) + await crypto.subtle.digest('SHA-1', (new TextEncoder()).encode(txt.toLowerCase())) + )); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string + }, + fromChars = from => +// (from.name?.split(/[^\p{Lu}]+/gu) || []).reduce((a, s) => a + (s || '')), '') + (from.name?.split(/[^\p{L}]+/gu) || []).reduce((a, s) => a + (s[0] || ''), '') + .slice(0,2) + .toUpperCase(), + setIdenticon = (msg, fn, cb) => { + const from = email(msg); + if (identicons.get(from)) { + fn(identicons.get(from)); + } else { + hash(from).then(hash => { + const uri = 'data:image/svg+xml;base64,' + b64(window.identiconSvg( + hash, + fromChars(msg.from[0]), + window.getComputedStyle(getEl('rl-app'), null).getPropertyValue('font-family') + )); + fn(uri); + identicons.set(email(msg), uri); + cb?.(uri); + }); + } + }, + + addQueue = (msg, fn) => { + if (msg.from?.[0]) { + if (getAvatarUri(msg)) { + if (rl.pluginSettingsGet('avatars', 'delay')) { + setIdenticon(msg, fn, ()=>{ + queue.push([msg, fn]); + runQueue(); + }); + } else { + fn(msg.avatar); + } + } else { + setIdenticon(msg, fn); + } + } + }, + runQueue = (() => { + let item = queue.shift(); + while (item) { + let url = getAvatar(item[0]); + if (url) { + item[1](url); + item = queue.shift(); + continue; + } else if (item[0].avatar) { + item[1](item[0].avatar); + } + runQueue(); + break; + } + }).debounce(1000); + + /** + * Modify templates + */ + getEl('MailMessageList').content.querySelectorAll('.messageCheckbox') + .forEach(el => el.append(Element.fromHTML(`<img class="fromPic" data-bind="fromPic:$data" loading="lazy">`))); + const messageItemHeader = getEl(templateId).content.querySelector('.messageItemHeader'); + if (messageItemHeader) { + messageItemHeader.prepend(Element.fromHTML( + `<img class="fromPic" data-bind="visible: viewUserPicVisible, attr: {'src': viewUserPic() }" loading="lazy">` + )); + } + + /** + * Loads images from Nextcloud contacts + */ +// rl.pluginSettingsGet('avatars', 'nextcloud'); + 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; + 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: '<x4:addressbook-query xmlns:x4="urn:ietf:params:xml:ns:carddav"><x0:prop xmlns:x0="DAV:"><x4:address-data><x4:prop name="EMAIL"/></x4:address-data><x3:has-photo xmlns:x3="http://nextcloud.com/ns"/></x0:prop></x4:addressbook-query>' + }) + .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), + fn = url=>{element.src = url}; + element.onerror = ()=>{ + element.onerror = null; + setIdenticon(msg, fn, uri=>avatars.set(getAvatarUid(msg), uri)); + }; + if (url) { + fn(url); + } else if (msg.avatar?.startsWith('data:')) { + fn(msg.avatar); + } else { + element.onload = ()=>{ + if (!element.src.startsWith('data:')) { + element.onload = null; + avatars.set(getAvatarUid(msg), element.src); + } + }; + addQueue(msg, fn); + } + } + } catch (e) { + console.error(e); + } + } + }; + + addEventListener('rl-view-model.create', e => { + if (templateId === e.detail.viewModelTemplateID) { + let view = e.detail; + view.viewUserPic = ko.observable(''); + view.viewUserPicVisible = ko.observable(false); + + view.message.subscribe(msg => { + view.viewUserPicVisible(false); + if (msg) { + let url = msg.from?.[0] ? getAvatar(msg) : 0, + fn = url => { + view.viewUserPic(url); + view.viewUserPicVisible(true); + }; + if (url) { + fn(url); + } else if (msg.avatar) { + fn(getAvatarUri(msg)); + } else { + addQueue(msg, fn); + } + } + }); + } + }); + +})(window.rl); diff --git a/plugins/avatars/identicon.js b/plugins/avatars/identicon.js new file mode 100644 index 0000000000..451f57b966 --- /dev/null +++ b/plugins/avatars/identicon.js @@ -0,0 +1,64 @@ + +(()=>{ + +const size = 50, + margin = 0.08; + +window.identiconSvg = (hash, txt, font) => { + // color defaults to last 7 chars as hue at 70% saturation, 50% brightness + // hsl2rgb adapted from: https://gist.github.com/aemkei/1325937 + let h = (parseInt(hash.substr(-7), 16) / 0xfffffff) * 6, s = 0.7, l = 0.5, + v = [ + l += s *= l < .5 ? l : 1 - l, + l - h % 1 * s * 2, + l -= s *= 2, + l, + l + h % 1 * s, + l + s + ], + m = txt ? 128 : 255, + color = 'rgb(' + [ + v[ ~~h % 6 ] * m, // red + v[ (h | 16) % 6 ] * m, // green + v[ (h | 8) % 6 ] * m // blue + ].map(Math.round).join(',') + ')'; + + if (txt) { + txt = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" version="1.1"> + <circle fill="${color}" width="${size}" height="${size}" cx="${size/2}" cy="${size/2}" r="${size/2}"/> + <text x="${size}%" y="${size}%" style="color:#FFF" alignment-baseline="middle" text-anchor="middle" + font-weight="bold" font-size="${Math.round(size*0.5)}" font-family="${font.replace(/"/g, "'")}" + dy=".1em" dominant-baseline="middle" fill="#FFF">${txt}</text> + </svg>`; + } else { + txt = `<path fill="${color}" d="m 404.4267,343.325 c -5.439,-16.32 -15.298,-32.782 -29.839,-42.362 -27.979,-18.572 -60.578,-28.479 -92.099,-39.085 -7.604,-2.664 -15.33,-5.568 -22.279,-9.7 -6.204,-3.686 -8.533,-11.246 -9.974,-17.886 -0.636,-3.512 -1.026,-7.116 -1.228,-10.661 22.857,-31.267 38.019,-82.295 38.019,-124.136 0,-65.298 -36.896,-83.495 -82.402,-83.495 -45.515,0 -82.403,18.17 -82.403,83.468 0,43.338 16.255,96.5 40.489,127.383 -0.221,2.438 -0.511,4.876 -0.95,7.303 -1.444,6.639 -3.77,14.058 -9.97,17.743 -6.957,4.133 -14.682,6.756 -22.287,9.42 -31.520996,10.605 -64.118996,19.957 -92.090996,38.529 -14.549,9.58 -24.403,27.159 -29.838,43.479 -5.597,16.938 -7.88600003,37.917 -7.54100003,54.917 H 205.9917 411.9657 c 0.348,-16.999 -1.946,-37.978 -7.539,-54.917 z"/>`; + + const cell = Math.floor((size - ((Math.floor(size * margin)) * 2)) / 5), + imargin = Math.floor((size - cell * 5) / 2), + rectangles = [], + add = (x, y) => rectangles.push("<rect x='" + (x * cell + imargin) + + "' y='" + (y * cell + imargin) + + "' width='" + cell + "' height='" + cell + "'/>"); + // the first 15 characters of the hash control the pixels (even/odd) + // they are drawn down the middle first, then mirrored outwards + for (let i = 0; i < 15; ++i) { + if (!(parseInt(hash.charAt(i), 16) % 2)) { + if (i < 5) { + add(2, i); + } else if (i < 10) { + add(1, (i - 5)); + add(3, (i - 5)); + } else { + add(0, (i - 10)); + add(4, (i - 10)); + } + } + } + txt = "<g style='fill:" + color + "; stroke:" + color + "; stroke-width:" + (size * 0.005) + ";'>" + + rectangles.join('') + + "</g>"; + } + return "<svg xmlns='http://www.w3.org/2000/svg' width='" + size + "' height='" + size + "'>" + txt + "</svg>"; +}; + +})(); diff --git a/plugins/avatars/images/services/amazon.com.png b/plugins/avatars/images/services/amazon.com.png new file mode 100644 index 0000000000..ec102e472a Binary files /dev/null and b/plugins/avatars/images/services/amazon.com.png differ diff --git a/plugins/avatars/images/services/apple.com.png b/plugins/avatars/images/services/apple.com.png new file mode 100644 index 0000000000..a787a45802 Binary files /dev/null and b/plugins/avatars/images/services/apple.com.png differ diff --git a/plugins/avatars/images/services/asana.com.png b/plugins/avatars/images/services/asana.com.png new file mode 100644 index 0000000000..17565906d7 Binary files /dev/null and b/plugins/avatars/images/services/asana.com.png differ diff --git a/rainloop/v/0.0.0/app/resources/images/services/battle.net.png b/plugins/avatars/images/services/battle.net.png similarity index 100% rename from rainloop/v/0.0.0/app/resources/images/services/battle.net.png rename to plugins/avatars/images/services/battle.net.png diff --git a/rainloop/v/0.0.0/app/resources/images/services/blizzard.com.png b/plugins/avatars/images/services/blizzard.com.png similarity index 100% rename from rainloop/v/0.0.0/app/resources/images/services/blizzard.com.png rename to plugins/avatars/images/services/blizzard.com.png diff --git a/plugins/avatars/images/services/dhl.com.png b/plugins/avatars/images/services/dhl.com.png new file mode 100644 index 0000000000..9969fed2b0 Binary files /dev/null and b/plugins/avatars/images/services/dhl.com.png differ diff --git a/plugins/avatars/images/services/disneyplus.com.png b/plugins/avatars/images/services/disneyplus.com.png new file mode 100644 index 0000000000..3609fb3e75 Binary files /dev/null and b/plugins/avatars/images/services/disneyplus.com.png differ diff --git a/plugins/avatars/images/services/ea.com.png b/plugins/avatars/images/services/ea.com.png new file mode 100644 index 0000000000..08ed67940b Binary files /dev/null and b/plugins/avatars/images/services/ea.com.png differ diff --git a/plugins/avatars/images/services/ebay.com.png b/plugins/avatars/images/services/ebay.com.png new file mode 100644 index 0000000000..943b60e9a1 Binary files /dev/null and b/plugins/avatars/images/services/ebay.com.png differ diff --git a/plugins/avatars/images/services/facebook.com.png b/plugins/avatars/images/services/facebook.com.png new file mode 100644 index 0000000000..ef15c9325a Binary files /dev/null and b/plugins/avatars/images/services/facebook.com.png differ diff --git a/plugins/avatars/images/services/github.com.png b/plugins/avatars/images/services/github.com.png new file mode 100644 index 0000000000..1c9d1f2f01 Binary files /dev/null and b/plugins/avatars/images/services/github.com.png differ diff --git a/plugins/avatars/images/services/google.com.png b/plugins/avatars/images/services/google.com.png new file mode 100644 index 0000000000..a6a717ce4a Binary files /dev/null and b/plugins/avatars/images/services/google.com.png differ diff --git a/plugins/avatars/images/services/linkedin.com.png b/plugins/avatars/images/services/linkedin.com.png new file mode 100644 index 0000000000..8756d6f0c2 Binary files /dev/null and b/plugins/avatars/images/services/linkedin.com.png differ diff --git a/plugins/avatars/images/services/microsoft.com.png b/plugins/avatars/images/services/microsoft.com.png new file mode 100644 index 0000000000..6283572655 Binary files /dev/null and b/plugins/avatars/images/services/microsoft.com.png differ diff --git a/plugins/avatars/images/services/onlive.com.png b/plugins/avatars/images/services/onlive.com.png new file mode 100644 index 0000000000..bfc038fea1 Binary files /dev/null and b/plugins/avatars/images/services/onlive.com.png differ diff --git a/plugins/avatars/images/services/paypal.com.png b/plugins/avatars/images/services/paypal.com.png new file mode 100644 index 0000000000..a678fa6cee Binary files /dev/null and b/plugins/avatars/images/services/paypal.com.png differ diff --git a/plugins/avatars/images/services/skype.com.png b/plugins/avatars/images/services/skype.com.png new file mode 100644 index 0000000000..74d6047349 Binary files /dev/null and b/plugins/avatars/images/services/skype.com.png differ diff --git a/plugins/avatars/images/services/steampowered.com.png b/plugins/avatars/images/services/steampowered.com.png new file mode 100644 index 0000000000..13c4d526f1 Binary files /dev/null and b/plugins/avatars/images/services/steampowered.com.png differ diff --git a/plugins/avatars/images/services/ted.com.png b/plugins/avatars/images/services/ted.com.png new file mode 100644 index 0000000000..0b1e14cdcc Binary files /dev/null and b/plugins/avatars/images/services/ted.com.png differ diff --git a/plugins/avatars/images/services/twitter.com.png b/plugins/avatars/images/services/twitter.com.png new file mode 100644 index 0000000000..b8582c00b7 Binary files /dev/null and b/plugins/avatars/images/services/twitter.com.png differ diff --git a/plugins/avatars/images/services/youtube.com.png b/plugins/avatars/images/services/youtube.com.png new file mode 100644 index 0000000000..5002d84392 Binary files /dev/null and b/plugins/avatars/images/services/youtube.com.png differ diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php new file mode 100644 index 0000000000..5e66ed3e56 --- /dev/null +++ b/plugins/avatars/index.php @@ -0,0 +1,423 @@ +<?php +/** + * You may store your own custom domain icons in `data/_data_/_default_/avatars/` + * Like: `data/_data_/_default_/avatars/snappymail.eu.svg` + */ + +class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Avatars', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '1.21', + RELEASE = '2024-10-28', + REQUIRED = '2.33.0', + CATEGORY = 'Contacts', + LICENSE = 'MIT', + DESCRIPTION = 'Show graphic of sender in message and messages list (supports BIMI, Gravatar, favicon and identicon, Contacts is still TODO)'; + + public function Init() : void + { + $this->addCss('style.css'); + $this->addJs('avatars.js'); + $this->addJsonHook('Avatar', 'DoAvatar'); + $this->addPartHook('Avatar', 'ServiceAvatar'); + $identicon = $this->Config()->Get('plugin', 'identicon', ''); + if ($identicon && \is_file(__DIR__ . "/{$identicon}.js")) { + $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) + || $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; + } + } + + public function JsonMessageList(array &$aResponse) + { + if (!empty($aResponse['Result']['@Collection'])) { + foreach ($aResponse['Result']['@Collection'] as &$message) { + if ($icon = $this->JsonAvatar($message)) { + $message['avatar'] = $icon; + } + } + } + } + + private function JsonAvatar($message) : ?string + { + $mFrom = empty($message['from'][0]) ? null : $message['from'][0]; + if ($mFrom instanceof \MailSo\Mime\Email) { + $mFrom = $mFrom->jsonSerialize(); + } + if (\is_array($mFrom)) { + 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', 'favicon', false) && 'pass' == $mFrom['dkimStatus']) + ) + ) { + return 'remote'; + } + 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; + } + + /** + * POST method handling + */ + public function DoAvatar() : array + { + $bBimi = !empty($this->jsonParam('bimi')); + $sBimiSelector = $this->jsonParam('bimiSelector') ?: ''; + $sEmail = $this->jsonParam('email'); + $aResult = $this->getAvatar($sEmail, $bBimi, $sBimiSelector); + if ($aResult) { + $aResult = [ + 'type' => $aResult[0], + 'data' => \base64_encode($aResult[1]) + ]; + } + return $this->jsonResponse(__FUNCTION__, $aResult); + } + + /** + * GET /?Avatar/${bimi}/Encoded(${from.email}) + * Nextcloud Mail uses insecure unencoded 'index.php/apps/mail/api/avatars/url/local%40example.com' + */ +// public function ServiceAvatar(...$aParts) + public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEncodedEmail) + { + $maxAge = 86400; + $sEmail = \MailSo\Base\Utils::UrlSafeBase64Decode($sEncodedEmail); + $aBimi = \explode('-', $sBimi, 2); + $sBimiSelector = isset($aBimi[1]) ? $aBimi[1] : 'default'; +// $sEmail && \MailSo\Base\Http::setETag("{$sBimiSelector}-{$sEncodedEmail}"); + 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 { + \MailSo\Base\Http::StatusHeader(404); + } + exit; + } + + protected function configMapping() : array + { + $group = new \RainLoop\Plugins\PropertyCollection('Lookup'); + $group->exchangeArray([ + \RainLoop\Plugins\Property::NewInstance('delay')->SetLabel('Delay lookup') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetAllowedInJs(true) + ->SetDefaultValue(true), + \RainLoop\Plugins\Property::NewInstance('bimi')->SetLabel('BIMI') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->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) + ->SetDefaultValue(false) + ->SetDescription('https://wikipedia.org/wiki/Gravatar'), + ]); + $aResult = array( + defined('RainLoop\\Enumerations\\PluginPropertyType::SELECT') + ? \RainLoop\Plugins\Property::NewInstance('identicon')->SetLabel('Identicon') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECT) + ->SetDefaultValue([ + ['id' => '', 'name' => 'Name characters else silhouette'], + ['id' => 'identicon', 'name' => 'Name characters else squares'], + ['id' => 'jdenticon', 'name' => 'Triangles shape'] + ]) + ->SetDescription('https://wikipedia.org/wiki/Identicon') + : \RainLoop\Plugins\Property::NewInstance('identicon')->SetLabel('Identicon') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) + ->SetDefaultValue(['','identicon','jdenticon']) + ->SetDescription('empty = default, identicon = squares, jdenticon = Triangles shape') + , + \RainLoop\Plugins\Property::NewInstance('service')->SetLabel('Preload valid domain icons') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetAllowedInJs(true) + ->SetDefaultValue(true) + ->SetDescription('DKIM header must be valid and icon is found in avatars/images/services directory'), + $group + ); +/* + if (\class_exists('OC') && isset(\OC::$server)) { + $aResult[] = \RainLoop\Plugins\Property::NewInstance('nextcloud')->SetLabel('Lookup Nextcloud Contacts') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) +// ->SetAllowedInJs(true) + ->SetDefaultValue(false); + } +*/ + return $aResult; + } + + private static function getServicePng(string $sDomain) : ?string + { + $aServices = [ + "services/{$sDomain}", + 'services/' . static::serviceDomain($sDomain) + ]; + foreach ($aServices as $service) { + $file = __DIR__ . "/images/{$service}.png"; + if (\file_exists($file)) { + return $file; + } + } + return null; + } + + // Only allow service icon when DKIM is valid. $bBimi is true when DKIM is valid. + private static function getServiceIcon(string $sEmail) : ?string + { + $aParts = \explode('@', $sEmail); + $file = static::getServicePng(\array_pop($aParts)); + if ($file) { + return 'data:image/png;base64,' . \base64_encode(\file_get_contents($file)); + } + + $aResult = static::getCachedImage($sEmail); + if ($aResult) { + return 'data:'.$aResult[0].';base64,' . \base64_encode($aResult[1]); + } + + return null; + } + + private function getAvatar(string $sEmail, bool $bBimi, string $sBimiSelector = '') : ?array + { + if (!\strpos($sEmail, '@')) { + return null; + } + + $sAsciiEmail = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail)); + $sEmailId = \sha1($sAsciiEmail); + + \MailSo\Base\Http::setETag($sEmailId); + \header('Cache-Control: private'); +// \header('Expires: '.\gmdate('D, j M Y H:i:s', \time() + 86400).' UTC'); + + $aResult = static::getCachedImage($sEmail); + if ($aResult) { + return $aResult; + } + + // TODO: lookup contacts vCard and return PHOTO value + /* + if (!$aResult) { + $oActions = \RainLoop\Api::Actions(); + $oAccount = $oActions->getAccountFromToken(); + if ($oAccount) { + $oAddressBookProvider = $oActions->AddressBookProvider($oAccount); + if ($oAddressBookProvider) { + $oContact = $oAddressBookProvider->GetContactByEmail($sEmail); + if ($oContact && $oContact->vCard && $oContact->vCard['PHOTO']) { + $aResult = [ + 'text/vcard', + $oContact->vCard + ]; + } + } + } + } + */ + + if (!$aResult) { + $sDomain = \explode('@', $sEmail); + $sDomain = \array_pop($sDomain); + + $aUrls = []; + + if ($this->Config()->Get('plugin', 'bimi', false)) { + $BIMI = $bBimi ? \SnappyMail\DNS::BIMI($sDomain, $sBimiSelector) : null; + if ($BIMI) { + $aUrls[] = $BIMI; +// $aResult = ['text/uri-list', $BIMI]; + \SnappyMail\Log::debug('Avatar', "BIMI {$sDomain}: {$BIMI}"); + } else { + \SnappyMail\Log::notice('Avatar', "BIMI 404 for {$sDomain}"); + } + } + + if ($this->Config()->Get('plugin', 'gravatar', false)) { + $aUrls[] = 'https://gravatar.com/avatar/'.\md5(\strtolower($sAsciiEmail)).'?s=80&d=404'; + } + + foreach ($aUrls as $sUrl) { + if ($aResult = static::getUrl($sUrl)) { + break; + } + } + } + + if ($aResult) { + static::cacheImage($sEmail, $aResult); + } + + // Only allow service icon when DKIM is valid. $bBimi is true when DKIM is valid. + if ($bBimi && !$aResult) { + $file = static::getServicePng($sDomain); + if ($file) { + \MailSo\Base\Http::setLastModified(\filemtime($file)); + $aResult = [ + 'image/png', + \file_get_contents($file) + ]; + } + + if (!$aResult && $this->Config()->Get('plugin', 'favicon', false)) { + $aResult = static::getFavicon($sDomain); + } + } + + return $aResult; + } + + private static function serviceDomain(string $sDomain) : string + { + $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 + { + 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] + ); + \MailSo\Base\Http::setLastModified(\time()); + } + + private static function getCachedImage(string $sEmail) : ?array + { + $sEmail = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail)); + $aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sEmail}.*"); + 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}.*"); + } + } + if ($aFiles) { + return [ + \SnappyMail\File\MimeType::fromFile($aFiles[0]), + \file_get_contents($aFiles[0]) + ]; + } + return null; + } + + 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') + ?: 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? + + <link sizes="16x16" rel="shortcut icon" type="image/x-icon" href="/..." /> + <link sizes="16x16" rel="shortcut icon" type="image/png" href="/..." /> + <link sizes="32x32" rel="shortcut icon" type="image/png" href="/..." /> + <link sizes="96x96" rel="shortcut icon" type="image/png" href="/..." /> + + <link sizes="36x36" rel="icon" type="image/png" href="/..." /> + <link sizes="48x48" rel="icon" type="image/png" href="/..." /> + <link sizes="72x72" rel="icon" type="image/png" href="/..." /> + <link sizes="96x96" rel="icon" type="image/png" href="/..." /> + <link sizes="144x144" rel="icon" type="image/png" href="/..." /> + <link sizes="192x192" rel="icon" type="image/png" href="/..." /> + + <link sizes="57x57" rel="apple-touch-icon" type="image/png" href="/..." /> + <link sizes="60x60" rel="apple-touch-icon" type="image/png" href="/..." /> + <link sizes="72x72" rel="apple-touch-icon" type="image/png" href="/..." /> + <link sizes="76x76" rel="apple-touch-icon" type="image/png" href="/..." /> + <link sizes="114x114" rel="apple-touch-icon" type="image/png" href="/..." /> + <link sizes="120x120" rel="apple-touch-icon" type="image/png" href="/..." /> + <link sizes="144x144" rel="apple-touch-icon" type="image/png" href="/..." /> + <link sizes="152x152" rel="apple-touch-icon" type="image/png" href="/" /> + <link sizes="180x180" rel="apple-touch-icon" type="image/png" href="/..." /> + <link sizes="192x192" rel="apple-touch-icon" type="image/png" href="/..." /> +*/ + if ($aResult) { + static::cacheImage($sDomain, $aResult); + } + return $aResult; + } + + private static function getUrl(string $sUrl) : ?array + { + $oHTTP = \SnappyMail\HTTP\Request::factory(/*'socket' or 'curl'*/); + $oHTTP->proxy = \RainLoop\Api::Config()->Get('labs', 'curl_proxy', ''); + $oHTTP->proxy_auth = \RainLoop\Api::Config()->Get('labs', 'curl_proxy_auth', ''); + $oHTTP->max_response_kb = 0; + $oHTTP->timeout = 15; // timeout in seconds. + try { + $oResponse = $oHTTP->doRequest('GET', $sUrl); + if ($oResponse) { + if (200 === $oResponse->status && \str_starts_with($oResponse->getHeader('content-type'), 'image/')) { + return [ + $oResponse->getHeader('content-type'), + $oResponse->body + ]; + } + \SnappyMail\Log::notice('Avatar', "error {$oResponse->status} for {$sUrl}"); + } else { + \SnappyMail\Log::warning('Avatar', "failed for {$sUrl}"); + } + } catch (\Throwable $e) { + \SnappyMail\Log::notice('Avatar', "error {$e->getMessage()}"); + } + return null; + } +} diff --git a/plugins/avatars/jdenticon.js b/plugins/avatars/jdenticon.js new file mode 100644 index 0000000000..6652881a5c --- /dev/null +++ b/plugins/avatars/jdenticon.js @@ -0,0 +1,677 @@ +/** + * Jdenticon 3.2.0 + * http://jdenticon.com + * + * Built: 2022-08-07T11:23:11.640Z + * + * MIT License + * + * Copyright (c) 2014-2021 Daniel Mester Pirttijärvi + * + * 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. + */ + +(()=>{ +/** + * Parses a substring of the hash as a number. + * @param {number} startPosition + * @param {number=} octets + */ + +class HSLColor +{ + constructor(h, s, l) + { + this.h = h; + this.s = s; + this.l = l; +// this.a = a; + } + + toString() + { + const h = this.h, s = this.s, l = this.l/*, a = this.a*/, + hex = v => { + v = between(0, 255, v * 255).toString(16); + return v.slice(0,2).padStart(2, "0"); + }; + let r=l, g=l, b=l; + if (0 != s) { + let q=l<0.5?l*(s+1):l+s-l*s, + p=l*2-q, + hue2rgb = h => { + h += h<0 ? 1 : (h>1 ? -1 : 0); + return (h*6<1) ? (p+(q-p)*h*6) : ((h*2<1) ? q : ((h*3<2) ? p+(q-p)*(2/3-h)*6 : p)); + }; + r = hue2rgb(h+1/3); + g = hue2rgb(h); + b = hue2rgb(h-1/3); + } + return '#' + hex(r) + hex(g) + hex(b) // isNaN(a)?100:a*100); + } +} + +const + between = (l, h, v) => Math.max(l, Math.min(h, v)), + + parseHex = (hash, startPosition, octets) => parseInt(hash.substr(startPosition, octets), 16), + + /** + * Converts an HSL color to a hexadecimal RGB color. This function will correct the lightness for the "dark" hues + * @param {number} hue Hue in range [0, 1] + * @param {number} saturation Saturation in range [0, 1] + * @param {number} lightness Lightness in range [0, 1] + * @returns {string} + */ + correctedHsl = (hue, saturation, lightness) => { + // The corrector specifies the perceived middle lightness for each hue + var correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ], + corrector = correctors[(hue * 6 + 0.5) | 0]; + + // Adjust the input lightness relative to the corrector + lightness = lightness < 0.5 ? lightness * corrector * 2 : corrector + (lightness - 0.5) * (1 - corrector) * 2; + + return new HSLColor(hue, saturation, lightness).toString(); + }; + +/** + * Represents a point. + */ +class Point +{ + constructor(x, y) { + this.x = x; + this.y = y; + } +} + +/** + * Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself, + * but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly. + */ +class Transform +{ + constructor(x, y, size, rotation) { + this.u/*_x*/ = x; + this.v/*_y*/ = y; + this.K/*_size*/ = size; + this.Z/*_rotation*/ = rotation; + } + + /** + * Transforms the specified point based on the translation and rotation specification for this Transform. + * @param {number} x x-coordinate + * @param {number} y y-coordinate + * @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. + * @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. + */ + L/*transformIconPoint*/(x, y, w, h) { + var right = this.u/*_x*/ + this.K/*_size*/, + bottom = this.v/*_y*/ + this.K/*_size*/, + rotation = this.Z/*_rotation*/; + return rotation === 1 ? new Point(right - y - (h || 0), this.v/*_y*/ + x) : + rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) : + rotation === 3 ? new Point(this.u/*_x*/ + y, bottom - x - (w || 0)) : + new Point(this.u/*_x*/ + x, this.v/*_y*/ + y); + } +} + +var NO_TRANSFORM = new Transform(0, 0, 0, 0); + +/** + * Provides helper functions for rendering common basic shapes. + */ +class Graphics +{ + constructor(renderer) { + /** + * @type {Renderer} + * @private + */ + this.M/*_renderer*/ = renderer; + + /** + * @type {Transform} + */ + this.A/*currentTransform*/ = NO_TRANSFORM; + } + + /** + * Adds a polygon to the underlying renderer. + * @param {Array<number>} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ] + * @param {boolean=} invert Specifies if the polygon will be inverted. + */ + g/*addPolygon*/(points, invert) { + var this$1 = this; + + var di = invert ? -2 : 2, + transformedPoints = []; + + for (var i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) { + transformedPoints.push(this$1.A/*currentTransform*/.L/*transformIconPoint*/(points[i], points[i + 1])); + } + + this.M/*_renderer*/.g/*addPolygon*/(transformedPoints); + } + + /** + * Adds a polygon to the underlying renderer. + * Source: http://stackoverflow.com/a/2173084 + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse. + * @param {number} size The size of the ellipse. + * @param {boolean=} invert Specifies if the ellipse will be inverted. + */ + h/*addCircle*/(x, y, size, invert) { + var p = this.A/*currentTransform*/.L/*transformIconPoint*/(x, y, size, size); + this.M/*_renderer*/.h/*addCircle*/(p, size, invert); + } + + /** + * Adds a rectangle to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle. + * @param {number} y The y-coordinate of the upper left corner of the rectangle. + * @param {number} w The width of the rectangle. + * @param {number} h The height of the rectangle. + * @param {boolean=} invert Specifies if the rectangle will be inverted. + */ + i/*addRectangle*/(x, y, w, h, invert) { + this.g/*addPolygon*/([ + x, y, + x + w, y, + x + w, y + h, + x, y + h + ], invert); + } + + /** + * Adds a right triangle to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle. + * @param {number} w The width of the triangle. + * @param {number} h The height of the triangle. + * @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle. + * @param {boolean=} invert Specifies if the triangle will be inverted. + */ + j/*addTriangle*/(x, y, w, h, r, invert) { + var points = [ + x + w, y, + x + w, y + h, + x, y + h, + x, y + ]; + points.splice(((r || 0) % 4) * 2, 2); + this.g/*addPolygon*/(points, invert); + } + + /** + * Adds a rhombus to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus. + * @param {number} w The width of the rhombus. + * @param {number} h The height of the rhombus. + * @param {boolean=} invert Specifies if the rhombus will be inverted. + */ + N/*addRhombus*/(x, y, w, h, invert) { + this.g/*addPolygon*/([ + x + w / 2, y, + x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2 + ], invert); + } +} + +/** + * @param {number} index + * @param {Graphics} g + * @param {number} cell + * @param {number} positionIndex + */ +function centerShape(index, g, cell, positionIndex) { + index = index % 14; + + var k, m, w, h, inner, outer; + + !index ? ( + k = cell * 0.42, + g.g/*addPolygon*/([ + 0, 0, + cell, 0, + cell, cell - k * 2, + cell - k, cell, + 0, cell + ])) : + + index == 1 ? ( + w = 0 | (cell * 0.5), + h = 0 | (cell * 0.8), + + g.j/*addTriangle*/(cell - w, 0, w, h, 2)) : + + index == 2 ? ( + w = 0 | (cell / 3), + g.i/*addRectangle*/(w, w, cell - w, cell - w)) : + + index == 3 ? ( + inner = cell * 0.1, + // Use fixed outer border widths in small icons to ensure the border is drawn + outer = + cell < 6 ? 1 : + cell < 8 ? 2 : + (0 | (cell * 0.25)), + + inner = + inner > 1 ? (0 | inner) : // large icon => truncate decimals + inner > 0.5 ? 1 : // medium size icon => fixed width + inner, // small icon => anti-aliased border + + g.i/*addRectangle*/(outer, outer, cell - inner - outer, cell - inner - outer)) : + + index == 4 ? ( + m = 0 | (cell * 0.15), + w = 0 | (cell * 0.5), + g.h/*addCircle*/(cell - w - m, cell - w - m, w)) : + + index == 5 ? ( + inner = cell * 0.1, + outer = inner * 4, + + // Align edge to nearest pixel in large icons + outer > 3 && (outer = 0 | outer), + + g.i/*addRectangle*/(0, 0, cell, cell), + g.g/*addPolygon*/([ + outer, outer, + cell - inner, outer, + outer + (cell - outer - inner) / 2, cell - inner + ], true)) : + + index == 6 ? + g.g/*addPolygon*/([ + 0, 0, + cell, 0, + cell, cell * 0.7, + cell * 0.4, cell * 0.4, + cell * 0.7, cell, + 0, cell + ]) : + + index == 7 ? + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : + + index == 8 ? ( + g.i/*addRectangle*/(0, 0, cell, cell / 2), + g.i/*addRectangle*/(0, cell / 2, cell / 2, cell / 2), + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 1)) : + + index == 9 ? ( + inner = cell * 0.14, + // Use fixed outer border widths in small icons to ensure the border is drawn + outer = + cell < 4 ? 1 : + cell < 6 ? 2 : + (0 | (cell * 0.35)), + + inner = + cell < 8 ? inner : // small icon => anti-aliased border + (0 | inner), // large icon => truncate decimals + + g.i/*addRectangle*/(0, 0, cell, cell), + g.i/*addRectangle*/(outer, outer, cell - outer - inner, cell - outer - inner, true)) : + + index == 10 ? ( + inner = cell * 0.12, + outer = inner * 3, + + g.i/*addRectangle*/(0, 0, cell, cell), + g.h/*addCircle*/(outer, outer, cell - inner - outer, true)) : + + index == 11 ? + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : + + index == 12 ? ( + m = cell * 0.25, + g.i/*addRectangle*/(0, 0, cell, cell), + g.N/*addRhombus*/(m, m, cell - m, cell - m, true)) : + + // 13 + ( + !positionIndex && ( + m = cell * 0.4, w = cell * 1.2, + g.h/*addCircle*/(m, m, w) + ) + ); +} + +/** + * @param {number} index + * @param {Graphics} g + * @param {number} cell + */ +function outerShape(index, g, cell) { + index = index % 4; + + var m; + + !index ? + g.j/*addTriangle*/(0, 0, cell, cell, 0) : + + index == 1 ? + g.j/*addTriangle*/(0, cell / 2, cell, cell / 2, 0) : + + index == 2 ? + g.N/*addRhombus*/(0, 0, cell, cell) : + + // 3 + ( + m = cell / 6, + g.h/*addCircle*/(m, m, cell - 2 * m) + ); +} + +/** + * Prepares a measure to be used as a measure in an SVG path, by + * rounding the measure to a single decimal. This reduces the file + * size of the generated SVG with more than 50% in some cases. + */ +function svgValue(value) { + return ((value * 10 + 0.5) | 0) / 10; +} + +/** + * Represents an SVG path element. + */ +class SvgPath +{ + constructor() { + /** + * This property holds the data string (path.d) of the SVG path. + * @type {string} + */ + this.B/*dataString*/ = ""; + } + + /** + * Adds a polygon with the current fill color to the SVG path. + * @param points An array of Point objects. + */ + g/*addPolygon*/(points) { + var dataString = ""; + for (var i = 0; i < points.length; i++) { + dataString += (i ? "L" : "M") + svgValue(points[i].x) + " " + svgValue(points[i].y); + } + this.B/*dataString*/ += dataString + "Z"; + } + + /** + * Adds a circle with the current fill color to the SVG path. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ + h/*addCircle*/(point, diameter, counterClockwise) { + var sweepFlag = counterClockwise ? 0 : 1, + svgRadius = svgValue(diameter / 2), + svgDiameter = svgValue(diameter), + svgArc = "a" + svgRadius + "," + svgRadius + " 0 1," + sweepFlag + " "; + + this.B/*dataString*/ += + "M" + svgValue(point.x) + " " + svgValue(point.y + diameter / 2) + + svgArc + svgDiameter + ",0" + + svgArc + (-svgDiameter) + ",0"; + } +} + + +/** + * Renderer producing SVG output. + * @implements {Renderer} + */ +class SvgRenderer +{ + constructor(target) { + /** + * @type {SvgPath} + * @private + */ + this.C/*_path*/; + + /** + * @type {Object.<string,SvgPath>} + * @private + */ + this.D/*_pathsByColor*/ = { }; + + /** + * @type {SvgElement|SvgWriter} + * @private + */ + this.R/*_target*/ = target; + + /** + * @type {number} + */ + this.k/*iconSize*/ = target.k/*iconSize*/; + } + + /** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb[aa]. + */ + m/*setBackground*/(fillColor) { + var match = /^(#......)(..)?/.exec(fillColor), + opacity = match[2] ? parseHex(match[2], 0) / 255 : 1; + this.R/*_target*/.m/*setBackground*/(match[1], opacity); + } + + /** + * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. + * @param {string} color Fill color on format #xxxxxx. + */ + O/*beginShape*/(color) { + this.C/*_path*/ = this.D/*_pathsByColor*/[color] || (this.D/*_pathsByColor*/[color] = new SvgPath()); + } + + /** + * Marks the end of the currently drawn shape. + */ + P/*endShape*/() { } + + /** + * Adds a polygon with the current fill color to the SVG. + * @param points An array of Point objects. + */ + g/*addPolygon*/(points) { + this.C/*_path*/.g/*addPolygon*/(points); + } + + /** + * Adds a circle with the current fill color to the SVG. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ + h/*addCircle*/(point, diameter, counterClockwise) { + this.C/*_path*/.h/*addCircle*/(point, diameter, counterClockwise); + } + + /** + * Called when the icon has been completely drawn. + */ + finish() { + var pathsByColor = this.D/*_pathsByColor*/; + for (var color in pathsByColor) { + // hasOwnProperty cannot be shadowed in pathsByColor + // eslint-disable-next-line no-prototype-builtins + if (pathsByColor.hasOwnProperty(color)) { + this.R/*_target*/.S/*appendPath*/(color, pathsByColor[color].B/*dataString*/); + } + } + } +} + +/** + * Renderer producing SVG output. + */ +class SvgWriter +{ + constructor(iconSize) { + /** + * @type {number} + */ + this.k/*iconSize*/ = iconSize; + + /** + * @type {string} + * @private + */ + this.F = + '<svg xmlns="http://www.w3.org/2000/svg" width="' + + iconSize + '" height="' + iconSize + '" viewBox="0 0 ' + + iconSize + ' ' + iconSize + '">'; + } + + /** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb. + * @param {number} opacity Opacity in the range [0.0, 1.0]. + */ + m/*setBackground*/(fillColor, opacity) { + if (opacity) { + this.F += '<rect width="100%" height="100%" fill="' + + fillColor + '" opacity="' + opacity.toFixed(2) + '"/>'; + } + } + + /** + * Writes a path to the SVG string. + * @param {string} color Fill color on format #rrggbb. + * @param {string} dataString The SVG path data string. + */ + S/*appendPath*/(color, dataString) { + this.F += '<path fill="' + color + '" d="' + dataString + '"/>'; + } + + /** + * Gets the rendered image as an SVG string. + */ + toString() { + return this.F + "</svg>"; + } +} + +/** + * Draws an identicon as an SVG string. + * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. + * @returns {string} SVG string + */ +window.identiconSvg = hash => { + const writer = new SvgWriter(50), + renderer = new SvgRenderer(writer), + config = { + p/*colorSaturation*/: 0.5, + H/*grayscaleSaturation*/: 0, + q/*colorLightness*/: value => between(0, 1, 0.4 + value * (0.8 - 0.4)), + I/*grayscaleLightness*/: value => between(0, 1, 0.3 + value * (0.9 - 0.3)) + }; + + var index; + + // Calculate padding and round to nearest integer + var size = renderer.k/*iconSize*/; + var padding = (0.5 + size * 0.08) | 0; + size -= padding * 2; + + const graphics = new Graphics(renderer), + + // Calculate cell size and ensure it is an integer + cell = 0 | (size / 4), + + // Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon + s = 0 | (padding + size / 2 - cell * 2), + + // AVAILABLE COLORS + hue = parseHex(hash, -7) / 0xfffffff, + + // Available colors for this icon + availableColors = [ + // Dark gray + correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(0)), + // Mid color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0.5)), + // Light gray + correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(1)), + // Light color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(1)), + // Dark color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0)) + ], + + // The index of the selected colors + selectedColorIndexes = [], + + isDuplicate = values => { + if (values.indexOf(index) >= 0) { + for (var i = 0; i < values.length; i++) { + if (selectedColorIndexes.indexOf(values[i]) >= 0) { + return true; + } + } + } + }, + + renderShape = (colorIndex, shapes, index, rotationIndex, positions) => { + var shapeIndex = parseHex(hash, index, 1); + var r = rotationIndex ? parseHex(hash, rotationIndex, 1) : 0; + + renderer.O/*beginShape*/(availableColors[selectedColorIndexes[colorIndex]]); + + for (var i = 0; i < positions.length; i++) { + graphics.A/*currentTransform*/ + = new Transform(s + positions[i][0] * cell, s + positions[i][1] * cell, cell, r++ % 4); + shapes(shapeIndex, graphics, cell, i); + } + + renderer.P/*endShape*/(); + }; + + for (var i = 0; i < 3; i++) { + index = parseHex(hash, 8 + i, 1) % availableColors.length; + if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo + isDuplicate([2, 3])) { // Disallow light gray and light color combo + index = 1; + } + selectedColorIndexes.push(index); + } + + // ACTUAL RENDERING + // Sides + renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]); + // Corners + renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]); + // Center + renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]); + + renderer.finish(); + + return writer; +}; + +})(); diff --git a/plugins/avatars/style.css b/plugins/avatars/style.css new file mode 100644 index 0000000000..2cf69ccd01 --- /dev/null +++ b/plugins/avatars/style.css @@ -0,0 +1,27 @@ +.messageView .fromPic { + float: left; + width: 50px; + height: 50px; + margin: 0 4px 0 0; + padding: 1px; + border-radius: var(--border-radius, 5px); +} +/* +.checkboxMessage { + background: #000 no-repeat center / contain; + background: #000 no-repeat right / 32px; + width:68px +} +*/ +.messageCheckbox .fromPic { + margin: 0 -0.5em -0.4em 0.4em; + height: 1.5em; + width: 1.5em; +} +[dir="rtl"] .messageView .fromPic { + float: right; + margin: 0 0 0 4px; +} +[dir="rtl"] .messageCheckbox .fromPic { + margin: 0 0.4em -0.4em -0.5em; +} diff --git a/plugins/backup/LICENSE b/plugins/backup/LICENSE new file mode 100644 index 0000000000..f709b02e27 --- /dev/null +++ b/plugins/backup/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +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 +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/backup/index.php b/plugins/backup/index.php new file mode 100644 index 0000000000..1dc65f4752 --- /dev/null +++ b/plugins/backup/index.php @@ -0,0 +1,95 @@ +<?php + +class BackupPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Backup', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '1.2', + RELEASE = '2024-03-18', + REQUIRED = '2.30.0', + CATEGORY = 'General', + LICENSE = 'MIT', + DESCRIPTION = ''; + + public function Init() : void + { + // Admin Settings tab + $this->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 0000000000..221b5d1f05 --- /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/README b/plugins/black-list/README deleted file mode 100644 index 86ca74eb97..0000000000 --- a/plugins/black-list/README +++ /dev/null @@ -1 +0,0 @@ -Simple black list plugin \ No newline at end of file diff --git a/plugins/black-list/VERSION b/plugins/black-list/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/black-list/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/black-list/index.php b/plugins/black-list/index.php index db77846c98..dea7e90edb 100644 --- a/plugins/black-list/index.php +++ b/plugins/black-list/index.php @@ -2,29 +2,33 @@ class BlackListPlugin extends \RainLoop\Plugins\AbstractPlugin { - public function Init() + const + NAME = 'Blacklist', + 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('filter.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 + ); } } } @@ -32,13 +36,13 @@ public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword) /** * @return array */ - public function configMapping() + protected function configMapping() : array { return array( \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/plugins/cache-apcu/APCU.php b/plugins/cache-apcu/APCU.php new file mode 100644 index 0000000000..8ca581ca05 --- /dev/null +++ b/plugins/cache-apcu/APCU.php @@ -0,0 +1,61 @@ +<?php + +/* + * This file is part of MailSo. + * + * (c) 2014 Usenko Timur + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace MailSo\Cache\Drivers; + +/** + * @category MailSo + * @package Cache + * @subpackage Drivers + */ +class APCU implements \MailSo\Cache\DriverInterface +{ + private string $sKeyPrefix; + + public function setPrefix(string $sKeyPrefix) : void + { + $sKeyPrefix = \rtrim(\trim($sKeyPrefix), '\\/'); + $this->sKeyPrefix = empty($sKeyPrefix) + ? $sKeyPrefix + : \preg_replace('/[^a-zA-Z0-9_]/', '_', $sKeyPrefix).'/'; + } + + public function Set(string $sKey, string $sValue) : bool + { + return \apcu_store($this->generateCachedKey($sKey), (string) $sValue); + } + + 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 : null; + } + + public function Delete(string $sKey) : void + { + \apcu_delete($this->generateCachedKey($sKey)); + } + + public function GC(int $iTimeToClearInHours = 24) : bool + { + return (0 === $iTimeToClearInHours) ? \apcu_clear_cache('user') : false; + } + + private function generateCachedKey(string $sKey) : string + { + return $this->sKeyPrefix.\sha1($sKey); + } +} diff --git a/plugins/cache-apcu/index.php b/plugins/cache-apcu/index.php new file mode 100644 index 0000000000..2e17413a4e --- /dev/null +++ b/plugins/cache-apcu/index.php @@ -0,0 +1,34 @@ +<?php + +class CacheAPCuPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Cache APCu', + VERSION = '2.36', + RELEASE = '2024-03-22', + REQUIRED = '2.36.0', + CATEGORY = 'Cache', + DESCRIPTION = 'Cache handler using PHP APCu'; + + public function Init() : void + { + if (\MailSo\Base\Utils::FunctionsCallable(array('apcu_store', 'apcu_fetch', 'apcu_delete', 'apcu_clear_cache'))) { + $this->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/plugins/cache-memcache/Memcache.php b/plugins/cache-memcache/Memcache.php new file mode 100644 index 0000000000..30a2812c2f --- /dev/null +++ b/plugins/cache-memcache/Memcache.php @@ -0,0 +1,84 @@ +<?php + +/* + * This file is part of MailSo. + * + * (c) 2014 Usenko Timur + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace MailSo\Cache\Drivers; + +/** + * @category MailSo + * @package Cache + * @subpackage Drivers + */ +class Memcache implements \MailSo\Cache\DriverInterface +{ + private int $iExpire; + + /** + * @var \Memcache|\Memcached|null + */ + private $oMem; + + private string $sKeyPrefix; + + function __construct(string $sHost = '127.0.0.1', int $iPort = 11211, int $iExpire = 43200) + { + $this->iExpire = 0 < $iExpire ? $iExpire : 43200; + + $this->oMem = \class_exists('Memcache',false) ? new \Memcache : new \Memcached; + 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_]/', '_', $sKeyPrefix).'/'; + } + + public function Set(string $sKey, string $sValue) : bool + { + if ($this->oMem instanceof \Memcache) { + return $this->oMem->set($this->generateCachedKey($sKey), $sValue, 0, $this->iExpire); + } + return $this->oMem ? $this->oMem->set($this->generateCachedKey($sKey), $sValue, $this->iExpire) : false; + } + + public function Exists(string $sKey) : bool + { + 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 + { + $this->oMem && $this->oMem->delete($this->generateCachedKey($sKey)); + } + + public function GC(int $iTimeToClearInHours = 24) : bool + { + if (0 === $iTimeToClearInHours && $this->oMem) { + return $this->oMem->flush(); + } + return false; + } + + private function generateCachedKey(string $sKey) : string + { + return $this->sKeyPrefix.\sha1($sKey); + } +} diff --git a/plugins/cache-memcache/index.php b/plugins/cache-memcache/index.php new file mode 100644 index 0000000000..7760ab3987 --- /dev/null +++ b/plugins/cache-memcache/index.php @@ -0,0 +1,54 @@ +<?php + +class CacheMemcachePlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Cache Memcache', + VERSION = '2.37', + RELEASE = '2024-09-15', + REQUIRED = '2.36.0', + CATEGORY = 'Cache', + DESCRIPTION = 'Cache handler using PHP Memcache or PHP Memcached'; + + public function Init() : void + { + if (\class_exists('Memcache',false) || \class_exists('Memcached',false)) { + $this->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 0000000000..9a8cd865be --- /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/rainloop/v/0.0.0/app/libraries/Predis/Autoloader.php b/plugins/cache-redis/Predis/Autoloader.php similarity index 81% rename from rainloop/v/0.0.0/app/libraries/Predis/Autoloader.php rename to plugins/cache-redis/Predis/Autoloader.php index 17ec2ff1df..054f7bbb2a 100644 --- a/rainloop/v/0.0.0/app/libraries/Predis/Autoloader.php +++ b/plugins/cache-redis/Predis/Autoloader.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri <suppakilla@gmail.com> + * (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,6 +17,7 @@ * * @author Eric Naeseth <eric@thumbtack.com> * @author Daniele Alessandri <suppakilla@gmail.com> + * @codeCoverageIgnore */ class Autoloader { @@ -29,7 +31,7 @@ class Autoloader public function __construct($baseDirectory = __DIR__) { $this->directory = $baseDirectory; - $this->prefix = __NAMESPACE__.'\\'; + $this->prefix = __NAMESPACE__ . '\\'; $this->prefixLength = strlen($this->prefix); } @@ -40,7 +42,7 @@ public function __construct($baseDirectory = __DIR__) */ public static function register($prepend = false) { - spl_autoload_register(array(new self(), 'autoload'), true, $prepend); + spl_autoload_register([new self(), 'autoload'], true, $prepend); } /** @@ -52,7 +54,7 @@ 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'; + $filepath = $this->directory . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts) . '.php'; if (is_file($filepath)) { require $filepath; diff --git a/plugins/cache-redis/Predis/Client.php b/plugins/cache-redis/Predis/Client.php new file mode 100644 index 0000000000..4257667bd6 --- /dev/null +++ b/plugins/cache-redis/Predis/Client.php @@ -0,0 +1,612 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +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\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. + * + * This is the main high-level abstraction of Predis upon which various other + * abstractions are built. Internally it aggregates various other classes each + * one with its own responsibility and scope. + * + * @template-implements \IteratorAggregate<string, static> + */ +class Client implements ClientInterface, IteratorAggregate +{ + public const VERSION = '2.2.2'; + + /** @var OptionsInterface */ + private $options; + + /** @var ConnectionInterface */ + private $connection; + + /** @var Command\FactoryInterface */ + private $commands; + + /** + * @param mixed $parameters Connection parameters for one or more servers. + * @param mixed $options Options to configure some behaviours of the client. + */ + public function __construct($parameters = null, $options = null) + { + $this->options = static::createOptions($options ?? new Options()); + $this->connection = static::createConnection($this->options, $parameters ?? new Parameters()); + $this->commands = $this->options->commands; + } + + /** + * Creates a new set of client options for the client. + * + * @param array|OptionsInterface $options Set of client options + * + * @return OptionsInterface + * @throws InvalidArgumentException + */ + protected static function createOptions($options) + { + if (is_array($options)) { + return new Options($options); + } elseif ($options instanceof OptionsInterface) { + return $options; + } else { + throw new InvalidArgumentException('Invalid type for client options'); + } + } + + /** + * Creates single or aggregate connections from supplied arguments. + * + * This method accepts the following types to create a connection instance: + * + * - 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) + * + * When a callable is passed, it receives the original set of client options + * and must return an instance of Predis\Connection\ConnectionInterface. + * + * 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 static function createConnection(OptionsInterface $options, $parameters) + { + if ($parameters instanceof ConnectionInterface) { + return $parameters; + } + + if ($parameters instanceof ParametersInterface || is_string($parameters)) { + return $options->connections->create($parameters); + } + + if (is_array($parameters)) { + if (!isset($parameters[0])) { + 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 { + throw new InvalidArgumentException( + 'Array of connection parameters requires `cluster`, `replication` or `aggregate` client option' + ); + } + } + + if (is_callable($parameters)) { + $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'); + } + + /** + * {@inheritdoc} + */ + public function getCommandFactory() + { + return $this->commands; + } + + /** + * {@inheritdoc} + */ + public function getOptions() + { + return $this->options; + } + + /** + * Creates a new client using a specific underlying 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. + * + * The specified selector defines which logic to use to look for a suitable + * connection by the specified value. Supported selectors are: + * + * - `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 getClientBy($selector, $value) + { + $selector = strtolower($selector); + + if (!in_array($selector, ['id', 'key', 'slot', 'role', 'alias', 'command'])) { + throw new InvalidArgumentException("Invalid selector type: `$selector`"); + } + + 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()); + } + + /** + * Opens the underlying connection and connects to the server. + */ + public function connect() + { + $this->connection->connect(); + } + + /** + * Closes the underlying connection and disconnects from the server. + */ + public function disconnect() + { + $this->connection->disconnect(); + } + + /** + * Closes the underlying connection and disconnects from the server. + * + * This is the same as `Client::disconnect()` as it does not actually send + * the `QUIT` command to Redis, but simply closes the connection. + */ + public function quit() + { + $this->disconnect(); + } + + /** + * Returns the current state of the underlying connection. + * + * @return bool + */ + public function isConnected() + { + return $this->connection->isConnected(); + } + + /** + * {@inheritdoc} + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Applies the configured serializer and compression to given value. + * + * @param mixed $value + * @return string + */ + public function pack($value) + { + return $this->connection instanceof RelayConnection + ? $this->connection->pack($value) + : $value; + } + + /** + * 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; + } + + /** + * Executes a command without filtering its arguments, parsing the response, + * applying any prefix to keys or throwing exceptions on Redis errors even + * regardless of client options. + * + * 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. + * @param bool $error Set to TRUE when Redis returned an error response. + * + * @return mixed + */ + public function executeRaw(array $arguments, &$error = null) + { + $error = false; + $commandID = array_shift($arguments); + + $response = $this->connection->executeCommand( + new RawCommand($commandID, $arguments) + ); + + if ($response instanceof ResponseInterface) { + if ($response instanceof ErrorResponseInterface) { + $error = true; + } + + return (string) $response; + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function __call($commandID, $arguments) + { + return $this->executeCommand( + $this->createCommand($commandID, $arguments) + ); + } + + /** + * {@inheritdoc} + */ + public function 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'); + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + $response = $this->connection->executeCommand($command); + + if ($response instanceof ResponseInterface) { + if ($response instanceof ErrorResponseInterface) { + $response = $this->onErrorResponse($command, $response); + } + + return $response; + } + + return $command->parseResponse($response); + } + + /** + * Handles -ERR responses returned by Redis. + * + * @param CommandInterface $command Redis command that generated the error. + * @param ErrorResponseInterface $response Instance of the error response. + * + * @return mixed + * @throws ServerException + */ + protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $response) + { + if ($command instanceof ScriptCommand && $response->getErrorType() === 'NOSCRIPT') { + $response = $this->executeCommand($command->getEvalCommand()); + + if (!$response instanceof ResponseInterface) { + $response = $command->parseResponse($response); + } + + return $response; + } + + if ($this->options->exceptions) { + throw new ServerException($response->getMessage()); + } + + return $response; + } + + /** + * Executes the specified initializer method on `$this` by adjusting the + * 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. + * + * @param string $initializer Method name. + * @param array $argv Arguments for the method. + * + * @return mixed + */ + private function sharedContextFactory($initializer, $argv = null) + { + switch (count($argv)) { + case 0: + return $this->$initializer(); + + case 1: + return is_array($argv[0]) + ? $this->$initializer($argv[0]) + : $this->$initializer(null, $argv[0]); + + case 2: + [$arg0, $arg1] = $argv; + + return $this->$initializer($arg0, $arg1); + + default: + return $this->$initializer($this, $argv); + } + } + + /** + * Creates a new pipeline context and returns it, or returns the results of + * a pipeline executed inside the optionally provided callable object. + * + * @param mixed ...$arguments Array of options, a callable for execution, or both. + * + * @return Pipeline|array + */ + public function pipeline(...$arguments) + { + return $this->sharedContextFactory('createPipeline', func_get_args()); + } + + /** + * Actual pipeline context initializer method. + * + * @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 = Atomic::class; + } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) { + $class = FireAndForget::class; + } else { + $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; + } + } + + /* + * @var ClientContextInterface + */ + $pipeline = new $class($this); + + if (isset($callable)) { + return $pipeline->execute($callable); + } + + return $pipeline; + } + + /** + * Creates a new transaction context and returns it, or returns the results + * of a transaction executed inside the optionally provided callable object. + * + * @param mixed ...$arguments Array of options, a callable for execution, or both. + * + * @return MultiExecTransaction|array + */ + public function transaction(...$arguments) + { + return $this->sharedContextFactory('createTransaction', func_get_args()); + } + + /** + * Actual transaction context initializer method. + * + * @param array $options Options for the context. + * @param mixed $callable Optional callable used to execute the context. + * + * @return MultiExecTransaction|array + */ + protected function createTransaction(?array $options = null, $callable = null) + { + $transaction = new MultiExecTransaction($this, $options); + + if (isset($callable)) { + return $transaction->execute($callable); + } + + return $transaction; + } + + /** + * Creates a new publish/subscribe context and returns it, or starts its loop + * inside the optionally provided callable object. + * + * @param mixed ...$arguments Array of options, a callable for execution, or both. + * + * @return PubSubConsumer|null + */ + public function pubSubLoop(...$arguments) + { + return $this->sharedContextFactory('createPubSub', func_get_args()); + } + + /** + * Actual publish/subscribe context initializer method. + * + * @param array $options Options for the context. + * @param mixed $callable Optional callable used to execute the context. + * + * @return PubSubConsumer|null + */ + protected function createPubSub(?array $options = null, $callable = null) + { + if ($this->connection instanceof RelayConnection) { + $pubsub = new RelayPubSubConsumer($this, $options); + } else { + $pubsub = new PubSubConsumer($this, $options); + } + + if (!isset($callable)) { + return $pubsub; + } + + foreach ($pubsub as $message) { + if (call_user_func($callable, $pubsub, $message) === false) { + $pubsub->stop(); + } + } + + return null; + } + + /** + * Creates a new monitor consumer and returns it. + * + * @return MonitorConsumer + */ + public function monitor() + { + return new MonitorConsumer($this); + } + + /** + * @return Traversable<string, static> + */ + #[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 0000000000..c70cd61cf6 --- /dev/null +++ b/plugins/cache-redis/Predis/ClientConfiguration.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +class ClientConfiguration +{ + /** + * @var array{modules: array}|string[][] + */ + private static $config = [ + 'modules' => [ + ['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 0000000000..e443ee5d1c --- /dev/null +++ b/plugins/cache-redis/Predis/ClientContextInterface.php @@ -0,0 +1,380 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +use Predis\Command\Argument\Geospatial\ByInterface; +use Predis\Command\Argument\Geospatial\FromInterface; +use Predis\Command\Argument\Search\AggregateArguments; +use Predis\Command\Argument\Search\AlterArguments; +use Predis\Command\Argument\Search\CreateArguments; +use Predis\Command\Argument\Search\DropArguments; +use Predis\Command\Argument\Search\ExplainArguments; +use Predis\Command\Argument\Search\ProfileArguments; +use Predis\Command\Argument\Search\SchemaFields\FieldInterface; +use Predis\Command\Argument\Search\SearchArguments; +use Predis\Command\Argument\Search\SugAddArguments; +use Predis\Command\Argument\Search\SugGetArguments; +use Predis\Command\Argument\Search\SynUpdateArguments; +use Predis\Command\Argument\Server\LimitOffsetCount; +use Predis\Command\Argument\Server\To; +use Predis\Command\Argument\TimeSeries\AddArguments; +use Predis\Command\Argument\TimeSeries\AlterArguments as TSAlterArguments; +use Predis\Command\Argument\TimeSeries\CreateArguments as TSCreateArguments; +use Predis\Command\Argument\TimeSeries\DecrByArguments; +use Predis\Command\Argument\TimeSeries\GetArguments; +use Predis\Command\Argument\TimeSeries\IncrByArguments; +use Predis\Command\Argument\TimeSeries\InfoArguments; +use Predis\Command\Argument\TimeSeries\MGetArguments; +use Predis\Command\Argument\TimeSeries\MRangeArguments; +use Predis\Command\Argument\TimeSeries\RangeArguments; +use Predis\Command\CommandInterface; +use Predis\Command\Redis\Container\ACL; +use Predis\Command\Redis\Container\CLUSTER; +use Predis\Command\Redis\Container\FunctionContainer; +use Predis\Command\Redis\Container\Json\JSONDEBUG; +use Predis\Command\Redis\Container\Search\FTCONFIG; +use Predis\Command\Redis\Container\Search\FTCURSOR; + +/** + * Interface defining a client-side context such as a pipeline or transaction. + * + * @method $this copy(string $source, string $destination, int $db = -1, bool $replace = false) + * @method $this del(array|string $keys) + * @method $this dump($key) + * @method $this exists($key) + * @method $this expire($key, $seconds, string $expireOption = '') + * @method $this expireat($key, $timestamp, string $expireOption = '') + * @method $this expiretime(string $key) + * @method $this keys($pattern) + * @method $this move($key, $db) + * @method $this object($subcommand, $key) + * @method $this persist($key) + * @method $this pexpire($key, $milliseconds) + * @method $this pexpireat($key, $timestamp) + * @method $this pttl($key) + * @method $this randomkey() + * @method $this rename($key, $target) + * @method $this renamenx($key, $target) + * @method $this scan($cursor, array $options = null) + * @method $this sort($key, array $options = null) + * @method $this sort_ro(string $key, ?string $byPattern = null, ?LimitOffsetCount $limit = null, array $getPatterns = [], ?string $sorting = null, bool $alpha = false) + * @method $this ttl($key) + * @method $this type($key) + * @method $this append($key, $value) + * @method $this bfadd(string $key, $item) + * @method $this bfexists(string $key, $item) + * @method $this bfinfo(string $key, string $modifier = '') + * @method $this bfinsert(string $key, int $capacity = -1, float $error = -1, int $expansion = -1, bool $noCreate = false, bool $nonScaling = false, string ...$item) + * @method $this bfloadchunk(string $key, int $iterator, $data) + * @method $this bfmadd(string $key, ...$item) + * @method $this bfmexists(string $key, ...$item) + * @method $this bfreserve(string $key, float $errorRate, int $capacity, int $expansion = -1, bool $nonScaling = false) + * @method $this bfscandump(string $key, int $iterator) + * @method $this bitcount(string $key, $start = null, $end = null, string $index = 'byte') + * @method $this bitop($operation, $destkey, $key) + * @method $this bitfield($key, $subcommand, ...$subcommandArg) + * @method $this bitpos($key, $bit, $start = null, $end = null, string $index = 'byte') + * @method $this blmpop(int $timeout, array $keys, string $modifier = 'left', int $count = 1) + * @method $this bzpopmax(array $keys, int $timeout) + * @method $this bzpopmin(array $keys, int $timeout) + * @method $this bzmpop(int $timeout, array $keys, string $modifier = 'min', int $count = 1) + * @method $this cfadd(string $key, $item) + * @method $this cfaddnx(string $key, $item) + * @method $this cfcount(string $key, $item) + * @method $this cfdel(string $key, $item) + * @method $this cfexists(string $key, $item) + * @method $this cfloadchunk(string $key, int $iterator, $data) + * @method $this cfmexists(string $key, ...$item) + * @method $this cfinfo(string $key) + * @method $this cfinsert(string $key, int $capacity = -1, bool $noCreate = false, string ...$item) + * @method $this cfinsertnx(string $key, int $capacity = -1, bool $noCreate = false, string ...$item) + * @method $this cfreserve(string $key, int $capacity, int $bucketSize = -1, int $maxIterations = -1, int $expansion = -1) + * @method $this cfscandump(string $key, int $iterator) + * @method $this cmsincrby(string $key, string|int...$itemIncrementDictionary) + * @method $this cmsinfo(string $key) + * @method $this cmsinitbydim(string $key, int $width, int $depth) + * @method $this cmsinitbyprob(string $key, float $errorRate, float $probability) + * @method $this cmsmerge(string $destination, array $sources, array $weights = []) + * @method $this cmsquery(string $key, string ...$item) + * @method $this decr($key) + * @method $this decrby($key, $decrement) + * @method $this failover(?To $to = null, bool $abort = false, int $timeout = -1) + * @method $this fcall(string $function, array $keys, ...$args) + * @method $this fcall_ro(string $function, array $keys, ...$args) + * @method $this ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null) + * @method $this ftaliasadd(string $alias, string $index) + * @method $this ftaliasdel(string $alias) + * @method $this ftaliasupdate(string $alias, string $index) + * @method $this ftalter(string $index, FieldInterface[] $schema, ?AlterArguments $arguments = null) + * @method $this ftcreate(string $index, FieldInterface[] $schema, ?CreateArguments $arguments = null) + * @method $this ftdictadd(string $dict, ...$term) + * @method $this ftdictdel(string $dict, ...$term) + * @method $this ftdictdump(string $dict) + * @method $this ftdropindex(string $index, ?DropArguments $arguments = null) + * @method $this ftexplain(string $index, string $query, ?ExplainArguments $arguments = null) + * @method $this ftinfo(string $index) + * @method $this ftprofile(string $index, ProfileArguments $arguments) + * @method $this ftsearch(string $index, string $query, ?SearchArguments $arguments = null) + * @method $this ftspellcheck(string $index, string $query, ?SearchArguments $arguments = null) + * @method $this ftsugadd(string $key, string $string, float $score, ?SugAddArguments $arguments = null) + * @method $this ftsugdel(string $key, string $string) + * @method $this ftsugget(string $key, string $prefix, ?SugGetArguments $arguments = null) + * @method $this ftsuglen(string $key) + * @method $this ftsyndump(string $index) + * @method $this ftsynupdate(string $index, string $synonymGroupId, ?SynUpdateArguments $arguments = null, string ...$terms) + * @method $this fttagvals(string $index, string $fieldName) + * @method $this get($key) + * @method $this getbit($key, $offset) + * @method $this getex(string $key, $modifier = '', $value = false) + * @method $this getrange($key, $start, $end) + * @method $this getdel(string $key) + * @method $this getset($key, $value) + * @method $this incr($key) + * @method $this incrby($key, $increment) + * @method $this incrbyfloat($key, $increment) + * @method $this mget(array $keys) + * @method $this mset(array $dictionary) + * @method $this msetnx(array $dictionary) + * @method $this psetex($key, $milliseconds, $value) + * @method $this set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null) + * @method $this setbit($key, $offset, $value) + * @method $this setex($key, $seconds, $value) + * @method $this setnx($key, $value) + * @method $this setrange($key, $offset, $value) + * @method $this strlen($key) + * @method $this hdel($key, array $fields) + * @method $this hexists($key, $field) + * @method $this hget($key, $field) + * @method $this hgetall($key) + * @method $this hincrby($key, $field, $increment) + * @method $this hincrbyfloat($key, $field, $increment) + * @method $this hkeys($key) + * @method $this hlen($key) + * @method $this hmget($key, array $fields) + * @method $this hmset($key, array $dictionary) + * @method $this hrandfield(string $key, int $count = 1, bool $withValues = false) + * @method $this hscan($key, $cursor, array $options = null) + * @method $this hset($key, $field, $value) + * @method $this hsetnx($key, $field, $value) + * @method $this hvals($key) + * @method $this hstrlen($key, $field) + * @method $this jsonarrappend(string $key, string $path = '$', ...$value) + * @method $this jsonarrindex(string $key, string $path, string $value, int $start = 0, int $stop = 0) + * @method $this jsonarrinsert(string $key, string $path, int $index, string ...$value) + * @method $this jsonarrlen(string $key, string $path = '$') + * @method $this jsonarrpop(string $key, string $path = '$', int $index = -1) + * @method $this jsonarrtrim(string $key, string $path, int $start, int $stop) + * @method $this jsonclear(string $key, string $path = '$') + * @method $this jsondel(string $key, string $path = '$') + * @method $this jsonforget(string $key, string $path = '$') + * @method $this jsonget(string $key, string $indent = '', string $newline = '', string $space = '', string ...$paths) + * @method $this jsonnumincrby(string $key, string $path, int $value) + * @method $this jsonmerge(string $key, string $path, string $value) + * @method $this jsonmget(array $keys, string $path) + * @method $this jsonmset(string ...$keyPathValue) + * @method $this jsonobjkeys(string $key, string $path = '$') + * @method $this jsonobjlen(string $key, string $path = '$') + * @method $this jsonresp(string $key, string $path = '$') + * @method $this jsonset(string $key, string $path, string $value, ?string $subcommand = null) + * @method $this jsonstrappend(string $key, string $path, string $value) + * @method $this jsonstrlen(string $key, string $path = '$') + * @method $this jsontoggle(string $key, string $path) + * @method $this jsontype(string $key, string $path = '$') + * @method $this blmove(string $source, string $destination, string $where, string $to, int $timeout) + * @method $this blpop(array|string $keys, $timeout) + * @method $this brpop(array|string $keys, $timeout) + * @method $this brpoplpush($source, $destination, $timeout) + * @method $this lcs(string $key1, string $key2, bool $len = false, bool $idx = false, int $minMatchLen = 0, bool $withMatchLen = false) + * @method $this lindex($key, $index) + * @method $this linsert($key, $whence, $pivot, $value) + * @method $this llen($key) + * @method $this lmove(string $source, string $destination, string $where, string $to) + * @method $this lmpop(array $keys, string $modifier = 'left', int $count = 1) + * @method $this lpop($key) + * @method $this lpush($key, array $values) + * @method $this lpushx($key, array $values) + * @method $this lrange($key, $start, $stop) + * @method $this lrem($key, $count, $value) + * @method $this lset($key, $index, $value) + * @method $this ltrim($key, $start, $stop) + * @method $this rpop($key) + * @method $this rpoplpush($source, $destination) + * @method $this rpush($key, array $values) + * @method $this rpushx($key, array $values) + * @method $this sadd($key, array $members) + * @method $this scard($key) + * @method $this sdiff(array|string $keys) + * @method $this sdiffstore($destination, array|string $keys) + * @method $this sinter(array|string $keys) + * @method $this sintercard(array $keys, int $limit = 0) + * @method $this sinterstore($destination, array|string $keys) + * @method $this sismember($key, $member) + * @method $this smembers($key) + * @method $this smismember(string $key, string ...$members) + * @method $this smove($source, $destination, $member) + * @method $this spop($key, $count = null) + * @method $this srandmember($key, $count = null) + * @method $this srem($key, $member) + * @method $this sscan($key, $cursor, array $options = null) + * @method $this sunion(array|string $keys) + * @method $this sunionstore($destination, array|string $keys) + * @method $this tdigestadd(string $key, float ...$value) + * @method $this tdigestbyrank(string $key, int ...$rank) + * @method $this tdigestbyrevrank(string $key, int ...$reverseRank) + * @method $this tdigestcdf(string $key, int ...$value) + * @method $this tdigestcreate(string $key, int $compression = 0) + * @method $this tdigestinfo(string $key) + * @method $this tdigestmax(string $key) + * @method $this tdigestmerge(string $destinationKey, array $sourceKeys, int $compression = 0, bool $override = false) + * @method $this tdigestquantile(string $key, float ...$quantile) + * @method $this tdigestmin(string $key) + * @method $this tdigestrank(string $key, ...$value) + * @method $this tdigestreset(string $key) + * @method $this tdigestrevrank(string $key, float ...$value) + * @method $this tdigesttrimmed_mean(string $key, float $lowCutQuantile, float $highCutQuantile) + * @method $this topkadd(string $key, ...$items) + * @method $this topkincrby(string $key, ...$itemIncrement) + * @method $this topkinfo(string $key) + * @method $this topklist(string $key, bool $withCount = false) + * @method $this topkquery(string $key, ...$items) + * @method $this topkreserve(string $key, int $topK, int $width = 8, int $depth = 7, float $decay = 0.9) + * @method $this tsadd(string $key, int $timestamp, float $value, ?AddArguments $arguments = null) + * @method $this tsalter(string $key, ?TSAlterArguments $arguments = null) + * @method $this tscreate(string $key, ?TSCreateArguments $arguments = null) + * @method $this tscreaterule(string $sourceKey, string $destKey, string $aggregator, int $bucketDuration, int $alignTimestamp = 0) + * @method $this tsdecrby(string $key, float $value, ?DecrByArguments $arguments = null) + * @method $this tsdel(string $key, int $fromTimestamp, int $toTimestamp) + * @method $this tsdeleterule(string $sourceKey, string $destKey) + * @method $this tsget(string $key, GetArguments $arguments = null) + * @method $this tsincrby(string $key, float $value, ?IncrByArguments $arguments = null) + * @method $this tsinfo(string $key, ?InfoArguments $arguments = null) + * @method $this tsmadd(mixed ...$keyTimestampValue) + * @method $this tsmget(MGetArguments $arguments, string ...$filterExpression) + * @method $this tsmrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments) + * @method $this tsmrevrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments) + * @method $this tsqueryindex(string ...$filterExpression) + * @method $this tsrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null) + * @method $this tsrevrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null) + * @method $this zadd($key, array $membersAndScoresDictionary) + * @method $this zcard($key) + * @method $this zcount($key, $min, $max) + * @method $this zdiff(array $keys, bool $withScores = false) + * @method $this zdiffstore(string $destination, array $keys) + * @method $this zincrby($key, $increment, $member) + * @method $this zintercard(array $keys, int $limit = 0) + * @method $this zinterstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum') + * @method $this zinter(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false) + * @method $this zmpop(array $keys, string $modifier = 'min', int $count = 1) + * @method $this zmscore(string $key, string ...$member) + * @method $this zrandmember(string $key, int $count = 1, bool $withScores = false) + * @method $this zrange($key, $start, $stop, array $options = null) + * @method $this zrangebyscore($key, $min, $max, array $options = null) + * @method $this zrangestore(string $destination, string $source, int|string $min, string|int $max, string|bool $by = false, bool $reversed = false, bool $limit = false, int $offset = 0, int $count = 0) + * @method $this zrank($key, $member) + * @method $this zrem($key, $member) + * @method $this zremrangebyrank($key, $start, $stop) + * @method $this zremrangebyscore($key, $min, $max) + * @method $this zrevrange($key, $start, $stop, array $options = null) + * @method $this zrevrangebyscore($key, $max, $min, array $options = null) + * @method $this zrevrank($key, $member) + * @method $this zunion(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false) + * @method $this zunionstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum') + * @method $this zscore($key, $member) + * @method $this zscan($key, $cursor, array $options = null) + * @method $this zrangebylex($key, $start, $stop, array $options = null) + * @method $this zrevrangebylex($key, $start, $stop, array $options = null) + * @method $this zremrangebylex($key, $min, $max) + * @method $this zlexcount($key, $min, $max) + * @method $this pexpiretime(string $key) + * @method $this pfadd($key, array $elements) + * @method $this pfmerge($destinationKey, array|string $sourceKeys) + * @method $this pfcount(array|string $keys) + * @method $this pubsub($subcommand, $argument) + * @method $this publish($channel, $message) + * @method $this discard() + * @method $this exec() + * @method $this multi() + * @method $this unwatch() + * @method $this waitaof(int $numLocal, int $numReplicas, int $timeout) + * @method $this watch($key) + * @method $this eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null) + * @method $this eval_ro(string $script, array $keys, ...$argument) + * @method $this evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null) + * @method $this evalsha_ro(string $sha1, array $keys, ...$argument) + * @method $this script($subcommand, $argument = null) + * @method $this shutdown(bool $noSave = null, bool $now = false, bool $force = false, bool $abort = false) + * @method $this auth($password) + * @method $this echo($message) + * @method $this ping($message = null) + * @method $this select($database) + * @method $this bgrewriteaof() + * @method $this bgsave() + * @method $this client($subcommand, $argument = null) + * @method $this config($subcommand, $argument = null) + * @method $this dbsize() + * @method $this flushall() + * @method $this flushdb() + * @method $this info($section = null) + * @method $this lastsave() + * @method $this save() + * @method $this slaveof($host, $port) + * @method $this slowlog($subcommand, $argument = null) + * @method $this time() + * @method $this command() + * @method $this geoadd($key, $longitude, $latitude, $member) + * @method $this geohash($key, array $members) + * @method $this geopos($key, array $members) + * @method $this geodist($key, $member1, $member2, $unit = null) + * @method $this georadius($key, $longitude, $latitude, $radius, $unit, array $options = null) + * @method $this georadiusbymember($key, $member, $radius, $unit, array $options = null) + * @method $this geosearch(string $key, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $withCoord = false, bool $withDist = false, bool $withHash = false) + * @method $this geosearchstore(string $destination, string $source, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $storeDist = false) + * + * Container commands + * @property CLUSTER $cluster + * @property FunctionContainer $function + * @property FTCONFIG $ftconfig + * @property FTCURSOR $ftcursor + * @property JSONDEBUG $jsondebug + * @property ACL $acl + */ +interface ClientContextInterface +{ + /** + * Sends the specified command instance to Redis. + * + * @param CommandInterface $command Command instance. + * + * @return mixed + */ + public function executeCommand(CommandInterface $command); + + /** + * Sends the specified command with its arguments to Redis. + * + * @param string $method Command ID. + * @param array $arguments Arguments for the command. + * + * @return mixed + */ + public function __call($method, $arguments); + + /** + * Starts the execution of the context. + * + * @param mixed $callable Optional callback for execution. + * + * @return array + */ + public function execute($callable = null); +} diff --git a/plugins/cache-redis/Predis/ClientException.php b/plugins/cache-redis/Predis/ClientException.php new file mode 100644 index 0000000000..e52ab0244c --- /dev/null +++ b/plugins/cache-redis/Predis/ClientException.php @@ -0,0 +1,20 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +/** + * Exception class that identifies client-side errors. + */ +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 0000000000..2937c58048 --- /dev/null +++ b/plugins/cache-redis/Predis/ClientInterface.php @@ -0,0 +1,431 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +use Predis\Command\Argument\Geospatial\ByInterface; +use Predis\Command\Argument\Geospatial\FromInterface; +use Predis\Command\Argument\Search\AggregateArguments; +use Predis\Command\Argument\Search\AlterArguments; +use Predis\Command\Argument\Search\CreateArguments; +use Predis\Command\Argument\Search\DropArguments; +use Predis\Command\Argument\Search\ExplainArguments; +use Predis\Command\Argument\Search\ProfileArguments; +use Predis\Command\Argument\Search\SchemaFields\FieldInterface; +use Predis\Command\Argument\Search\SearchArguments; +use Predis\Command\Argument\Search\SugAddArguments; +use Predis\Command\Argument\Search\SugGetArguments; +use Predis\Command\Argument\Search\SynUpdateArguments; +use Predis\Command\Argument\Server\LimitOffsetCount; +use Predis\Command\Argument\Server\To; +use Predis\Command\Argument\TimeSeries\AddArguments; +use Predis\Command\Argument\TimeSeries\AlterArguments as TSAlterArguments; +use Predis\Command\Argument\TimeSeries\CreateArguments as TSCreateArguments; +use Predis\Command\Argument\TimeSeries\DecrByArguments; +use Predis\Command\Argument\TimeSeries\GetArguments; +use Predis\Command\Argument\TimeSeries\IncrByArguments; +use Predis\Command\Argument\TimeSeries\InfoArguments; +use Predis\Command\Argument\TimeSeries\MGetArguments; +use Predis\Command\Argument\TimeSeries\MRangeArguments; +use Predis\Command\Argument\TimeSeries\RangeArguments; +use Predis\Command\CommandInterface; +use Predis\Command\FactoryInterface; +use Predis\Command\Redis\Container\ACL; +use Predis\Command\Redis\Container\CLUSTER; +use Predis\Command\Redis\Container\FunctionContainer; +use Predis\Command\Redis\Container\Json\JSONDEBUG; +use Predis\Command\Redis\Container\Search\FTCONFIG; +use Predis\Command\Redis\Container\Search\FTCURSOR; +use Predis\Configuration\OptionsInterface; +use Predis\Connection\ConnectionInterface; +use Predis\Response\Status; + +/** + * Interface defining a client able to execute commands against Redis. + * + * All the commands exposed by the client generally have the same signature as + * described by the Redis documentation, but some of them offer an additional + * and more friendly interface to ease programming which is described in the + * following list of methods: + * + * @method int copy(string $source, string $destination, int $db = -1, bool $replace = false) + * @method int del(string[]|string $keyOrKeys, string ...$keys = null) + * @method string|null dump(string $key) + * @method int exists(string $key) + * @method int expire(string $key, int $seconds, string $expireOption = '') + * @method int expireat(string $key, int $timestamp, string $expireOption = '') + * @method int expiretime(string $key) + * @method array keys(string $pattern) + * @method int move(string $key, int $db) + * @method mixed object($subcommand, string $key) + * @method int persist(string $key) + * @method int pexpire(string $key, int $milliseconds) + * @method int pexpireat(string $key, int $timestamp) + * @method int pttl(string $key) + * @method string|null randomkey() + * @method mixed rename(string $key, string $target) + * @method int renamenx(string $key, string $target) + * @method array scan($cursor, array $options = null) + * @method array sort(string $key, array $options = null) + * @method array sort_ro(string $key, ?string $byPattern = null, ?LimitOffsetCount $limit = null, array $getPatterns = [], ?string $sorting = null, bool $alpha = false) + * @method int ttl(string $key) + * @method mixed type(string $key) + * @method int append(string $key, $value) + * @method int bfadd(string $key, $item) + * @method int bfexists(string $key, $item) + * @method array bfinfo(string $key, string $modifier = '') + * @method array bfinsert(string $key, int $capacity = -1, float $error = -1, int $expansion = -1, bool $noCreate = false, bool $nonScaling = false, string ...$item) + * @method Status bfloadchunk(string $key, int $iterator, $data) + * @method array bfmadd(string $key, ...$item) + * @method array bfmexists(string $key, ...$item) + * @method Status bfreserve(string $key, float $errorRate, int $capacity, int $expansion = -1, bool $nonScaling = false) + * @method array bfscandump(string $key, int $iterator) + * @method int bitcount(string $key, $start = null, $end = null, string $index = 'byte') + * @method int bitop($operation, $destkey, $key) + * @method array|null bitfield(string $key, $subcommand, ...$subcommandArg) + * @method int bitpos(string $key, $bit, $start = null, $end = null, string $index = 'byte') + * @method array blmpop(int $timeout, array $keys, string $modifier = 'left', int $count = 1) + * @method array bzpopmax(array $keys, int $timeout) + * @method array bzpopmin(array $keys, int $timeout) + * @method array bzmpop(int $timeout, array $keys, string $modifier = 'min', int $count = 1) + * @method int cfadd(string $key, $item) + * @method int cfaddnx(string $key, $item) + * @method int cfcount(string $key, $item) + * @method int cfdel(string $key, $item) + * @method int cfexists(string $key, $item) + * @method Status cfloadchunk(string $key, int $iterator, $data) + * @method int cfmexists(string $key, ...$item) + * @method array cfinfo(string $key) + * @method array cfinsert(string $key, int $capacity = -1, bool $noCreate = false, string ...$item) + * @method array cfinsertnx(string $key, int $capacity = -1, bool $noCreate = false, string ...$item) + * @method Status cfreserve(string $key, int $capacity, int $bucketSize = -1, int $maxIterations = -1, int $expansion = -1) + * @method array cfscandump(string $key, int $iterator) + * @method array cmsincrby(string $key, string|int...$itemIncrementDictionary) + * @method array cmsinfo(string $key) + * @method Status cmsinitbydim(string $key, int $width, int $depth) + * @method Status cmsinitbyprob(string $key, float $errorRate, float $probability) + * @method Status cmsmerge(string $destination, array $sources, array $weights = []) + * @method array cmsquery(string $key, string ...$item) + * @method int decr(string $key) + * @method int decrby(string $key, int $decrement) + * @method Status failover(?To $to = null, bool $abort = false, int $timeout = -1) + * @method mixed fcall(string $function, array $keys, ...$args) + * @method mixed fcall_ro(string $function, array $keys, ...$args) + * @method array ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null) + * @method Status ftaliasadd(string $alias, string $index) + * @method Status ftaliasdel(string $alias) + * @method Status ftaliasupdate(string $alias, string $index) + * @method Status ftalter(string $index, FieldInterface[] $schema, ?AlterArguments $arguments = null) + * @method Status ftcreate(string $index, FieldInterface[] $schema, ?CreateArguments $arguments = null) + * @method int ftdictadd(string $dict, ...$term) + * @method int ftdictdel(string $dict, ...$term) + * @method array ftdictdump(string $dict) + * @method Status ftdropindex(string $index, ?DropArguments $arguments = null) + * @method string ftexplain(string $index, string $query, ?ExplainArguments $arguments = null) + * @method array ftinfo(string $index) + * @method array ftprofile(string $index, ProfileArguments $arguments) + * @method array ftsearch(string $index, string $query, ?SearchArguments $arguments = null) + * @method array ftspellcheck(string $index, string $query, ?SearchArguments $arguments = null) + * @method int ftsugadd(string $key, string $string, float $score, ?SugAddArguments $arguments = null) + * @method int ftsugdel(string $key, string $string) + * @method array ftsugget(string $key, string $prefix, ?SugGetArguments $arguments = null) + * @method int ftsuglen(string $key) + * @method array ftsyndump(string $index) + * @method Status ftsynupdate(string $index, string $synonymGroupId, ?SynUpdateArguments $arguments = null, string ...$terms) + * @method array fttagvals(string $index, string $fieldName) + * @method string|null get(string $key) + * @method int getbit(string $key, $offset) + * @method int|null getex(string $key, $modifier = '', $value = false) + * @method string getrange(string $key, $start, $end) + * @method string getdel(string $key) + * @method string|null getset(string $key, $value) + * @method int incr(string $key) + * @method int incrby(string $key, int $increment) + * @method string incrbyfloat(string $key, int|float $increment) + * @method array mget(string[]|string $keyOrKeys, string ...$keys = null) + * @method mixed mset(array $dictionary) + * @method int msetnx(array $dictionary) + * @method Status psetex(string $key, $milliseconds, $value) + * @method Status set(string $key, $value, $expireResolution = null, $expireTTL = null, $flag = null) + * @method int setbit(string $key, $offset, $value) + * @method Status setex(string $key, $seconds, $value) + * @method int setnx(string $key, $value) + * @method int setrange(string $key, $offset, $value) + * @method int strlen(string $key) + * @method int hdel(string $key, array $fields) + * @method int hexists(string $key, string $field) + * @method string|null hget(string $key, string $field) + * @method array hgetall(string $key) + * @method int hincrby(string $key, string $field, int $increment) + * @method string hincrbyfloat(string $key, string $field, int|float $increment) + * @method array hkeys(string $key) + * @method int hlen(string $key) + * @method array hmget(string $key, array $fields) + * @method mixed hmset(string $key, array $dictionary) + * @method array hrandfield(string $key, int $count = 1, bool $withValues = false) + * @method array hscan(string $key, $cursor, array $options = null) + * @method int hset(string $key, string $field, string $value) + * @method int hsetnx(string $key, string $field, string $value) + * @method array hvals(string $key) + * @method int hstrlen(string $key, string $field) + * @method array jsonarrappend(string $key, string $path = '$', ...$value) + * @method array jsonarrindex(string $key, string $path, string $value, int $start = 0, int $stop = 0) + * @method array jsonarrinsert(string $key, string $path, int $index, string ...$value) + * @method array jsonarrlen(string $key, string $path = '$') + * @method array jsonarrpop(string $key, string $path = '$', int $index = -1) + * @method int jsonclear(string $key, string $path = '$') + * @method array jsonarrtrim(string $key, string $path, int $start, int $stop) + * @method int jsondel(string $key, string $path = '$') + * @method int jsonforget(string $key, string $path = '$') + * @method string jsonget(string $key, string $indent = '', string $newline = '', string $space = '', string ...$paths) + * @method string jsonnumincrby(string $key, string $path, int $value) + * @method Status jsonmerge(string $key, string $path, string $value) + * @method array jsonmget(array $keys, string $path) + * @method Status jsonmset(string ...$keyPathValue) + * @method array jsonobjkeys(string $key, string $path = '$') + * @method array jsonobjlen(string $key, string $path = '$') + * @method array jsonresp(string $key, string $path = '$') + * @method string jsonset(string $key, string $path, string $value, ?string $subcommand = null) + * @method array jsonstrappend(string $key, string $path, string $value) + * @method array jsonstrlen(string $key, string $path = '$') + * @method array jsontoggle(string $key, string $path) + * @method array jsontype(string $key, string $path = '$') + * @method string blmove(string $source, string $destination, string $where, string $to, int $timeout) + * @method array|null blpop(array|string $keys, int|float $timeout) + * @method array|null brpop(array|string $keys, int|float $timeout) + * @method string|null brpoplpush(string $source, string $destination, int|float $timeout) + * @method mixed lcs(string $key1, string $key2, bool $len = false, bool $idx = false, int $minMatchLen = 0, bool $withMatchLen = false) + * @method string|null lindex(string $key, int $index) + * @method int linsert(string $key, $whence, $pivot, $value) + * @method int llen(string $key) + * @method string lmove(string $source, string $destination, string $where, string $to) + * @method array|null lmpop(array $keys, string $modifier = 'left', int $count = 1) + * @method string|null lpop(string $key) + * @method int lpush(string $key, array $values) + * @method int lpushx(string $key, array $values) + * @method string[] lrange(string $key, int $start, int $stop) + * @method int lrem(string $key, int $count, string $value) + * @method mixed lset(string $key, int $index, string $value) + * @method mixed ltrim(string $key, int $start, int $stop) + * @method string|null rpop(string $key) + * @method string|null rpoplpush(string $source, string $destination) + * @method int rpush(string $key, array $values) + * @method int rpushx(string $key, array $values) + * @method int sadd(string $key, array $members) + * @method int scard(string $key) + * @method string[] sdiff(array|string $keys) + * @method int sdiffstore(string $destination, array|string $keys) + * @method string[] sinter(array|string $keys) + * @method int sintercard(array $keys, int $limit = 0) + * @method int sinterstore(string $destination, array|string $keys) + * @method int sismember(string $key, string $member) + * @method string[] smembers(string $key) + * @method array smismember(string $key, string ...$members) + * @method int smove(string $source, string $destination, string $member) + * @method string|array|null spop(string $key, int $count = null) + * @method string|null srandmember(string $key, int $count = null) + * @method int srem(string $key, array|string $member) + * @method array sscan(string $key, int $cursor, array $options = null) + * @method string[] sunion(array|string $keys) + * @method int sunionstore(string $destination, array|string $keys) + * @method int touch(string[]|string $keyOrKeys, string ...$keys = null) + * @method Status tdigestadd(string $key, float ...$value) + * @method array tdigestbyrank(string $key, int ...$rank) + * @method array tdigestbyrevrank(string $key, int ...$reverseRank) + * @method array tdigestcdf(string $key, int ...$value) + * @method Status tdigestcreate(string $key, int $compression = 0) + * @method array tdigestinfo(string $key) + * @method string tdigestmax(string $key) + * @method Status tdigestmerge(string $destinationKey, array $sourceKeys, int $compression = 0, bool $override = false) + * @method string[] tdigestquantile(string $key, float ...$quantile) + * @method string tdigestmin(string $key) + * @method array tdigestrank(string $key, float ...$value) + * @method Status tdigestreset(string $key) + * @method array tdigestrevrank(string $key, float ...$value) + * @method string tdigesttrimmed_mean(string $key, float $lowCutQuantile, float $highCutQuantile) + * @method array topkadd(string $key, ...$items) + * @method array topkincrby(string $key, ...$itemIncrement) + * @method array topkinfo(string $key) + * @method array topklist(string $key, bool $withCount = false) + * @method array topkquery(string $key, ...$items) + * @method Status topkreserve(string $key, int $topK, int $width = 8, int $depth = 7, float $decay = 0.9) + * @method int tsadd(string $key, int $timestamp, float $value, ?AddArguments $arguments = null) + * @method Status tsalter(string $key, ?TSAlterArguments $arguments = null) + * @method Status tscreate(string $key, ?TSCreateArguments $arguments = null) + * @method Status tscreaterule(string $sourceKey, string $destKey, string $aggregator, int $bucketDuration, int $alignTimestamp = 0) + * @method int tsdecrby(string $key, float $value, ?DecrByArguments $arguments = null) + * @method int tsdel(string $key, int $fromTimestamp, int $toTimestamp) + * @method Status tsdeleterule(string $sourceKey, string $destKey) + * @method array tsget(string $key, GetArguments $arguments = null) + * @method int tsincrby(string $key, float $value, ?IncrByArguments $arguments = null) + * @method array tsinfo(string $key, ?InfoArguments $arguments = null) + * @method array tsmadd(mixed ...$keyTimestampValue) + * @method array tsmget(MGetArguments $arguments, string ...$filterExpression) + * @method array tsmrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments) + * @method array tsmrevrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments) + * @method array tsqueryindex(string ...$filterExpression) + * @method array tsrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null) + * @method array tsrevrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null) + * @method string xadd(string $key, array $dictionary, string $id = '*', array $options = null) + * @method int xdel(string $key, string ...$id) + * @method int xlen(string $key) + * @method array xrevrange(string $key, string $end, string $start, ?int $count = null) + * @method array xrange(string $key, string $start, string $end, ?int $count = null) + * @method string xtrim(string $key, array|string $strategy, string $threshold, array $options = null) + * @method int zadd(string $key, array $membersAndScoresDictionary) + * @method int zcard(string $key) + * @method string zcount(string $key, int|string $min, int|string $max) + * @method array zdiff(array $keys, bool $withScores = false) + * @method int zdiffstore(string $destination, array $keys) + * @method string zincrby(string $key, int $increment, string $member) + * @method int zintercard(array $keys, int $limit = 0) + * @method int zinterstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum') + * @method array zinter(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false) + * @method array zmpop(array $keys, string $modifier = 'min', int $count = 1) + * @method array zmscore(string $key, string ...$member) + * @method array zpopmin(string $key, int $count = 1) + * @method array zpopmax(string $key, int $count = 1) + * @method mixed zrandmember(string $key, int $count = 1, bool $withScores = false) + * @method array zrange(string $key, int|string $start, int|string $stop, array $options = null) + * @method array zrangebyscore(string $key, int|string $min, int|string $max, array $options = null) + * @method int zrangestore(string $destination, string $source, int|string $min, int|string $max, string|bool $by = false, bool $reversed = false, bool $limit = false, int $offset = 0, int $count = 0) + * @method int|null zrank(string $key, string $member) + * @method int zrem(string $key, string ...$member) + * @method int zremrangebyrank(string $key, int|string $start, int|string $stop) + * @method int zremrangebyscore(string $key, int|string $min, int|string $max) + * @method array zrevrange(string $key, int|string $start, int|string $stop, array $options = null) + * @method array zrevrangebyscore(string $key, int|string $max, int|string $min, array $options = null) + * @method int|null zrevrank(string $key, string $member) + * @method array zunion(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false) + * @method int zunionstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum') + * @method string|null zscore(string $key, string $member) + * @method array zscan(string $key, int $cursor, array $options = null) + * @method array zrangebylex(string $key, string $start, string $stop, array $options = null) + * @method array zrevrangebylex(string $key, string $start, string $stop, array $options = null) + * @method int zremrangebylex(string $key, string $min, string $max) + * @method int zlexcount(string $key, string $min, string $max) + * @method int pexpiretime(string $key) + * @method int pfadd(string $key, array $elements) + * @method mixed pfmerge(string $destinationKey, array|string $sourceKeys) + * @method int pfcount(string[]|string $keyOrKeys, string ...$keys = null) + * @method mixed pubsub($subcommand, $argument) + * @method int publish($channel, $message) + * @method mixed discard() + * @method array|null exec() + * @method mixed multi() + * @method mixed unwatch() + * @method array waitaof(int $numLocal, int $numReplicas, int $timeout) + * @method mixed watch(string $key) + * @method mixed eval(string $script, int $numkeys, string ...$keyOrArg = null) + * @method mixed eval_ro(string $script, array $keys, ...$argument) + * @method mixed evalsha(string $script, int $numkeys, string ...$keyOrArg = null) + * @method mixed evalsha_ro(string $sha1, array $keys, ...$argument) + * @method mixed script($subcommand, $argument = null) + * @method Status shutdown(bool $noSave = null, bool $now = false, bool $force = false, bool $abort = false) + * @method mixed auth(string $password) + * @method string echo(string $message) + * @method mixed ping(string $message = null) + * @method mixed select(int $database) + * @method mixed bgrewriteaof() + * @method mixed bgsave() + * @method mixed client($subcommand, $argument = null) + * @method mixed config($subcommand, $argument = null) + * @method int dbsize() + * @method mixed flushall() + * @method mixed flushdb() + * @method array info($section = null) + * @method int lastsave() + * @method mixed save() + * @method mixed slaveof(string $host, int $port) + * @method mixed slowlog($subcommand, $argument = null) + * @method array time() + * @method array command() + * @method int geoadd(string $key, $longitude, $latitude, $member) + * @method array geohash(string $key, array $members) + * @method array geopos(string $key, array $members) + * @method string|null geodist(string $key, $member1, $member2, $unit = null) + * @method array georadius(string $key, $longitude, $latitude, $radius, $unit, array $options = null) + * @method array georadiusbymember(string $key, $member, $radius, $unit, array $options = null) + * @method array geosearch(string $key, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $withCoord = false, bool $withDist = false, bool $withHash = false) + * @method int geosearchstore(string $destination, string $source, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $storeDist = false) + * + * Container commands + * @property CLUSTER $cluster + * @property FunctionContainer $function + * @property FTCONFIG $ftconfig + * @property FTCURSOR $ftcursor + * @property JSONDEBUG $jsondebug + * @property ACL $acl + */ +interface ClientInterface +{ + /** + * Returns the command factory used by the client. + * + * @return FactoryInterface + */ + public function getCommandFactory(); + + /** + * Returns the client options specified upon initialization. + * + * @return OptionsInterface + */ + public function getOptions(); + + /** + * Opens the underlying connection to the server. + */ + public function connect(); + + /** + * Closes the underlying connection from the server. + */ + public function disconnect(); + + /** + * Returns the underlying connection instance. + * + * @return ConnectionInterface + */ + public function getConnection(); + + /** + * Creates a new instance of the specified Redis command. + * + * @param string $method Command ID. + * @param array $arguments Arguments for the command. + * + * @return CommandInterface + */ + public function createCommand($method, $arguments = []); + + /** + * Executes the specified Redis command. + * + * @param CommandInterface $command Command instance. + * + * @return mixed + */ + public function executeCommand(CommandInterface $command); + + /** + * Creates a Redis command with the specified arguments and sends a request + * to the server. + * + * @param string $method Command ID. + * @param array $arguments Arguments for the command. + * + * @return mixed + */ + public function __call($method, $arguments); +} diff --git a/plugins/cache-redis/Predis/Cluster/ClusterStrategy.php b/plugins/cache-redis/Predis/Cluster/ClusterStrategy.php new file mode 100644 index 0000000000..1965e26dd3 --- /dev/null +++ b/plugins/cache-redis/Predis/Cluster/ClusterStrategy.php @@ -0,0 +1,493 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Cluster; + +use InvalidArgumentException; +use Predis\Command\CommandInterface; +use Predis\Command\ScriptCommand; + +/** + * Common class implementing the logic needed to support clustering strategies. + */ +abstract class ClusterStrategy implements StrategyInterface +{ + protected $commands; + + public function __construct() + { + $this->commands = $this->getDefaultCommands(); + } + + /** + * Returns the default map of supported commands with their handlers. + * + * @return array + */ + protected function getDefaultCommands() + { + $getKeyFromFirstArgument = [$this, 'getKeyFromFirstArgument']; + $getKeyFromAllArguments = [$this, 'getKeyFromAllArguments']; + + return [ + /* commands operating on the key space */ + 'EXISTS' => $getKeyFromAllArguments, + 'DEL' => $getKeyFromAllArguments, + 'TYPE' => $getKeyFromFirstArgument, + 'EXPIRE' => $getKeyFromFirstArgument, + 'EXPIREAT' => $getKeyFromFirstArgument, + 'PERSIST' => $getKeyFromFirstArgument, + 'PEXPIRE' => $getKeyFromFirstArgument, + 'PEXPIREAT' => $getKeyFromFirstArgument, + 'TTL' => $getKeyFromFirstArgument, + 'PTTL' => $getKeyFromFirstArgument, + 'SORT' => [$this, 'getKeyFromSortCommand'], + 'DUMP' => $getKeyFromFirstArgument, + 'RESTORE' => $getKeyFromFirstArgument, + 'FLUSHDB' => [$this, 'getFakeKey'], + + /* commands operating on string values */ + 'APPEND' => $getKeyFromFirstArgument, + 'DECR' => $getKeyFromFirstArgument, + 'DECRBY' => $getKeyFromFirstArgument, + 'GET' => $getKeyFromFirstArgument, + 'GETBIT' => $getKeyFromFirstArgument, + 'MGET' => $getKeyFromAllArguments, + 'SET' => $getKeyFromFirstArgument, + 'GETRANGE' => $getKeyFromFirstArgument, + 'GETSET' => $getKeyFromFirstArgument, + 'INCR' => $getKeyFromFirstArgument, + 'INCRBY' => $getKeyFromFirstArgument, + 'INCRBYFLOAT' => $getKeyFromFirstArgument, + 'SETBIT' => $getKeyFromFirstArgument, + 'SETEX' => $getKeyFromFirstArgument, + 'MSET' => [$this, 'getKeyFromInterleavedArguments'], + 'MSETNX' => [$this, 'getKeyFromInterleavedArguments'], + 'SETNX' => $getKeyFromFirstArgument, + 'SETRANGE' => $getKeyFromFirstArgument, + 'STRLEN' => $getKeyFromFirstArgument, + 'SUBSTR' => $getKeyFromFirstArgument, + 'BITOP' => [$this, 'getKeyFromBitOp'], + 'BITCOUNT' => $getKeyFromFirstArgument, + 'BITFIELD' => $getKeyFromFirstArgument, + + /* commands operating on lists */ + 'LINSERT' => $getKeyFromFirstArgument, + 'LINDEX' => $getKeyFromFirstArgument, + 'LLEN' => $getKeyFromFirstArgument, + 'LPOP' => $getKeyFromFirstArgument, + 'RPOP' => $getKeyFromFirstArgument, + 'RPOPLPUSH' => $getKeyFromAllArguments, + 'BLPOP' => [$this, 'getKeyFromBlockingListCommands'], + 'BRPOP' => [$this, 'getKeyFromBlockingListCommands'], + 'BRPOPLPUSH' => [$this, 'getKeyFromBlockingListCommands'], + 'LPUSH' => $getKeyFromFirstArgument, + 'LPUSHX' => $getKeyFromFirstArgument, + 'RPUSH' => $getKeyFromFirstArgument, + 'RPUSHX' => $getKeyFromFirstArgument, + 'LRANGE' => $getKeyFromFirstArgument, + 'LREM' => $getKeyFromFirstArgument, + 'LSET' => $getKeyFromFirstArgument, + 'LTRIM' => $getKeyFromFirstArgument, + + /* commands operating on sets */ + 'SADD' => $getKeyFromFirstArgument, + 'SCARD' => $getKeyFromFirstArgument, + 'SDIFF' => $getKeyFromAllArguments, + 'SDIFFSTORE' => $getKeyFromAllArguments, + 'SINTER' => $getKeyFromAllArguments, + 'SINTERSTORE' => $getKeyFromAllArguments, + 'SUNION' => $getKeyFromAllArguments, + 'SUNIONSTORE' => $getKeyFromAllArguments, + 'SISMEMBER' => $getKeyFromFirstArgument, + 'SMEMBERS' => $getKeyFromFirstArgument, + 'SSCAN' => $getKeyFromFirstArgument, + 'SPOP' => $getKeyFromFirstArgument, + 'SRANDMEMBER' => $getKeyFromFirstArgument, + 'SREM' => $getKeyFromFirstArgument, + + /* commands operating on sorted sets */ + 'ZADD' => $getKeyFromFirstArgument, + 'ZCARD' => $getKeyFromFirstArgument, + 'ZCOUNT' => $getKeyFromFirstArgument, + 'ZINCRBY' => $getKeyFromFirstArgument, + 'ZINTERSTORE' => [$this, 'getKeyFromZsetAggregationCommands'], + 'ZRANGE' => $getKeyFromFirstArgument, + 'ZRANGEBYSCORE' => $getKeyFromFirstArgument, + 'ZRANK' => $getKeyFromFirstArgument, + 'ZREM' => $getKeyFromFirstArgument, + 'ZREMRANGEBYRANK' => $getKeyFromFirstArgument, + 'ZREMRANGEBYSCORE' => $getKeyFromFirstArgument, + 'ZREVRANGE' => $getKeyFromFirstArgument, + 'ZREVRANGEBYSCORE' => $getKeyFromFirstArgument, + 'ZREVRANK' => $getKeyFromFirstArgument, + 'ZSCORE' => $getKeyFromFirstArgument, + 'ZUNIONSTORE' => [$this, 'getKeyFromZsetAggregationCommands'], + 'ZSCAN' => $getKeyFromFirstArgument, + 'ZLEXCOUNT' => $getKeyFromFirstArgument, + 'ZRANGEBYLEX' => $getKeyFromFirstArgument, + 'ZREMRANGEBYLEX' => $getKeyFromFirstArgument, + 'ZREVRANGEBYLEX' => $getKeyFromFirstArgument, + + /* commands operating on hashes */ + 'HDEL' => $getKeyFromFirstArgument, + 'HEXISTS' => $getKeyFromFirstArgument, + 'HGET' => $getKeyFromFirstArgument, + 'HGETALL' => $getKeyFromFirstArgument, + 'HMGET' => $getKeyFromFirstArgument, + 'HMSET' => $getKeyFromFirstArgument, + 'HINCRBY' => $getKeyFromFirstArgument, + 'HINCRBYFLOAT' => $getKeyFromFirstArgument, + 'HKEYS' => $getKeyFromFirstArgument, + 'HLEN' => $getKeyFromFirstArgument, + 'HSET' => $getKeyFromFirstArgument, + 'HSETNX' => $getKeyFromFirstArgument, + 'HVALS' => $getKeyFromFirstArgument, + 'HSCAN' => $getKeyFromFirstArgument, + 'HSTRLEN' => $getKeyFromFirstArgument, + + /* commands operating on HyperLogLog */ + 'PFADD' => $getKeyFromFirstArgument, + 'PFCOUNT' => $getKeyFromAllArguments, + 'PFMERGE' => $getKeyFromAllArguments, + + /* scripting */ + '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'], + ]; + } + + /** + * Returns the list of IDs for the supported commands. + * + * @return array + */ + public function getSupportedCommands() + { + return array_keys($this->commands); + } + + /** + * Sets an handler for the specified command ID. + * + * The signature of the callback must have a single parameter of type + * Predis\Command\CommandInterface. + * + * When the callback argument is omitted or NULL, the previously associated + * handler for the specified command ID is removed. + * + * @param string $commandID Command ID. + * @param mixed $callback A valid callable object, or NULL to unset the handler. + * + * @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( + 'The argument must be a callable object or NULL.' + ); + } + + $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. + * + * @param CommandInterface $command Command instance. + * + * @return string + */ + protected function getKeyFromFirstArgument(CommandInterface $command) + { + return $command->getArgument(0); + } + + /** + * Extracts the key from a command with multiple keys only when all keys in + * the arguments array produce the same hash. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromAllArguments(CommandInterface $command) + { + $arguments = $command->getArguments(); + + if (!$this->checkSameSlotForKeys($arguments)) { + return null; + } + + return $arguments[0]; + } + + /** + * Extracts the key from a command with multiple keys only when all keys in + * the arguments array produce the same hash. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromInterleavedArguments(CommandInterface $command) + { + $arguments = $command->getArguments(); + $keys = []; + + for ($i = 0; $i < count($arguments); $i += 2) { + $keys[] = $arguments[$i]; + } + + 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; + } + + /** + * Extracts the key from BLPOP and BRPOP commands. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromBlockingListCommands(CommandInterface $command) + { + $arguments = $command->getArguments(); + + if (!$this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) { + return null; + } + + return $arguments[0]; + } + + /** + * Extracts the key from BITOP command. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromBitOp(CommandInterface $command) + { + $arguments = $command->getArguments(); + + 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]; + } + + /** + * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromZsetAggregationCommands(CommandInterface $command) + { + $arguments = $command->getArguments(); + $keys = array_merge([$arguments[0]], array_slice($arguments, 2, $arguments[1])); + + if (!$this->checkSameSlotForKeys($keys)) { + return null; + } + + return $arguments[0]; + } + + /** + * Extracts the key from EVAL and EVALSHA commands. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromScriptingCommands(CommandInterface $command) + { + $keys = $command instanceof ScriptCommand + ? $command->getKeys() + : array_slice($args = $command->getArguments(), 2, $args[1]); + + if (!$keys || !$this->checkSameSlotForKeys($keys)) { + return null; + } + + return $keys[0]; + } + + /** + * {@inheritdoc} + */ + public function getSlot(CommandInterface $command) + { + $slot = $command->getSlot(); + + if (!isset($slot) && isset($this->commands[$cmdID = $command->getId()])) { + $key = call_user_func($this->commands[$cmdID], $command); + + if (isset($key)) { + $slot = $this->getSlotByKey($key); + $command->setSlot($slot); + } + } + + return $slot; + } + + /** + * Checks if the specified array of keys will generate the same hash. + * + * @param array $keys Array of keys. + * + * @return bool + */ + protected function checkSameSlotForKeys(array $keys) + { + if (!$count = count($keys)) { + return false; + } + + $currentSlot = $this->getSlotByKey($keys[0]); + + for ($i = 1; $i < $count; ++$i) { + $nextSlot = $this->getSlotByKey($keys[$i]); + + if ($currentSlot !== $nextSlot) { + return false; + } + + $currentSlot = $nextSlot; + } + + return true; + } + + /** + * Returns only the hashable part of a key (delimited by "{...}"), or the + * whole key if a key tag is not found in the string. + * + * @param string $key A key. + * + * @return string + */ + protected function extractKeyTag($key) + { + if (false !== $start = strpos($key, '{')) { + if (false !== ($end = strpos($key, '}', $start)) && $end !== ++$start) { + $key = substr($key, $start, $end - $start); + } + } + + return $key; + } +} diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Cluster/Distributor/DistributorInterface.php rename to plugins/cache-redis/Predis/Cluster/Distributor/DistributorInterface.php index 831f52c529..593d9bb3c0 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * A distributor implements the logic to automatically distribute keys among * several nodes for client-side sharding. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface DistributorInterface { diff --git a/plugins/cache-redis/Predis/Cluster/Distributor/EmptyRingException.php b/plugins/cache-redis/Predis/Cluster/Distributor/EmptyRingException.php new file mode 100644 index 0000000000..68172066e8 --- /dev/null +++ b/plugins/cache-redis/Predis/Cluster/Distributor/EmptyRingException.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Cluster\Distributor; + +use Exception; + +/** + * Exception class that identifies empty rings. + */ +class EmptyRingException extends Exception +{ +} diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Cluster/Distributor/HashRing.php rename to plugins/cache-redis/Predis/Cluster/Distributor/HashRing.php index db864d9127..03e75106f4 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> * @author Lorenzo Castelli <lcastelli@gmail.com> */ 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 @@ public function add($node, $weight = null) { // 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 @@ private function initialize() throw new EmptyRingException('Cannot initialize an empty hashring.'); } - $this->ring = array(); + $this->ring = []; $totalWeight = $this->computeTotalWeight(); $nodesCount = count($this->nodes); @@ -161,7 +160,7 @@ protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightR $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 @@ public function getSlot($hash) public function get($value) { $hash = $this->hash($value); - $node = $this->getByHash($hash); - return $node; + return $this->getByHash($hash); } /** diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Cluster/Distributor/KetamaRing.php rename to plugins/cache-redis/Predis/Cluster/Distributor/KetamaRing.php index dc77f320f4..af3b884556 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> * @author Lorenzo Castelli <lcastelli@gmail.com> */ 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/plugins/cache-redis/Predis/Cluster/Hash/CRC16.php b/plugins/cache-redis/Predis/Cluster/Hash/CRC16.php new file mode 100644 index 0000000000..4b21d5d219 --- /dev/null +++ b/plugins/cache-redis/Predis/Cluster/Hash/CRC16.php @@ -0,0 +1,73 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Cluster\Hash; + +/** + * Hash generator implementing the CRC-CCITT-16 algorithm used by redis-cluster. + */ +class CRC16 implements HashGeneratorInterface +{ + 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, + 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, + 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, + 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, + 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, + 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, + 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, + 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, + 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, + 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, + 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, + 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, + 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, + 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, + 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, + 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, + 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, + 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, + 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, + 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} + */ + public function hash($value) + { + // CRC-CCITT-16 algorithm + $crc = 0; + $CCITT_16 = self::$CCITT_16; + + $value = (string) $value; + $strlen = strlen($value); + + for ($i = 0; $i < $strlen; ++$i) { + $crc = (($crc << 8) ^ $CCITT_16[($crc >> 8) ^ ord($value[$i])]) & 0xFFFF; + } + + return $crc; + } +} diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Cluster/Hash/HashGeneratorInterface.php rename to plugins/cache-redis/Predis/Cluster/Hash/HashGeneratorInterface.php index 271b9e720e..c835c0e66d 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * An hash generator implements the logic used to calculate the hash of a key to * distribute operations among Redis nodes. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ 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 0000000000..04f58a0ece --- /dev/null +++ b/plugins/cache-redis/Predis/Cluster/Hash/PhpiredisCRC16.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Cluster\Hash; + +use Predis\NotSupportedException; + +/** + * Hash generator implementing the CRC-CCITT-16 algorithm used by redis-cluster. + * + * @deprecated 2.1.2 + */ +class PhpiredisCRC16 implements HashGeneratorInterface +{ + public function __construct() + { + if (!function_exists('phpiredis_utils_crc16')) { + // @codeCoverageIgnoreStart + throw new NotSupportedException( + 'This hash generator requires a compatible version of ext-phpiredis' + ); + // @codeCoverageIgnoreEnd + } + } + + /** + * {@inheritdoc} + */ + public function hash($value) + { + return phpiredis_utils_crc16($value); + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Cluster/PredisStrategy.php b/plugins/cache-redis/Predis/Cluster/PredisStrategy.php similarity index 89% rename from rainloop/v/0.0.0/app/libraries/Predis/Cluster/PredisStrategy.php rename to plugins/cache-redis/Predis/Cluster/PredisStrategy.php index 2066842798..574b86f9ed 100644 --- a/rainloop/v/0.0.0/app/libraries/Predis/Cluster/PredisStrategy.php +++ b/plugins/cache-redis/Predis/Cluster/PredisStrategy.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri <suppakilla@gmail.com> + * (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 @@ /** * Default cluster strategy used by Predis to handle client-side sharding. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ class PredisStrategy extends ClusterStrategy { @@ -40,9 +39,8 @@ public function getSlotByKey($key) { $key = $this->extractKeyTag($key); $hash = $this->distributor->hash($key); - $slot = $this->distributor->getSlot($hash); - return $slot; + return $this->distributor->getSlot($hash); } /** diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Cluster/RedisStrategy.php b/plugins/cache-redis/Predis/Cluster/RedisStrategy.php similarity index 76% rename from rainloop/v/0.0.0/app/libraries/Predis/Cluster/RedisStrategy.php rename to plugins/cache-redis/Predis/Cluster/RedisStrategy.php index df0bdb49b2..8ae5c0f5e5 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Default class used by Predis to calculate hashes out of keys of * commands supported by redis-cluster. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ class RedisStrategy extends ClusterStrategy { @@ -41,9 +40,8 @@ public function __construct(HashGeneratorInterface $hashGenerator = null) 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 @@ public function getSlotByKey($key) */ 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 0000000000..1af63077a6 --- /dev/null +++ b/plugins/cache-redis/Predis/Cluster/SlotMap.php @@ -0,0 +1,209 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Cluster; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use OutOfBoundsException; +use Predis\Connection\NodeConnectionInterface; +use ReturnTypeWillChange; +use Traversable; + +/** + * Slot map for redis-cluster. + */ +class SlotMap implements ArrayAccess, IteratorAggregate, Countable +{ + private $slots = []; + + /** + * Checks if the given slot is valid. + * + * @param int $slot Slot index. + * + * @return bool + */ + public static function isValid($slot) + { + return $slot >= 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<int, string> + */ + #[ReturnTypeWillChange] + public function getIterator() + { + return new ArrayIterator($this->slots); + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Cluster/StrategyInterface.php b/plugins/cache-redis/Predis/Cluster/StrategyInterface.php similarity index 89% rename from rainloop/v/0.0.0/app/libraries/Predis/Cluster/StrategyInterface.php rename to plugins/cache-redis/Predis/Cluster/StrategyInterface.php index cdf7d09fac..83801ae6f9 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * keys extracted from supported commands. * * This is mostly useful to support clustering via client-side sharding. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ 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 @@ public function getSlot(CommandInterface $command); * * @param string $key Key string. * - * @return int + * @return int|null */ public function getSlotByKey($key); diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Collection/Iterator/CursorBasedIterator.php rename to plugins/cache-redis/Predis/Collection/Iterator/CursorBasedIterator.php index 922883f05e..946bbc3a09 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * can change several times during the iteration process. * * @see http://redis.io/commands/scan - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ -abstract class CursorBasedIterator implements \Iterator +abstract class CursorBasedIterator implements Iterator { protected $client; protected $match; @@ -65,8 +66,8 @@ public function __construct(ClientInterface $client, $match = null, $count = nul */ 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 @@ protected function reset() { $this->valid = true; $this->fetchmore = true; - $this->elements = array(); + $this->elements = []; $this->cursor = 0; $this->position = -1; $this->current = null; @@ -90,9 +91,9 @@ protected function reset() */ 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 protected function executeCommand(); */ protected function fetch() { - list($cursor, $elements) = $this->executeCommand(); + [$cursor, $elements] = $this->executeCommand(); if (!$cursor) { $this->fetchmore = false; @@ -137,8 +138,9 @@ protected function extractNext() } /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function rewind() { $this->reset(); @@ -146,27 +148,30 @@ public function rewind() } /** - * {@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 @@ public function next() } else { $this->valid = false; } - } } /** - * {@inheritdoc} + * @return bool */ + #[ReturnTypeWillChange] public function valid() { return $this->valid; diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Collection/Iterator/HashKey.php rename to plugins/cache-redis/Predis/Collection/Iterator/HashKey.php index aa8aeaf02b..91b7d27ff1 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> - * - * @link http://redis.io/commands/scan + * @see http://redis.io/commands/scan */ class HashKey extends CursorBasedIterator { @@ -51,6 +50,8 @@ protected function executeCommand() 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/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Collection/Iterator/Keyspace.php rename to plugins/cache-redis/Predis/Collection/Iterator/Keyspace.php index 5d985b9bc3..b5fa022aa8 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> - * - * @link http://redis.io/commands/scan + * @see http://redis.io/commands/scan */ class Keyspace extends CursorBasedIterator { diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Collection/Iterator/ListKey.php rename to plugins/cache-redis/Predis/Collection/Iterator/ListKey.php index 7a6eb479e3..79ab3aa192 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * guarantees on the returned elements because the collection can change several * times (trimmed, deleted, overwritten) during the iteration process. * - * @author Daniele Alessandri <suppakilla@gmail.com> - * - * @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 @@ public function __construct(ClientInterface $client, $key, $count = 10) */ 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 @@ protected function reset() { $this->valid = true; $this->fetchmore = true; - $this->elements = array(); + $this->elements = []; $this->position = -1; $this->current = null; } @@ -126,8 +128,9 @@ protected function extractNext() } /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function rewind() { $this->reset(); @@ -135,24 +138,27 @@ public function rewind() } /** - * {@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 @@ public function next() } /** - * {@inheritdoc} + * @return bool */ + #[ReturnTypeWillChange] public function valid() { return $this->valid; diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Collection/Iterator/SetKey.php rename to plugins/cache-redis/Predis/Collection/Iterator/SetKey.php index bf25439751..74f982346c 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> - * - * @link http://redis.io/commands/scan + * @see http://redis.io/commands/scan */ class SetKey extends CursorBasedIterator { diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Collection/Iterator/SortedSetKey.php rename to plugins/cache-redis/Predis/Collection/Iterator/SortedSetKey.php index e2f1789227..abee8c252c 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> - * - * @link http://redis.io/commands/scan + * @see http://redis.io/commands/scan */ class SortedSetKey extends CursorBasedIterator { @@ -50,11 +49,9 @@ protected function executeCommand() */ 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 0000000000..11073c0548 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/ArrayableArgument.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument; + +/** + * Allows to use object-oriented approach to handle complex conditional arguments. + */ +interface ArrayableArgument +{ + /** + * Get the instance as an array. + * + * @return array + */ + public function toArray(): array; +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/AbstractBy.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/AbstractBy.php new file mode 100644 index 0000000000..970833f34a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/AbstractBy.php @@ -0,0 +1,46 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Geospatial; + +use UnexpectedValueException; + +abstract class AbstractBy implements ByInterface +{ + /** + * @var string[] + */ + private static $unitEnum = ['m', 'km', 'ft', 'mi']; + + /** + * @var string + */ + protected $unit; + + /** + * {@inheritDoc} + */ + abstract public function toArray(): array; + + /** + * @param string $unit + * @return void + */ + protected function setUnit(string $unit): void + { + if (!in_array($unit, self::$unitEnum, true)) { + throw new UnexpectedValueException('Wrong value given for unit'); + } + + $this->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 0000000000..7dd9f23b7b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByBox.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Geospatial; + +class ByBox extends AbstractBy +{ + private const KEYWORD = 'BYBOX'; + + /** + * @var int + */ + private $width; + + /** + * @var int + */ + private $height; + + public function __construct(int $width, int $height, string $unit) + { + $this->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 0000000000..767886cefd --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByInterface.php @@ -0,0 +1,19 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Geospatial; + +use Predis\Command\Argument\ArrayableArgument; + +interface ByInterface extends ArrayableArgument +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByRadius.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByRadius.php new file mode 100644 index 0000000000..e2a4b49f70 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByRadius.php @@ -0,0 +1,37 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Geospatial; + +class ByRadius extends AbstractBy +{ + private const KEYWORD = 'BYRADIUS'; + + /** + * @var int + */ + private $radius; + + public function __construct(int $radius, string $unit) + { + $this->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 0000000000..44700bda4c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromInterface.php @@ -0,0 +1,19 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Geospatial; + +use Predis\Command\Argument\ArrayableArgument; + +interface FromInterface extends ArrayableArgument +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromLonLat.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromLonLat.php new file mode 100644 index 0000000000..f69be0f8c9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromLonLat.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Geospatial; + +class FromLonLat implements FromInterface +{ + private const KEYWORD = 'FROMLONLAT'; + + /** + * @var float + */ + private $longitude; + + /** + * @var float + */ + private $latitude; + + public function __construct(float $longitude, float $latitude) + { + $this->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 0000000000..9e24b2ba0f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromMember.php @@ -0,0 +1,36 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Geospatial; + +class FromMember implements FromInterface +{ + private const KEYWORD = 'FROMMEMBER'; + + /** + * @var string + */ + private $member; + + public function __construct(string $member) + { + $this->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 0000000000..950967070f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/AggregateArguments.php @@ -0,0 +1,161 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +class AggregateArguments extends CommonArguments +{ + /** + * @var string[] + */ + private $sortingEnum = [ + 'asc' => '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 0000000000..5acd2fe1ef --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/AlterArguments.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +class AlterArguments extends CommonArguments +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/CommonArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/CommonArguments.php new file mode 100644 index 0000000000..96268d4f7a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/CommonArguments.php @@ -0,0 +1,182 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +use Predis\Command\Argument\ArrayableArgument; + +class CommonArguments implements ArrayableArgument +{ + /** + * @var array + */ + protected $arguments = []; + + /** + * Adds default language for documents within an index. + * + * @param string $defaultLanguage + * @return $this + */ + public function language(string $defaultLanguage = 'english'): self + { + $this->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 0000000000..b8e0176bac --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/CreateArguments.php @@ -0,0 +1,191 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +use InvalidArgumentException; + +class CreateArguments extends CommonArguments +{ + /** + * @var string[] + */ + private $supportedDataTypesEnum = [ + 'hash' => '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 0000000000..a8bd6b56fc --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/CursorArguments.php @@ -0,0 +1,44 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +use Predis\Command\Argument\ArrayableArgument; + +class CursorArguments implements ArrayableArgument +{ + /** + * @var array + */ + protected $arguments = []; + + /** + * Is number of results to read. This parameter overrides COUNT specified in FT.AGGREGATE. + * + * @param int $readSize + * @return $this + */ + public function count(int $readSize): self + { + array_push($this->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 0000000000..0c6313201f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/DropArguments.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +use Predis\Command\Argument\ArrayableArgument; + +class DropArguments implements ArrayableArgument +{ + /** + * @var array + */ + protected $arguments = []; + + /** + * Drop operation that, if set, deletes the actual document hashes. + * + * @return $this + */ + public function dd(): self + { + $this->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 0000000000..b4bd235b93 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/ExplainArguments.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +class ExplainArguments extends CommonArguments +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/ProfileArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/ProfileArguments.php new file mode 100644 index 0000000000..4c8a8c4f21 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/ProfileArguments.php @@ -0,0 +1,81 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +use Predis\Command\Argument\ArrayableArgument; + +class ProfileArguments implements ArrayableArgument +{ + /** + * @var array + */ + protected $arguments = []; + + /** + * Adds search context. + * + * @return $this + */ + public function search(): self + { + $this->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 0000000000..eb49f09959 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/AbstractField.php @@ -0,0 +1,69 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search\SchemaFields; + +abstract class AbstractField implements FieldInterface +{ + public const SORTABLE = true; + public const NOT_SORTABLE = false; + public const SORTABLE_UNF = 'UNF'; + + /** + * @var array + */ + protected $fieldArguments = []; + + /** + * @param string $fieldType + * @param string $identifier + * @param string $alias + * @param bool|string $sortable + * @param bool $noIndex + * @return void + */ + protected function setCommonOptions( + string $fieldType, + string $identifier, + string $alias = '', + $sortable = self::NOT_SORTABLE, + bool $noIndex = false + ): void { + $this->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 0000000000..80e57eba06 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/FieldInterface.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search\SchemaFields; + +use Predis\Command\Argument\ArrayableArgument; + +/** + * Represents field in search schema. + */ +interface FieldInterface extends ArrayableArgument +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/GeoField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/GeoField.php new file mode 100644 index 0000000000..23e7cc7801 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/GeoField.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search\SchemaFields; + +class GeoField extends AbstractField +{ + /** + * @param string $identifier + * @param string $alias + * @param bool|string $sortable + * @param bool $noIndex + */ + public function __construct( + string $identifier, + string $alias = '', + $sortable = self::NOT_SORTABLE, + bool $noIndex = false + ) { + $this->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 0000000000..758b4e9c60 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/NumericField.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search\SchemaFields; + +class NumericField extends AbstractField +{ + /** + * @param string $identifier + * @param string $alias + * @param bool|string $sortable + * @param bool $noIndex + */ + public function __construct( + string $identifier, + string $alias = '', + $sortable = self::NOT_SORTABLE, + bool $noIndex = false + ) { + $this->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 0000000000..358b3090e6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TagField.php @@ -0,0 +1,44 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search\SchemaFields; + +class TagField extends AbstractField +{ + /** + * @param string $identifier + * @param string $alias + * @param bool|string $sortable + * @param bool $noIndex + * @param string $separator + * @param bool $caseSensitive + */ + public function __construct( + string $identifier, + string $alias = '', + $sortable = self::NOT_SORTABLE, + bool $noIndex = false, + string $separator = ',', + bool $caseSensitive = false + ) { + $this->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 0000000000..d72c623836 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TextField.php @@ -0,0 +1,57 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search\SchemaFields; + +class TextField extends AbstractField +{ + /** + * @param string $identifier + * @param string $alias + * @param bool|string $sortable + * @param bool $noIndex + * @param bool $noStem + * @param string $phonetic + * @param int $weight + * @param bool $withSuffixTrie + */ + public function __construct( + string $identifier, + string $alias = '', + $sortable = self::NOT_SORTABLE, + bool $noIndex = false, + bool $noStem = false, + string $phonetic = '', + int $weight = 1, + bool $withSuffixTrie = false + ) { + $this->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 0000000000..5228c22502 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/VectorField.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search\SchemaFields; + +class VectorField extends AbstractField +{ + /** + * @var array + */ + protected $fieldArguments = []; + + /** + * @param string $fieldName + * @param string $algorithm + * @param array $attributeNameValueDictionary + * @param string $alias + */ + public function __construct( + string $fieldName, + string $algorithm, + array $attributeNameValueDictionary, + string $alias = '' + ) { + $this->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 0000000000..d1eb70580c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SearchArguments.php @@ -0,0 +1,306 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +use InvalidArgumentException; + +class SearchArguments extends CommonArguments +{ + /** + * @var string[] + */ + private $sortingEnum = [ + 'asc' => '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 0000000000..7a6c248913 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SpellcheckArguments.php @@ -0,0 +1,59 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +use InvalidArgumentException; + +class SpellcheckArguments extends CommonArguments +{ + /** + * @var string[] + */ + private $termsEnum = [ + 'include' => '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 0000000000..c8b9769782 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SugAddArguments.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +class SugAddArguments extends CommonArguments +{ + /** + * Adds INCR modifier. + * + * @return $this + */ + public function incr(): self + { + $this->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 0000000000..1176c77369 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SugGetArguments.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +class SugGetArguments extends CommonArguments +{ + /** + * Performs a fuzzy prefix search, including prefixes at Levenshtein distance of 1 from the prefix sent. + * + * @return $this + */ + public function fuzzy(): self + { + $this->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 0000000000..a6b286a48f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SynUpdateArguments.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Search; + +class SynUpdateArguments extends CommonArguments +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Server/LimitInterface.php b/plugins/cache-redis/Predis/Command/Argument/Server/LimitInterface.php new file mode 100644 index 0000000000..95ebd6433b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Server/LimitInterface.php @@ -0,0 +1,19 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Server; + +use Predis\Command\Argument\ArrayableArgument; + +interface LimitInterface extends ArrayableArgument +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Server/LimitOffsetCount.php b/plugins/cache-redis/Predis/Command/Argument/Server/LimitOffsetCount.php new file mode 100644 index 0000000000..9de6733e19 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Server/LimitOffsetCount.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Server; + +class LimitOffsetCount implements LimitInterface +{ + private const KEYWORD = 'LIMIT'; + + /** + * @var int + */ + private $offset; + + /** + * @var int + */ + private $count; + + public function __construct(int $offset, int $count) + { + $this->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 0000000000..1d77ef6817 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Server/To.php @@ -0,0 +1,57 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\Server; + +use Predis\Command\Argument\ArrayableArgument; + +class To implements ArrayableArgument +{ + private const KEYWORD = 'TO'; + private const FORCE_KEYWORD = 'FORCE'; + + /** + * @var string + */ + private $host; + + /** + * @var int + */ + private $port; + + /** + * @var bool + */ + private $isForce; + + public function __construct(string $host, int $port, bool $isForce = false) + { + $this->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 0000000000..a9fe6f79f8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AddArguments.php @@ -0,0 +1,30 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\TimeSeries; + +class AddArguments extends CommonArguments +{ + /** + * Is overwrite key and database configuration for DUPLICATE_POLICY, + * the policy for handling samples with identical timestamps. + * + * @param string $policy + * @return $this + */ + public function onDuplicate(string $policy = self::POLICY_BLOCK): self + { + array_push($this->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 0000000000..238ebc36d6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AlterArguments.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\TimeSeries; + +class AlterArguments extends CommonArguments +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CommonArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CommonArguments.php new file mode 100644 index 0000000000..a2841bf388 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CommonArguments.php @@ -0,0 +1,143 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\TimeSeries; + +use Predis\Command\Argument\ArrayableArgument; + +class CommonArguments implements ArrayableArgument +{ + public const POLICY_BLOCK = 'BLOCK'; + public const POLICY_FIRST = 'FIRST'; + public const POLICY_LAST = 'LAST'; + public const POLICY_MIN = 'MIN'; + public const POLICY_MAX = 'MAX'; + public const POLICY_SUM = 'SUM'; + + public const ENCODING_UNCOMPRESSED = 'UNCOMPRESSED'; + public const ENCODING_COMPRESSED = 'COMPRESSED'; + + /** + * @var array + */ + protected $arguments = []; + + /** + * Is maximum age for samples compared to the highest reported timestamp, in milliseconds. + * + * @param int $retentionPeriod + * @return $this + */ + public function retentionMsecs(int $retentionPeriod): self + { + array_push($this->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 0000000000..e47d8ef722 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CreateArguments.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\TimeSeries; + +class CreateArguments extends CommonArguments +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/DecrByArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/DecrByArguments.php new file mode 100644 index 0000000000..c0da3f0b9c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/DecrByArguments.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\TimeSeries; + +class DecrByArguments extends IncrByArguments +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/GetArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/GetArguments.php new file mode 100644 index 0000000000..765685cbf4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/GetArguments.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\TimeSeries; + +class GetArguments extends CommonArguments +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/IncrByArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/IncrByArguments.php new file mode 100644 index 0000000000..103efb5f17 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/IncrByArguments.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\TimeSeries; + +class IncrByArguments extends CommonArguments +{ + /** + * Is (integer) UNIX sample timestamp in milliseconds or * to set the timestamp according to the server clock. + * + * @param string|int $timeStamp + * @return $this + */ + public function timestamp($timeStamp): self + { + array_push($this->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 0000000000..1b2cec6647 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/InfoArguments.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\TimeSeries; + +use Predis\Command\Argument\ArrayableArgument; + +class InfoArguments implements ArrayableArgument +{ + /** + * @var array + */ + private $arguments = []; + + /** + * Is an optional flag to get a more detailed information about the chunks. + * + * @return $this + */ + public function debug(): self + { + $this->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 0000000000..585f574e60 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MGetArguments.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\TimeSeries; + +class MGetArguments extends CommonArguments +{ +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MRangeArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MRangeArguments.php new file mode 100644 index 0000000000..c1f89c17d2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MRangeArguments.php @@ -0,0 +1,44 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\TimeSeries; + +class MRangeArguments extends RangeArguments +{ + /** + * Filters time series based on their labels and label values. + * + * @param string ...$filterExpressions + * @return $this + */ + public function filter(string ...$filterExpressions): self + { + array_push($this->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 0000000000..00d3772d79 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/RangeArguments.php @@ -0,0 +1,85 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Argument\TimeSeries; + +class RangeArguments extends CommonArguments +{ + /** + * Filters samples by a list of specific timestamps. + * + * @param int ...$ts + * @return $this + */ + public function filterByTs(int ...$ts): self + { + array_push($this->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/rainloop/v/0.0.0/app/libraries/Predis/Command/Command.php b/plugins/cache-redis/Predis/Command/Command.php similarity index 75% rename from rainloop/v/0.0.0/app/libraries/Predis/Command/Command.php rename to plugins/cache-redis/Predis/Command/Command.php index bb538e7c5e..68629c454e 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Base class for Redis commands. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ 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 @@ public function setSlot($slot) */ public function getSlot() { - if (isset($this->slot)) { - return $this->slot; - } + return $this->slot ?? null; } /** @@ -104,7 +89,7 @@ public function parseResponse($data) */ 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 @@ public static function normalizeArguments(array $arguments) 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/rainloop/v/0.0.0/app/libraries/Predis/Command/CommandInterface.php b/plugins/cache-redis/Predis/Command/CommandInterface.php similarity index 90% rename from rainloop/v/0.0.0/app/libraries/Predis/Command/CommandInterface.php rename to plugins/cache-redis/Predis/Command/CommandInterface.php index 9f349e1dfc..20480316d0 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Defines an abstraction representing a Redis command. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface CommandInterface { @@ -73,7 +72,7 @@ public function getArgument($index); /** * 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 0000000000..ed94dc5720 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Factory.php @@ -0,0 +1,143 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +use InvalidArgumentException; +use Predis\ClientException; +use Predis\Command\Processor\ProcessorInterface; + +/** + * Base command factory class. + * + * This class provides all of the common functionalities required for a command + * factory to create new instances of Redis commands objects. It also allows to + * define or undefine command handler classes for each command ID. + */ +abstract class Factory implements FactoryInterface +{ + protected $commands = []; + protected $processor; + + /** + * {@inheritdoc} + */ + public function supports(string ...$commandIDs): bool + { + foreach ($commandIDs as $commandID) { + if ($this->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 0000000000..e81951086f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/FactoryInterface.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +/** + * Command factory interface. + * + * A command factory is used through the library to create instances of commands + * classes implementing Predis\Command\CommandInterface mapped to Redis commands + * by their command ID string (SET, GET, etc...). + */ +interface FactoryInterface +{ + /** + * Checks if the command factory supports the specified list of commands. + * + * @param string ...$commandIDs List of command IDs + * + * @return bool + */ + public function supports(string ...$commandIDs): bool; + + /** + * Creates a new command instance. + * + * @param string $commandID Command ID + * @param array $arguments Arguments for the command + * + * @return CommandInterface + */ + public function create(string $commandID, array $arguments = []): CommandInterface; +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Command/PrefixableCommandInterface.php b/plugins/cache-redis/Predis/Command/PrefixableCommandInterface.php similarity index 83% rename from rainloop/v/0.0.0/app/libraries/Predis/Command/PrefixableCommandInterface.php rename to plugins/cache-redis/Predis/Command/PrefixableCommandInterface.php index 6d54554faa..66b5d19c25 100644 --- a/rainloop/v/0.0.0/app/libraries/Predis/Command/PrefixableCommandInterface.php +++ b/plugins/cache-redis/Predis/Command/PrefixableCommandInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri <suppakilla@gmail.com> + * (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 @@ /** * Defines a command whose keys can be prefixed. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ 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 0000000000..d7a30c8804 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Processor/KeyPrefixProcessor.php @@ -0,0 +1,577 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Processor; + +use InvalidArgumentException; +use Predis\Command\CommandInterface; +use Predis\Command\PrefixableCommandInterface; + +/** + * Command processor capable of prefixing keys stored in the arguments of Redis + * commands supported. + */ +class KeyPrefixProcessor implements ProcessorInterface +{ + private $prefix; + private $commands; + + /** + * @param string $prefix Prefix for the keys. + */ + public function __construct($prefix) + { + $this->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/plugins/cache-redis/Predis/Command/Processor/ProcessorChain.php b/plugins/cache-redis/Predis/Command/Processor/ProcessorChain.php new file mode 100644 index 0000000000..1ce915e2bb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Processor/ProcessorChain.php @@ -0,0 +1,142 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Processor; + +use ArrayAccess; +use ArrayIterator; +use InvalidArgumentException; +use Predis\Command\CommandInterface; +use ReturnTypeWillChange; +use Traversable; + +/** + * Default implementation of a command processors chain. + */ +class ProcessorChain implements ArrayAccess, ProcessorInterface +{ + private $processors = []; + + /** + * @param array $processors List of instances of ProcessorInterface. + */ + public function __construct($processors = []) + { + foreach ($processors as $processor) { + $this->add($processor); + } + } + + /** + * {@inheritdoc} + */ + public function add(ProcessorInterface $processor) + { + $this->processors[] = $processor; + } + + /** + * {@inheritdoc} + */ + public function remove(ProcessorInterface $processor) + { + if (false !== $index = array_search($processor, $this->processors, true)) { + unset($this[$index]); + } + } + + /** + * {@inheritdoc} + */ + public function process(CommandInterface $command) + { + for ($i = 0; $i < $count = count($this->processors); ++$i) { + $this->processors[$i]->process($command); + } + } + + /** + * {@inheritdoc} + */ + public function getProcessors() + { + return $this->processors; + } + + /** + * Returns an iterator over the list of command processor in the chain. + * + * @return Traversable<int, ProcessorInterface> + */ + public function getIterator() + { + return new ArrayIterator($this->processors); + } + + /** + * Returns the number of command processors in the chain. + * + * @return int + */ + public function count() + { + return count($this->processors); + } + + /** + * @param int $index + * @return bool + */ + #[ReturnTypeWillChange] + public function offsetExists($index) + { + return isset($this->processors[$index]); + } + + /** + * @param int $index + * @return ProcessorInterface + */ + #[ReturnTypeWillChange] + public function offsetGet($index) + { + return $this->processors[$index]; + } + + /** + * @param int $index + * @param ProcessorInterface $processor + * @return void + */ + #[ReturnTypeWillChange] + public function offsetSet($index, $processor) + { + if (!$processor instanceof ProcessorInterface) { + throw new InvalidArgumentException( + 'Processor chain accepts only instances of `Predis\Command\Processor\ProcessorInterface`' + ); + } + + $this->processors[$index] = $processor; + } + + /** + * @param int $index + * @return void + */ + #[ReturnTypeWillChange] + public function offsetUnset($index) + { + unset($this->processors[$index]); + $this->processors = array_values($this->processors); + } +} diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorInterface.php rename to plugins/cache-redis/Predis/Command/Processor/ProcessorInterface.php index 2f9105802b..f915b9eb69 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * A command processor processes Redis commands before they are sent to Redis. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ProcessorInterface { diff --git a/plugins/cache-redis/Predis/Command/RawCommand.php b/plugins/cache-redis/Predis/Command/RawCommand.php new file mode 100644 index 0000000000..61b223112f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/RawCommand.php @@ -0,0 +1,123 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +/** + * Class representing a generic Redis command. + * + * Arguments and responses for these commands are not normalized and they follow + * what is defined by the Redis documentation. + * + * 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. + */ +final class RawCommand implements CommandInterface +{ + private $slot; + private $commandID; + private $arguments; + + /** + * @param string $commandID Command ID + * @param array $arguments Command arguments + */ + public function __construct($commandID, array $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 ...$args Arguments list for the command + * + * @return CommandInterface + */ + public static function create($commandID, ...$args) + { + $arguments = func_get_args(); + + return new static(array_shift($arguments), $arguments); + } + + /** + * {@inheritdoc} + */ + public function getId() + { + return $this->commandID; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $this->arguments = $arguments; + unset($this->slot); + } + + /** + * {@inheritdoc} + */ + public function setRawArguments(array $arguments) + { + $this->setArguments($arguments); + } + + /** + * {@inheritdoc} + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * {@inheritdoc} + */ + public function getArgument($index) + { + if (isset($this->arguments[$index])) { + return $this->arguments[$index]; + } + } + + /** + * {@inheritdoc} + */ + public function setSlot($slot) + { + $this->slot = $slot; + } + + /** + * {@inheritdoc} + */ + public function getSlot() + { + return $this->slot ?? null; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/RawFactory.php b/plugins/cache-redis/Predis/Command/RawFactory.php new file mode 100644 index 0000000000..2c0637f225 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/RawFactory.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +/** + * Command factory creating raw command instances out of command IDs. + * + * Any command ID will produce a command instance even for unknown commands that + * are not implemented by Redis (the server will return a "-ERR unknown command" + * error responses). + * + * When using this factory the client does not process arguments before sending + * commands to Redis and server responses are not further processed before being + * returned to the caller. + */ +class RawFactory implements FactoryInterface +{ + /** + * {@inheritdoc} + */ + public function supports(string ...$commandIDs): bool + { + return true; + } + + /** + * {@inheritdoc} + */ + public function create(string $commandID, array $arguments = []): CommandInterface + { + return new RawCommand($commandID, $arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ACL.php b/plugins/cache-redis/Predis/Command/Redis/ACL.php new file mode 100644 index 0000000000..e8999ea2ed --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ACL.php @@ -0,0 +1,53 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/?name=ACL + * + * Container command corresponds to any ACL *. + * Represents any ACL command with subcommand as first argument. + */ +class ACL extends RedisCommand +{ + public function getId() + { + return 'ACL'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (!is_array($data)) { + return $data; + } + + if ($data === array_values($data)) { + return $data; + } + + // flatten Relay (RESP3) maps + $return = []; + + array_walk($data, function ($value, $key) use (&$return) { + $return[] = $key; + $return[] = $value; + }); + + return $return; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/APPEND.php b/plugins/cache-redis/Predis/Command/Redis/APPEND.php new file mode 100644 index 0000000000..6276245793 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/APPEND.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/append + */ +class APPEND extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'APPEND'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/AUTH.php b/plugins/cache-redis/Predis/Command/Redis/AUTH.php new file mode 100644 index 0000000000..cc2020e5fb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/AUTH.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/auth + */ +class AUTH extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'AUTH'; + } +} 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 0000000000..ed4a53b808 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/AbstractCommand/BZPOPBase.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\AbstractCommand; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Keys; + +abstract class BZPOPBase extends RedisCommand +{ + use Keys { + Keys::setArguments as setKeys; + } + + protected static $keysArgumentPositionOffset = 0; + + abstract public function getId(): string; + + public function setArguments(array $arguments) + { + $this->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/plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php b/plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php new file mode 100644 index 0000000000..97d443d90e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php @@ -0,0 +1,37 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/bgrewriteaof + */ +class BGREWRITEAOF extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BGREWRITEAOF'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + return $data == 'Background append only file rewriting started'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BGSAVE.php b/plugins/cache-redis/Predis/Command/Redis/BGSAVE.php new file mode 100644 index 0000000000..9be85773fe --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BGSAVE.php @@ -0,0 +1,37 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/bgsave + */ +class BGSAVE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BGSAVE'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + return $data === 'Background saving started' ? true : $data; + } +} 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 0000000000..859daf4a67 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BITCOUNT.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\BitByte; + +/** + * @see http://redis.io/commands/bitcount + * + * Count the number of set bits (population counting) in a string. + */ +class BITCOUNT extends RedisCommand +{ + use BitByte; + + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BITCOUNT'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BITFIELD.php b/plugins/cache-redis/Predis/Command/Redis/BITFIELD.php new file mode 100644 index 0000000000..5ded947948 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BITFIELD.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/bitfield + */ +class BITFIELD extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BITFIELD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BITOP.php b/plugins/cache-redis/Predis/Command/Redis/BITOP.php new file mode 100644 index 0000000000..fcf0495cde --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BITOP.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/bitop + */ +class BITOP extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BITOP'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 3 && is_array($arguments[2])) { + [$operation, $destination] = $arguments; + $arguments = $arguments[2]; + array_unshift($arguments, $operation, $destination); + } + + 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 0000000000..6ea4188174 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BITPOS.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\BitByte; + +/** + * @see http://redis.io/commands/bitpos + * + * Return the position of the first bit set to 1 or 0 in a string. + */ +class BITPOS extends RedisCommand +{ + use BitByte; + + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BITPOS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BLMOVE.php b/plugins/cache-redis/Predis/Command/Redis/BLMOVE.php new file mode 100644 index 0000000000..eec6952594 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BLMOVE.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +class BLMOVE extends LMOVE +{ + public function getId() + { + return 'BLMOVE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BLMPOP.php b/plugins/cache-redis/Predis/Command/Redis/BLMPOP.php new file mode 100644 index 0000000000..e5be9017f9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BLMPOP.php @@ -0,0 +1,25 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +class BLMPOP extends LMPOP +{ + protected static $keysArgumentPositionOffset = 1; + protected static $leftRightArgumentPositionOffset = 2; + protected static $countArgumentPositionOffset = 3; + + public function getId() + { + return 'BLMPOP'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BLPOP.php b/plugins/cache-redis/Predis/Command/Redis/BLPOP.php new file mode 100644 index 0000000000..feb7ee4b88 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BLPOP.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/blpop + */ +class BLPOP extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BLPOP'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[0])) { + [$arguments, $timeout] = $arguments; + array_push($arguments, $timeout); + } + + 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 0000000000..cf88dc906a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BRPOP.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/brpop + */ +class BRPOP extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BRPOP'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[0])) { + [$arguments, $timeout] = $arguments; + array_push($arguments, $timeout); + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BRPOPLPUSH.php b/plugins/cache-redis/Predis/Command/Redis/BRPOPLPUSH.php new file mode 100644 index 0000000000..5cd9cb9410 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BRPOPLPUSH.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/brpoplpush + */ +class BRPOPLPUSH extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BRPOPLPUSH'; + } +} 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 0000000000..2ed6f78939 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BZMPOP.php @@ -0,0 +1,30 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @see https://redis.io/commands/bzmpop/ + * + * BZMPOP is the blocking variant of ZMPOP. + */ +class BZMPOP extends ZMPOP +{ + protected static $keysArgumentPositionOffset = 1; + protected static $countArgumentPositionOffset = 3; + protected static $modifierArgumentPositionOffset = 2; + + public function getId() + { + return 'BZMPOP'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BZPOPMAX.php b/plugins/cache-redis/Predis/Command/Redis/BZPOPMAX.php new file mode 100644 index 0000000000..1abd1bddd5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BZPOPMAX.php @@ -0,0 +1,33 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Redis\AbstractCommand\BZPOPBase; + +/** + * @see https://redis.io/commands/bzpopmax/ + * + * BZPOPMAX is the blocking variant of the sorted set ZPOPMAX primitive. + * + * It is the blocking version because it blocks the connection when there are + * no members to pop from any of the given sorted sets. + * A member with the highest score is popped from first sorted set that is non-empty, + * with the given keys being checked in the order that they are given. + */ +class BZPOPMAX extends BZPOPBase +{ + public function getId(): string + { + return 'BZPOPMAX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BZPOPMIN.php b/plugins/cache-redis/Predis/Command/Redis/BZPOPMIN.php new file mode 100644 index 0000000000..5b1a325249 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BZPOPMIN.php @@ -0,0 +1,33 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Redis\AbstractCommand\BZPOPBase; + +/** + * @see https://redis.io/commands/bzpopmin/ + * + * BZPOPMIN is the blocking variant of the sorted set ZPOPMIN primitive. + * + * It is the blocking version because it blocks the connection when there are + * no members to pop from any of the given sorted sets. + * A member with the lowest score is popped from first sorted set that is non-empty, + * with the given keys being checked in the order that they are given. + */ +class BZPOPMIN extends BZPOPBase +{ + public function getId(): string + { + return 'BZPOPMIN'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFADD.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFADD.php new file mode 100644 index 0000000000..af30de08de --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFADD.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\BloomFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/bf.add/ + * + * Creates an empty Bloom Filter with a single sub-filter for the + * initial capacity requested and with an upper bound error_rate. + */ +class BFADD extends RedisCommand +{ + public function getId() + { + return 'BF.ADD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFEXISTS.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFEXISTS.php new file mode 100644 index 0000000000..73f0fbc5bb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFEXISTS.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\BloomFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/bf.exists/ + * + * Determines whether an item may exist in the Bloom Filter or not. + */ +class BFEXISTS extends RedisCommand +{ + public function getId() + { + return 'BF.EXISTS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINFO.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINFO.php new file mode 100644 index 0000000000..00acd4d579 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINFO.php @@ -0,0 +1,79 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\BloomFilter; + +use Predis\Command\Command as RedisCommand; +use UnexpectedValueException; + +/** + * @see https://redis.io/commands/bf.info/ + * + * Return information about key filter. + */ +class BFINFO extends RedisCommand +{ + /** + * @var string[] + */ + private $modifierEnum = [ + 'capacity' => '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 0000000000..50e23eeaf9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINSERT.php @@ -0,0 +1,72 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\BloomFilter; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\BloomFilters\Capacity; +use Predis\Command\Traits\BloomFilters\Error; +use Predis\Command\Traits\BloomFilters\Expansion; +use Predis\Command\Traits\BloomFilters\Items; +use Predis\Command\Traits\BloomFilters\NoCreate; + +class BFINSERT extends RedisCommand +{ + use Capacity { + Capacity::setArguments as setCapacity; + } + use Error { + Error::setArguments as setErrorRate; + } + use Expansion { + Expansion::setArguments as setExpansion; + } + use Items { + Items::setArguments as setItems; + } + use NoCreate { + NoCreate::setArguments as setNoCreate; + } + + protected static $capacityArgumentPositionOffset = 1; + protected static $errorArgumentPositionOffset = 2; + protected static $expansionArgumentPositionOffset = 3; + protected static $noCreateArgumentPositionOffset = 4; + protected static $itemsArgumentPositionOffset = 6; + + public function getId() + { + return 'BF.INSERT'; + } + + public function setArguments(array $arguments) + { + $this->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 0000000000..76a78e997e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFLOADCHUNK.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\BloomFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/bf.loadchunk/ + * + * Restores a filter previously saved using SCANDUMP. See the SCANDUMP command for example usage. + */ +class BFLOADCHUNK extends RedisCommand +{ + public function getId() + { + return 'BF.LOADCHUNK'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFMADD.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFMADD.php new file mode 100644 index 0000000000..1646646532 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFMADD.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\BloomFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/bf.madd/ + * + * Adds one or more items to the Bloom Filter and creates the filter if it does not exist yet. + * This command operates identically to BF.ADD except that it allows multiple inputs and returns multiple values. + */ +class BFMADD extends RedisCommand +{ + public function getId() + { + return 'BF.MADD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFMEXISTS.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFMEXISTS.php new file mode 100644 index 0000000000..e71a368b7a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFMEXISTS.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\BloomFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/bf.mexists/ + * + * Determines if one or more items may exist in the filter or not. + */ +class BFMEXISTS extends RedisCommand +{ + public function getId() + { + return 'BF.MEXISTS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFRESERVE.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFRESERVE.php new file mode 100644 index 0000000000..2364db4802 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFRESERVE.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\BloomFilter; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\BloomFilters\Expansion; + +/** + * @see https://redis.io/commands/bf.reserve/ + * + * Creates an empty Bloom Filter with a single sub-filter for the initial capacity + * requested and with an upper bound error_rate. + * + * By default, the filter auto-scales by creating additional sub-filters when capacity is reached. + * The new sub-filter is created with size of the previous sub-filter multiplied by expansion. + */ +class BFRESERVE extends RedisCommand +{ + use Expansion { + Expansion::setArguments as setExpansion; + } + + protected static $expansionArgumentPositionOffset = 3; + + public function getId() + { + return 'BF.RESERVE'; + } + + public function setArguments(array $arguments) + { + if (array_key_exists(4, $arguments) && $arguments[4]) { + $arguments[4] = 'NONSCALING'; + } + + $this->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 0000000000..f8576d2d06 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFSCANDUMP.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\BloomFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/bf.scandump/ + * + * Begins an incremental save of the bloom filter. + * This is useful for large bloom filters which cannot fit into the normal DUMP and RESTORE model. + */ +class BFSCANDUMP extends RedisCommand +{ + public function getId() + { + return 'BF.SCANDUMP'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CLIENT.php b/plugins/cache-redis/Predis/Command/Redis/CLIENT.php new file mode 100644 index 0000000000..6c7bdc9ca7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CLIENT.php @@ -0,0 +1,75 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @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 CLIENT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'CLIENT'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + $args = array_change_key_case($this->getArguments(), CASE_UPPER); + + switch (strtoupper($args[0])) { + case 'LIST': + return $this->parseClientList($data); + case 'KILL': + case 'GETNAME': + case 'SETNAME': + default: + return $data; + } // @codeCoverageIgnore + } + + /** + * Parses the response to CLIENT LIST and returns a structured list. + * + * @param string $data Response buffer. + * + * @return array + */ + protected function parseClientList($data) + { + $clients = []; + + foreach (explode("\n", $data, -1) as $clientData) { + $client = []; + + foreach (explode(' ', $clientData) as $kv) { + @[$k, $v] = explode('=', $kv); + $client[$k] = $v; + } + + $clients[] = $client; + } + + return $clients; + } +} 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 0000000000..3bbb77cc8d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CLUSTER.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/?name=cluster + */ +class CLUSTER extends RedisCommand +{ + public function getId() + { + return 'CLUSTER'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/COMMAND.php b/plugins/cache-redis/Predis/Command/Redis/COMMAND.php new file mode 100644 index 0000000000..385d54ee9c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/COMMAND.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as BaseCommand; + +/** + * @see http://redis.io/commands/command + */ +class COMMAND extends BaseCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'COMMAND'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + // Relay (RESP3) uses maps and it might be good + // to make the return value a breaking change + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CONFIG.php b/plugins/cache-redis/Predis/Command/Redis/CONFIG.php new file mode 100644 index 0000000000..c60ac2dcc0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CONFIG.php @@ -0,0 +1,54 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @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 CONFIG extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'CONFIG'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (is_array($data)) { + if ($data !== array_values($data)) { + return $data; // Relay + } + + $result = []; + + for ($i = 0; $i < count($data); ++$i) { + $result[$data[$i]] = $data[++$i]; + } + + return $result; + } + + return $data; + } +} 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 0000000000..cb6ec659b3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/COPY.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\DB; +use Predis\Command\Traits\Replace; + +/** + * @see https://redis.io/commands/copy/ + * + * This command copies the value stored at the source key to the destination key. + */ +class COPY extends RedisCommand +{ + use DB { + DB::setArguments as setDB; + } + use Replace { + Replace::setArguments as setReplace; + } + + protected static $dbArgumentPositionOffset = 2; + + public function getId() + { + return 'COPY'; + } + + public function setArguments(array $arguments) + { + $this->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 0000000000..2699d37e8e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/ACL.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Container; + +use Predis\Response\Status; + +/** + * @method Status dryRun(string $username, string $command, ...$arguments) + * @method array getUser(string $username) + * @method Status setUser(string $username, string ...$rules) + */ +class ACL extends AbstractContainer +{ + public function getContainerCommandId(): string + { + return 'acl'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/AbstractContainer.php b/plugins/cache-redis/Predis/Command/Redis/Container/AbstractContainer.php new file mode 100644 index 0000000000..4aff4ef040 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/AbstractContainer.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Container; + +use Predis\ClientInterface; + +abstract class AbstractContainer implements ContainerInterface +{ + /** + * @var ClientInterface + */ + protected $client; + + public function __construct(ClientInterface $client) + { + $this->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 0000000000..b2925b19b8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/CLUSTER.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Container; + +use Predis\Response\Status; + +/** + * @method Status addSlotsRange(int ...$startEndSlots) + * @method Status delSlotsRange(int ...$startEndSlots) + * @method array links() + * @method array shards() + */ +class CLUSTER extends AbstractContainer +{ + public function getContainerCommandId(): string + { + return 'CLUSTER'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/ContainerFactory.php b/plugins/cache-redis/Predis/Command/Redis/Container/ContainerFactory.php new file mode 100644 index 0000000000..79d3de0997 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/ContainerFactory.php @@ -0,0 +1,81 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Container; + +use Predis\ClientConfiguration; +use Predis\ClientInterface; +use UnexpectedValueException; + +class ContainerFactory +{ + private const CONTAINER_NAMESPACE = "Predis\Command\Redis\Container"; + + /** + * Mappings for class names that corresponds to PHP reserved words. + * + * @var array + */ + private static $specialMappings = [ + 'FUNCTION' => 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 0000000000..e30c539e39 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/ContainerInterface.php @@ -0,0 +1,33 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Container; + +interface ContainerInterface +{ + /** + * Creates Redis container command with subcommand as virtual method name + * and sends a request to the server. + * + * @param string $subcommandID + * @param array $arguments + * @return mixed + */ + public function __call(string $subcommandID, array $arguments); + + /** + * Returns containerCommandId of specific container command. + * + * @return string + */ + public function getContainerCommandId(): string; +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/FunctionContainer.php b/plugins/cache-redis/Predis/Command/Redis/Container/FunctionContainer.php new file mode 100644 index 0000000000..8f2a673d10 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/FunctionContainer.php @@ -0,0 +1,33 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Container; + +use Predis\Response\Status; + +/** + * @method Status delete(string $libraryName) + * @method string dump() + * @method Status flush(?string $mode = null) + * @method Status kill() + * @method array list(string $libraryNamePattern = null, bool $withCode = false) + * @method string load(string $functionCode, bool $replace = 'false') + * @method Status restore(string $value, string $policy = null) + * @method array stats() + */ +class FunctionContainer extends AbstractContainer +{ + public function getContainerCommandId(): string + { + return 'FUNCTIONS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/Json/JSONDEBUG.php b/plugins/cache-redis/Predis/Command/Redis/Container/Json/JSONDEBUG.php new file mode 100644 index 0000000000..b20baca091 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/Json/JSONDEBUG.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Container\Json; + +use Predis\Command\Redis\Container\AbstractContainer; + +/** + * @method array memory(string $key, string $path) + * @method array help() + */ +class JSONDEBUG extends AbstractContainer +{ + public function getContainerCommandId(): string + { + return 'JSONDEBUG'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/Search/FTCONFIG.php b/plugins/cache-redis/Predis/Command/Redis/Container/Search/FTCONFIG.php new file mode 100644 index 0000000000..9e80b8c54b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/Search/FTCONFIG.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Container\Search; + +use Predis\Command\Redis\Container\AbstractContainer; +use Predis\Response\Status; + +/** + * @method array get(string $option) + * @method array help(string $option) + * @method Status set(string $option, $value) + */ +class FTCONFIG extends AbstractContainer +{ + public function getContainerCommandId(): string + { + return 'FTCONFIG'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/Search/FTCURSOR.php b/plugins/cache-redis/Predis/Command/Redis/Container/Search/FTCURSOR.php new file mode 100644 index 0000000000..4d47c9c0ea --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/Search/FTCURSOR.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Container\Search; + +use Predis\Command\Argument\Search\CursorArguments; +use Predis\Command\Redis\Container\AbstractContainer; +use Predis\Response\Status; + +/** + * @method Status del(string $index, int $cursorId) + * @method array read(string $index, int $cursorId, ?CursorArguments $arguments = null) + */ +class FTCURSOR extends AbstractContainer +{ + public function getContainerCommandId(): string + { + return 'FTCURSOR'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINCRBY.php b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINCRBY.php new file mode 100644 index 0000000000..6ce4338525 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINCRBY.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CountMinSketch; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cms.incrby/ + * + * Increases the count of item by increment. + * Multiple items can be increased with one call. + */ +class CMSINCRBY extends RedisCommand +{ + public function getId() + { + return 'CMS.INCRBY'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINFO.php b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINFO.php new file mode 100644 index 0000000000..ed6d2249aa --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINFO.php @@ -0,0 +1,45 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CountMinSketch; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cms.info/ + * + * Returns width, depth and total count of the sketch. + */ +class CMSINFO extends RedisCommand +{ + public function getId() + { + return 'CMS.INFO'; + } + + 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/CountMinSketch/CMSINITBYDIM.php b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM.php new file mode 100644 index 0000000000..8f1f23ff30 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CountMinSketch; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cms.initbydim/ + * + * Initializes a Count-Min Sketch to dimensions specified by user. + */ +class CMSINITBYDIM extends RedisCommand +{ + public function getId() + { + return 'CMS.INITBYDIM'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYPROB.php b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYPROB.php new file mode 100644 index 0000000000..76e0a788e0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYPROB.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CountMinSketch; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cms.initbyprob/ + * + * Initializes a Count-Min Sketch to accommodate requested tolerances. + */ +class CMSINITBYPROB extends RedisCommand +{ + public function getId() + { + return 'CMS.INITBYPROB'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSMERGE.php b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSMERGE.php new file mode 100644 index 0000000000..0e85c3b066 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSMERGE.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CountMinSketch; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cms.merge/ + * + * Merges several sketches into one sketch. + * All sketches must have identical width and depth. + * Weights can be used to multiply certain sketches. Default weight is 1. + */ +class CMSMERGE extends RedisCommand +{ + public function getId() + { + return 'CMS.MERGE'; + } + + public function setArguments(array $arguments) + { + $processedArguments = array_merge([$arguments[0], count($arguments[1])], $arguments[1]); + + if (!empty($arguments[2])) { + $processedArguments[] = 'WEIGHTS'; + $processedArguments = array_merge($processedArguments, $arguments[2]); + } + + parent::setArguments($processedArguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSQUERY.php b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSQUERY.php new file mode 100644 index 0000000000..b9f7343165 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSQUERY.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CountMinSketch; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cms.query/ + * + * Returns the count for one or more items in a sketch. + */ +class CMSQUERY extends RedisCommand +{ + public function getId() + { + return 'CMS.QUERY'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFADD.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFADD.php new file mode 100644 index 0000000000..92d05889a6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFADD.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cf.add/ + * + * Adds an item to the cuckoo filter, creating the filter if it does not exist. + */ +class CFADD extends RedisCommand +{ + public function getId() + { + return 'CF.ADD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFADDNX.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFADDNX.php new file mode 100644 index 0000000000..ff972fd336 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFADDNX.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cf.addnx/ + * + * Adds an item to a cuckoo filter if the item did not exist previously. + */ +class CFADDNX extends RedisCommand +{ + public function getId() + { + return 'CF.ADDNX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFCOUNT.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFCOUNT.php new file mode 100644 index 0000000000..7610c2c081 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFCOUNT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cf.count/ + * + * Returns the number of times an item may be in the filter. + * Because this is a probabilistic data structure, this may not necessarily be accurate. + */ +class CFCOUNT extends RedisCommand +{ + public function getId() + { + return 'CF.COUNT'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFDEL.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFDEL.php new file mode 100644 index 0000000000..adbd29ea2a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFDEL.php @@ -0,0 +1,30 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cf.del/ + * + * Deletes an item once from the filter. + * If the item exists only once, it will be removed from the filter. + * If the item was added multiple times, it will still be present. + */ +class CFDEL extends RedisCommand +{ + public function getId() + { + return 'CF.DEL'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFEXISTS.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFEXISTS.php new file mode 100644 index 0000000000..2d3e635a8e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFEXISTS.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cf.exists/ + * + * Check if an item exists in a Cuckoo Filter key. + */ +class CFEXISTS extends RedisCommand +{ + public function getId() + { + return 'CF.EXISTS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINFO.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINFO.php new file mode 100644 index 0000000000..9a34640eea --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINFO.php @@ -0,0 +1,45 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cf.info/ + * + * Return information about key + */ +class CFINFO extends RedisCommand +{ + public function getId() + { + return 'CF.INFO'; + } + + 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/CuckooFilter/CFINSERT.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERT.php new file mode 100644 index 0000000000..3a49a38d74 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERT.php @@ -0,0 +1,52 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\BloomFilters\Capacity; +use Predis\Command\Traits\BloomFilters\Items; +use Predis\Command\Traits\BloomFilters\NoCreate; + +class CFINSERT extends RedisCommand +{ + use Capacity { + Capacity::setArguments as setCapacity; + } + use NoCreate { + NoCreate::setArguments as setNoCreate; + } + use Items { + Items::setArguments as setItems; + } + + protected static $capacityArgumentPositionOffset = 1; + protected static $noCreateArgumentPositionOffset = 2; + protected static $itemsArgumentPositionOffset = 3; + + public function getId() + { + return 'CF.INSERT'; + } + + public function setArguments(array $arguments) + { + $this->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 0000000000..629327b4f0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERTNX.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +/** + * @see https://redis.io/commands/cf.insertnx/ + * + * Adds one or more items to a cuckoo filter, allowing the filter + * to be created with a custom capacity if it does not exist yet. + */ +class CFINSERTNX extends CFINSERT +{ + public function getId() + { + return 'CF.INSERTNX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFLOADCHUNK.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFLOADCHUNK.php new file mode 100644 index 0000000000..6a96155245 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFLOADCHUNK.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cf.loadchunk/ + * + * Restores a filter previously saved using SCANDUMP. + * See the SCANDUMP command for example usage. + */ +class CFLOADCHUNK extends RedisCommand +{ + public function getId() + { + return 'CF.LOADCHUNK'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFMEXISTS.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFMEXISTS.php new file mode 100644 index 0000000000..61ae95584b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFMEXISTS.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cf.mexists/ + * + * Check if one or more items exists in a Cuckoo Filter key. + */ +class CFMEXISTS extends RedisCommand +{ + public function getId() + { + return 'CF.MEXISTS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFRESERVE.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFRESERVE.php new file mode 100644 index 0000000000..bfdb93aa51 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFRESERVE.php @@ -0,0 +1,52 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\BloomFilters\BucketSize; +use Predis\Command\Traits\BloomFilters\Expansion; +use Predis\Command\Traits\BloomFilters\MaxIterations; + +class CFRESERVE extends RedisCommand +{ + use BucketSize { + BucketSize::setArguments as setBucketSize; + } + use MaxIterations { + MaxIterations::setArguments as setMaxIterations; + } + use Expansion { + Expansion::setArguments as setExpansion; + } + + protected static $bucketSizeArgumentPositionOffset = 2; + protected static $maxIterationsArgumentPositionOffset = 3; + protected static $expansionArgumentPositionOffset = 4; + + public function getId() + { + return 'CF.RESERVE'; + } + + public function setArguments(array $arguments) + { + $this->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 0000000000..59caa651c3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFSCANDUMP.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\CuckooFilter; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/cf.scandump/ + * + * Begins an incremental save of the cuckoo filter. + * This is useful for large cuckoo filters which cannot fit into the normal DUMP and RESTORE model. + */ +class CFSCANDUMP extends RedisCommand +{ + public function getId() + { + return 'CF.SCANDUMP'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/DBSIZE.php b/plugins/cache-redis/Predis/Command/Redis/DBSIZE.php new file mode 100644 index 0000000000..f0dc10b146 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/DBSIZE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/dbsize + */ +class DBSIZE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DBSIZE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/DECR.php b/plugins/cache-redis/Predis/Command/Redis/DECR.php new file mode 100644 index 0000000000..453f683a87 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/DECR.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/decr + */ +class DECR extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DECR'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/DECRBY.php b/plugins/cache-redis/Predis/Command/Redis/DECRBY.php new file mode 100644 index 0000000000..15eace6bea --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/DECRBY.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/decrby + */ +class DECRBY extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DECRBY'; + } +} 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 0000000000..359bbf4ff0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/DEL.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/del + */ +class DEL extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DEL'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/DISCARD.php b/plugins/cache-redis/Predis/Command/Redis/DISCARD.php new file mode 100644 index 0000000000..251e304996 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/DISCARD.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/discard + */ +class DISCARD extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DISCARD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/DUMP.php b/plugins/cache-redis/Predis/Command/Redis/DUMP.php new file mode 100644 index 0000000000..32f47c5753 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/DUMP.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/dump + */ +class DUMP extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'DUMP'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ECHO_.php b/plugins/cache-redis/Predis/Command/Redis/ECHO_.php new file mode 100644 index 0000000000..c04adea33a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ECHO_.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/echo + */ +class ECHO_ extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ECHO'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/EVALSHA.php b/plugins/cache-redis/Predis/Command/Redis/EVALSHA.php new file mode 100644 index 0000000000..ee7a380877 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EVALSHA.php @@ -0,0 +1,37 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @see http://redis.io/commands/evalsha + */ +class EVALSHA extends EVAL_ +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EVALSHA'; + } + + /** + * Returns the SHA1 hash of the body of the script. + * + * @return string SHA1 hash. + */ + public function getScriptHash() + { + return $this->getArgument(0); + } +} 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 0000000000..809a08757c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EVALSHA_RO.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @see https://redis.io/commands/evalsha_ro/ + * + * This is a read-only variant of the EVALSHA command + * that cannot execute commands that modify data. + */ +class EVALSHA_RO extends EVAL_RO +{ + public function getId() + { + return 'EVALSHA_RO'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/EVAL_.php b/plugins/cache-redis/Predis/Command/Redis/EVAL_.php new file mode 100644 index 0000000000..0e18fde943 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EVAL_.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/eval + */ +class EVAL_ extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EVAL'; + } + + /** + * Calculates the SHA1 hash of the body of the script. + * + * @return string SHA1 hash. + */ + public function getScriptHash() + { + return sha1($this->getArgument(0)); + } +} 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 0000000000..cef8bd35e0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EVAL_RO.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Keys; + +/** + * @see https://redis.io/commands/eval_ro/ + * + * This is a read-only variant of the EVAL command + * that cannot execute commands that modify data. + */ +class EVAL_RO extends RedisCommand +{ + use Keys; + + protected static $keysArgumentPositionOffset = 1; + + public function getId() + { + return 'EVAL_RO'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/EXEC.php b/plugins/cache-redis/Predis/Command/Redis/EXEC.php new file mode 100644 index 0000000000..7fea7094df --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EXEC.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/exec + */ +class EXEC extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EXEC'; + } +} 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 0000000000..8731c9d848 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EXISTS.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/exists + */ +class EXISTS extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EXISTS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/EXPIRE.php b/plugins/cache-redis/Predis/Command/Redis/EXPIRE.php new file mode 100644 index 0000000000..9957acde6f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EXPIRE.php @@ -0,0 +1,36 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Expire\ExpireOptions; + +/** + * @see http://redis.io/commands/expire + * + * Set a timeout on key. + * After the timeout has expired, the key will automatically be deleted. + * A key with an associated timeout is often said to be volatile in Redis terminology. + */ +class EXPIRE extends RedisCommand +{ + use ExpireOptions; + + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EXPIRE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/EXPIREAT.php b/plugins/cache-redis/Predis/Command/Redis/EXPIREAT.php new file mode 100644 index 0000000000..ad9314de2c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EXPIREAT.php @@ -0,0 +1,35 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Expire\ExpireOptions; + +/** + * @see http://redis.io/commands/expireat + * + * EXPIREAT has the same effect and semantic as EXPIRE, but instead of specifying + * the number of seconds representing the TTL (time to live), it takes an absolute Unix timestamp + */ +class EXPIREAT extends RedisCommand +{ + use ExpireOptions; + + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EXPIREAT'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/EXPIRETIME.php b/plugins/cache-redis/Predis/Command/Redis/EXPIRETIME.php new file mode 100644 index 0000000000..8825589da1 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EXPIRETIME.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/expiretime/ + * + * Returns the absolute Unix timestamp (since January 1, 1970) + * in seconds at which the given key will expire. + */ +class EXPIRETIME extends RedisCommand +{ + public function getId() + { + return 'EXPIRETIME'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/FAILOVER.php b/plugins/cache-redis/Predis/Command/Redis/FAILOVER.php new file mode 100644 index 0000000000..61fdb7bbe2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/FAILOVER.php @@ -0,0 +1,48 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Timeout; +use Predis\Command\Traits\To\ServerTo; + +class FAILOVER extends RedisCommand +{ + use ServerTo { + ServerTo::setArguments as setTo; + } + use Timeout { + Timeout::setArguments as setTimeout; + } + + protected static $toArgumentPositionOffset = 0; + protected static $timeoutArgumentPositionOffset = 2; + + public function getId() + { + return 'FAILOVER'; + } + + public function setArguments(array $arguments) + { + if (array_key_exists(1, $arguments) && false !== $arguments[1]) { + $arguments[1] = 'ABORT'; + } + + $this->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 0000000000..cc5639321f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/FCALL.php @@ -0,0 +1,33 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Keys; + +/** + * @see https://redis.io/commands/fcall/ + * + * Invoke a function. + */ +class FCALL extends RedisCommand +{ + use Keys; + + protected static $keysArgumentPositionOffset = 1; + + public function getId() + { + return 'FCALL'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/FCALL_RO.php b/plugins/cache-redis/Predis/Command/Redis/FCALL_RO.php new file mode 100644 index 0000000000..8680e70555 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/FCALL_RO.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/fcall_ro/ + * + * This is a read-only variant of the FCALL command that cannot execute commands that modify data. + */ +class FCALL_RO extends RedisCommand +{ + public function getId() + { + return 'FCALL_RO'; + } + + public function setArguments(array $arguments) + { + $processedArguments = array_merge([$arguments[0], count($arguments[1])], $arguments[1]); + + if (count($arguments) > 2) { + for ($i = 2, $iMax = count($arguments); $i < $iMax; $i++) { + $processedArguments[] = $arguments[$i]; + } + } + + parent::setArguments($processedArguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php b/plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php new file mode 100644 index 0000000000..a03a3133dc --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/flushall + */ +class FLUSHALL extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'FLUSHALL'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/FLUSHDB.php b/plugins/cache-redis/Predis/Command/Redis/FLUSHDB.php new file mode 100644 index 0000000000..67a2d485b3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/FLUSHDB.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/flushdb + */ +class FLUSHDB extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'FLUSHDB'; + } +} 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 0000000000..7f4fde79a2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/FUNCTIONS.php @@ -0,0 +1,50 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Strategy\StrategyResolverInterface; +use Predis\Command\Strategy\SubcommandStrategyResolver; + +/** + * @see https://redis.io/commands/?name=function + * + * Container command corresponds to any FUNCTION *. + * Represents any FUNCTION command with subcommand as first argument. + */ +class FUNCTIONS extends RedisCommand +{ + /** + * @var StrategyResolverInterface + */ + private $strategyResolver; + + public function __construct() + { + $this->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 0000000000..56156b72b9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEOADD.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/geoadd + */ +class GEOADD extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEOADD'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[1])) { + foreach (array_pop($arguments) as $item) { + $arguments = array_merge($arguments, $item); + } + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GEODIST.php b/plugins/cache-redis/Predis/Command/Redis/GEODIST.php new file mode 100644 index 0000000000..bf2bf28731 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEODIST.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/geodist + */ +class GEODIST extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEODIST'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GEOHASH.php b/plugins/cache-redis/Predis/Command/Redis/GEOHASH.php new file mode 100644 index 0000000000..11df0dcae5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEOHASH.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/geohash + */ +class GEOHASH extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEOHASH'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[1])) { + $members = array_pop($arguments); + $arguments = array_merge($arguments, $members); + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GEOPOS.php b/plugins/cache-redis/Predis/Command/Redis/GEOPOS.php new file mode 100644 index 0000000000..2938f276ba --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEOPOS.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/geopos + */ +class GEOPOS extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEOPOS'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[1])) { + $members = array_pop($arguments); + $arguments = array_merge($arguments, $members); + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GEORADIUS.php b/plugins/cache-redis/Predis/Command/Redis/GEORADIUS.php new file mode 100644 index 0000000000..c94ee81ac2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEORADIUS.php @@ -0,0 +1,77 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @deprecated As of Redis version 6.2.0, this command is regarded as deprecated. + * + * It can be replaced by GEOSEARCH and GEOSEARCHSTORE with the BYRADIUS argument + * when migrating or writing new code. + * + * @see http://redis.io/commands/georadius + */ +class GEORADIUS extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEORADIUS'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if ($arguments && is_array(end($arguments))) { + $options = array_change_key_case(array_pop($arguments), CASE_UPPER); + + if (isset($options['WITHCOORD']) && $options['WITHCOORD'] == true) { + $arguments[] = 'WITHCOORD'; + } + + if (isset($options['WITHDIST']) && $options['WITHDIST'] == true) { + $arguments[] = 'WITHDIST'; + } + + if (isset($options['WITHHASH']) && $options['WITHHASH'] == true) { + $arguments[] = 'WITHHASH'; + } + + if (isset($options['COUNT'])) { + $arguments[] = 'COUNT'; + $arguments[] = $options['COUNT']; + } + + if (isset($options['SORT'])) { + $arguments[] = strtoupper($options['SORT']); + } + + if (isset($options['STORE'])) { + $arguments[] = 'STORE'; + $arguments[] = $options['STORE']; + } + + if (isset($options['STOREDIST'])) { + $arguments[] = 'STOREDIST'; + $arguments[] = $options['STOREDIST']; + } + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GEORADIUSBYMEMBER.php b/plugins/cache-redis/Predis/Command/Redis/GEORADIUSBYMEMBER.php new file mode 100644 index 0000000000..542737172d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEORADIUSBYMEMBER.php @@ -0,0 +1,32 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @deprecated As of Redis version 6.2.0, this command is regarded as deprecated. + * + * It can be replaced by GEOSEARCH and GEOSEARCHSTORE with the FROMMEMBER arguments + * when migrating or writing new code. + * + * @see http://redis.io/commands/georadiusbymember + */ +class GEORADIUSBYMEMBER extends GEORADIUS +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEORADIUSBYMEMBER'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GEOSEARCH.php b/plugins/cache-redis/Predis/Command/Redis/GEOSEARCH.php new file mode 100644 index 0000000000..f1fcd2aef3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEOSEARCH.php @@ -0,0 +1,122 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\By\GeoBy; +use Predis\Command\Traits\Count; +use Predis\Command\Traits\From\GeoFrom; +use Predis\Command\Traits\Sorting; +use Predis\Command\Traits\With\WithCoord; +use Predis\Command\Traits\With\WithDist; +use Predis\Command\Traits\With\WithHash; + +/** + * @see https://redis.io/commands/geosearch/ + * + * Return the members of a sorted set populated with geospatial information using GEOADD, + * which are within the borders of the area specified by a given shape. + * + * This command extends the GEORADIUS command, so in addition to searching + * within circular areas, it supports searching within rectangular areas. + */ +class GEOSEARCH extends RedisCommand +{ + use GeoFrom { + GeoFrom::setArguments as setFrom; + } + use GeoBy { + GeoBy::setArguments as setBy; + } + use Sorting { + Sorting::setArguments as setSorting; + } + use Count { + Count::setArguments as setCount; + } + use WithCoord { + WithCoord::setArguments as setWithCoord; + } + use WithDist { + WithDist::setArguments as setWithDist; + } + use WithHash { + WithHash::setArguments as setWithHash; + } + + protected static $sortArgumentPositionOffset = 3; + protected static $countArgumentPositionOffset = 4; + protected static $withCoordArgumentPositionOffset = 6; + protected static $withDistArgumentPositionOffset = 7; + protected static $withHashArgumentPositionOffset = 8; + + public function getId() + { + return 'GEOSEARCH'; + } + + public function setArguments(array $arguments) + { + $this->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 0000000000..6798db7edb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEOSEARCHSTORE.php @@ -0,0 +1,71 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\By\GeoBy; +use Predis\Command\Traits\Count; +use Predis\Command\Traits\From\GeoFrom; +use Predis\Command\Traits\Sorting; +use Predis\Command\Traits\Storedist; + +/** + * @see https://redis.io/commands/geosearchstore/ + * + * This command is like GEOSEARCH, but stores the result in destination key. + */ +class GEOSEARCHSTORE extends RedisCommand +{ + use GeoFrom { + GeoFrom::setArguments as setFrom; + } + use GeoBy { + GeoBy::setArguments as setBy; + } + use Sorting { + Sorting::setArguments as setSorting; + } + use Count { + Count::setArguments as setCount; + } + use Storedist { + Storedist::setArguments as setStoreDist; + } + + protected static $sortArgumentPositionOffset = 4; + protected static $countArgumentPositionOffset = 5; + protected static $storeDistArgumentPositionOffset = 7; + + public function getId() + { + return 'GEOSEARCHSTORE'; + } + + public function setArguments(array $arguments) + { + $this->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/plugins/cache-redis/Predis/Command/Redis/GET.php b/plugins/cache-redis/Predis/Command/Redis/GET.php new file mode 100644 index 0000000000..e9177ab289 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GET.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/get + */ +class GET extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GET'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GETBIT.php b/plugins/cache-redis/Predis/Command/Redis/GETBIT.php new file mode 100644 index 0000000000..00ee97085e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GETBIT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/getbit + */ +class GETBIT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GETBIT'; + } +} 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 0000000000..7e4df93c63 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GETDEL.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +class GETDEL extends RedisCommand +{ + public function getId() + { + return 'GETDEL'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GETEX.php b/plugins/cache-redis/Predis/Command/Redis/GETEX.php new file mode 100644 index 0000000000..b2e10e1145 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GETEX.php @@ -0,0 +1,63 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use UnexpectedValueException; + +class GETEX extends RedisCommand +{ + /** + * @var string[] + */ + private static $modifierEnum = [ + 'ex' => '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/plugins/cache-redis/Predis/Command/Redis/GETRANGE.php b/plugins/cache-redis/Predis/Command/Redis/GETRANGE.php new file mode 100644 index 0000000000..c6feb408b5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GETRANGE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/getrange + */ +class GETRANGE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GETRANGE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GETSET.php b/plugins/cache-redis/Predis/Command/Redis/GETSET.php new file mode 100644 index 0000000000..37a38619c7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GETSET.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/getset + */ +class GETSET extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GETSET'; + } +} 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 0000000000..bb1fba3457 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HDEL.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hdel + */ +class HDEL extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HDEL'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeVariadic($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HEXISTS.php b/plugins/cache-redis/Predis/Command/Redis/HEXISTS.php new file mode 100644 index 0000000000..c461e2af8a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HEXISTS.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hexists + */ +class HEXISTS extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HEXISTS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HGET.php b/plugins/cache-redis/Predis/Command/Redis/HGET.php new file mode 100644 index 0000000000..12d54dafef --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HGET.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hget + */ +class HGET extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HGET'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HGETALL.php b/plugins/cache-redis/Predis/Command/Redis/HGETALL.php new file mode 100644 index 0000000000..c5f566bae0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HGETALL.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hgetall + */ +class HGETALL extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HGETALL'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if ($data !== array_values($data)) { + return $data; // Relay + } + + $result = []; + + for ($i = 0; $i < count($data); ++$i) { + $result[$data[$i]] = $data[++$i]; + } + + return $result; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HINCRBY.php b/plugins/cache-redis/Predis/Command/Redis/HINCRBY.php new file mode 100644 index 0000000000..cf60ecea18 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HINCRBY.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hincrby + */ +class HINCRBY extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HINCRBY'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HINCRBYFLOAT.php b/plugins/cache-redis/Predis/Command/Redis/HINCRBYFLOAT.php new file mode 100644 index 0000000000..566ee874ab --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HINCRBYFLOAT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hincrbyfloat + */ +class HINCRBYFLOAT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HINCRBYFLOAT'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HKEYS.php b/plugins/cache-redis/Predis/Command/Redis/HKEYS.php new file mode 100644 index 0000000000..43986ec3f7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HKEYS.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hkeys + */ +class HKEYS extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HKEYS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HLEN.php b/plugins/cache-redis/Predis/Command/Redis/HLEN.php new file mode 100644 index 0000000000..32903ea622 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HLEN.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hlen + */ +class HLEN extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HLEN'; + } +} 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 0000000000..077373f684 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HMGET.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hmget + */ +class HMGET extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HMGET'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeVariadic($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HMSET.php b/plugins/cache-redis/Predis/Command/Redis/HMSET.php new file mode 100644 index 0000000000..98e4574522 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HMSET.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hmset + */ +class HMSET extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HMSET'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[1])) { + $flattenedKVs = [$arguments[0]]; + $args = $arguments[1]; + + foreach ($args as $k => $v) { + $flattenedKVs[] = $k; + $flattenedKVs[] = $v; + } + + $arguments = $flattenedKVs; + } + + 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 0000000000..62ce7dbe99 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HRANDFIELD.php @@ -0,0 +1,53 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\With\WithValues; + +/** + * @see https://redis.io/commands/hrandfield/ + * + * When called with just the key argument, return a random field from the hash value stored at key. + * + * If the provided count argument is positive, return an array of distinct fields. + * The array's length is either count or the hash's number of fields (HLEN), whichever is lower. + */ +class HRANDFIELD extends RedisCommand +{ + use WithValues; + + public function getId() + { + return 'HRANDFIELD'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (!is_array($data)) { + return $data; + } + + // flatten Relay (RESP3) maps + $return = []; + + array_walk_recursive($data, function ($value) use (&$return) { + $return[] = $value; + }); + + return $return; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HSCAN.php b/plugins/cache-redis/Predis/Command/Redis/HSCAN.php new file mode 100644 index 0000000000..5d4dde92d7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HSCAN.php @@ -0,0 +1,86 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hscan + */ +class HSCAN extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HSCAN'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 3 && is_array($arguments[2])) { + $options = $this->prepareOptions(array_pop($arguments)); + $arguments = array_merge($arguments, $options); + } + + parent::setArguments($arguments); + } + + /** + * Returns a list of options and modifiers compatible with Redis. + * + * @param array $options List of options. + * + * @return array + */ + protected function prepareOptions($options) + { + $options = array_change_key_case($options, CASE_UPPER); + $normalized = []; + + if (!empty($options['MATCH'])) { + $normalized[] = 'MATCH'; + $normalized[] = $options['MATCH']; + } + + if (!empty($options['COUNT'])) { + $normalized[] = 'COUNT'; + $normalized[] = $options['COUNT']; + } + + return $normalized; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (is_array($data)) { + $fields = $data[1]; + $result = []; + + for ($i = 0; $i < count($fields); ++$i) { + $result[$fields[$i]] = $fields[++$i]; + } + + $data[1] = $result; + } + + return $data; + } +} 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 0000000000..662094a3cd --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HSET.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hset + */ +class HSET extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HSET'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HSETNX.php b/plugins/cache-redis/Predis/Command/Redis/HSETNX.php new file mode 100644 index 0000000000..ca0c5dd622 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HSETNX.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hsetnx + */ +class HSETNX extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HSETNX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HSTRLEN.php b/plugins/cache-redis/Predis/Command/Redis/HSTRLEN.php new file mode 100644 index 0000000000..c2056ae0f9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HSTRLEN.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hstrlen + */ +class HSTRLEN extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HSTRLEN'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HVALS.php b/plugins/cache-redis/Predis/Command/Redis/HVALS.php new file mode 100644 index 0000000000..71172c7096 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HVALS.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/hvals + */ +class HVALS extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'HVALS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/INCR.php b/plugins/cache-redis/Predis/Command/Redis/INCR.php new file mode 100644 index 0000000000..80bc1ee0ef --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/INCR.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/incr + */ +class INCR extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'INCR'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/INCRBY.php b/plugins/cache-redis/Predis/Command/Redis/INCRBY.php new file mode 100644 index 0000000000..f4ce4e32c2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/INCRBY.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/incrby + */ +class INCRBY extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'INCRBY'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/INCRBYFLOAT.php b/plugins/cache-redis/Predis/Command/Redis/INCRBYFLOAT.php new file mode 100644 index 0000000000..fb626edbeb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/INCRBYFLOAT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/incrbyfloat + */ +class INCRBYFLOAT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'INCRBYFLOAT'; + } +} 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 0000000000..26a62c2a31 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/INFO.php @@ -0,0 +1,157 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/info + */ +class INFO extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'INFO'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (empty($data) || !$lines = preg_split('/\r?\n/', $data)) { + return []; + } + + if (strpos($lines[0], '#') === 0) { + return $this->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 0000000000..1b2a92036c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRAPPEND.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.arrappend/ + * + * Append the json values into the array at path after the last element in it + */ +class JSONARRAPPEND extends RedisCommand +{ + public function getId() + { + return 'JSON.ARRAPPEND'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRINDEX.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRINDEX.php new file mode 100644 index 0000000000..4aab182744 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRINDEX.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.arrindex/ + * + * Search for the first occurrence of a JSON value in an array + */ +class JSONARRINDEX extends RedisCommand +{ + public function getId() + { + return 'JSON.ARRINDEX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRINSERT.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRINSERT.php new file mode 100644 index 0000000000..5e1ff2510d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRINSERT.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.arrinsert/ + * + * Insert the json values into the array at path before the index (shifts to the right) + */ +class JSONARRINSERT extends RedisCommand +{ + public function getId() + { + return 'JSON.ARRINSERT'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRLEN.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRLEN.php new file mode 100644 index 0000000000..5ed9506499 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRLEN.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.arrlen/ + * + * Report the length of the JSON array at path in key + */ +class JSONARRLEN extends RedisCommand +{ + public function getId() + { + return 'JSON.ARRLEN'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRPOP.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRPOP.php new file mode 100644 index 0000000000..a8e239b946 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRPOP.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.arrpop/ + * + * Remove and return an element from the index in the array + */ +class JSONARRPOP extends RedisCommand +{ + public function getId() + { + return 'JSON.ARRPOP'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRTRIM.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRTRIM.php new file mode 100644 index 0000000000..7ea5620db9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRTRIM.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.arrtrim/ + * + * Trim an array so that it contains only the specified inclusive range of elements + */ +class JSONARRTRIM extends RedisCommand +{ + public function getId() + { + return 'JSON.ARRTRIM'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONCLEAR.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONCLEAR.php new file mode 100644 index 0000000000..89797eea50 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONCLEAR.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.clear/ + * + * Clear container values (arrays/objects) and set numeric values to 0 + */ +class JSONCLEAR extends RedisCommand +{ + public function getId() + { + return 'JSON.CLEAR'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONDEBUG.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONDEBUG.php new file mode 100644 index 0000000000..83f6453027 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONDEBUG.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.debug/ + * + * This is a container command for debugging related tasks. + */ +class JSONDEBUG extends RedisCommand +{ + public function getId() + { + return 'JSON.DEBUG'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONDEL.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONDEL.php new file mode 100644 index 0000000000..86e8a01b76 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONDEL.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.del/ + * + * Delete a value + */ +class JSONDEL extends RedisCommand +{ + public function getId() + { + return 'JSON.DEL'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONFORGET.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONFORGET.php new file mode 100644 index 0000000000..c99ca532e3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONFORGET.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.forget/ + * + * @see https://redis.io/commands/json.del/ + */ +class JSONFORGET extends RedisCommand +{ + public function getId() + { + return 'JSON.FORGET'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONGET.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONGET.php new file mode 100644 index 0000000000..df8fddb7e8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONGET.php @@ -0,0 +1,57 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Json\Indent; +use Predis\Command\Traits\Json\Newline; +use Predis\Command\Traits\Json\Space; + +/** + * @see https://redis.io/commands/json.get/ + * + * Return the value at path in JSON serialized form + */ +class JSONGET extends RedisCommand +{ + use Indent { + Indent::setArguments as setIndent; + } + use Newline { + Newline::setArguments as setNewline; + } + use Space { + Space::setArguments as setSpace; + } + + protected static $indentArgumentPositionOffset = 1; + protected static $newlineArgumentPositionOffset = 2; + protected static $spaceArgumentPositionOffset = 3; + + public function getId() + { + return 'JSON.GET'; + } + + public function setArguments(array $arguments) + { + $this->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 0000000000..a132228330 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONMERGE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.merge/ + * + * Merge a given JSON value into matching paths. + * Consequently, JSON values at matching paths are updated, deleted, or expanded with new children. + */ +class JSONMERGE extends RedisCommand +{ + public function getId() + { + return 'JSON.MERGE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONMGET.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONMGET.php new file mode 100644 index 0000000000..1a5163fb78 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONMGET.php @@ -0,0 +1,36 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +class JSONMGET extends RedisCommand +{ + public function getId() + { + return 'JSON.MGET'; + } + + public function setArguments(array $arguments) + { + $unpackedArguments = []; + + foreach ($arguments[0] as $key) { + $unpackedArguments[] = $key; + } + + $unpackedArguments[] = $arguments[1]; + + parent::setArguments($unpackedArguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONMSET.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONMSET.php new file mode 100644 index 0000000000..4fc6cd8546 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONMSET.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.mset/ + * + * Set or update one or more JSON values according to the specified key-path-value triplets. + */ +class JSONMSET extends RedisCommand +{ + public function getId() + { + return 'JSON.MSET'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONNUMINCRBY.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONNUMINCRBY.php new file mode 100644 index 0000000000..1bc62b1b1b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONNUMINCRBY.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.numincrby/ + * + * Increment the number value stored at path by number + */ +class JSONNUMINCRBY extends RedisCommand +{ + public function getId() + { + return 'JSON.NUMINCRBY'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONOBJKEYS.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONOBJKEYS.php new file mode 100644 index 0000000000..09c5c22d29 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONOBJKEYS.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.objkeys/ + * + * Return the keys in the object that's referenced by path + */ +class JSONOBJKEYS extends RedisCommand +{ + public function getId() + { + return 'JSON.OBJKEYS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONOBJLEN.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONOBJLEN.php new file mode 100644 index 0000000000..f6b164a751 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONOBJLEN.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.objlen/ + * + * Report the number of keys in the JSON object at path in key + */ +class JSONOBJLEN extends RedisCommand +{ + public function getId() + { + return 'JSON.OBJLEN'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONRESP.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONRESP.php new file mode 100644 index 0000000000..6fbfa6f017 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONRESP.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.resp/ + * + * Return the JSON in key in Redis serialization protocol specification form + */ +class JSONRESP extends RedisCommand +{ + public function getId() + { + return 'JSON.RESP'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONSET.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONSET.php new file mode 100644 index 0000000000..c044cff83f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONSET.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Json\NxXxArgument; + +/** + * @see https://redis.io/commands/json.set/ + * + * Set the JSON value at path in key + */ +class JSONSET extends RedisCommand +{ + use NxXxArgument { + setArguments as setSubcommand; + } + + protected static $nxXxArgumentPositionOffset = 3; + + public function getId() + { + return 'JSON.SET'; + } + + public function setArguments(array $arguments) + { + $this->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 0000000000..9b11458e4f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRAPPEND.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.strappend/ + * + * Append the json-string values to the string at path + */ +class JSONSTRAPPEND extends RedisCommand +{ + public function getId() + { + return 'JSON.STRAPPEND'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRLEN.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRLEN.php new file mode 100644 index 0000000000..92ed80d3d4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRLEN.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.strlen/ + * + * Report the length of the JSON String at path in key + */ +class JSONSTRLEN extends RedisCommand +{ + public function getId() + { + return 'JSON.STRLEN'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONTOGGLE.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONTOGGLE.php new file mode 100644 index 0000000000..7bd33da998 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONTOGGLE.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.toggle/ + * + * Toggle a Boolean value stored at path + */ +class JSONTOGGLE extends RedisCommand +{ + public function getId() + { + return 'JSON.TOGGLE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONTYPE.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONTYPE.php new file mode 100644 index 0000000000..83a69d0543 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONTYPE.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Json; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/json.type/ + * + * Report the type of JSON value at path + */ +class JSONTYPE extends RedisCommand +{ + public function getId() + { + return 'JSON.TYPE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/KEYS.php b/plugins/cache-redis/Predis/Command/Redis/KEYS.php new file mode 100644 index 0000000000..fadb8d7dde --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/KEYS.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/keys + */ +class KEYS extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'KEYS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/LASTSAVE.php b/plugins/cache-redis/Predis/Command/Redis/LASTSAVE.php new file mode 100644 index 0000000000..0672845d2c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LASTSAVE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/lastsave + */ +class LASTSAVE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LASTSAVE'; + } +} 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 0000000000..e8663f4576 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LCS.php @@ -0,0 +1,69 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/lcs/ + * + * The LCS command implements the longest common subsequence algorithm. + */ +class LCS extends RedisCommand +{ + public function getId() + { + return 'LCS'; + } + + public function setArguments(array $arguments) + { + if (isset($arguments[2]) && $arguments[2]) { + $arguments[2] = 'LEN'; + } + + if (isset($arguments[3]) && $arguments[3]) { + $arguments[3] = 'IDX'; + } + + if (isset($arguments[5]) && $arguments[5]) { + $arguments[5] = 'WITHMATCHLEN'; + } + + if (isset($arguments[4])) { + if ($arguments[4] !== 0) { + $argumentsBefore = array_slice($arguments, 0, 4); + $argumentsAfter = array_slice($arguments, 5); + $arguments = array_merge($argumentsBefore, ['MINMATCHLEN', $arguments[4]], $argumentsAfter); + } else { + $arguments[4] = false; + } + } + + parent::setArguments($arguments); + $this->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/plugins/cache-redis/Predis/Command/Redis/LINDEX.php b/plugins/cache-redis/Predis/Command/Redis/LINDEX.php new file mode 100644 index 0000000000..80510e1e40 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LINDEX.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/lindex + */ +class LINDEX extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LINDEX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/LINSERT.php b/plugins/cache-redis/Predis/Command/Redis/LINSERT.php new file mode 100644 index 0000000000..a81cf34143 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LINSERT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/linsert + */ +class LINSERT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LINSERT'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/LLEN.php b/plugins/cache-redis/Predis/Command/Redis/LLEN.php new file mode 100644 index 0000000000..628e602807 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LLEN.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/llen + */ +class LLEN extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LLEN'; + } +} 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 0000000000..f7bad007d6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LMOVE.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +class LMOVE extends RedisCommand +{ + public function getId() + { + return 'LMOVE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/LMPOP.php b/plugins/cache-redis/Predis/Command/Redis/LMPOP.php new file mode 100644 index 0000000000..12ef06bf31 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LMPOP.php @@ -0,0 +1,61 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Count; +use Predis\Command\Traits\Keys; +use Predis\Command\Traits\LeftRight; + +class LMPOP extends RedisCommand +{ + use Keys { + Keys::setArguments as setKeys; + } + use LeftRight { + LeftRight::setArguments as setLeftRight; + } + use Count { + Count::setArguments as setCount; + } + + protected static $keysArgumentPositionOffset = 0; + protected static $leftRightArgumentPositionOffset = 1; + protected static $countArgumentPositionOffset = 2; + + public function getId() + { + return 'LMPOP'; + } + + public function setArguments(array $arguments) + { + $this->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/plugins/cache-redis/Predis/Command/Redis/LPOP.php b/plugins/cache-redis/Predis/Command/Redis/LPOP.php new file mode 100644 index 0000000000..d375bacaf1 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LPOP.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/lpop + */ +class LPOP extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LPOP'; + } +} 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 0000000000..f3e0f9e73f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LPUSH.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/lpush + */ +class LPUSH extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LPUSH'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeVariadic($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/LPUSHX.php b/plugins/cache-redis/Predis/Command/Redis/LPUSHX.php new file mode 100644 index 0000000000..3dcacc389b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LPUSHX.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/lpushx + */ +class LPUSHX extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LPUSHX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/LRANGE.php b/plugins/cache-redis/Predis/Command/Redis/LRANGE.php new file mode 100644 index 0000000000..4092dae735 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LRANGE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/lrange + */ +class LRANGE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LRANGE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/LREM.php b/plugins/cache-redis/Predis/Command/Redis/LREM.php new file mode 100644 index 0000000000..47e06c2fd5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LREM.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/lrem + */ +class LREM extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LREM'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/LSET.php b/plugins/cache-redis/Predis/Command/Redis/LSET.php new file mode 100644 index 0000000000..255bb8976c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LSET.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/lset + */ +class LSET extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LSET'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/LTRIM.php b/plugins/cache-redis/Predis/Command/Redis/LTRIM.php new file mode 100644 index 0000000000..c958ef6ec8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LTRIM.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/ltrim + */ +class LTRIM extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'LTRIM'; + } +} 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 0000000000..249cc43f8e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/MGET.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/mget + */ +class MGET extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MGET'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/MIGRATE.php b/plugins/cache-redis/Predis/Command/Redis/MIGRATE.php new file mode 100644 index 0000000000..5772997e78 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/MIGRATE.php @@ -0,0 +1,51 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/migrate + */ +class MIGRATE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MIGRATE'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (is_array(end($arguments))) { + foreach (array_pop($arguments) as $modifier => $value) { + $modifier = strtoupper($modifier); + + if ($modifier === 'COPY' && $value == true) { + $arguments[] = $modifier; + } + + if ($modifier === 'REPLACE' && $value == true) { + $arguments[] = $modifier; + } + } + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/MONITOR.php b/plugins/cache-redis/Predis/Command/Redis/MONITOR.php new file mode 100644 index 0000000000..06e9e59b63 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/MONITOR.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/monitor + */ +class MONITOR extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MONITOR'; + } +} 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 0000000000..cd7a8e5226 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/MOVE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/move + */ +class MOVE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MOVE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/MSET.php b/plugins/cache-redis/Predis/Command/Redis/MSET.php new file mode 100644 index 0000000000..2783716c33 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/MSET.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/mset + */ +class MSET extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MSET'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 1 && is_array($arguments[0])) { + $flattenedKVs = []; + $args = $arguments[0]; + + foreach ($args as $k => $v) { + $flattenedKVs[] = $k; + $flattenedKVs[] = $v; + } + + $arguments = $flattenedKVs; + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/MSETNX.php b/plugins/cache-redis/Predis/Command/Redis/MSETNX.php new file mode 100644 index 0000000000..94d9ce2696 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/MSETNX.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @see http://redis.io/commands/msetnx + */ +class MSETNX extends MSET +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MSETNX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/MULTI.php b/plugins/cache-redis/Predis/Command/Redis/MULTI.php new file mode 100644 index 0000000000..d8f96e64bf --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/MULTI.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/multi + */ +class MULTI extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'MULTI'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/OBJECT_.php b/plugins/cache-redis/Predis/Command/Redis/OBJECT_.php new file mode 100644 index 0000000000..856a8740e2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/OBJECT_.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/object + */ +class OBJECT_ extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'OBJECT'; + } +} 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 0000000000..8fd70e7aae --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PERSIST.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/persist + */ +class PERSIST extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PERSIST'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/PEXPIRE.php b/plugins/cache-redis/Predis/Command/Redis/PEXPIRE.php new file mode 100644 index 0000000000..c362b304be --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PEXPIRE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/pexpire + */ +class PEXPIRE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PEXPIRE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/PEXPIREAT.php b/plugins/cache-redis/Predis/Command/Redis/PEXPIREAT.php new file mode 100644 index 0000000000..6723a71e84 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PEXPIREAT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/pexpireat + */ +class PEXPIREAT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PEXPIREAT'; + } +} 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 0000000000..de12896982 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PEXPIRETIME.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/pexpiretime/ + * + * PEXPIRETIME has the same semantic as EXPIRETIME, + * but returns the absolute Unix expiration timestamp in milliseconds instead of seconds. + */ +class PEXPIRETIME extends RedisCommand +{ + public function getId() + { + return 'PEXPIRETIME'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/PFADD.php b/plugins/cache-redis/Predis/Command/Redis/PFADD.php new file mode 100644 index 0000000000..a57d9d5a28 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PFADD.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/pfadd + */ +class PFADD extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PFADD'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeVariadic($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/PFCOUNT.php b/plugins/cache-redis/Predis/Command/Redis/PFCOUNT.php new file mode 100644 index 0000000000..4c5c28b259 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PFCOUNT.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/pfcount + */ +class PFCOUNT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PFCOUNT'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/PFMERGE.php b/plugins/cache-redis/Predis/Command/Redis/PFMERGE.php new file mode 100644 index 0000000000..fcfa0980e8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PFMERGE.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/pfmerge + */ +class PFMERGE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PFMERGE'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/PING.php b/plugins/cache-redis/Predis/Command/Redis/PING.php new file mode 100644 index 0000000000..db1d6727b9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PING.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/ping + */ +class PING extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PING'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/PSETEX.php b/plugins/cache-redis/Predis/Command/Redis/PSETEX.php new file mode 100644 index 0000000000..3b8131e4a8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PSETEX.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/psetex + */ +class PSETEX extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PSETEX'; + } +} 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 0000000000..7377c7b161 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PSUBSCRIBE.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/psubscribe + */ +class PSUBSCRIBE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PSUBSCRIBE'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/PTTL.php b/plugins/cache-redis/Predis/Command/Redis/PTTL.php new file mode 100644 index 0000000000..d87dd63544 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PTTL.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/pttl + */ +class PTTL extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PTTL'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/PUBLISH.php b/plugins/cache-redis/Predis/Command/Redis/PUBLISH.php new file mode 100644 index 0000000000..d38cd1949c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PUBLISH.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/publish + */ +class PUBLISH extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PUBLISH'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/PUBSUB.php b/plugins/cache-redis/Predis/Command/Redis/PUBSUB.php new file mode 100644 index 0000000000..cd6396f5b9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PUBSUB.php @@ -0,0 +1,62 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/pubsub + */ +class PUBSUB extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PUBSUB'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + switch (strtolower($this->getArgument(0))) { + case 'numsub': + return self::processNumsub($data); + + default: + return $data; + } + } + + /** + * Returns the processed response to PUBSUB NUMSUB. + * + * @param array $channels List of channels + * + * @return array + */ + protected static function processNumsub(array $channels) + { + $processed = []; + $count = count($channels); + + for ($i = 0; $i < $count; ++$i) { + $processed[$channels[$i]] = $channels[++$i]; + } + + return $processed; + } +} 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 0000000000..f15a39fbb4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PUNSUBSCRIBE.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/punsubscribe + */ +class PUNSUBSCRIBE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'PUNSUBSCRIBE'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/QUIT.php b/plugins/cache-redis/Predis/Command/Redis/QUIT.php new file mode 100644 index 0000000000..330bcebfcd --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/QUIT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/quit + */ +class QUIT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'QUIT'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/RANDOMKEY.php b/plugins/cache-redis/Predis/Command/Redis/RANDOMKEY.php new file mode 100644 index 0000000000..c77e562438 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/RANDOMKEY.php @@ -0,0 +1,37 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/randomkey + */ +class RANDOMKEY extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RANDOMKEY'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + return $data !== '' ? $data : null; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/RENAME.php b/plugins/cache-redis/Predis/Command/Redis/RENAME.php new file mode 100644 index 0000000000..78a216ec06 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/RENAME.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/rename + */ +class RENAME extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RENAME'; + } +} 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 0000000000..ce1306fb74 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/RENAMENX.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/renamenx + */ +class RENAMENX extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RENAMENX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/RESTORE.php b/plugins/cache-redis/Predis/Command/Redis/RESTORE.php new file mode 100644 index 0000000000..ae0d482b2f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/RESTORE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/restore + */ +class RESTORE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RESTORE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/RPOP.php b/plugins/cache-redis/Predis/Command/Redis/RPOP.php new file mode 100644 index 0000000000..3bc3f977f6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/RPOP.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/rpop + */ +class RPOP extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RPOP'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/RPOPLPUSH.php b/plugins/cache-redis/Predis/Command/Redis/RPOPLPUSH.php new file mode 100644 index 0000000000..d158f2d55b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/RPOPLPUSH.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/rpoplpush + */ +class RPOPLPUSH extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RPOPLPUSH'; + } +} 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 0000000000..a8a72f0184 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/RPUSH.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/rpush + */ +class RPUSH extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RPUSH'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeVariadic($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/RPUSHX.php b/plugins/cache-redis/Predis/Command/Redis/RPUSHX.php new file mode 100644 index 0000000000..f5b20d45eb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/RPUSHX.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/rpushx + */ +class RPUSHX extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'RPUSHX'; + } +} 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 0000000000..e2213448b5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SADD.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/sadd + */ +class SADD extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SADD'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeVariadic($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SAVE.php b/plugins/cache-redis/Predis/Command/Redis/SAVE.php new file mode 100644 index 0000000000..b7fdb6ebdc --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SAVE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/save + */ +class SAVE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SAVE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SCAN.php b/plugins/cache-redis/Predis/Command/Redis/SCAN.php new file mode 100644 index 0000000000..bf1da9625c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SCAN.php @@ -0,0 +1,67 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/scan + */ +class SCAN extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SCAN'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[1])) { + $options = $this->prepareOptions(array_pop($arguments)); + $arguments = array_merge($arguments, $options); + } + + parent::setArguments($arguments); + } + + /** + * Returns a list of options and modifiers compatible with Redis. + * + * @param array $options List of options. + * + * @return array + */ + protected function prepareOptions($options) + { + $options = array_change_key_case($options, CASE_UPPER); + $normalized = []; + + if (!empty($options['MATCH'])) { + $normalized[] = 'MATCH'; + $normalized[] = $options['MATCH']; + } + + if (!empty($options['COUNT'])) { + $normalized[] = 'COUNT'; + $normalized[] = $options['COUNT']; + } + + return $normalized; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SCARD.php b/plugins/cache-redis/Predis/Command/Redis/SCARD.php new file mode 100644 index 0000000000..daf1393da5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SCARD.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/scard + */ +class SCARD extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SCARD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SCRIPT.php b/plugins/cache-redis/Predis/Command/Redis/SCRIPT.php new file mode 100644 index 0000000000..5df20cf724 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SCRIPT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/script + */ +class SCRIPT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SCRIPT'; + } +} 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 0000000000..b59e63df8d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SDIFF.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/sdiff + */ +class SDIFF extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SDIFF'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SDIFFSTORE.php b/plugins/cache-redis/Predis/Command/Redis/SDIFFSTORE.php new file mode 100644 index 0000000000..008782e9de --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SDIFFSTORE.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/sdiffstore + */ +class SDIFFSTORE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SDIFFSTORE'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[1])) { + $arguments = array_merge([$arguments[0]], $arguments[1]); + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SELECT.php b/plugins/cache-redis/Predis/Command/Redis/SELECT.php new file mode 100644 index 0000000000..f14bb72d9b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SELECT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/select + */ +class SELECT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SELECT'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SENTINEL.php b/plugins/cache-redis/Predis/Command/Redis/SENTINEL.php new file mode 100644 index 0000000000..bd22ffba58 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SENTINEL.php @@ -0,0 +1,70 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/topics/sentinel + */ +class SENTINEL extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SENTINEL'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + $argument = $this->getArgument(0); + $argument = is_null($argument) ? null : strtolower($argument); + + switch ($argument) { + case 'masters': + case 'slaves': + return self::processMastersOrSlaves($data); + + default: + return $data; + } + } + + /** + * Returns a processed response to SENTINEL MASTERS or SENTINEL SLAVES. + * + * @param array $servers List of Redis servers. + * + * @return array + */ + protected static function processMastersOrSlaves(array $servers) + { + foreach ($servers as $idx => $node) { + $processed = []; + $count = count($node); + + for ($i = 0; $i < $count; ++$i) { + $processed[$node[$i]] = $node[++$i]; + } + + $servers[$idx] = $processed; + } + + return $servers; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SET.php b/plugins/cache-redis/Predis/Command/Redis/SET.php new file mode 100644 index 0000000000..f8956f40bc --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SET.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/set + */ +class SET extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SET'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SETBIT.php b/plugins/cache-redis/Predis/Command/Redis/SETBIT.php new file mode 100644 index 0000000000..6c602ae1d4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SETBIT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/setbit + */ +class SETBIT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SETBIT'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SETEX.php b/plugins/cache-redis/Predis/Command/Redis/SETEX.php new file mode 100644 index 0000000000..66189128fd --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SETEX.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/setex + */ +class SETEX extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SETEX'; + } +} 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 0000000000..d347965049 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SETNX.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/setnx + */ +class SETNX extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SETNX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SETRANGE.php b/plugins/cache-redis/Predis/Command/Redis/SETRANGE.php new file mode 100644 index 0000000000..b368e1d42c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SETRANGE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/setrange + */ +class SETRANGE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + 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 0000000000..4d2b74794f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SHUTDOWN.php @@ -0,0 +1,61 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/shutdown + */ +class SHUTDOWN extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SHUTDOWN'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (empty($arguments)) { + parent::setArguments($arguments); + + return; + } + + $processedArguments = []; + + if (array_key_exists(0, $arguments) && null !== $arguments[0]) { + $processedArguments[] = ($arguments[0]) ? 'SAVE' : 'NOSAVE'; + } + + if (array_key_exists(1, $arguments) && false !== $arguments[1]) { + $processedArguments[] = 'NOW'; + } + + if (array_key_exists(2, $arguments) && false !== $arguments[2]) { + $processedArguments[] = 'FORCE'; + } + + if (array_key_exists(3, $arguments) && false !== $arguments[3]) { + $processedArguments[] = 'ABORT'; + } + + parent::setArguments($processedArguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SINTER.php b/plugins/cache-redis/Predis/Command/Redis/SINTER.php new file mode 100644 index 0000000000..2036714a30 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SINTER.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/sinter + */ +class SINTER extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SINTER'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SINTERCARD.php b/plugins/cache-redis/Predis/Command/Redis/SINTERCARD.php new file mode 100644 index 0000000000..f841db365d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SINTERCARD.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Keys; +use Predis\Command\Traits\Limit\Limit; + +class SINTERCARD extends RedisCommand +{ + use Keys { + Keys::setArguments as setKeys; + } + use Limit { + Limit::setArguments as setLimit; + } + + protected static $keysArgumentPositionOffset = 0; + protected static $limitArgumentPositionOffset = 1; + + public function getId() + { + return 'SINTERCARD'; + } + + public function setArguments(array $arguments) + { + $this->setLimit($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php b/plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php new file mode 100644 index 0000000000..144335a1f2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/sinterstore + */ +class SINTERSTORE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SINTERSTORE'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[1])) { + $arguments = array_merge([$arguments[0]], $arguments[1]); + } + + 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 0000000000..3991c4c9e1 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SISMEMBER.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/sismember + */ +class SISMEMBER extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SISMEMBER'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SLAVEOF.php b/plugins/cache-redis/Predis/Command/Redis/SLAVEOF.php new file mode 100644 index 0000000000..ef3d4c499f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SLAVEOF.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/slaveof + */ +class SLAVEOF extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SLAVEOF'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 0 || $arguments[0] === 'NO ONE') { + $arguments = ['NO', 'ONE']; + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SLOWLOG.php b/plugins/cache-redis/Predis/Command/Redis/SLOWLOG.php new file mode 100644 index 0000000000..41fbbcdc46 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SLOWLOG.php @@ -0,0 +1,52 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/slowlog + */ +class SLOWLOG extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SLOWLOG'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (is_array($data)) { + $log = []; + + foreach ($data as $index => $entry) { + $log[$index] = [ + 'id' => $entry[0], + 'timestamp' => $entry[1], + 'duration' => $entry[2], + 'command' => $entry[3], + ]; + } + + return $log; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php b/plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php new file mode 100644 index 0000000000..8f32be40ee --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/smembers + */ +class SMEMBERS extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SMEMBERS'; + } +} 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 0000000000..735b01d4f3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SMISMEMBER.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/smismember/ + * + * Returns whether each member is a member of the set stored at key. + */ +class SMISMEMBER extends RedisCommand +{ + public function getId() + { + return 'SMISMEMBER'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SMOVE.php b/plugins/cache-redis/Predis/Command/Redis/SMOVE.php new file mode 100644 index 0000000000..2c33e28c2c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SMOVE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/smove + */ +class SMOVE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SMOVE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SORT.php b/plugins/cache-redis/Predis/Command/Redis/SORT.php new file mode 100644 index 0000000000..1338c81100 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SORT.php @@ -0,0 +1,86 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/sort + */ +class SORT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SORT'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 1) { + parent::setArguments($arguments); + + return; + } + + $query = [$arguments[0]]; + $sortParams = array_change_key_case($arguments[1], CASE_UPPER); + + if (isset($sortParams['BY'])) { + $query[] = 'BY'; + $query[] = $sortParams['BY']; + } + + if (isset($sortParams['GET'])) { + $getargs = $sortParams['GET']; + + if (is_array($getargs)) { + foreach ($getargs as $getarg) { + $query[] = 'GET'; + $query[] = $getarg; + } + } else { + $query[] = 'GET'; + $query[] = $getargs; + } + } + + if (isset($sortParams['LIMIT']) + && is_array($sortParams['LIMIT']) + && count($sortParams['LIMIT']) == 2) { + $query[] = 'LIMIT'; + $query[] = $sortParams['LIMIT'][0]; + $query[] = $sortParams['LIMIT'][1]; + } + + if (isset($sortParams['SORT'])) { + $query[] = strtoupper($sortParams['SORT']); + } + + if (isset($sortParams['ALPHA']) && $sortParams['ALPHA'] == true) { + $query[] = 'ALPHA'; + } + + if (isset($sortParams['STORE'])) { + $query[] = 'STORE'; + $query[] = $sortParams['STORE']; + } + + 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 0000000000..f302b6e3ba --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SORT_RO.php @@ -0,0 +1,74 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\By\ByArgument; +use Predis\Command\Traits\Get\Get; +use Predis\Command\Traits\Limit\LimitObject; +use Predis\Command\Traits\Sorting; + +/** + * @see https://redis.io/commands/sort_ro/ + * + * Read-only variant of the SORT command. + * It is exactly like the original SORT but refuses the STORE option + * and can safely be used in read-only replicas. + */ +class SORT_RO extends RedisCommand +{ + use ByArgument { + ByArgument::setArguments as setBy; + } + use LimitObject { + LimitObject::setArguments as setLimit; + } + use Get { + Get::setArguments as setGetArgument; + } + use Sorting { + Sorting::setArguments as setSorting; + } + + protected static $byArgumentPositionOffset = 1; + protected static $getArgumentPositionOffset = 3; + protected static $sortArgumentPositionOffset = 4; + + public function getId() + { + return 'SORT_RO'; + } + + public function setArguments(array $arguments) + { + $alpha = array_pop($arguments); + + if (is_bool($alpha) && $alpha) { + $arguments[] = 'ALPHA'; + } elseif (!is_bool($alpha)) { + $arguments[] = $alpha; + } + + $this->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/plugins/cache-redis/Predis/Command/Redis/SPOP.php b/plugins/cache-redis/Predis/Command/Redis/SPOP.php new file mode 100644 index 0000000000..e09a3d55a9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SPOP.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/spop + */ +class SPOP extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SPOP'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SRANDMEMBER.php b/plugins/cache-redis/Predis/Command/Redis/SRANDMEMBER.php new file mode 100644 index 0000000000..61d8f03b5d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SRANDMEMBER.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/srandmember + */ +class SRANDMEMBER extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SRANDMEMBER'; + } +} 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 0000000000..ad3cf062f3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SREM.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/srem + */ +class SREM extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SREM'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeVariadic($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SSCAN.php b/plugins/cache-redis/Predis/Command/Redis/SSCAN.php new file mode 100644 index 0000000000..1991a029f5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SSCAN.php @@ -0,0 +1,67 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/sscan + */ +class SSCAN extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SSCAN'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 3 && is_array($arguments[2])) { + $options = $this->prepareOptions(array_pop($arguments)); + $arguments = array_merge($arguments, $options); + } + + parent::setArguments($arguments); + } + + /** + * Returns a list of options and modifiers compatible with Redis. + * + * @param array $options List of options. + * + * @return array + */ + protected function prepareOptions($options) + { + $options = array_change_key_case($options, CASE_UPPER); + $normalized = []; + + if (!empty($options['MATCH'])) { + $normalized[] = 'MATCH'; + $normalized[] = $options['MATCH']; + } + + if (!empty($options['COUNT'])) { + $normalized[] = 'COUNT'; + $normalized[] = $options['COUNT']; + } + + return $normalized; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/STRLEN.php b/plugins/cache-redis/Predis/Command/Redis/STRLEN.php new file mode 100644 index 0000000000..f51775662b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/STRLEN.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/strlen + */ +class STRLEN extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'STRLEN'; + } +} 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 0000000000..b133c2252c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SUBSCRIBE.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/subscribe + */ +class SUBSCRIBE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SUBSCRIBE'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SUBSTR.php b/plugins/cache-redis/Predis/Command/Redis/SUBSTR.php new file mode 100644 index 0000000000..ec7e5fed01 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SUBSTR.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/substr + */ +class SUBSTR extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SUBSTR'; + } +} 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 0000000000..7a94e8da2f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SUNION.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/sunion + */ +class SUNION extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SUNION'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SUNIONSTORE.php b/plugins/cache-redis/Predis/Command/Redis/SUNIONSTORE.php new file mode 100644 index 0000000000..80e531d2ee --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SUNIONSTORE.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/sunionstore + */ +class SUNIONSTORE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'SUNIONSTORE'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 2 && is_array($arguments[1])) { + $arguments = array_merge([$arguments[0]], $arguments[1]); + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTAGGREGATE.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTAGGREGATE.php new file mode 100644 index 0000000000..948455ed0d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTAGGREGATE.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.aggregate/ + * + * Run a search query on an index, and perform aggregate transformations + * on the results, extracting statistics etc. from them + */ +class FTAGGREGATE extends RedisCommand +{ + public function getId() + { + return 'FT.AGGREGATE'; + } + + public function setArguments(array $arguments) + { + [$index, $query] = $arguments; + $commandArguments = (!empty($arguments[2])) ? $arguments[2]->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 0000000000..a2511ea326 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASADD.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.aliasadd/ + * + * Add an alias to an index. + */ +class FTALIASADD extends RedisCommand +{ + public function getId() + { + return 'FT.ALIASADD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASDEL.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASDEL.php new file mode 100644 index 0000000000..821d1c02e2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASDEL.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.aliasdel/ + * + * Remove an alias from an index. + */ +class FTALIASDEL extends RedisCommand +{ + public function getId() + { + return 'FT.ALIASDEL'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASUPDATE.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASUPDATE.php new file mode 100644 index 0000000000..ef5328779e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASUPDATE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.aliasupdate/ + * + * Add an alias to an index. If the alias is already associated with another index, + * FT.ALIASUPDATE removes the alias association with the previous index. + */ +class FTALIASUPDATE extends RedisCommand +{ + public function getId() + { + return 'FT.ALIASUPDATE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTALTER.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTALTER.php new file mode 100644 index 0000000000..a405efe958 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTALTER.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Argument\Search\SchemaFields\FieldInterface; +use Predis\Command\Command as RedisCommand; + +class FTALTER extends RedisCommand +{ + public function getId() + { + return 'FT.ALTER'; + } + + public function setArguments(array $arguments) + { + [$index, $schema] = $arguments; + $commandArguments = (!empty($arguments[2])) ? $arguments[2]->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 0000000000..9cba60e053 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTCONFIG.php @@ -0,0 +1,30 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.config-get/ + * @see https://redis.io/commands/ft.config-set/ + * + * Container command corresponds to any FT.CONFIG *. + * Represents any FUNCTION command with subcommand as first argument. + */ +class FTCONFIG extends RedisCommand +{ + public function getId() + { + return 'FT.CONFIG'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTCREATE.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTCREATE.php new file mode 100644 index 0000000000..9e43a6cfe9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTCREATE.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Argument\Search\SchemaFields\FieldInterface; +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.create/ + * + * Create an index with the given specification + */ +class FTCREATE extends RedisCommand +{ + public function getId() + { + return 'FT.CREATE'; + } + + public function setArguments(array $arguments) + { + [$index, $schema] = $arguments; + $commandArguments = (!empty($arguments[2])) ? $arguments[2]->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 0000000000..14a01948a8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTCURSOR.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +class FTCURSOR extends RedisCommand +{ + public function getId() + { + return 'FT.CURSOR'; + } + + public function setArguments(array $arguments) + { + [$subcommand, $index, $cursorId] = $arguments; + $commandArguments = (!empty($arguments[3])) ? $arguments[3]->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 0000000000..c0bc3da729 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTADD.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.dictadd/ + * + * Add terms to a dictionary. + */ +class FTDICTADD extends RedisCommand +{ + public function getId() + { + return 'FT.DICTADD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTDEL.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTDEL.php new file mode 100644 index 0000000000..19e0633b78 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTDEL.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.dictdel/ + * + * Delete terms from a dictionary. + */ +class FTDICTDEL extends RedisCommand +{ + public function getId() + { + return 'FT.DICTDEL'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTDUMP.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTDUMP.php new file mode 100644 index 0000000000..b8282b5601 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTDUMP.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.dictdump/ + * + * Dump all terms in the given dictionary. + */ +class FTDICTDUMP extends RedisCommand +{ + public function getId() + { + return 'FT.DICTDUMP'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTDROPINDEX.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTDROPINDEX.php new file mode 100644 index 0000000000..0da330fa96 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTDROPINDEX.php @@ -0,0 +1,38 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +class FTDROPINDEX extends RedisCommand +{ + public function getId() + { + return 'FT.DROPINDEX'; + } + + public function setArguments(array $arguments) + { + [$index] = $arguments; + $commandArguments = []; + + if (!empty($arguments[1])) { + $commandArguments = $arguments[1]->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 0000000000..e2aa35d47c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTEXPLAIN.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.explain/ + * + * Return the execution plan for a complex query. + */ +class FTEXPLAIN extends RedisCommand +{ + public function getId() + { + return 'FT.EXPLAIN'; + } + + public function setArguments(array $arguments) + { + [$index, $query] = $arguments; + $commandArguments = []; + + if (!empty($arguments[2])) { + $commandArguments = $arguments[2]->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 0000000000..02fa959749 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTINFO.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.info/ + * + * Return information and statistics on the index. + */ +class FTINFO extends RedisCommand +{ + public function getId() + { + return 'FT.INFO'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTPROFILE.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTPROFILE.php new file mode 100644 index 0000000000..63be21f40a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTPROFILE.php @@ -0,0 +1,38 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.profile/ + * + * Perform a FT.SEARCH or FT.AGGREGATE command and collects performance information. + */ +class FTPROFILE extends RedisCommand +{ + public function getId() + { + return 'FT.PROFILE'; + } + + public function setArguments(array $arguments) + { + [$index, $arguments] = $arguments; + + parent::setArguments(array_merge( + [$index], + $arguments->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 0000000000..9fbe8a5638 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSEARCH.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.search/ + * + * Search the index with a textual query, returning either documents or just ids + */ +class FTSEARCH extends RedisCommand +{ + public function getId() + { + return 'FT.SEARCH'; + } + + public function setArguments(array $arguments) + { + [$index, $query] = $arguments; + $commandArguments = (!empty($arguments[2])) ? $arguments[2]->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 0000000000..ac0232bb2c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSPELLCHECK.php @@ -0,0 +1,38 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +class FTSPELLCHECK extends RedisCommand +{ + public function getId() + { + return 'FT.SPELLCHECK'; + } + + public function setArguments(array $arguments) + { + [$index, $query] = $arguments; + $commandArguments = []; + + if (!empty($arguments[2])) { + $commandArguments = $arguments[2]->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 0000000000..71c42a4a35 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGADD.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.sugadd/ + * + * Add a suggestion string to an auto-complete suggestion dictionary. + */ +class FTSUGADD extends RedisCommand +{ + public function getId() + { + return 'FT.SUGADD'; + } + + public function setArguments(array $arguments) + { + [$key, $string, $score] = $arguments; + $commandArguments = (!empty($arguments[3])) ? $arguments[3]->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 0000000000..15457d10b7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGDEL.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.sugdel/ + * + * Delete a string from a suggestion index. + */ +class FTSUGDEL extends RedisCommand +{ + public function getId() + { + return 'FT.SUGDEL'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGGET.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGGET.php new file mode 100644 index 0000000000..e37d643a14 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGGET.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.sugget/ + * + * Get completion suggestions for a prefix. + */ +class FTSUGGET extends RedisCommand +{ + public function getId() + { + return 'FT.SUGGET'; + } + + public function setArguments(array $arguments) + { + [$key, $prefix] = $arguments; + $commandArguments = (!empty($arguments[2])) ? $arguments[2]->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 0000000000..1e11d8d307 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGLEN.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.suglen/ + * + * Get the size of an auto-complete suggestion dictionary. + */ +class FTSUGLEN extends RedisCommand +{ + public function getId() + { + return 'FT.SUGLEN'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSYNDUMP.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSYNDUMP.php new file mode 100644 index 0000000000..b9d9152b4e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSYNDUMP.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.syndump/ + * + * Dump the contents of a synonym group. + */ +class FTSYNDUMP extends RedisCommand +{ + public function getId() + { + return 'FT.SYNDUMP'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSYNUPDATE.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSYNUPDATE.php new file mode 100644 index 0000000000..3007ddca2b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSYNUPDATE.php @@ -0,0 +1,46 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.synupdate/ + * + * Update a synonym group + */ +class FTSYNUPDATE extends RedisCommand +{ + public function getId() + { + return 'FT.SYNUPDATE'; + } + + public function setArguments(array $arguments) + { + [$index, $synonymGroupId] = $arguments; + $commandArguments = []; + + if (!empty($arguments[2])) { + $commandArguments = $arguments[2]->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 0000000000..b323949fef --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTTAGVALS.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\Search; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ft.tagvals/ + * + * Return a distinct set of values indexed in a Tag field. + */ +class FTTAGVALS extends RedisCommand +{ + public function getId() + { + return 'FT.TAGVALS'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTADD.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTADD.php new file mode 100644 index 0000000000..0ef789e37d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTADD.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.add/ + * + * Adds one or more observations to a t-digest sketch. + */ +class TDIGESTADD extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.ADD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTBYRANK.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTBYRANK.php new file mode 100644 index 0000000000..8fba75ecc4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTBYRANK.php @@ -0,0 +1,55 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.byrank/ + * + * Returns, for each input rank, an estimation of the value (floating-point) with that rank. + */ +class TDIGESTBYRANK extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.BYRANK'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (!is_array($data)) { + return $data; + } + + // convert Relay (RESP3) constants to strings + return array_map(function ($value) { + if (is_string($value) || !is_float($value)) { + return $value; + } + + if (is_nan($value)) { + return 'nan'; + } + + switch ($value) { + case INF: return 'inf'; + case -INF: return '-inf'; + default: return $value; + } + }, $data); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTBYREVRANK.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTBYREVRANK.php new file mode 100644 index 0000000000..979270ccbd --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTBYREVRANK.php @@ -0,0 +1,55 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.byrevrank/ + * + * Returns, for each input reverse rank, an estimation of the value (floating-point) with that reverse rank. + */ +class TDIGESTBYREVRANK extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.BYREVRANK'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (!is_array($data)) { + return $data; + } + + // convert Relay (RESP3) constants to strings + return array_map(function ($value) { + if (is_string($value) || !is_float($value)) { + return $value; + } + + if (is_nan($value)) { + return 'nan'; + } + + switch ($value) { + case INF: return 'inf'; + case -INF: return '-inf'; + default: return $value; + } + }, $data); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTCDF.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTCDF.php new file mode 100644 index 0000000000..3f58f9a78d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTCDF.php @@ -0,0 +1,57 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.cdf/ + * + * Returns, for each input value, an estimation of the fraction (floating-point) + * of (observations smaller than the given value + half + * the observations equal to the given value). + */ +class TDIGESTCDF extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.CDF'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (!is_array($data)) { + return $data; + } + + // convert Relay (RESP3) constants to strings + return array_map(function ($value) { + if (is_string($value) || !is_float($value)) { + return $value; + } + + if (is_nan($value)) { + return 'nan'; + } + + switch ($value) { + case INF: return 'inf'; + case -INF: return '-inf'; + default: return $value; + } + }, $data); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTCREATE.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTCREATE.php new file mode 100644 index 0000000000..68db397e0d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTCREATE.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.create/ + * + * Allocates memory and initializes a new t-digest sketch. + */ +class TDIGESTCREATE extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.CREATE'; + } + + public function setArguments(array $arguments) + { + if (!empty($arguments[1])) { + $arguments[2] = $arguments[1]; + $arguments[1] = 'COMPRESSION'; + } elseif (array_key_exists(1, $arguments) && $arguments[1] < 1) { + array_pop($arguments); + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTINFO.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTINFO.php new file mode 100644 index 0000000000..8cb100abeb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTINFO.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.info/ + * + * Returns information and statistics about a t-digest sketch. + */ +class TDIGESTINFO extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.INFO'; + } + + public function parseResponse($data) + { + $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; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTMAX.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTMAX.php new file mode 100644 index 0000000000..6441f24ce3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTMAX.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.max/ + * + * Returns the maximum observation value from a t-digest sketch. + */ +class TDIGESTMAX extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.MAX'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (is_string($data) || !is_float($data)) { + return $data; + } + + // convert Relay (RESP3) constants to strings + if (is_nan($data)) { + return 'nan'; + } + + switch ($data) { + case INF: return 'inf'; + case -INF: return '-inf'; + default: return $data; + } + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTMERGE.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTMERGE.php new file mode 100644 index 0000000000..e9d5f3a96c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTMERGE.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.merge/ + * + * Merges multiple t-digest sketches into a single sketch. + */ +class TDIGESTMERGE extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.MERGE'; + } + + public function setArguments(array $arguments) + { + $processedArguments = array_merge([$arguments[0], count($arguments[1])], $arguments[1]); + + for ($i = 2, $iMax = count($arguments); $i < $iMax; $i++) { + if (is_int($arguments[$i]) && $arguments[$i] !== 0) { + array_push($processedArguments, 'COMPRESSION', $arguments[$i]); + } elseif (is_bool($arguments[$i]) && $arguments[$i]) { + $processedArguments[] = 'OVERRIDE'; + } + } + + parent::setArguments($processedArguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTMIN.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTMIN.php new file mode 100644 index 0000000000..472f07da39 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTMIN.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.min/ + * + * Returns the minimum observation value from a t-digest sketch. + */ +class TDIGESTMIN extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.MIN'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (is_string($data) || !is_float($data)) { + return $data; + } + + // convert Relay (RESP3) constants to strings + if (is_nan($data)) { + return 'nan'; + } + + switch ($data) { + case INF: return 'inf'; + case -INF: return '-inf'; + default: return $data; + } + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTQUANTILE.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTQUANTILE.php new file mode 100644 index 0000000000..001ec0e310 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTQUANTILE.php @@ -0,0 +1,55 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.quantile/ + * + * Returns, for each input fraction, an estimation of the value (floating point) that is smaller than the given fraction of observations. + */ +class TDIGESTQUANTILE extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.QUANTILE'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (!is_array($data)) { + return $data; + } + + // convert Relay (RESP3) constants to strings + return array_map(function ($value) { + if (is_string($value) || !is_float($value)) { + return $value; + } + + if (is_nan($value)) { + return 'nan'; + } + + switch ($value) { + case INF: return 'inf'; + case -INF: return '-inf'; + default: return $value; + } + }, $data); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTRANK.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTRANK.php new file mode 100644 index 0000000000..3e41db1ef2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTRANK.php @@ -0,0 +1,30 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.rank/ + * + * Returns, for each input value (floating-point), the estimated rank + * of the value (the number of observations in the sketch that are smaller than + * the value + half the number of observations that are equal to the value). + */ +class TDIGESTRANK extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.RANK'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTRESET.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTRESET.php new file mode 100644 index 0000000000..0456f4f90c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTRESET.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.reset/ + * + * Resets a t-digest sketch: empty the sketch and re-initializes it. + */ +class TDIGESTRESET extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.RESET'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTREVRANK.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTREVRANK.php new file mode 100644 index 0000000000..dec7780385 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTREVRANK.php @@ -0,0 +1,30 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.revrank/ + * + * Returns, for each input value (floating-point), the estimated reverse rank + * of the value (the number of observations in the sketch that are larger than + * the value + half the number of observations that are equal to the value). + */ +class TDIGESTREVRANK extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.REVRANK'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTTRIMMED_MEAN.php b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTTRIMMED_MEAN.php new file mode 100644 index 0000000000..2ccd3d345a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TDigest/TDIGESTTRIMMED_MEAN.php @@ -0,0 +1,50 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TDigest; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/tdigest.trimmed_mean/ + * + * Returns an estimation of the mean value from the sketch, + * excluding observation values outside the low and high cutoff quantiles. + */ +class TDIGESTTRIMMED_MEAN extends RedisCommand +{ + public function getId() + { + return 'TDIGEST.TRIMMED_MEAN'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (is_string($data) || !is_float($data)) { + return $data; + } + + // convert Relay (RESP3) constants to strings + if (is_nan($data)) { + return 'nan'; + } + + switch ($data) { + case INF: return 'inf'; + case -INF: return '-inf'; + default: return $data; + } + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TIME.php b/plugins/cache-redis/Predis/Command/Redis/TIME.php new file mode 100644 index 0000000000..da0ff399fa --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TIME.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/time + */ +class TIME extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'TIME'; + } +} 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 0000000000..82c13fef8b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TOUCH.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/touch + */ +class TOUCH extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'TOUCH'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TTL.php b/plugins/cache-redis/Predis/Command/Redis/TTL.php new file mode 100644 index 0000000000..b8fd322927 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TTL.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/ttl + */ +class TTL extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'TTL'; + } +} 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 0000000000..dece637781 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TYPE.php @@ -0,0 +1,51 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/type + */ +class TYPE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'TYPE'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (is_string($data)) { + return $data; + } + + // Relay types + switch ($data) { + case 0: return 'none'; + case 1: return 'string'; + case 2: return 'set'; + case 3: return 'list'; + case 4: return 'zset'; + case 5: return 'hash'; + case 6: return 'stream'; + default: return $data; + } + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSADD.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSADD.php new file mode 100644 index 0000000000..9936151e16 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSADD.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.add/ + * + * Append a sample to a time series. + */ +class TSADD extends RedisCommand +{ + public function getId() + { + return 'TS.ADD'; + } + + public function setArguments(array $arguments) + { + [$key, $timestamp, $value] = $arguments; + $commandArguments = (!empty($arguments[3])) ? $arguments[3]->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 0000000000..8797d68a38 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSALTER.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.alter/ + * + * Update the retention, chunk size, duplicate policy, and labels of an existing time series. + */ +class TSALTER extends RedisCommand +{ + public function getId() + { + return 'TS.ALTER'; + } + + public function setArguments(array $arguments) + { + [$key] = $arguments; + $commandArguments = (!empty($arguments[1])) ? $arguments[1]->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 0000000000..0d88072c02 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATE.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.create/ + * + * Create a new time series. + */ +class TSCREATE extends RedisCommand +{ + public function getId() + { + return 'TS.CREATE'; + } + + public function setArguments(array $arguments) + { + [$key] = $arguments; + $commandArguments = (!empty($arguments[1])) ? $arguments[1]->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 0000000000..c8bd62bcb4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATERULE.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.createrule/ + * + * Create a compaction rule + */ +class TSCREATERULE extends RedisCommand +{ + public function getId() + { + return 'TS.CREATERULE'; + } + + public function setArguments(array $arguments) + { + [$sourceKey, $destKey, $aggregator, $bucketDuration] = $arguments; + $processedArguments = [$sourceKey, $destKey, 'AGGREGATION', $aggregator, $bucketDuration]; + + if (count($arguments) === 5) { + $processedArguments[] = $arguments[4]; + } + + parent::setArguments($processedArguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDECRBY.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDECRBY.php new file mode 100644 index 0000000000..4d786713b5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDECRBY.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.decrby/ + * + * Decrease the value of the sample with the maximum existing timestamp, + * or create a new sample with a value equal to the value of the sample + * with the maximum existing timestamp with a given decrement. + */ +class TSDECRBY extends RedisCommand +{ + public function getId() + { + return 'TS.DECRBY'; + } + + public function setArguments(array $arguments) + { + [$key, $value] = $arguments; + $commandArguments = (!empty($arguments[2])) ? $arguments[2]->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 0000000000..747c5a629c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDEL.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.del/ + * + * Delete all samples between two timestamps for a given time series. + */ +class TSDEL extends RedisCommand +{ + public function getId() + { + return 'TS.DEL'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDELETERULE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDELETERULE.php new file mode 100644 index 0000000000..4b2279fc4f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDELETERULE.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.deleterule/ + * + * Delete a compaction rule. + */ +class TSDELETERULE extends RedisCommand +{ + public function getId() + { + return 'TS.DELETERULE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSGET.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSGET.php new file mode 100644 index 0000000000..d5cad68435 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSGET.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.get/ + * + * Get the sample with the highest timestamp from a given time series. + */ +class TSGET extends RedisCommand +{ + public function getId() + { + return 'TS.GET'; + } + + public function setArguments(array $arguments) + { + [$key] = $arguments; + $commandArguments = (!empty($arguments[1])) ? $arguments[1]->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 0000000000..3141e5ded7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINCRBY.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.incrby/ + * + * Increase the value of the sample with the maximum existing timestamp, + * or create a new sample with a value equal to the value of the sample + * with the maximum existing timestamp with a given increment + */ +class TSINCRBY extends RedisCommand +{ + public function getId() + { + return 'TS.INCRBY'; + } + + public function setArguments(array $arguments) + { + [$key, $value] = $arguments; + $commandArguments = (!empty($arguments[2])) ? $arguments[2]->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 0000000000..d4941d407b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINFO.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.info/ + * + * Return information and statistics for a time series. + */ +class TSINFO extends RedisCommand +{ + public function getId() + { + return 'TS.INFO'; + } + + public function setArguments(array $arguments) + { + [$key] = $arguments; + $commandArguments = (!empty($arguments[1])) ? $arguments[1]->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 0000000000..085224693e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMADD.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.madd/ + * + * Append new samples to one or more time series. + */ +class TSMADD extends RedisCommand +{ + public function getId() + { + return 'TS.MADD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMGET.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMGET.php new file mode 100644 index 0000000000..78c43d2b54 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMGET.php @@ -0,0 +1,37 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +class TSMGET extends RedisCommand +{ + public function getId() + { + return 'TS.MGET'; + } + + public function setArguments(array $arguments) + { + $processedArguments = []; + $argumentsObject = array_shift($arguments); + $commandArguments = $argumentsObject->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 0000000000..3d68cdd998 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMRANGE.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.mrange/ + * + * Query a range across multiple time series by filters in forward direction. + */ +class TSMRANGE extends RedisCommand +{ + public function getId() + { + return 'TS.MRANGE'; + } + + public function setArguments(array $arguments) + { + [$fromTimestamp, $toTimestamp] = $arguments; + $commandArguments = $arguments[2]->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 0000000000..987fd82010 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMREVRANGE.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +/** + * @see https://redis.io/commands/ts.mrevrange/ + * + * Query a range across multiple time series by filters in reverse direction. + */ +class TSMREVRANGE extends TSMRANGE +{ + public function getId() + { + return 'TS.MREVRANGE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSQUERYINDEX.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSQUERYINDEX.php new file mode 100644 index 0000000000..0645426ebd --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSQUERYINDEX.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.queryindex/ + * + * Get all time series keys matching a filter list. + */ +class TSQUERYINDEX extends RedisCommand +{ + public function getId() + { + return 'TS.QUERYINDEX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSRANGE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSRANGE.php new file mode 100644 index 0000000000..f80cfa7029 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSRANGE.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/ts.range/ + * + * Query a range in forward direction. + */ +class TSRANGE extends RedisCommand +{ + public function getId() + { + return 'TS.RANGE'; + } + + public function setArguments(array $arguments) + { + [$key, $fromTimestamp, $toTimestamp] = $arguments; + $commandArguments = (!empty($arguments[3])) ? $arguments[3]->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 0000000000..1e8768e969 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSREVRANGE.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TimeSeries; + +/** + * @see https://redis.io/commands/ts.revrange/ + * + * Query a range in reverse direction. + */ +class TSREVRANGE extends TSRANGE +{ + public function getId() + { + return 'TS.REVRANGE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKADD.php b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKADD.php new file mode 100644 index 0000000000..a2e78cc40e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKADD.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TopK; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/topk.add/ + * + * Adds an item to the data structure. + * Multiple items can be added at once. + * If an item enters the Top-K list, the item which is expelled is returned. + * This allows dynamic heavy-hitter detection of items being entered or expelled from Top-K list. + */ +class TOPKADD extends RedisCommand +{ + public function getId() + { + return 'TOPK.ADD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKINCRBY.php b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKINCRBY.php new file mode 100644 index 0000000000..c9df9f6087 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKINCRBY.php @@ -0,0 +1,30 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TopK; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/topk.incrby/ + * + * Increase the score of an item in the data structure by increment. + * Multiple items' score can be increased at once. + * If an item enters the Top-K list, the item which is expelled is returned. + */ +class TOPKINCRBY extends RedisCommand +{ + public function getId() + { + return 'TOPK.INCRBY'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKINFO.php b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKINFO.php new file mode 100644 index 0000000000..108b1172a6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKINFO.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TopK; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/topk.info/ + * + * Returns number of required items (k), width, depth and decay values. + */ +class TOPKINFO extends RedisCommand +{ + public function getId() + { + return 'TOPK.INFO'; + } + + public function parseResponse($data) + { + $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; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKLIST.php b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKLIST.php new file mode 100644 index 0000000000..11cbcfa5e6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKLIST.php @@ -0,0 +1,68 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TopK; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/topk.list/ + * + * Return full list of items in Top K list. + */ +class TOPKLIST extends RedisCommand +{ + public function getId() + { + return 'TOPK.LIST'; + } + + public function setArguments(array $arguments) + { + if (!empty($arguments[1])) { + $arguments[1] = 'WITHCOUNT'; + } + + parent::setArguments($arguments); + $this->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 0000000000..1289021944 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKQUERY.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TopK; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/topk.query/ + * + * Checks whether an item is one of Top-K items. + * Multiple items can be checked at once. + */ +class TOPKQUERY extends RedisCommand +{ + public function getId() + { + return 'TOPK.QUERY'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKRESERVE.php b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKRESERVE.php new file mode 100644 index 0000000000..7f464b2bdb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKRESERVE.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis\TopK; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/topk.reserve/ + * + * Initializes a TopK with specified parameters. + */ +class TOPKRESERVE extends RedisCommand +{ + public function getId() + { + return 'TOPK.RESERVE'; + } + + public function setArguments(array $arguments) + { + switch (count($arguments)) { + case 3: + $arguments[] = 7; // default depth + $arguments[] = 0.9; // default decay + break; + case 4: + $arguments[] = 0.9; // default decay + break; + default: + parent::setArguments($arguments); + + return; + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/UNSUBSCRIBE.php b/plugins/cache-redis/Predis/Command/Redis/UNSUBSCRIBE.php new file mode 100644 index 0000000000..b5ea6855ec --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/UNSUBSCRIBE.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/unsubscribe + */ +class UNSUBSCRIBE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'UNSUBSCRIBE'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeArguments($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/UNWATCH.php b/plugins/cache-redis/Predis/Command/Redis/UNWATCH.php new file mode 100644 index 0000000000..901aa07232 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/UNWATCH.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/unwatch + */ +class UNWATCH extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'UNWATCH'; + } +} 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 0000000000..f58cc05470 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/WAITAOF.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/waitaof/ + * + * This command blocks the current client until all the previous write commands are acknowledged + * as having been fsynced to the AOF of the local Redis and/or at least the specified number of replicas. + */ +class WAITAOF extends RedisCommand +{ + public function getId() + { + return 'WAITAOF'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/WATCH.php b/plugins/cache-redis/Predis/Command/Redis/WATCH.php new file mode 100644 index 0000000000..d7a93e696d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/WATCH.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/watch + */ +class WATCH extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'WATCH'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (isset($arguments[0]) && is_array($arguments[0])) { + $arguments = $arguments[0]; + } + + 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 0000000000..21c1322723 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/XADD.php @@ -0,0 +1,64 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/xadd + */ +class XADD extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'XADD'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $args = []; + + $args[] = $arguments[0]; + $options = $arguments[3] ?? []; + + if (isset($options['nomkstream']) && $options['nomkstream']) { + $args[] = 'NOMKSTREAM'; + } + + if (isset($options['trim']) && is_array($options['trim'])) { + array_push($args, ...$options['trim']); + + if (isset($options['limit'])) { + $args[] = 'LIMIT'; + $args[] = $options['limit']; + } + } + + // ID, default to * to let Redis set it + $args[] = $arguments[2] ?? '*'; + if (isset($arguments[1]) && is_array($arguments[1])) { + foreach ($arguments[1] as $key => $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 0000000000..f1c509e350 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/XDEL.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/xdel + */ +class XDEL extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'XDEL'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeVariadic($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/XLEN.php b/plugins/cache-redis/Predis/Command/Redis/XLEN.php new file mode 100644 index 0000000000..aa1f65be3e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/XLEN.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/xlen + */ +class XLEN extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'XLEN'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/XRANGE.php b/plugins/cache-redis/Predis/Command/Redis/XRANGE.php new file mode 100644 index 0000000000..1be1491b5a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/XRANGE.php @@ -0,0 +1,62 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/xrange + */ +class XRANGE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'XRANGE'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 4) { + $arguments[] = $arguments[3]; + $arguments[3] = 'COUNT'; + } + + parent::setArguments($arguments); + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + $result = []; + foreach ($data as $entry) { + $processed = []; + $count = count($entry[1]); + + for ($i = 0; $i < $count; ++$i) { + $processed[$entry[1][$i]] = $entry[1][++$i]; + } + + $result[$entry[0]] = $processed; + } + + return $result; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/XREVRANGE.php b/plugins/cache-redis/Predis/Command/Redis/XREVRANGE.php new file mode 100644 index 0000000000..23b66f7bed --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/XREVRANGE.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @see http://redis.io/commands/xrevrange + */ +class XREVRANGE extends XRANGE +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'XREVRANGE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/XTRIM.php b/plugins/cache-redis/Predis/Command/Redis/XTRIM.php new file mode 100644 index 0000000000..6756b51b9b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/XTRIM.php @@ -0,0 +1,54 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/xtrim + */ +class XTRIM extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'XTRIM'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $args = []; + $options = $arguments[3] ?? []; + + $args[] = $arguments[0]; + // Either e.g. 'MAXLEN' or ['MAXLEN', '~'] + if (is_array($arguments[1])) { + array_push($args, ...$arguments[1]); + } else { + $args[] = $arguments[1]; + } + + $args[] = $arguments[2]; + if (isset($options['limit'])) { + $args[] = 'LIMIT'; + $args[] = $options['limit']; + } + + parent::setArguments($args); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZADD.php b/plugins/cache-redis/Predis/Command/Redis/ZADD.php new file mode 100644 index 0000000000..0ac7ce193b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZADD.php @@ -0,0 +1,44 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zadd + */ +class ZADD extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZADD'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (is_array(end($arguments))) { + foreach (array_pop($arguments) as $member => $score) { + $arguments[] = $score; + $arguments[] = $member; + } + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZCARD.php b/plugins/cache-redis/Predis/Command/Redis/ZCARD.php new file mode 100644 index 0000000000..7f6cfd4d06 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZCARD.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zcard + */ +class ZCARD extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZCARD'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZCOUNT.php b/plugins/cache-redis/Predis/Command/Redis/ZCOUNT.php new file mode 100644 index 0000000000..32b54cb338 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZCOUNT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zcount + */ +class ZCOUNT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZCOUNT'; + } +} 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 0000000000..573dcae681 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZDIFF.php @@ -0,0 +1,48 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Keys; +use Predis\Command\Traits\With\WithScores; + +/** + * @see https://redis.io/commands/zdiff/ + * + * This command is similar to ZDIFFSTORE, but instead of + * storing the resulting sorted set, it is returned to the client. + */ +class ZDIFF extends RedisCommand +{ + use WithScores { + WithScores::setArguments as setWithScore; + } + use Keys { + Keys::setArguments as setKeys; + } + + protected static $keysArgumentPositionOffset = 0; + + public function getId() + { + return 'ZDIFF'; + } + + public function setArguments(array $arguments) + { + $this->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 0000000000..7299883720 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZDIFFSTORE.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Keys; + +/** + * @see https://redis.io/commands/zdiffstore/ + * + * Computes the difference between the first and all successive input sorted sets + * and stores the result in destination. The total number of input keys is specified by numkeys. + * + * Keys that do not exist are considered to be empty sets. + * + * If destination already exists, it is overwritten. + */ +class ZDIFFSTORE extends RedisCommand +{ + use Keys { + Keys::setArguments as setKeys; + } + + public static $keysArgumentPositionOffset = 1; + + public function getId() + { + return 'ZDIFFSTORE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZINCRBY.php b/plugins/cache-redis/Predis/Command/Redis/ZINCRBY.php new file mode 100644 index 0000000000..c46044e3dc --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZINCRBY.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zincrby + */ +class ZINCRBY extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZINCRBY'; + } +} 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 0000000000..787db2bbe1 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZINTER.php @@ -0,0 +1,35 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Traits\With\WithScores; + +/** + * @see https://redis.io/commands/zinter/ + * + * This command is similar to ZINTERSTORE, but instead of + * storing the resulting sorted set, it is returned to the client. + */ +class ZINTER extends ZINTERSTORE +{ + use WithScores; + + protected static $keysArgumentPositionOffset = 0; + protected static $weightsArgumentPositionOffset = 1; + protected static $aggregateArgumentPositionOffset = 2; + + public function getId() + { + return 'ZINTER'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZINTERCARD.php b/plugins/cache-redis/Predis/Command/Redis/ZINTERCARD.php new file mode 100644 index 0000000000..e6b2b45017 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZINTERCARD.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Keys; +use Predis\Command\Traits\Limit\Limit; + +/** + * @see https://redis.io/commands/zintercard/ + * + * This command is similar to ZINTER, but instead of returning the result set, + * it returns just the cardinality of the result. + */ +class ZINTERCARD extends RedisCommand +{ + use Keys { + Keys::setArguments as setKeys; + } + use Limit { + Limit::setArguments as setLimit; + } + + protected static $keysArgumentPositionOffset = 0; + protected static $limitArgumentPositionOffset = 1; + + public function getId() + { + return 'ZINTERCARD'; + } + + public function setArguments(array $arguments) + { + $this->setLimit($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php b/plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php new file mode 100644 index 0000000000..7f0aa44981 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @see http://redis.io/commands/zinterstore + */ +class ZINTERSTORE extends ZUNIONSTORE +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZINTERSTORE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZLEXCOUNT.php b/plugins/cache-redis/Predis/Command/Redis/ZLEXCOUNT.php new file mode 100644 index 0000000000..c216b99d92 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZLEXCOUNT.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zlexcount + */ +class ZLEXCOUNT extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZLEXCOUNT'; + } +} 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 0000000000..4adc0cca9e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZMPOP.php @@ -0,0 +1,79 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Count; +use Predis\Command\Traits\Keys; +use Predis\Command\Traits\MinMaxModifier; + +/** + * @see https://redis.io/commands/zmpop/ + * + * Pops one or more elements, that are member-score pairs, + * from the first non-empty sorted set in the provided list of key names. + */ +class ZMPOP extends RedisCommand +{ + use Keys { + Keys::setArguments as setKeys; + } + use Count { + Count::setArguments as setCount; + } + use MinMaxModifier; + + protected static $keysArgumentPositionOffset = 0; + protected static $countArgumentPositionOffset = 2; + protected static $modifierArgumentPositionOffset = 1; + + public function getId() + { + return 'ZMPOP'; + } + + public function setArguments(array $arguments) + { + $this->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 0000000000..2dd76fd651 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZMSCORE.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see https://redis.io/commands/zmscore/ + * + * Returns the scores associated with the specified members + * in the sorted set stored at key. + * + * For every member that does not exist in the sorted set, a null value is returned. + */ +class ZMSCORE extends RedisCommand +{ + /** + * {@inheritDoc} + */ + public function getId() + { + return 'ZMSCORE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZPOPMAX.php b/plugins/cache-redis/Predis/Command/Redis/ZPOPMAX.php new file mode 100644 index 0000000000..1ebf45b099 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZPOPMAX.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zpopmax + */ +class ZPOPMAX extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZPOPMAX'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + $result = []; + + for ($i = 0; $i < count($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/plugins/cache-redis/Predis/Command/Redis/ZPOPMIN.php b/plugins/cache-redis/Predis/Command/Redis/ZPOPMIN.php new file mode 100644 index 0000000000..5f561c8ee1 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZPOPMIN.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zpopmin + */ +class ZPOPMIN extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZPOPMIN'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + $result = []; + + for ($i = 0; $i < count($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/plugins/cache-redis/Predis/Command/Redis/ZRANDMEMBER.php b/plugins/cache-redis/Predis/Command/Redis/ZRANDMEMBER.php new file mode 100644 index 0000000000..e5597dcef0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANDMEMBER.php @@ -0,0 +1,36 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\With\WithScores; + +/** + * @see https://redis.io/commands/zrandmember/ + * + * Return a random element from the sorted set value stored at key. + * + * If the provided count argument is positive, return an array of distinct elements. + * + * If called with a negative count, the behavior changes and the command + * is allowed to return the same element multiple times. + */ +class ZRANDMEMBER extends RedisCommand +{ + use WithScores; + + public function getId() + { + return 'ZRANDMEMBER'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZRANGE.php b/plugins/cache-redis/Predis/Command/Redis/ZRANGE.php new file mode 100644 index 0000000000..2024ad4e46 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGE.php @@ -0,0 +1,109 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zrange + */ +class ZRANGE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZRANGE'; + } + + /** + * {@inheritdoc} + */ + 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] = ['WITHSCORES' => true]; + $lastType = 'array'; + } + + if ($lastType === 'array') { + $options = $this->prepareOptions(array_pop($arguments)); + $arguments = array_merge($arguments, $options); + } + } + + parent::setArguments($arguments); + } + + /** + * Returns a list of options and modifiers compatible with Redis. + * + * @param array $options List of options. + * + * @return array + */ + protected function prepareOptions($options) + { + $opts = array_change_key_case($options, CASE_UPPER); + $finalizedOpts = []; + + if (!empty($opts['WITHSCORES'])) { + $finalizedOpts[] = 'WITHSCORES'; + } + + return $finalizedOpts; + } + + /** + * Checks for the presence of the WITHSCORES modifier. + * + * @return bool + */ + protected function withScores() + { + $arguments = $this->getArguments(); + + if (count($arguments) < 4) { + return false; + } + + return strtoupper($arguments[3]) === 'WITHSCORES'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if ($this->withScores()) { + $result = []; + + for ($i = 0; $i < count($data); ++$i) { + if (is_array($data[$i])) { + $result[$data[$i][0]] = $data[$i][1]; // Relay + } else { + $result[$data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php new file mode 100644 index 0000000000..18b4a6d30b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php @@ -0,0 +1,54 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @see http://redis.io/commands/zrangebylex + */ +class ZRANGEBYLEX extends ZRANGE +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZRANGEBYLEX'; + } + + /** + * {@inheritdoc} + */ + protected function prepareOptions($options) + { + $opts = array_change_key_case($options, CASE_UPPER); + $finalizedOpts = []; + + if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) { + $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER); + + $finalizedOpts[] = 'LIMIT'; + $finalizedOpts[] = $limit['OFFSET'] ?? $limit[0]; + $finalizedOpts[] = $limit['COUNT'] ?? $limit[1]; + } + + return $finalizedOpts; + } + + /** + * {@inheritdoc} + */ + protected function withScores() + { + return false; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYSCORE.php b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYSCORE.php new file mode 100644 index 0000000000..66cbe4eabb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYSCORE.php @@ -0,0 +1,67 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @see http://redis.io/commands/zrangebyscore + */ +class ZRANGEBYSCORE extends ZRANGE +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZRANGEBYSCORE'; + } + + /** + * {@inheritdoc} + */ + protected function prepareOptions($options) + { + $opts = array_change_key_case($options, CASE_UPPER); + $finalizedOpts = []; + + if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) { + $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER); + + $finalizedOpts[] = 'LIMIT'; + $finalizedOpts[] = $limit['OFFSET'] ?? $limit[0]; + $finalizedOpts[] = $limit['COUNT'] ?? $limit[1]; + } + + return array_merge($finalizedOpts, parent::prepareOptions($options)); + } + + /** + * {@inheritdoc} + */ + protected function withScores() + { + $arguments = $this->getArguments(); + + for ($i = 3; $i < count($arguments); ++$i) { + switch (strtoupper($arguments[$i])) { + case 'WITHSCORES': + return true; + + case 'LIMIT': + $i += 2; + break; + } + } + + return false; + } +} 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 0000000000..4f820b06c2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGESTORE.php @@ -0,0 +1,57 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\By\ByLexByScore; +use Predis\Command\Traits\Limit\Limit; +use Predis\Command\Traits\Rev; + +/** + * @see https://redis.io/commands/zrangestore/ + * + * This command is like ZRANGE, but stores the result in the destination key. + */ +class ZRANGESTORE extends RedisCommand +{ + use ByLexByScore { + ByLexByScore::setArguments as setByLexByScoreArgument; + } + use Rev { + Rev::setArguments as setReversedArgument; + } + use Limit { + Limit::setArguments as setLimitArguments; + } + + protected static $byLexByScoreArgumentPositionOffset = 4; + protected static $revArgumentPositionOffset = 5; + protected static $limitArgumentPositionOffset = 6; + + public function getId() + { + return 'ZRANGESTORE'; + } + + public function setArguments(array $arguments) + { + $this->setByLexByScoreArgument($arguments); + $arguments = $this->getArguments(); + + $this->setReversedArgument($arguments); + $arguments = $this->getArguments(); + + $this->setLimitArguments($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZRANK.php b/plugins/cache-redis/Predis/Command/Redis/ZRANK.php new file mode 100644 index 0000000000..3c6ac2a6ea --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANK.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zrank + */ +class ZRANK extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZRANK'; + } +} 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 0000000000..f14a0790a5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZREM.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zrem + */ +class ZREM extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREM'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + $arguments = self::normalizeVariadic($arguments); + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYLEX.php b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYLEX.php new file mode 100644 index 0000000000..f4f8a5776f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYLEX.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zremrangebylex + */ +class ZREMRANGEBYLEX extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREMRANGEBYLEX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYRANK.php b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYRANK.php new file mode 100644 index 0000000000..3aeff30c03 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYRANK.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zremrangebyrank + */ +class ZREMRANGEBYRANK extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREMRANGEBYRANK'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYSCORE.php b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYSCORE.php new file mode 100644 index 0000000000..6f88c5d1de --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYSCORE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zremrangebyscore + */ +class ZREMRANGEBYSCORE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREMRANGEBYSCORE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZREVRANGE.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGE.php new file mode 100644 index 0000000000..a225661911 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGE.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @see http://redis.io/commands/zrevrange + */ +class ZREVRANGE extends ZRANGE +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREVRANGE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYLEX.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYLEX.php new file mode 100644 index 0000000000..75cad0bce2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYLEX.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @see http://redis.io/commands/zrevrangebylex + */ +class ZREVRANGEBYLEX extends ZRANGEBYLEX +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREVRANGEBYLEX'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYSCORE.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYSCORE.php new file mode 100644 index 0000000000..9acc450df0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYSCORE.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +/** + * @see http://redis.io/commands/zrevrangebyscore + */ +class ZREVRANGEBYSCORE extends ZRANGEBYSCORE +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREVRANGEBYSCORE'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZREVRANK.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANK.php new file mode 100644 index 0000000000..620d2fe706 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANK.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zrevrank + */ +class ZREVRANK extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZREVRANK'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZSCAN.php b/plugins/cache-redis/Predis/Command/Redis/ZSCAN.php new file mode 100644 index 0000000000..ee0b131052 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZSCAN.php @@ -0,0 +1,86 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zscan + */ +class ZSCAN extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZSCAN'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (count($arguments) === 3 && is_array($arguments[2])) { + $options = $this->prepareOptions(array_pop($arguments)); + $arguments = array_merge($arguments, $options); + } + + parent::setArguments($arguments); + } + + /** + * Returns a list of options and modifiers compatible with Redis. + * + * @param array $options List of options. + * + * @return array + */ + protected function prepareOptions($options) + { + $options = array_change_key_case($options, CASE_UPPER); + $normalized = []; + + if (!empty($options['MATCH'])) { + $normalized[] = 'MATCH'; + $normalized[] = $options['MATCH']; + } + + if (!empty($options['COUNT'])) { + $normalized[] = 'COUNT'; + $normalized[] = $options['COUNT']; + } + + return $normalized; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (is_array($data)) { + $members = $data[1]; + $result = []; + + for ($i = 0; $i < count($members); ++$i) { + $result[$members[$i]] = (float) $members[++$i]; + } + + $data[1] = $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZSCORE.php b/plugins/cache-redis/Predis/Command/Redis/ZSCORE.php new file mode 100644 index 0000000000..978a172921 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZSCORE.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; + +/** + * @see http://redis.io/commands/zscore + */ +class ZSCORE extends RedisCommand +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZSCORE'; + } +} 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 0000000000..e0e7f233fe --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZUNION.php @@ -0,0 +1,35 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Traits\With\WithScores; + +/** + * @see https://redis.io/commands/zunion/ + * + * This command is similar to ZUNIONSTORE, but instead of + * storing the resulting sorted set, it is returned to the client. + */ +class ZUNION extends ZUNIONSTORE +{ + use WithScores; + + protected static $keysArgumentPositionOffset = 0; + protected static $weightsArgumentPositionOffset = 1; + protected static $aggregateArgumentPositionOffset = 2; + + public function getId() + { + return 'ZUNION'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZUNIONSTORE.php b/plugins/cache-redis/Predis/Command/Redis/ZUNIONSTORE.php new file mode 100644 index 0000000000..0d213c75d0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZUNIONSTORE.php @@ -0,0 +1,67 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Redis; + +use Predis\Command\Command as RedisCommand; +use Predis\Command\Traits\Aggregate; +use Predis\Command\Traits\Keys; +use Predis\Command\Traits\Weights; + +/** + * @see http://redis.io/commands/zunionstore + */ +class ZUNIONSTORE extends RedisCommand +{ + use Keys { + Keys::setArguments as setKeys; + } + use Weights { + Weights::setArguments as setWeights; + } + use Aggregate{ + Aggregate::setArguments as setAggregate; + } + + protected static $keysArgumentPositionOffset = 1; + protected static $weightsArgumentPositionOffset = 2; + protected static $aggregateArgumentPositionOffset = 3; + + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZUNIONSTORE'; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + // support old `$options` array for backwards compatibility + if (!isset($arguments[3]) && (isset($arguments[2]['weights']) || isset($arguments[2]['aggregate']))) { + $options = array_pop($arguments); + array_push($arguments, $options['weights'] ?? []); + array_push($arguments, $options['aggregate'] ?? 'sum'); + } + + $this->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 0000000000..10c4541d56 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/RedisFactory.php @@ -0,0 +1,112 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +use Predis\ClientConfiguration; +use Predis\Command\Redis\FUNCTIONS; + +/** + * Command factory for mainline Redis servers. + * + * This factory is intended to handle standard commands implemented by mainline + * Redis servers. By default it maps a command ID to a specific command handler + * class in the Predis\Command\Redis namespace but this can be overridden for + * any command ID simply by defining a new command handler class implementing + * Predis\Command\CommandInterface. + */ +class RedisFactory extends Factory +{ + private const COMMANDS_NAMESPACE = "Predis\Command\Redis"; + + public function __construct() + { + $this->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/plugins/cache-redis/Predis/Command/ScriptCommand.php b/plugins/cache-redis/Predis/Command/ScriptCommand.php new file mode 100644 index 0000000000..330ee94b3f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/ScriptCommand.php @@ -0,0 +1,108 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +/** + * Base class used to implement an higher level abstraction for commands based + * on Lua scripting with EVAL and EVALSHA. + * + * @see http://redis.io/commands/eval + */ +abstract class ScriptCommand extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EVALSHA'; + } + + /** + * Gets the body of a Lua script. + * + * @return string + */ + 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. + * + * The default behaviour for the base class is to return 0 to indicate that + * all the elements of the arguments array should be considered as keys, but + * subclasses can enforce a static number of keys. + * + * @return int + */ + protected function getKeysCount() + { + return 0; + } + + /** + * Returns the elements from the arguments that are identified as keys. + * + * @return array + */ + public function getKeys() + { + return array_slice($this->getArguments(), 2, $this->getKeysCount()); + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments) + { + if (($numkeys = $this->getKeysCount()) && $numkeys < 0) { + $numkeys = count($arguments) + $numkeys; + } + + $arguments = array_merge([$this->getScriptHash(), (int) $numkeys], $arguments); + + parent::setArguments($arguments); + } + + /** + * Returns arguments for EVAL command. + * + * @return array + */ + public function getEvalArguments() + { + $arguments = $this->getArguments(); + $arguments[0] = $this->getScript(); + + 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 0000000000..250ae744e2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DeleteStrategy.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Strategy\ContainerCommands\Functions; + +use Predis\Command\Strategy\SubcommandStrategyInterface; + +class DeleteStrategy implements SubcommandStrategyInterface +{ + /** + * {@inheritDoc} + */ + public function processArguments(array $arguments): array + { + return $arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DumpStrategy.php b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DumpStrategy.php new file mode 100644 index 0000000000..13f49ab062 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DumpStrategy.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Strategy\ContainerCommands\Functions; + +use Predis\Command\Strategy\SubcommandStrategyInterface; + +class DumpStrategy implements SubcommandStrategyInterface +{ + /** + * {@inheritDoc} + */ + public function processArguments(array $arguments): array + { + return $arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/FlushStrategy.php b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/FlushStrategy.php new file mode 100644 index 0000000000..f0244d3246 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/FlushStrategy.php @@ -0,0 +1,32 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Strategy\ContainerCommands\Functions; + +use Predis\Command\Strategy\SubcommandStrategyInterface; + +class FlushStrategy implements SubcommandStrategyInterface +{ + /** + * {@inheritDoc} + */ + public function processArguments(array $arguments): array + { + $processedArguments = [$arguments[0]]; + + if (array_key_exists(1, $arguments) && null !== $arguments[1]) { + $processedArguments[] = strtoupper($arguments[1]); + } + + return $processedArguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/KillStrategy.php b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/KillStrategy.php new file mode 100644 index 0000000000..0c9d6d669a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/KillStrategy.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Strategy\ContainerCommands\Functions; + +use Predis\Command\Strategy\SubcommandStrategyInterface; + +class KillStrategy implements SubcommandStrategyInterface +{ + /** + * {@inheritDoc} + */ + public function processArguments(array $arguments): array + { + return $arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/ListStrategy.php b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/ListStrategy.php new file mode 100644 index 0000000000..cb3a845896 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/ListStrategy.php @@ -0,0 +1,36 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Strategy\ContainerCommands\Functions; + +use Predis\Command\Strategy\SubcommandStrategyInterface; + +class ListStrategy implements SubcommandStrategyInterface +{ + /** + * {@inheritDoc} + */ + public function processArguments(array $arguments): array + { + $processedArguments = [$arguments[0]]; + + if (array_key_exists(1, $arguments) && null !== $arguments[1]) { + array_push($processedArguments, 'LIBRARYNAME', $arguments[1]); + } + + if (array_key_exists(2, $arguments) && true === $arguments[2]) { + $processedArguments[] = 'WITHCODE'; + } + + return $processedArguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/LoadStrategy.php b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/LoadStrategy.php new file mode 100644 index 0000000000..10269add89 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/LoadStrategy.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Strategy\ContainerCommands\Functions; + +use Predis\Command\Strategy\SubcommandStrategyInterface; + +class LoadStrategy implements SubcommandStrategyInterface +{ + /** + * {@inheritdoc} + */ + public function processArguments(array $arguments): array + { + if (count($arguments) <= 2) { + return $arguments; + } + + $processedArguments = [$arguments[0]]; + $replace = array_pop($arguments); + + if (is_bool($replace) && $replace) { + $processedArguments[] = 'REPLACE'; + } elseif (!is_bool($replace)) { + $processedArguments[] = $replace; + } + + $processedArguments[] = $arguments[1]; + + return $processedArguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/RestoreStrategy.php b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/RestoreStrategy.php new file mode 100644 index 0000000000..11850fa7a3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/RestoreStrategy.php @@ -0,0 +1,32 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Strategy\ContainerCommands\Functions; + +use Predis\Command\Strategy\SubcommandStrategyInterface; + +class RestoreStrategy implements SubcommandStrategyInterface +{ + /** + * {@inheritDoc} + */ + public function processArguments(array $arguments): array + { + $processedArguments = [$arguments[0], $arguments[1]]; + + if (array_key_exists(2, $arguments) && null !== $arguments[2]) { + $processedArguments[] = strtoupper($arguments[2]); + } + + return $processedArguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/StatsStrategy.php b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/StatsStrategy.php new file mode 100644 index 0000000000..22915059e9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/StatsStrategy.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Strategy\ContainerCommands\Functions; + +use Predis\Command\Strategy\SubcommandStrategyInterface; + +class StatsStrategy implements SubcommandStrategyInterface +{ + /** + * {@inheritDoc} + */ + public function processArguments(array $arguments): array + { + return $arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Strategy/StrategyResolverInterface.php b/plugins/cache-redis/Predis/Command/Strategy/StrategyResolverInterface.php new file mode 100644 index 0000000000..43c9502879 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/StrategyResolverInterface.php @@ -0,0 +1,25 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Strategy; + +interface StrategyResolverInterface +{ + /** + * Resolves subcommand strategy. + * + * @param string $commandId + * @param string $subcommandId + * @return SubcommandStrategyInterface + */ + public function resolve(string $commandId, string $subcommandId): SubcommandStrategyInterface; +} diff --git a/plugins/cache-redis/Predis/Command/Strategy/SubcommandStrategyInterface.php b/plugins/cache-redis/Predis/Command/Strategy/SubcommandStrategyInterface.php new file mode 100644 index 0000000000..acbcb3d6d5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/SubcommandStrategyInterface.php @@ -0,0 +1,24 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Strategy; + +interface SubcommandStrategyInterface +{ + /** + * Process arguments for given subcommand. + * + * @param array $arguments + * @return array + */ + public function processArguments(array $arguments): array; +} diff --git a/plugins/cache-redis/Predis/Command/Strategy/SubcommandStrategyResolver.php b/plugins/cache-redis/Predis/Command/Strategy/SubcommandStrategyResolver.php new file mode 100644 index 0000000000..cda84268ed --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/SubcommandStrategyResolver.php @@ -0,0 +1,52 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Strategy; + +use InvalidArgumentException; + +class SubcommandStrategyResolver implements StrategyResolverInterface +{ + private const CONTAINER_COMMANDS_NAMESPACE = 'Predis\Command\Strategy\ContainerCommands'; + + /** + * @var ?string + */ + private $separator; + + public function __construct(string $separator = null) + { + $this->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 0000000000..c49c310853 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Aggregate.php @@ -0,0 +1,66 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait Aggregate +{ + /** + * @var string[] + */ + private static $aggregateValuesEnum = [ + 'min' => '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 0000000000..067302d22d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BitByte.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +trait BitByte +{ + private static $argumentEnum = [ + 'bit' => '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 0000000000..99e22be053 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/BucketSize.php @@ -0,0 +1,57 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\BloomFilters; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait BucketSize +{ + private static $bucketSizeModifier = 'BUCKETSIZE'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$bucketSizeArgumentPositionOffset >= $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 0000000000..c0dccc8a43 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Capacity.php @@ -0,0 +1,57 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\BloomFilters; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait Capacity +{ + private static $capacityModifier = 'CAPACITY'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$capacityArgumentPositionOffset >= $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 0000000000..661def80f1 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Error.php @@ -0,0 +1,57 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\BloomFilters; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait Error +{ + private static $errorModifier = 'ERROR'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$errorArgumentPositionOffset >= $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 0000000000..74d916f4c7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Expansion.php @@ -0,0 +1,53 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\BloomFilters; + +use UnexpectedValueException; + +trait Expansion +{ + private static $expansionModifier = 'EXPANSION'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$expansionArgumentPositionOffset >= $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 0000000000..9d2e4dcfe2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Items.php @@ -0,0 +1,45 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\BloomFilters; + +use Predis\Command\Command; + +/** + * @mixin Command + */ +trait Items +{ + private static $itemsModifier = 'ITEMS'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$itemsArgumentPositionOffset >= $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 0000000000..fb307e6d90 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/MaxIterations.php @@ -0,0 +1,57 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\BloomFilters; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait MaxIterations +{ + private static $maxIterationsModifier = 'MAXITERATIONS'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$maxIterationsArgumentPositionOffset >= $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 0000000000..7fc084ec8e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/NoCreate.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\BloomFilters; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait NoCreate +{ + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if ( + static::$noCreateArgumentPositionOffset >= $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 0000000000..99bd1722f4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/By/ByArgument.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\By; + +use Predis\Command\Command; + +/** + * @mixin Command + */ +trait ByArgument +{ + private $byModifier = 'BY'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$byArgumentPositionOffset >= $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 0000000000..66c5315849 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/By/ByLexByScore.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\By; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait ByLexByScore +{ + private static $argumentsEnum = [ + 'bylex' => '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 0000000000..c5ee2b7d15 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/By/GeoBy.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\By; + +use InvalidArgumentException; +use Predis\Command\Argument\Geospatial\ByInterface; + +trait GeoBy +{ + public function setArguments(array $arguments) + { + $argumentPositionOffset = $this->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 0000000000..46ae489d6e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Count.php @@ -0,0 +1,71 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait Count +{ + private $countModifier = 'COUNT'; + private $anyModifier = 'ANY'; + + public function setArguments(array $arguments, bool $any = false) + { + $argumentsLength = count($arguments); + + if (static::$countArgumentPositionOffset >= $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 0000000000..cf494f96a4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/DB.php @@ -0,0 +1,53 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use UnexpectedValueException; + +trait DB +{ + private $dbModifier = 'DB'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$dbArgumentPositionOffset >= $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 0000000000..4f683f3ca9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Expire/ExpireOptions.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\Expire; + +trait ExpireOptions +{ + private static $argumentEnum = [ + 'nx' => '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 0000000000..22688adba6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/From/GeoFrom.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\From; + +use InvalidArgumentException; +use Predis\Command\Argument\Geospatial\FromInterface; + +trait GeoFrom +{ + public function setArguments(array $arguments) + { + $argumentPositionOffset = $this->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 0000000000..256676e9e4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Get/Get.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\Get; + +use UnexpectedValueException; + +trait Get +{ + private static $getModifier = 'GET'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$getArgumentPositionOffset >= $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 0000000000..3e0dfb1332 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/Indent.php @@ -0,0 +1,54 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\Json; + +use UnexpectedValueException; + +trait Indent +{ + private static $indentModifier = 'INDENT'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$indentArgumentPositionOffset >= $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 0000000000..7bab8205bc --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/Newline.php @@ -0,0 +1,54 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\Json; + +use UnexpectedValueException; + +trait Newline +{ + private static $newlineModifier = 'NEWLINE'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$newlineArgumentPositionOffset >= $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 0000000000..39d3cb234f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/NxXxArgument.php @@ -0,0 +1,64 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\Json; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait NxXxArgument +{ + /** + * @var string[] + */ + private static $argumentEnum = [ + 'nx' => '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 0000000000..5c99828f4c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/Space.php @@ -0,0 +1,54 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\Json; + +use UnexpectedValueException; + +trait Space +{ + private static $spaceModifier = 'SPACE'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$spaceArgumentPositionOffset >= $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 0000000000..5dc86ad7d7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Keys.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait Keys +{ + public function setArguments(array $arguments, bool $withNumkeys = true) + { + $argumentsLength = count($arguments); + + if ( + static::$keysArgumentPositionOffset > $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 0000000000..181fbd143a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/LeftRight.php @@ -0,0 +1,60 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait LeftRight +{ + /** + * @var array{string: string} + */ + private static $leftRightEnum = [ + 'left' => '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 0000000000..e244994728 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Limit/Limit.php @@ -0,0 +1,54 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\Limit; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait Limit +{ + private static $limitModifier = 'LIMIT'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + $argumentsBefore = array_slice($arguments, 0, static::$limitArgumentPositionOffset); + + if ( + static::$limitArgumentPositionOffset >= $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 0000000000..3e47de9ab3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Limit/LimitObject.php @@ -0,0 +1,50 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\Limit; + +use Predis\Command\Argument\Server\LimitInterface; + +trait LimitObject +{ + public function setArguments(array $arguments) + { + $argumentPositionOffset = $this->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 0000000000..f48f4c1e83 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/MinMaxModifier.php @@ -0,0 +1,45 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait MinMaxModifier +{ + /** + * @var array{string: string} + */ + private $modifierEnum = [ + 'min' => '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 0000000000..d193d66d3c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Replace.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use Predis\Command\Command; + +/** + * @mixin Command + */ +trait Replace +{ + public function setArguments(array $arguments) + { + $replace = array_pop($arguments); + + if (is_bool($replace) && $replace) { + $arguments[] = 'REPLACE'; + } elseif (!is_bool($replace)) { + $arguments[] = $replace; + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Rev.php b/plugins/cache-redis/Predis/Command/Traits/Rev.php new file mode 100644 index 0000000000..20e7e686c1 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Rev.php @@ -0,0 +1,44 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait Rev +{ + public function setArguments(array $arguments) + { + $argument = $arguments[static::$revArgumentPositionOffset]; + + if (false === $argument) { + parent::setArguments($arguments); + + return; + } + + if (true === $argument) { + $argument = 'REV'; + } else { + throw new UnexpectedValueException('Wrong rev argument type'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$revArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$revArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Sorting.php b/plugins/cache-redis/Predis/Command/Traits/Sorting.php new file mode 100644 index 0000000000..ba4077156e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Sorting.php @@ -0,0 +1,57 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use UnexpectedValueException; + +trait Sorting +{ + private static $sortingEnum = [ + 'asc' => '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 0000000000..6feed1e8c8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Storedist.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait Storedist +{ + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if ( + static::$storeDistArgumentPositionOffset >= $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 0000000000..fd33ea9cf1 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Timeout.php @@ -0,0 +1,53 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use UnexpectedValueException; + +trait Timeout +{ + private static $timeoutModifier = 'TIMEOUT'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$timeoutArgumentPositionOffset >= $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 0000000000..1ab13eca91 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/To/ServerTo.php @@ -0,0 +1,48 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\To; + +use Predis\Command\Argument\Server\To; + +trait ServerTo +{ + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$toArgumentPositionOffset >= $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 0000000000..1f175ed8b3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Weights.php @@ -0,0 +1,61 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait Weights +{ + /** + * @var string + */ + private static $weightsModifier = 'WEIGHTS'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$weightsArgumentPositionOffset >= $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 0000000000..797ae2e529 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithCoord.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\With; + +use Predis\Command\Command; +use UnexpectedValueException; + +/** + * @mixin Command + */ +trait WithCoord +{ + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if ( + static::$withCoordArgumentPositionOffset >= $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 0000000000..479606dcaf --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithDist.php @@ -0,0 +1,45 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\With; + +use UnexpectedValueException; + +trait WithDist +{ + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if ( + static::$withDistArgumentPositionOffset >= $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 0000000000..c00f680b2b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithHash.php @@ -0,0 +1,45 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\With; + +use UnexpectedValueException; + +trait WithHash +{ + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if ( + static::$withHashArgumentPositionOffset >= $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 0000000000..bc81d36c98 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithScores.php @@ -0,0 +1,68 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\With; + +use Predis\Command\Command; + +/** + * Handles last argument passed into command as WITHSCORES. + * + * @mixin Command + */ +trait WithScores +{ + public function setArguments(array $arguments) + { + $withScores = array_pop($arguments); + + if (is_bool($withScores) && $withScores) { + $arguments[] = 'WITHSCORES'; + } elseif (!is_bool($withScores)) { + $arguments[] = $withScores; + } + + parent::setArguments($arguments); + } + + /** + * Checks for the presence of the WITHSCORES modifier. + * + * @return bool + */ + private function isWithScoreModifier(): bool + { + $arguments = parent::getArguments(); + $lastArgument = (!empty($arguments)) ? $arguments[count($arguments) - 1] : null; + + return is_string($lastArgument) && strtoupper($lastArgument) === 'WITHSCORES'; + } + + public function parseResponse($data) + { + if ($this->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 0000000000..4efb065842 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithValues.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Traits\With; + +use Predis\Command\Command; + +/** + * @mixin Command + */ +trait WithValues +{ + public function setArguments(array $arguments) + { + $withValues = array_pop($arguments); + + if (is_bool($withValues) && $withValues) { + $arguments[] = 'WITHVALUES'; + } elseif (!is_bool($withValues)) { + $arguments[] = $withValues; + } + + parent::setArguments($arguments); + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/CommunicationException.php b/plugins/cache-redis/Predis/CommunicationException.php similarity index 82% rename from rainloop/v/0.0.0/app/libraries/Predis/CommunicationException.php rename to plugins/cache-redis/Predis/CommunicationException.php index 13fe357c31..0fc7c07a60 100644 --- a/rainloop/v/0.0.0/app/libraries/Predis/CommunicationException.php +++ b/plugins/cache-redis/Predis/CommunicationException.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri <suppakilla@gmail.com> + * (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 <suppakilla@gmail.com> */ 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 0000000000..262f8d64d9 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Aggregate.php @@ -0,0 +1,114 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Option; + +use InvalidArgumentException; +use Predis\Configuration\OptionInterface; +use Predis\Configuration\OptionsInterface; +use Predis\Connection\AggregateConnectionInterface; +use Predis\Connection\NodeConnectionInterface; + +/** + * Client option for configuring generic aggregate connections. + * + * The only value accepted by this option is a callable that must return a valid + * connection instance of Predis\Connection\AggregateConnectionInterface when + * invoked by the client to create a new aggregate connection instance. + * + * Creation and configuration of the aggregate connection is up to the user. + */ +class Aggregate implements OptionInterface +{ + /** + * {@inheritdoc} + */ + public function filter(OptionsInterface $options, $value) + { + if (!is_callable($value)) { + throw new InvalidArgumentException(sprintf( + '%s expects a callable object acting as an aggregate connection initializer', + static::class + )); + } + + return $this->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 0000000000..b171449227 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/CRC16.php @@ -0,0 +1,74 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Option; + +use InvalidArgumentException; +use Predis\Cluster\Hash; +use Predis\Configuration\OptionInterface; +use Predis\Configuration\OptionsInterface; + +/** + * Configures an hash generator used by the redis-cluster connection backend. + */ +class CRC16 implements OptionInterface +{ + /** + * Returns an hash generator instance from a descriptive name. + * + * @param OptionsInterface $options Client options. + * @param string $description Identifier of a hash generator (`predis`, `phpiredis`) + * + * @return callable + */ + protected function getHashGeneratorByDescription(OptionsInterface $options, $description) + { + if ($description === 'predis') { + return new Hash\CRC16(); + } elseif ($description === 'phpiredis') { + return new Hash\PhpiredisCRC16(); + } else { + throw new InvalidArgumentException( + 'String value for the crc16 option must be either `predis` or `phpiredis`' + ); + } + } + + /** + * {@inheritdoc} + */ + public function filter(OptionsInterface $options, $value) + { + if (is_callable($value)) { + $value = call_user_func($value, $options); + } + + if (is_string($value)) { + return $this->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 0000000000..34b33de47b --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Cluster.php @@ -0,0 +1,99 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Option; + +use InvalidArgumentException; +use Predis\Cluster\RedisStrategy; +use Predis\Configuration\OptionsInterface; +use Predis\Connection\Cluster\PredisCluster; +use Predis\Connection\Cluster\RedisCluster; + +/** + * Configures an aggregate connection used for clustering + * multiple Redis nodes using various implementations with + * different algorithms or strategies. + */ +class Cluster extends Aggregate +{ + /** + * {@inheritdoc} + */ + public function filter(OptionsInterface $options, $value) + { + if (is_string($value)) { + $value = $this->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 0000000000..2fbe00e74a --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Commands.php @@ -0,0 +1,146 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Option; + +use InvalidArgumentException; +use Predis\Command\FactoryInterface; +use Predis\Command\RawFactory; +use Predis\Command\RedisFactory; +use Predis\Configuration\OptionInterface; +use Predis\Configuration\OptionsInterface; + +/** + * Configures a connection factory to be used by the client. + */ +class Commands implements OptionInterface +{ + /** + * {@inheritdoc} + */ + public function filter(OptionsInterface $options, $value) + { + if (is_callable($value)) { + $value = call_user_func($value, $options); + } + + if ($value instanceof FactoryInterface) { + return $value; + } elseif (is_array($value)) { + return $this->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 0000000000..e37de4cadd --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Connections.php @@ -0,0 +1,152 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Option; + +use InvalidArgumentException; +use Predis\Configuration\OptionInterface; +use Predis\Configuration\OptionsInterface; +use Predis\Connection\Factory; +use Predis\Connection\FactoryInterface; +use Predis\Connection\PhpiredisSocketConnection; +use Predis\Connection\PhpiredisStreamConnection; +use Predis\Connection\RelayConnection; + +/** + * Configures a new connection factory instance. + * + * The client uses the connection factory to create the underlying connections + * to single redis nodes in a single-server configuration or in replication and + * cluster configurations. + */ +class Connections implements OptionInterface +{ + /** + * {@inheritdoc} + */ + public function filter(OptionsInterface $options, $value) + { + if (is_callable($value)) { + $value = call_user_func($value, $options); + } + + if ($value instanceof FactoryInterface) { + return $value; + } elseif (is_array($value)) { + return $this->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/plugins/cache-redis/Predis/Configuration/Option/Exceptions.php b/plugins/cache-redis/Predis/Configuration/Option/Exceptions.php new file mode 100644 index 0000000000..6834272f0f --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Exceptions.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\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. + */ +class Exceptions implements OptionInterface +{ + /** + * {@inheritdoc} + */ + public function filter(OptionsInterface $options, $value) + { + return filter_var($value, FILTER_VALIDATE_BOOLEAN); + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + return true; + } +} diff --git a/plugins/cache-redis/Predis/Configuration/Option/Prefix.php b/plugins/cache-redis/Predis/Configuration/Option/Prefix.php new file mode 100644 index 0000000000..772454b36e --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Prefix.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\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. + */ +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((string) $value); + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + // NOOP + } +} 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 0000000000..b0f132d19d --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Replication.php @@ -0,0 +1,126 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Option; + +use InvalidArgumentException; +use Predis\Configuration\OptionsInterface; +use Predis\Connection\AggregateConnectionInterface; +use Predis\Connection\Replication\MasterSlaveReplication; +use Predis\Connection\Replication\SentinelReplication; + +/** + * Configures an aggregate connection used for master/slave replication among + * multiple Redis nodes. + */ +class Replication extends Aggregate +{ + /** + * {@inheritdoc} + */ + public function filter(OptionsInterface $options, $value) + { + if (is_string($value)) { + $value = $this->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/rainloop/v/0.0.0/app/libraries/Predis/Configuration/OptionInterface.php b/plugins/cache-redis/Predis/Configuration/OptionInterface.php similarity index 89% rename from rainloop/v/0.0.0/app/libraries/Predis/Configuration/OptionInterface.php rename to plugins/cache-redis/Predis/Configuration/OptionInterface.php index b31e0c98f2..538fc0ba71 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Defines an handler used by Predis\Configuration\Options to filter, validate * or return default values for a given option. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface OptionInterface { diff --git a/plugins/cache-redis/Predis/Configuration/Options.php b/plugins/cache-redis/Predis/Configuration/Options.php new file mode 100644 index 0000000000..3fff041296 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Options.php @@ -0,0 +1,116 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +/** + * 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} + */ +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; + + /** + * @param array $options Named array of client options + */ + public function __construct(array $options = null) + { + $this->input = $options ?? []; + } + + /** + * {@inheritdoc} + */ + public function getDefault($option) + { + if (isset($this->handlers[$option])) { + $handler = $this->handlers[$option]; + $handler = new $handler(); + + return $handler->getDefault($this); + } + } + + /** + * {@inheritdoc} + */ + public function defined($option) + { + return + array_key_exists($option, $this->options) + || array_key_exists($option, $this->input) + ; + } + + /** + * {@inheritdoc} + */ + public function __isset($option) + { + return ( + array_key_exists($option, $this->options) + || array_key_exists($option, $this->input) + ) && $this->__get($option) !== null; + } + + /** + * {@inheritdoc} + */ + public function __get($option) + { + if (isset($this->options[$option]) || array_key_exists($option, $this->options)) { + return $this->options[$option]; + } + + if (isset($this->input[$option]) || array_key_exists($option, $this->input)) { + $value = $this->input[$option]; + unset($this->input[$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; + } + + if (isset($this->handlers[$option])) { + return $this->options[$option] = $this->getDefault($option); + } + + return; + } +} diff --git a/plugins/cache-redis/Predis/Configuration/OptionsInterface.php b/plugins/cache-redis/Predis/Configuration/OptionsInterface.php new file mode 100644 index 0000000000..597a0579b1 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/OptionsInterface.php @@ -0,0 +1,63 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +use Predis\Command\Processor\ProcessorInterface; + +/** + * @property callable $aggregate Custom aggregate connection initializer + * @property callable $cluster Aggregate connection initializer for clustering + * @property \Predis\Connection\FactoryInterface $connections Connection factory for creating new connections + * @property bool $exceptions Toggles exceptions in client for -ERR responses + * @property ProcessorInterface $prefix Key prefixing strategy using the supplied string as prefix + * @property \Predis\Command\FactoryInterface $commands Command factory for creating Redis commands + * @property callable $replication Aggregate connection initializer for replication + */ +interface OptionsInterface +{ + /** + * Returns the default value for the given option. + * + * @param string $option Name of the option + * + * @return mixed|null + */ + public function getDefault($option); + + /** + * Checks if the given option has been set by the user upon initialization. + * + * @param string $option Name of the option + * + * @return bool + */ + public function defined($option); + + /** + * Checks if the given option has been set and does not evaluate to NULL. + * + * @param string $option Name of the option + * + * @return bool + */ + public function __isset($option); + + /** + * Returns the value of the given option. + * + * @param string $option Name of the option + * + * @return mixed|null + */ + public function __get($option); +} diff --git a/plugins/cache-redis/Predis/Connection/AbstractConnection.php b/plugins/cache-redis/Predis/Connection/AbstractConnection.php new file mode 100644 index 0000000000..3273f38669 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/AbstractConnection.php @@ -0,0 +1,215 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +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. + */ +abstract class AbstractConnection implements NodeConnectionInterface +{ + private $resource; + private $cachedId; + + protected $parameters; + + /** + * @var RawCommand[] + */ + protected $initCommands = []; + + /** + * @param ParametersInterface $parameters Initialization parameters for the connection. + */ + public function __construct(ParametersInterface $parameters) + { + $this->parameters = $this->assertParameters($parameters); + } + + /** + * Disconnects from the server and destroys the underlying resource when + * PHP's garbage collector kicks in. + */ + public function __destruct() + { + $this->disconnect(); + } + + /** + * Checks some of the parameters used to initialize the connection. + * + * @param ParametersInterface $parameters Initialization parameters for the connection. + * + * @return ParametersInterface + * @throws InvalidArgumentException + */ + abstract protected function assertParameters(ParametersInterface $parameters); + + /** + * Creates the underlying resource used to communicate with Redis. + * + * @return mixed + */ + abstract protected function createResource(); + + /** + * {@inheritdoc} + */ + public function isConnected() + { + return isset($this->resource); + } + + /** + * {@inheritdoc} + */ + public function connect() + { + if (!$this->isConnected()) { + $this->resource = $this->createResource(); + + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + unset($this->resource); + } + + /** + * {@inheritdoc} + */ + public function addConnectCommand(CommandInterface $command) + { + $this->initCommands[] = $command; + } + + /** + * {@inheritdoc} + */ + public function getInitCommands(): array + { + return $this->initCommands; + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + $this->writeRequest($command); + + return $this->readResponse($command); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + return $this->read(); + } + + /** + * Helper method to handle connection errors. + * + * @param string $message Error message. + * @param int $code Error code. + */ + protected function onConnectionError($message, $code = 0) + { + CommunicationException::handle( + new ConnectionException($this, "$message [{$this->getParameters()}]", $code) + ); + } + + /** + * Helper method to handle protocol errors. + * + * @param string $message Error message. + */ + protected function onProtocolError($message) + { + CommunicationException::handle( + new ProtocolException($this, "$message [{$this->getParameters()}]") + ); + } + + /** + * {@inheritdoc} + */ + public function getResource() + { + if (isset($this->resource)) { + return $this->resource; + } + + $this->connect(); + + return $this->resource; + } + + /** + * {@inheritdoc} + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * Gets an identifier for the connection. + * + * @return string + */ + protected function getIdentifier() + { + if ($this->parameters->scheme === 'unix') { + return $this->parameters->path; + } + + return "{$this->parameters->host}:{$this->parameters->port}"; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + if (!isset($this->cachedId)) { + $this->cachedId = $this->getIdentifier(); + } + + return $this->cachedId; + } + + /** + * {@inheritdoc} + */ + public function __sleep() + { + return ['parameters', 'initCommands']; + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Connection/AggregateConnectionInterface.php b/plugins/cache-redis/Predis/Connection/AggregateConnectionInterface.php similarity index 89% rename from rainloop/v/0.0.0/app/libraries/Predis/Connection/AggregateConnectionInterface.php rename to plugins/cache-redis/Predis/Connection/AggregateConnectionInterface.php index 7eeaede769..8864bba538 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Defines a virtual connection composed of multiple connection instances to * single Redis nodes. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface AggregateConnectionInterface extends ConnectionInterface { @@ -44,7 +43,7 @@ public function remove(NodeConnectionInterface $connection); * * @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/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Connection/Aggregate/ClusterInterface.php rename to plugins/cache-redis/Predis/Connection/Cluster/ClusterInterface.php index af0f5aab55..79c6f96e9a 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 <suppakilla@gmail.com> */ interface ClusterInterface extends AggregateConnectionInterface { diff --git a/plugins/cache-redis/Predis/Connection/Cluster/PredisCluster.php b/plugins/cache-redis/Predis/Connection/Cluster/PredisCluster.php new file mode 100644 index 0000000000..c30fd09d36 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Cluster/PredisCluster.php @@ -0,0 +1,244 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\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. + */ +class PredisCluster implements ClusterInterface, IteratorAggregate, Countable +{ + /** + * @var NodeConnectionInterface[] + */ + private $pool = []; + + /** + * @var NodeConnectionInterface[] + */ + private $aliases = []; + + /** + * @var StrategyInterface + */ + private $strategy; + + /** + * @var \Predis\Cluster\Distributor\DistributorInterface + */ + private $distributor; + + /** + * @param StrategyInterface $strategy Optional cluster strategy. + */ + public function __construct(StrategyInterface $strategy = null) + { + $this->strategy = $strategy ?: new PredisStrategy(); + $this->distributor = $this->strategy->getDistributor(); + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + foreach ($this->pool as $connection) { + if ($connection->isConnected()) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + foreach ($this->pool as $connection) { + $connection->connect(); + } + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + foreach ($this->pool as $connection) { + $connection->disconnect(); + } + } + + /** + * {@inheritdoc} + */ + public function add(NodeConnectionInterface $connection) + { + $parameters = $connection->getParameters(); + + $this->pool[(string) $connection] = $connection; + + if (isset($parameters->alias)) { + $this->aliases[$parameters->alias] = $connection; + } + + $this->distributor->add($connection, $parameters->weight); + } + + /** + * {@inheritdoc} + */ + public function remove(NodeConnectionInterface $connection) + { + 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; + } + + /** + * {@inheritdoc} + */ + public function getConnectionByCommand(CommandInterface $command) + { + $slot = $this->strategy->getSlot($command); + + if (!isset($slot)) { + throw new NotSupportedException( + "Cannot use '{$command->getId()}' over clusters of connections." + ); + } + + return $this->distributor->getBySlot($slot); + } + + /** + * {@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; + } + + /** + * Retrieves a connection instance by slot. + * + * @param string $slot Slot name. + * + * @return NodeConnectionInterface|null + */ + public function getConnectionBySlot($slot) + { + return $this->distributor->getBySlot($slot); + } + + /** + * Retrieves a connection instance from the cluster using a key. + * + * @param string $key Key string. + * + * @return NodeConnectionInterface + */ + public function getConnectionByKey($key) + { + $hash = $this->strategy->getSlotByKey($key); + + return $this->distributor->getBySlot($hash); + } + + /** + * Returns the underlying command hash strategy used to hash commands by + * using keys found in their arguments. + * + * @return StrategyInterface + */ + public function getClusterStrategy() + { + return $this->strategy; + } + + /** + * @return int + */ + #[ReturnTypeWillChange] + public function count() + { + return count($this->pool); + } + + /** + * @return Traversable<string, NodeConnectionInterface> + */ + #[ReturnTypeWillChange] + public function getIterator() + { + return new ArrayIterator($this->pool); + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + $this->getConnectionByCommand($command)->writeRequest($command); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + return $this->getConnectionByCommand($command)->readResponse($command); + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + return $this->getConnectionByCommand($command)->executeCommand($command); + } +} diff --git a/plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php b/plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php new file mode 100644 index 0000000000..7f3013c175 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php @@ -0,0 +1,673 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\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). + * + * This connection backend offers smart support for redis-cluster by handling + * automatic slots map (re)generation upon -MOVED or -ASK responses returned by + * Redis when redirecting a client to a different node. + * + * The cluster can be pre-initialized using only a subset of the actual nodes in + * the cluster, Predis will do the rest by adjusting the slots map and creating + * the missing underlying connection instances on the fly. + * + * It is possible to pre-associate connections to a slots range with the "slots" + * parameter in the form "$first-$last". This can greatly reduce runtime node + * guessing and redirections. + * + * It is also possible to ask for the full and updated slots map directly to one + * 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. + */ +class RedisCluster implements ClusterInterface, IteratorAggregate, Countable +{ + private $useClusterSlots = true; + private $pool = []; + private $slots = []; + private $slotmap; + private $strategy; + private $connections; + private $retryLimit = 5; + private $retryInterval = 10; + + /** + * @param FactoryInterface $connections Optional connection factory. + * @param StrategyInterface $strategy Optional cluster strategy. + */ + public function __construct( + FactoryInterface $connections, + StrategyInterface $strategy = null + ) { + $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; + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + foreach ($this->pool as $connection) { + if ($connection->isConnected()) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + if ($connection = $this->getRandomConnection()) { + $connection->connect(); + } + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + foreach ($this->pool as $connection) { + $connection->disconnect(); + } + } + + /** + * {@inheritdoc} + */ + public function add(NodeConnectionInterface $connection) + { + $this->pool[(string) $connection] = $connection; + $this->slotmap->reset(); + } + + /** + * {@inheritdoc} + */ + public function remove(NodeConnectionInterface $connection) + { + if (false !== $id = array_search($connection, $this->pool, true)) { + $this->slotmap->reset(); + $this->slots = array_diff($this->slots, [$connection]); + unset($this->pool[$id]); + + return true; + } + + return false; + } + + /** + * Removes a connection instance by using its identifier. + * + * @param string $connectionID Connection identifier. + * + * @return bool True if the connection was in the pool. + */ + public function removeById($connectionID) + { + if (isset($this->pool[$connectionID])) { + $this->slotmap->reset(); + $this->slots = array_diff($this->slots, [$connectionID]); + unset($this->pool[$connectionID]); + + return true; + } + + return false; + } + + /** + * Generates the current slots map by guessing the cluster configuration out + * of the connection parameters of the connections in the pool. + * + * Generation is based on the same algorithm used by Redis to generate the + * cluster, so it is most effective when all of the connections supplied on + * initialization have the "slots" parameter properly set accordingly to the + * current cluster configuration. + */ + public function buildSlotMap() + { + $this->slotmap->reset(); + + foreach ($this->pool as $connectionID => $connection) { + $parameters = $connection->getParameters(); + + if (!isset($parameters->slots)) { + continue; + } + + 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. + */ + public function askSlotMap(NodeConnectionInterface $connection = null) + { + if (!$connection && !$connection = $this->getRandomConnection()) { + return; + } + + $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. + [$start, $end, $master] = $slots; + + if ($master[0] === '') { + $this->slotmap->setSlots($start, $end, (string) $connection); + } else { + $this->slotmap->setSlots($start, $end, "{$master[0]}:{$master[1]}"); + } + } + } + + /** + * Guesses the correct node associated to a given slot using a precalculated + * slots map, falling back to the same logic used by Redis to initialize a + * cluster (best-effort). + * + * @param int $slot Slot index. + * + * @return string Connection ID. + */ + protected function guessNode($slot) + { + if (!$this->pool) { + throw new ClientException('No connections available in the pool'); + } + + if ($this->slotmap->isEmpty()) { + $this->buildSlotMap(); + } + + if ($node = $this->slotmap[$slot]) { + return $node; + } + + $count = count($this->pool); + $index = min((int) ($slot / (int) (16384 / $count)), $count - 1); + $nodes = array_keys($this->pool); + + return $nodes[$index]; + } + + /** + * Creates a new connection instance from the given connection ID. + * + * @param string $connectionID Identifier for the connection. + * + * @return NodeConnectionInterface + */ + protected function createConnection($connectionID) + { + $separator = strrpos($connectionID, ':'); + + return $this->connections->create([ + 'host' => substr($connectionID, 0, $separator), + 'port' => substr($connectionID, $separator + 1), + ]); + } + + /** + * {@inheritdoc} + */ + public function getConnectionByCommand(CommandInterface $command) + { + $slot = $this->strategy->getSlot($command); + + if (!isset($slot)) { + throw new NotSupportedException( + "Cannot use '{$command->getId()}' with redis-cluster." + ); + } + + if (isset($this->slots[$slot])) { + return $this->slots[$slot]; + } else { + return $this->getConnectionBySlot($slot); + } + } + + /** + * Returns the connection currently associated to a given slot. + * + * @param int $slot Slot index. + * + * @return NodeConnectionInterface + * @throws OutOfBoundsException + */ + public function getConnectionBySlot($slot) + { + if (!SlotMap::isValid($slot)) { + throw new OutOfBoundsException("Invalid slot [$slot]."); + } + + if (isset($this->slots[$slot])) { + return $this->slots[$slot]; + } + + $connectionID = $this->guessNode($slot); + + if (!$connection = $this->getConnectionById($connectionID)) { + $connection = $this->createConnection($connectionID); + $this->pool[$connectionID] = $connection; + } + + return $this->slots[$slot] = $connection; + } + + /** + * {@inheritdoc} + */ + public function getConnectionById($connectionID) + { + return $this->pool[$connectionID] ?? null; + } + + /** + * Returns a random connection from the pool. + * + * @return NodeConnectionInterface|null + */ + protected function getRandomConnection() + { + if (!$this->pool) { + return null; + } + + return $this->pool[array_rand($this->pool)]; + } + + /** + * Permanently associates the connection instance to a new slot. + * The connection is added to the connections pool if not yet included. + * + * @param NodeConnectionInterface $connection Connection instance. + * @param int $slot Target slot index. + */ + protected function move(NodeConnectionInterface $connection, $slot) + { + $this->pool[(string) $connection] = $connection; + $this->slots[(int) $slot] = $connection; + $this->slotmap[(int) $slot] = $connection; + } + + /** + * Handles -ERR responses returned by Redis. + * + * @param CommandInterface $command Command that generated the -ERR response. + * @param ErrorResponseInterface $error Redis error response object. + * + * @return mixed + */ + protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $error) + { + $details = explode(' ', $error->getMessage(), 2); + + switch ($details[0]) { + case 'MOVED': + return $this->onMovedResponse($command, $details[1]); + + case 'ASK': + return $this->onAskResponse($command, $details[1]); + + default: + return $error; + } + } + + /** + * Handles -MOVED responses by executing again the command against the node + * indicated by the Redis response. + * + * @param CommandInterface $command Command that generated the -MOVED response. + * @param string $details Parameters of the -MOVED response. + * + * @return mixed + */ + protected function onMovedResponse(CommandInterface $command, $details) + { + [$slot, $connectionID] = explode(' ', $details, 2); + + if (!$connection = $this->getConnectionById($connectionID)) { + $connection = $this->createConnection($connectionID); + } + + if ($this->useClusterSlots) { + $this->askSlotMap($connection); + } + + $this->move($connection, $slot); + + return $this->executeCommand($command); + } + + /** + * Handles -ASK responses by executing again the command against the node + * indicated by the Redis response. + * + * @param CommandInterface $command Command that generated the -ASK response. + * @param string $details Parameters of the -ASK response. + * + * @return mixed + */ + protected function onAskResponse(CommandInterface $command, $details) + { + [$slot, $connectionID] = explode(' ', $details, 2); + + if (!$connection = $this->getConnectionById($connectionID)) { + $connection = $this->createConnection($connectionID); + } + + $connection->executeCommand(RawCommand::create('ASKING')); + + 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; + } + + /** + * {@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) + { + $response = $this->retryCommandOnFailure($command, __FUNCTION__); + + if ($response instanceof ErrorResponseInterface) { + return $this->onErrorResponse($command, $response); + } + + return $response; + } + + /** + * @return int + */ + #[ReturnTypeWillChange] + public function count() + { + return count($this->pool); + } + + /** + * @return Traversable<string, NodeConnectionInterface> + */ + #[ReturnTypeWillChange] + public function getIterator() + { + 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; + } + + /** + * Returns the underlying command hash strategy used to hash commands by + * using keys found in their arguments. + * + * @return StrategyInterface + */ + public function getClusterStrategy() + { + return $this->strategy; + } + + /** + * Returns the underlying connection factory used to create new connection + * instances to Redis nodes indicated by redis-cluster. + * + * @return FactoryInterface + */ + public function getConnectionFactory() + { + return $this->connections; + } + + /** + * Enables automatic fetching of the current slots map from one of the nodes + * 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 askSlotMap() + * method whether or not this option is enabled. + * + * @param bool $value Enable or disable the use of CLUSTER SLOTS. + */ + public function useClusterSlots($value) + { + $this->useClusterSlots = (bool) $value; + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Connection/CompositeConnectionInterface.php b/plugins/cache-redis/Predis/Connection/CompositeConnectionInterface.php similarity index 83% rename from rainloop/v/0.0.0/app/libraries/Predis/Connection/CompositeConnectionInterface.php rename to plugins/cache-redis/Predis/Connection/CompositeConnectionInterface.php index 286e082ccf..22b8c5f775 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Defines a connection to communicate with a single Redis server that leverages * an external protocol processor to handle pluggable protocol handlers. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface CompositeConnectionInterface extends NodeConnectionInterface { @@ -34,7 +33,7 @@ public function writeBuffer($buffer); /** * 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 @@ public function readBuffer($length); /** * Reads a line from the connection. * - * @param string + * @return string */ public function readLine(); } diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Connection/CompositeStreamConnection.php b/plugins/cache-redis/Predis/Connection/CompositeStreamConnection.php similarity index 91% rename from rainloop/v/0.0.0/app/libraries/Predis/Connection/CompositeStreamConnection.php rename to plugins/cache-redis/Predis/Connection/CompositeStreamConnection.php index 7a35340542..ad69cbc12d 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * 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 <suppakilla@gmail.com> */ class CompositeStreamConnection extends StreamConnection implements CompositeConnectionInterface { @@ -59,7 +59,7 @@ public function writeBuffer($buffer) 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 @@ public function read() */ public function __sleep() { - return array_merge(parent::__sleep(), array('protocol')); + return array_merge(parent::__sleep(), ['protocol']); } } diff --git a/plugins/cache-redis/Predis/Connection/ConnectionException.php b/plugins/cache-redis/Predis/Connection/ConnectionException.php new file mode 100644 index 0000000000..77e7a15a06 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/ConnectionException.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +use Predis\CommunicationException; + +/** + * Exception class that identifies connection-related errors. + */ +class ConnectionException extends CommunicationException +{ +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Connection/ConnectionInterface.php b/plugins/cache-redis/Predis/Connection/ConnectionInterface.php similarity index 93% rename from rainloop/v/0.0.0/app/libraries/Predis/Connection/ConnectionInterface.php rename to plugins/cache-redis/Predis/Connection/ConnectionInterface.php index 11ace1b697..fc2014612f 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Defines a connection object used to communicate with one or multiple * Redis servers. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ConnectionInterface { diff --git a/plugins/cache-redis/Predis/Connection/Factory.php b/plugins/cache-redis/Predis/Connection/Factory.php new file mode 100644 index 0000000000..86b18c4a05 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Factory.php @@ -0,0 +1,194 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +use InvalidArgumentException; +use Predis\Client; +use Predis\Command\RawCommand; +use ReflectionClass; +use UnexpectedValueException; + +/** + * Standard connection factory for creating connections to Redis nodes. + */ +class Factory implements FactoryInterface +{ + 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 + * implementing Predis\Connection\NodeConnectionInterface. Optionally, + * callable objects are used for lazy initialization of connection objects. + * + * @param mixed $initializer FQN of a connection class or a callable for lazy initialization. + * + * @return mixed + * @throws InvalidArgumentException + */ + protected function checkInitializer($initializer) + { + if (is_callable($initializer)) { + return $initializer; + } + + $class = new ReflectionClass($initializer); + + if (!$class->isSubclassOf('Predis\Connection\NodeConnectionInterface')) { + throw new InvalidArgumentException( + 'A connection initializer must be a valid connection class or a callable object.' + ); + } + + return $initializer; + } + + /** + * {@inheritdoc} + */ + public function define($scheme, $initializer) + { + $this->schemes[$scheme] = $this->checkInitializer($initializer); + } + + /** + * {@inheritdoc} + */ + public function undefine($scheme) + { + unset($this->schemes[$scheme]); + } + + /** + * {@inheritdoc} + */ + public function create($parameters) + { + if (!$parameters instanceof ParametersInterface) { + $parameters = $this->createParameters($parameters); + } + + $scheme = $parameters->scheme; + + if (!isset($this->schemes[$scheme])) { + throw new InvalidArgumentException("Unknown connection scheme: '$scheme'."); + } + + $initializer = $this->schemes[$scheme]; + + if (is_callable($initializer)) { + $connection = call_user_func($initializer, $parameters, $this); + } else { + $connection = new $initializer($parameters); + $this->prepareConnection($connection); + } + + if (!$connection instanceof NodeConnectionInterface) { + throw new UnexpectedValueException( + 'Objects returned by connection initializers must implement ' . + "'Predis\Connection\NodeConnectionInterface'." + ); + } + + return $connection; + } + + /** + * 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 setDefaultParameters(array $parameters) + { + $this->defaults = $parameters; + } + + /** + * Returns the default set of parameters applied to new connections. + * + * @return array + */ + public function getDefaultParameters() + { + return $this->defaults; + } + + /** + * Creates a connection parameters instance from the supplied argument. + * + * @param mixed $parameters Original connection parameters. + * + * @return ParametersInterface + */ + protected function createParameters($parameters) + { + if (is_string($parameters)) { + $parameters = Parameters::parse($parameters); + } else { + $parameters = $parameters ?: []; + } + + if ($this->defaults) { + $parameters += $this->defaults; + } + + return new Parameters($parameters); + } + + /** + * Prepares a connection instance after its initialization. + * + * @param NodeConnectionInterface $connection Connection instance. + */ + protected function prepareConnection(NodeConnectionInterface $connection) + { + $parameters = $connection->getParameters(); + + if (isset($parameters->password) && strlen($parameters->password)) { + $cmdAuthArgs = isset($parameters->username) && strlen($parameters->username) + ? [$parameters->username, $parameters->password] + : [$parameters->password]; + + $connection->addConnectCommand( + new RawCommand('AUTH', $cmdAuthArgs) + ); + } + + if ($parameters->client_info ?? false && !$connection instanceof RelayConnection) { + $connection->addConnectCommand( + 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/plugins/cache-redis/Predis/Connection/FactoryInterface.php b/plugins/cache-redis/Predis/Connection/FactoryInterface.php new file mode 100644 index 0000000000..24dc782a81 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/FactoryInterface.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +/** + * Interface for classes providing a factory of connections to Redis nodes. + */ +interface FactoryInterface +{ + /** + * Defines or overrides the connection class identified by a scheme prefix. + * + * @param string $scheme Target connection scheme. + * @param mixed $initializer Fully-qualified name of a class or a callable for lazy initialization. + */ + public function define($scheme, $initializer); + + /** + * Undefines the connection identified by a scheme prefix. + * + * @param string $scheme Target connection scheme. + */ + public function undefine($scheme); + + /** + * Creates a new connection object. + * + * @param mixed $parameters Initialization parameters for the connection. + * + * @return NodeConnectionInterface + */ + public function create($parameters); +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Connection/NodeConnectionInterface.php b/plugins/cache-redis/Predis/Connection/NodeConnectionInterface.php similarity index 92% rename from rainloop/v/0.0.0/app/libraries/Predis/Connection/NodeConnectionInterface.php rename to plugins/cache-redis/Predis/Connection/NodeConnectionInterface.php index 665b862c1b..713331776f 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Defines a connection used to communicate with a single Redis node. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface NodeConnectionInterface extends ConnectionInterface { diff --git a/plugins/cache-redis/Predis/Connection/Parameters.php b/plugins/cache-redis/Predis/Connection/Parameters.php new file mode 100644 index 0000000000..170d7e28e7 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Parameters.php @@ -0,0 +1,199 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +use InvalidArgumentException; + +/** + * Container for connection parameters used to initialize connections to Redis. + * + * {@inheritdoc} + */ +class Parameters implements ParametersInterface +{ + protected static $defaults = [ + 'scheme' => 'tcp', + 'host' => '127.0.0.1', + 'port' => 6379, + ]; + + /** + * 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 = []) + { + $this->parameters = $this->filter($parameters + static::$defaults); + } + + /** + * Filters parameters removing entries with NULL or 0-length string values. + * + * @params array $parameters Array of parameters to be filtered + * + * @return array + */ + protected function filter(array $parameters) + { + return array_filter($parameters, function ($value) { + return $value !== null && $value !== ''; + }); + } + + /** + * Creates a new instance by supplying the initial parameters either in the + * form of an URI string or a named array. + * + * @param array|string $parameters Set of connection parameters. + * + * @return Parameters + */ + public static function create($parameters) + { + if (is_string($parameters)) { + $parameters = static::parse($parameters); + } + + return new static($parameters ?: []); + } + + /** + * Parses an URI string returning an array of connection parameters. + * + * When using the "redis" and "rediss" schemes the URI is parsed according + * to the rules defined by the provisional registration documents approved + * by IANA. If the URI has a password in its "user-information" part or a + * database number in the "path" part these values override the values of + * "password" and "database" if they are present in the "query" part. + * + * @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. + * + * @return array + * @throws InvalidArgumentException + */ + public static function parse($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"); + } + + if ( + isset($parsed['host']) + && false !== strpos($parsed['host'], '[') + && false !== strpos($parsed['host'], ']') + ) { + $parsed['host'] = substr($parsed['host'], 1, -1); + } + + if (isset($parsed['query'])) { + parse_str($parsed['query'], $queryarray); + unset($parsed['query']); + + $parsed = array_merge($parsed, $queryarray); + } + + if (stripos($uri, 'redis') === 0) { + if (isset($parsed['user'])) { + if (strlen($parsed['user'])) { + $parsed['username'] = $parsed['user']; + } + unset($parsed['user']); + } + + if (isset($parsed['pass'])) { + if (strlen($parsed['pass'])) { + $parsed['password'] = $parsed['pass']; + } + unset($parsed['pass']); + } + + if (isset($parsed['path']) && preg_match('/^\/(\d+)(\/.*)?/', $parsed['path'], $path)) { + $parsed['database'] = $path[1]; + + if (isset($path[2])) { + $parsed['path'] = $path[2]; + } else { + unset($parsed['path']); + } + } + } + + return $parsed; + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + return $this->parameters; + } + + /** + * {@inheritdoc} + */ + public function __get($parameter) + { + if (isset($this->parameters[$parameter])) { + return $this->parameters[$parameter]; + } + } + + /** + * {@inheritdoc} + */ + public function __isset($parameter) + { + return isset($this->parameters[$parameter]); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + 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"; + } + + /** + * {@inheritdoc} + */ + public function __sleep() + { + 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 0000000000..7893ea1170 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/ParametersInterface.php @@ -0,0 +1,72 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +/** + * Interface defining a container for connection parameters. + * + * The actual list of connection parameters depends on the features supported by + * each connection backend class (please refer to their specific documentation), + * but the most common parameters used through the library are: + * + * @property string $scheme Connection scheme, such as 'tcp' or 'unix'. + * @property string $host IP address or hostname of Redis. + * @property int $port TCP port on which Redis is listening to. + * @property string $path Path of a UNIX domain socket file. + * @property string $alias Alias for the connection. + * @property float $timeout Timeout for the connect() operation. + * @property float $read_write_timeout Timeout for read() and write() operations. + * @property bool $persistent Leaves the connection open after a GC collection. + * @property string $password Password to access Redis (see the AUTH command). + * @property string $database Database index (see the SELECT command). + * @property bool $async_connect Performs the connect() operation asynchronously. + * @property bool $tcp_nodelay Toggles the Nagle's algorithm for coalescing. + * @property bool $client_info Whether to set LIB-NAME and LIB-VER when connecting. + * @property bool $cache (Relay only) Whether to use in-memory caching. + * @property string $serializer (Relay only) Serializer used for data serialization. + * @property string $compression (Relay only) Algorithm used for data compression. + */ +interface ParametersInterface +{ + /** + * Checks if the specified parameters is set. + * + * @param string $parameter Name of the parameter. + * + * @return bool + */ + public function __isset($parameter); + + /** + * Returns the value of the specified parameter. + * + * @param string $parameter Name of the parameter. + * + * @return mixed|null + */ + public function __get($parameter); + + /** + * Returns basic connection parameters as a valid URI string. + * + * @return string + */ + public function __toString(); + + /** + * Returns an array representation of the connection parameters. + * + * @return array + */ + public function toArray(); +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Connection/PhpiredisSocketConnection.php b/plugins/cache-redis/Predis/Connection/PhpiredisSocketConnection.php similarity index 85% rename from rainloop/v/0.0.0/app/libraries/Predis/Connection/PhpiredisSocketConnection.php rename to plugins/cache-redis/Predis/Connection/PhpiredisSocketConnection.php index 6948f035f7..6ff1f77beb 100644 --- a/rainloop/v/0.0.0/app/libraries/Predis/Connection/PhpiredisSocketConnection.php +++ b/plugins/cache-redis/Predis/Connection/PhpiredisSocketConnection.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri <suppakilla@gmail.com> + * (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 @@ * - 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 <suppakilla@gmail.com> + * @see http://github.com/nrk/phpiredis + * @deprecated 2.1.2 */ class PhpiredisSocketConnection extends AbstractConnection { @@ -65,9 +68,9 @@ public function __construct(ParametersInterface $parameters) */ public function __destruct() { - phpiredis_reader_destroy($this->reader); - parent::__destruct(); + + phpiredis_reader_destroy($this->reader); } /** @@ -93,7 +96,15 @@ protected function assertExtensions() */ 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 @@ protected function getReader() /** * 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 @@ protected function createResource() $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 @@ private function setSocketOptions($socket, ParametersInterface $parameters) $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 @@ private function setSocketOptions($socket, ParametersInterface $parameters) * @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 @@ private function connectWithTimeout($socket, $address, ParametersInterface $para 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 @@ public function connect() { 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 @@ public function connect() public function disconnect() { if ($this->isConnected()) { + phpiredis_reader_reset($this->reader); socket_close($this->getResource()); + parent::disconnect(); } } diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Connection/PhpiredisStreamConnection.php b/plugins/cache-redis/Predis/Connection/PhpiredisStreamConnection.php similarity index 75% rename from rainloop/v/0.0.0/app/libraries/Predis/Connection/PhpiredisStreamConnection.php rename to plugins/cache-redis/Predis/Connection/PhpiredisStreamConnection.php index beb235758c..e3dbfd8ad3 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * - 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 <suppakilla@gmail.com> + * @see https://github.com/nrk/phpiredis + * @deprecated 2.1.2 */ class PhpiredisStreamConnection extends StreamConnection { @@ -67,9 +69,19 @@ public function __construct(ParametersInterface $parameters) */ public function __destruct() { + parent::__destruct(); + phpiredis_reader_destroy($this->reader); + } - parent::__destruct(); + /** + * {@inheritdoc} + */ + public function disconnect() + { + phpiredis_reader_reset($this->reader); + + parent::disconnect(); } /** @@ -87,24 +99,34 @@ private function assertExtensions() /** * {@inheritdoc} */ - protected function tcpStreamInitializer(ParametersInterface $parameters) + protected function assertParameters(ParametersInterface $parameters) { - $uri = "tcp://[{$parameters->host}]:{$parameters->port}"; - $flags = STREAM_CLIENT_CONNECT; - $socket = null; - - if (isset($parameters->async_connect) && (bool) $parameters->async_connect) { - $flags |= STREAM_CLIENT_ASYNC_CONNECT; + 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'."); } - if (isset($parameters->persistent) && (bool) $parameters->persistent) { - $flags |= STREAM_CLIENT_PERSISTENT; - $uri .= strpos($path = $parameters->path, '/') === 0 ? $path : "/$path"; - } + return $parameters; + } - $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags); + /** + * {@inheritdoc} + */ + protected function createStreamSocket(ParametersInterface $parameters, $address, $flags) + { + $socket = null; + $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0); + $context = stream_context_create(['socket' => ['tcp_nodelay' => (bool) $parameters->tcp_nodelay]]); - if (!$resource) { + if (!$resource = @stream_socket_client($address, $errno, $errstr, $timeout, $flags, $context)) { $this->onConnectionError(trim($errstr), $errno); } @@ -112,10 +134,10 @@ protected function tcpStreamInitializer(ParametersInterface $parameters) $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 @@ protected function getReader() /** * 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 0000000000..4ff674f5f4 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/RelayConnection.php @@ -0,0 +1,337 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +use InvalidArgumentException; +use Predis\ClientException; +use Predis\Command\CommandInterface; +use Predis\NotSupportedException; +use Predis\Response\ServerException; +use Relay\Exception as RelayException; +use Relay\Relay; + +/** + * This class provides the implementation of a Predis connection that + * uses Relay for network communication and in-memory caching. + * + * Using Relay allows for: + * 1) significantly faster reads thanks to in-memory caching + * 2) fast data serialization using igbinary + * 3) fast data compression using lzf, lz4 or zstd + * + * Usage of igbinary serialization and zstd compresses reduces + * network traffic and Redis memory usage by ~75%. + * + * For instructions on how to install the Relay extension, please consult + * the repository of the project: https://relay.so/docs/installation + * + * The connection parameters supported by this class are: + * + * - scheme: it can be either 'tcp', '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. + * - read_write_timeout: timeout of read / write operations. + * - cache: whether to use in-memory caching + * - serializer: data serializer + * - compression: data compression algorithm + * + * @see https://github.com/cachewerk/relay + */ +class RelayConnection extends StreamConnection +{ + use RelayMethods; + + /** + * The Relay instance. + * + * @var \Relay\Relay + */ + protected $client; + + /** + * These commands must be called on the client, not using `Relay::rawCommand()`. + * + * @var string[] + */ + public $atypicalCommands = [ + 'AUTH', + 'SELECT', + + 'TYPE', + + 'MULTI', + 'EXEC', + 'DISCARD', + + 'WATCH', + 'UNWATCH', + + 'SUBSCRIBE', + 'UNSUBSCRIBE', + 'PSUBSCRIBE', + 'PUNSUBSCRIBE', + 'SSUBSCRIBE', + 'SUNSUBSCRIBE', + ]; + + /** + * {@inheritdoc} + */ + public function __construct(ParametersInterface $parameters) + { + $this->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 0000000000..a52c4a035f --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/RelayMethods.php @@ -0,0 +1,136 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +trait RelayMethods +{ + /** + * Registers a new `flushed` event listener. + * + * @param callable $callback + * @return bool + */ + public function onFlushed(?callable $callback) + { + return $this->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<string, mixed> + */ + public function license() + { + return $this->client->license(); + } + + /** + * Returns statistics about Relay. + * + * @return array<string, array<string, mixed>> + */ + 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 0000000000..9d57a5ac39 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Replication/MasterSlaveReplication.php @@ -0,0 +1,553 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Replication; + +use InvalidArgumentException; +use Predis\ClientException; +use Predis\Command\CommandInterface; +use Predis\Command\RawCommand; +use Predis\Connection\ConnectionException; +use Predis\Connection\FactoryInterface; +use Predis\Connection\NodeConnectionInterface; +use Predis\Replication\MissingMasterException; +use Predis\Replication\ReplicationStrategy; +use Predis\Response\ErrorInterface as ResponseErrorInterface; + +/** + * Aggregate connection handling replication of Redis nodes configured in a + * single master / multiple slaves setup. + */ +class MasterSlaveReplication implements ReplicationInterface +{ + /** + * @var ReplicationStrategy + */ + protected $strategy; + + /** + * @var NodeConnectionInterface + */ + protected $master; + + /** + * @var NodeConnectionInterface[] + */ + protected $slaves = []; + + /** + * @var NodeConnectionInterface[] + */ + protected $pool = []; + + /** + * @var NodeConnectionInterface[] + */ + protected $aliases = []; + + /** + * @var NodeConnectionInterface + */ + protected $current; + + /** + * @var bool + */ + protected $autoDiscovery = false; + + /** + * @var FactoryInterface + */ + protected $connectionFactory; + + /** + * {@inheritdoc} + */ + public function __construct(ReplicationStrategy $strategy = null) + { + $this->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<host>.*),port=(?P<port>\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/plugins/cache-redis/Predis/Connection/Replication/ReplicationInterface.php b/plugins/cache-redis/Predis/Connection/Replication/ReplicationInterface.php new file mode 100644 index 0000000000..14fd2499e0 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Replication/ReplicationInterface.php @@ -0,0 +1,53 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Replication; + +use Predis\Connection\AggregateConnectionInterface; +use Predis\Connection\NodeConnectionInterface; + +/** + * Defines a group of Redis nodes in a master / slave replication setup. + */ +interface ReplicationInterface extends AggregateConnectionInterface +{ + /** + * Switches the internal connection in use to the master server. + */ + public function switchToMaster(); + + /** + * 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 to the master server. + * + * @return NodeConnectionInterface + */ + public function getMaster(); + + /** + * Returns a list of connections to slave servers. + * + * @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 0000000000..14dbe2758f --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Replication/SentinelReplication.php @@ -0,0 +1,775 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Replication; + +use InvalidArgumentException; +use Predis\Command\CommandInterface; +use Predis\Command\RawCommand; +use Predis\CommunicationException; +use Predis\Connection\ConnectionException; +use Predis\Connection\FactoryInterface as ConnectionFactoryInterface; +use Predis\Connection\NodeConnectionInterface; +use Predis\Connection\Parameters; +use Predis\Replication\ReplicationStrategy; +use Predis\Replication\RoleException; +use Predis\Response\Error; +use Predis\Response\ErrorInterface as ErrorResponseInterface; +use Predis\Response\ServerException; + +/** + * @author Daniele Alessandri <suppakilla@gmail.com> + * @author Ville Mattila <ville@eventio.fi> + */ +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/plugins/cache-redis/Predis/Connection/StreamConnection.php b/plugins/cache-redis/Predis/Connection/StreamConnection.php new file mode 100644 index 0000000000..2fe307067a --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/StreamConnection.php @@ -0,0 +1,374 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +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', '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 (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. + * - ssl: context options array (see http://php.net/manual/en/context.ssl.php) + */ +class StreamConnection extends AbstractConnection +{ + /** + * Disconnects from the server and destroys the underlying resource when the + * garbage collector kicks in only if the connection has not been marked as + * persistent. + */ + public function __destruct() + { + if (isset($this->parameters->persistent) && $this->parameters->persistent) { + return; + } + + $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} + */ + protected function createResource() + { + switch ($this->parameters->scheme) { + case 'tcp': + case 'redis': + return $this->tcpStreamInitializer($this->parameters); + + 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}'."); + } + } + + /** + * 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. + * + * @param ParametersInterface $parameters Initialization parameters for the connection. + * + * @return resource + */ + protected function tcpStreamInitializer(ParametersInterface $parameters) + { + if (!filter_var($parameters->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $address = "tcp://$parameters->host:$parameters->port"; + } else { + $address = "tcp://[$parameters->host]:$parameters->port"; + } + + $flags = STREAM_CLIENT_CONNECT; + + if (isset($parameters->async_connect) && $parameters->async_connect) { + $flags |= STREAM_CLIENT_ASYNC_CONNECT; + } + + 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}"; + } + } + } + + return $this->createStreamSocket($parameters, $address, $flags); + } + + /** + * Initializes a UNIX stream resource. + * + * @param ParametersInterface $parameters Initialization parameters for the connection. + * + * @return resource + */ + protected function unixStreamInitializer(ParametersInterface $parameters) + { + if (!isset($parameters->path)) { + throw new InvalidArgumentException('Missing UNIX domain socket path.'); + } + + $flags = STREAM_CLIENT_CONNECT; + + 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.' + ); + } + } + } + + return $this->createStreamSocket($parameters, "unix://{$parameters->path}", $flags); + } + + /** + * 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->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; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + if (parent::connect() && $this->initCommands) { + foreach ($this->initCommands as $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); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + if ($this->isConnected()) { + $resource = $this->getResource(); + if (is_resource($resource)) { + fclose($resource); + } + parent::disconnect(); + } + } + + /** + * Performs a write operation over the stream of the buffer containing a + * command serialized with the Redis wire protocol. + * + * @param string $buffer Representation of a command in the Redis wire protocol. + */ + protected function write($buffer) + { + $socket = $this->getResource(); + + while (($length = strlen($buffer)) > 0) { + $written = is_resource($socket) ? @fwrite($socket, $buffer) : false; + + if ($length === $written) { + return; + } + + if ($written === false || $written === 0) { + $this->onConnectionError('Error while writing bytes to the server.'); + } + + $buffer = substr($buffer, $written); + } + } + + /** + * {@inheritdoc} + */ + public function read() + { + $socket = $this->getResource(); + $chunk = fgets($socket); + + if ($chunk === false || $chunk === '') { + $this->onConnectionError('Error while reading line from the server.'); + } + + $prefix = $chunk[0]; + $payload = substr($chunk, 1, -2); + + switch ($prefix) { + case '+': + return StatusResponse::get($payload); + + case '$': + $size = (int) $payload; + + if ($size === -1) { + return; + } + + $bulkData = ''; + $bytesLeft = ($size += 2); + + do { + $chunk = is_resource($socket) ? fread($socket, min($bytesLeft, 4096)) : false; + + if ($chunk === false || $chunk === '') { + $this->onConnectionError('Error while reading bytes from the server.'); + } + + $bulkData .= $chunk; + $bytesLeft = $size - strlen($bulkData); + } while ($bytesLeft > 0); + + return substr($bulkData, 0, -2); + + case '*': + $count = (int) $payload; + + if ($count === -1) { + return; + } + + $multibulk = []; + + for ($i = 0; $i < $count; ++$i) { + $multibulk[$i] = $this->read(); + } + + return $multibulk; + + case ':': + $integer = (int) $payload; + + return $integer == $payload ? $integer : $payload; + + case '-': + return new ErrorResponse($payload); + + default: + $this->onProtocolError("Unknown response prefix: '$prefix'."); + + return; + } + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + $commandID = $command->getId(); + $arguments = $command->getArguments(); + + $cmdlen = strlen($commandID); + $reqlen = count($arguments) + 1; + + $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n"; + + foreach ($arguments as $argument) { + $arglen = strlen(strval($argument)); + $buffer .= "\${$arglen}\r\n{$argument}\r\n"; + } + + $this->write($buffer); + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Connection/WebdisConnection.php b/plugins/cache-redis/Predis/Connection/WebdisConnection.php similarity index 85% rename from rainloop/v/0.0.0/app/libraries/Predis/Connection/WebdisConnection.php rename to plugins/cache-redis/Predis/Connection/WebdisConnection.php index 9cff9d023b..bd533783b9 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * - 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 <suppakilla@gmail.com> + * @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 @@ private function assertExtensions() 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 @@ private function createReader() /** * 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 @@ public function isConnected() * * @param CommandInterface $command Command instance. * - * @throws NotSupportedException - * * @return string + * @throws NotSupportedException */ protected function getCommandId(CommandInterface $command) { @@ -239,7 +253,6 @@ protected function getCommandId(CommandInterface $command) case 'DISCARD': case 'MONITOR': throw new NotSupportedException("Command '$commandID' is not allowed by Webdis."); - default: return $commandID; } @@ -279,10 +292,10 @@ public function executeCommand(CommandInterface $command) 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 @@ public function __toString() */ public function __sleep() { - return array('parameters'); + return ['parameters']; } /** diff --git a/plugins/cache-redis/Predis/Monitor/Consumer.php b/plugins/cache-redis/Predis/Monitor/Consumer.php new file mode 100644 index 0000000000..9076bf12f5 --- /dev/null +++ b/plugins/cache-redis/Predis/Monitor/Consumer.php @@ -0,0 +1,179 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Monitor; + +use Iterator; +use Predis\ClientInterface; +use Predis\Connection\Cluster\ClusterInterface; +use Predis\NotSupportedException; +use ReturnTypeWillChange; + +/** + * Redis MONITOR consumer. + */ +class Consumer implements Iterator +{ + private $client; + private $valid; + private $position; + + /** + * @param ClientInterface $client Client instance used by the consumer. + */ + public function __construct(ClientInterface $client) + { + $this->assertClient($client); + + $this->client = $client; + + $this->start(); + } + + /** + * Automatically stops the consumer when the garbage collector kicks in. + */ + public function __destruct() + { + $this->stop(); + } + + /** + * Checks if the passed client instance satisfies the required conditions + * needed to initialize a monitor consumer. + * + * @param ClientInterface $client Client instance used by the consumer. + * + * @throws NotSupportedException + */ + private function assertClient(ClientInterface $client) + { + if ($client->getConnection() instanceof ClusterInterface) { + throw new NotSupportedException( + 'Cannot initialize a monitor consumer over cluster connections.' + ); + } + + if (!$client->getCommandFactory()->supports('MONITOR')) { + throw new NotSupportedException("'MONITOR' is not supported by the current command factory."); + } + } + + /** + * Initializes the consumer and sends the MONITOR command to the server. + */ + protected function start() + { + $this->client->executeCommand( + $this->client->createCommand('MONITOR') + ); + $this->valid = true; + } + + /** + * Stops the consumer. Internally this is done by disconnecting from server + * since there is no way to terminate the stream initialized by MONITOR. + */ + public function stop() + { + $this->client->disconnect(); + $this->valid = false; + } + + /** + * @return void + */ + #[ReturnTypeWillChange] + public function rewind() + { + // NOOP + } + + /** + * Returns the last message payload retrieved from the server. + * + * @return object + */ + #[ReturnTypeWillChange] + public function current() + { + return $this->getValue(); + } + + /** + * @return int|null + */ + #[ReturnTypeWillChange] + public function key() + { + return $this->position; + } + + /** + * @return void + */ + #[ReturnTypeWillChange] + public function next() + { + ++$this->position; + } + + /** + * Checks if the the consumer is still in a valid state to continue. + * + * @return bool + */ + #[ReturnTypeWillChange] + public function valid() + { + return $this->valid; + } + + /** + * Waits for a new message from the server generated by MONITOR and returns + * it when available. + * + * @return object + */ + private function getValue() + { + $database = 0; + $client = null; + $event = $this->client->getConnection()->read(); + + $callback = function ($matches) use (&$database, &$client) { + if (2 === $count = count($matches)) { + // Redis <= 2.4 + $database = (int) $matches[1]; + } + + if (4 === $count) { + // Redis >= 2.6 + $database = (int) $matches[2]; + $client = $matches[3]; + } + + return ' '; + }; + + $event = preg_replace_callback('/ \(db (\d+)\) | \[(\d+) (.*?)\] /', $callback, $event, 1); + @[$timestamp, $command, $arguments] = explode(' ', $event, 3); + + return (object) [ + 'timestamp' => (float) $timestamp, + 'database' => $database, + 'client' => $client, + 'command' => substr($command, 1, -1), + 'arguments' => $arguments, + ]; + } +} diff --git a/plugins/cache-redis/Predis/NotSupportedException.php b/plugins/cache-redis/Predis/NotSupportedException.php new file mode 100644 index 0000000000..037696b68e --- /dev/null +++ b/plugins/cache-redis/Predis/NotSupportedException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +/** + * Exception class thrown when trying to use features not supported by certain + * classes or abstractions of Predis. + */ +class NotSupportedException extends PredisException +{ +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Pipeline/Atomic.php b/plugins/cache-redis/Predis/Pipeline/Atomic.php similarity index 80% rename from rainloop/v/0.0.0/app/libraries/Predis/Pipeline/Atomic.php rename to plugins/cache-redis/Predis/Pipeline/Atomic.php index 1c9c92aa29..09e19ead6f 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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\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 <suppakilla@gmail.com> */ 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 @@ protected function getConnection() /** * {@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 @@ protected function executePipeline(ConnectionInterface $connection, \SplQueue $c $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 @@ protected function executePipeline(ConnectionInterface $connection, \SplQueue $c ); } - $responses = array(); + $responses = []; $sizeOfPipe = count($commands); $exceptions = $this->throwServerExceptions(); diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Pipeline/ConnectionErrorProof.php b/plugins/cache-redis/Predis/Pipeline/ConnectionErrorProof.php similarity index 86% rename from rainloop/v/0.0.0/app/libraries/Predis/Pipeline/ConnectionErrorProof.php rename to plugins/cache-redis/Predis/Pipeline/ConnectionErrorProof.php index d3bc732e41..8f995cf079 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 <suppakilla@gmail.com> */ class ConnectionErrorProof extends Pipeline { @@ -38,7 +36,7 @@ protected function getConnection() /** * {@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 @@ protected function executePipeline(ConnectionInterface $connection, \SplQueue $c /** * {@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 @@ protected function executeSingleNode(NodeConnectionInterface $connection, \SplQu /** * {@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 @@ protected function executeCluster(ClusterInterface $connection, \SplQueue $comma 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/rainloop/v/0.0.0/app/libraries/Predis/Pipeline/FireAndForget.php b/plugins/cache-redis/Predis/Pipeline/FireAndForget.php similarity index 80% rename from rainloop/v/0.0.0/app/libraries/Predis/Pipeline/FireAndForget.php rename to plugins/cache-redis/Predis/Pipeline/FireAndForget.php index 95a062b64e..75ee88eb2b 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 <suppakilla@gmail.com> */ 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 @@ protected function executePipeline(ConnectionInterface $connection, \SplQueue $c $connection->disconnect(); - return array(); + return []; } } diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Pipeline/Pipeline.php b/plugins/cache-redis/Predis/Pipeline/Pipeline.php similarity index 89% rename from rainloop/v/0.0.0/app/libraries/Predis/Pipeline/Pipeline.php rename to plugins/cache-redis/Predis/Pipeline/Pipeline.php index cf9c59e4fc..1f67d0b9fe 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 <suppakilla@gmail.com> */ 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 @@ protected function getConnection() $connection = $this->getClient()->getConnection(); if ($connection instanceof ReplicationInterface) { - $connection->switchTo('master'); + $connection->switchToMaster(); } return $connection; @@ -123,17 +125,17 @@ protected function getConnection() * 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 @@ public function flushPipeline($send = true) $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 @@ private function setRunning($bool) * * @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 @@ public function execute($callable = null) } $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 0000000000..c36e108684 --- /dev/null +++ b/plugins/cache-redis/Predis/Pipeline/RelayAtomic.php @@ -0,0 +1,69 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Pipeline; + +use Predis\Connection\ConnectionInterface; +use Predis\Response\Error; +use Predis\Response\ServerException; +use Relay\Exception as RelayException; +use SplQueue; + +class RelayAtomic extends Atomic +{ + /** + * {@inheritdoc} + */ + protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) + { + /** @var \Predis\Connection\RelayConnection $connection */ + $client = $connection->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 0000000000..5f36a0aa4a --- /dev/null +++ b/plugins/cache-redis/Predis/Pipeline/RelayPipeline.php @@ -0,0 +1,75 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Pipeline; + +use Predis\Connection\ConnectionInterface; +use Predis\Connection\RelayConnection; +use Predis\Response\Error; +use Predis\Response\ServerException; +use Relay\Exception as RelayException; +use SplQueue; + +class RelayPipeline extends Pipeline +{ + /** + * Implements the logic to flush the queued commands and read the responses + * from the current connection. + * + * @param RelayConnection $connection Current connection instance. + * @param SplQueue $commands Queued commands. + * @return array + */ + protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) + { + /** @var \Predis\Connection\RelayConnection $connection */ + $client = $connection->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/plugins/cache-redis/Predis/PredisException.php b/plugins/cache-redis/Predis/PredisException.php new file mode 100644 index 0000000000..8e124225d5 --- /dev/null +++ b/plugins/cache-redis/Predis/PredisException.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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; + +use Exception; + +/** + * Base exception class for Predis-related errors. + */ +abstract class PredisException extends Exception +{ +} diff --git a/plugins/cache-redis/Predis/Protocol/ProtocolException.php b/plugins/cache-redis/Predis/Protocol/ProtocolException.php new file mode 100644 index 0000000000..b6ab078a04 --- /dev/null +++ b/plugins/cache-redis/Predis/Protocol/ProtocolException.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Protocol; + +use Predis\CommunicationException; + +/** + * Exception used to identify errors encountered while parsing the Redis wire + * protocol. + */ +class ProtocolException extends CommunicationException +{ +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Protocol/ProtocolProcessorInterface.php b/plugins/cache-redis/Predis/Protocol/ProtocolProcessorInterface.php similarity index 91% rename from rainloop/v/0.0.0/app/libraries/Predis/Protocol/ProtocolProcessorInterface.php rename to plugins/cache-redis/Predis/Protocol/ProtocolProcessorInterface.php index b34ea18143..c36a9bb324 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Defines a pluggable protocol processor capable of serializing commands and * deserializing responses into PHP objects directly from a connection. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ProtocolProcessorInterface { diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Protocol/RequestSerializerInterface.php b/plugins/cache-redis/Predis/Protocol/RequestSerializerInterface.php similarity index 84% rename from rainloop/v/0.0.0/app/libraries/Predis/Protocol/RequestSerializerInterface.php rename to plugins/cache-redis/Predis/Protocol/RequestSerializerInterface.php index eef72a640f..ba2a14f432 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Defines a pluggable serializer for Redis commands. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface RequestSerializerInterface { diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Protocol/ResponseReaderInterface.php b/plugins/cache-redis/Predis/Protocol/ResponseReaderInterface.php similarity index 86% rename from rainloop/v/0.0.0/app/libraries/Predis/Protocol/ResponseReaderInterface.php rename to plugins/cache-redis/Predis/Protocol/ResponseReaderInterface.php index 86a7bdcce8..ce9c09395c 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Defines a pluggable reader capable of parsing responses returned by Redis and * deserializing them to PHP objects. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ResponseReaderInterface { diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Protocol/Text/CompositeProtocolProcessor.php rename to plugins/cache-redis/Predis/Protocol/Text/CompositeProtocolProcessor.php index ea85ed3039..3f7df02e54 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> + * @see http://redis.io/topics/protocol */ class CompositeProtocolProcessor implements ProtocolProcessorInterface { diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/BulkResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/BulkResponse.php index 5b0bf3c2d1..961c011883 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> + * @see http://redis.io/topics/protocol */ class BulkResponse implements ResponseHandlerInterface { @@ -34,7 +33,7 @@ public function handle(CompositeConnectionInterface $connection, $payload) 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 @@ public function handle(CompositeConnectionInterface $connection, $payload) } 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/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/ErrorResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/ErrorResponse.php index 3e18b7b9e9..aa400a47b2 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> + * @see http://redis.io/topics/protocol */ class ErrorResponse implements ResponseHandlerInterface { diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/IntegerResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/IntegerResponse.php index 4639d7792f..cb58a3d422 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> + * @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/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/MultiBulkResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/MultiBulkResponse.php index 820b9b4a6f..d9c51425de 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> + * @see http://redis.io/topics/protocol */ class MultiBulkResponse implements ResponseHandlerInterface { @@ -34,7 +33,7 @@ public function handle(CompositeConnectionInterface $connection, $payload) 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 @@ public function handle(CompositeConnectionInterface $connection, $payload) return; } - $list = array(); + $list = []; if ($length > 0) { - $handlersCache = array(); + $handlersCache = []; $reader = $connection->getProtocol()->getResponseReader(); for ($i = 0; $i < $length; ++$i) { diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/ResponseHandlerInterface.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/ResponseHandlerInterface.php index ca08a9c538..b1c90665a7 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Defines a pluggable handler used to parse a particular type of response. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ interface ResponseHandlerInterface { diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/StatusResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/StatusResponse.php index 7bde5558f2..efc13656a3 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> + * @see http://redis.io/topics/protocol */ class StatusResponse implements ResponseHandlerInterface { diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/StreamableMultiBulkResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/StreamableMultiBulkResponse.php index 7cdb736af3..7738e9dbe7 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> + * @see http://redis.io/topics/protocol */ class StreamableMultiBulkResponse implements ResponseHandlerInterface { @@ -38,7 +37,7 @@ public function handle(CompositeConnectionInterface $connection, $payload) 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/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Protocol/Text/ProtocolProcessor.php rename to plugins/cache-redis/Predis/Protocol/Text/ProtocolProcessor.php index f04c3ed5bb..02e0c5d58b 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Protocol processor for the standard Redis wire protocol. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri <suppakilla@gmail.com> + * @see http://redis.io/topics/protocol */ class ProtocolProcessor implements ProtocolProcessorInterface { protected $mbiterable; protected $serializer; - /** - * - */ public function __construct() { $this->mbiterable = false; @@ -81,7 +77,7 @@ public function read(CompositeConnectionInterface $connection) return new MultiBulkIterator($connection, $count); } - $multibulk = array(); + $multibulk = []; for ($i = 0; $i < $count; ++$i) { $multibulk[$i] = $this->read($connection); @@ -90,14 +86,16 @@ public function read(CompositeConnectionInterface $connection) 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/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Protocol/Text/RequestSerializer.php rename to plugins/cache-redis/Predis/Protocol/Text/RequestSerializer.php index c8cbbfbcd1..853bae03a2 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Request serializer for the standard Redis wire protocol. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri <suppakilla@gmail.com> + * @see http://redis.io/topics/protocol */ class RequestSerializer implements RequestSerializerInterface { @@ -36,8 +35,7 @@ public function serialize(CommandInterface $command) $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/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Protocol/Text/ResponseReader.php rename to plugins/cache-redis/Predis/Protocol/Text/ResponseReader.php index d96218dfa4..f49c96d265 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Response reader for the standard Redis wire protocol. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri <suppakilla@gmail.com> + * @see http://redis.io/topics/protocol */ class ResponseReader implements ResponseReaderInterface { protected $handlers; - /** - * - */ public function __construct() { $this->handlers = $this->getDefaultHandlers(); @@ -42,13 +38,13 @@ public function __construct() */ 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 @@ public function read(CompositeConnectionInterface $connection) $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 @@ public function read(CompositeConnectionInterface $connection) protected function onProtocolError(CompositeConnectionInterface $connection, $message) { CommunicationException::handle( - new ProtocolException($connection, $message) + new ProtocolException($connection, "$message [{$connection->getParameters()}]") ); } } diff --git a/rainloop/v/0.0.0/app/libraries/Predis/PubSub/AbstractConsumer.php b/plugins/cache-redis/Predis/PubSub/AbstractConsumer.php similarity index 75% rename from rainloop/v/0.0.0/app/libraries/Predis/PubSub/AbstractConsumer.php rename to plugins/cache-redis/Predis/PubSub/AbstractConsumer.php index d7423f1e30..c9ea2d658f 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 <suppakilla@gmail.com> */ -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 @@ protected function isFlagSet($value) /** * 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 @@ public function subscribe($channel /*, ... */) /** * 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 @@ public function unsubscribe(/* ... */) /** * 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 @@ public function psubscribe($pattern /* ... */) /** * 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 @@ public function punsubscribe(/* ... */) */ public function ping($payload = null) { - $this->writeRequest('PING', array($payload)); + $this->writeRequest('PING', [$payload]); } /** @@ -149,8 +151,9 @@ abstract protected function disconnect(); abstract protected function writeRequest($method, $arguments); /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function rewind() { // NOOP @@ -162,22 +165,25 @@ public function rewind() * * @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 @@ public function next() * * @return bool */ + #[ReturnTypeWillChange] public function valid() { $isValid = $this->isFlagSet(self::STATUS_VALID); diff --git a/plugins/cache-redis/Predis/PubSub/Consumer.php b/plugins/cache-redis/Predis/PubSub/Consumer.php new file mode 100644 index 0000000000..51ac3cd78d --- /dev/null +++ b/plugins/cache-redis/Predis/PubSub/Consumer.php @@ -0,0 +1,157 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\PubSub; + +use Predis\ClientException; +use Predis\ClientInterface; +use Predis\Command\Command; +use Predis\Connection\Cluster\ClusterInterface; +use Predis\NotSupportedException; + +/** + * PUB/SUB consumer. + */ +class Consumer extends AbstractConsumer +{ + protected $client; + protected $options; + + /** + * @param ClientInterface $client Client instance used by the consumer. + * @param array $options Options for the consumer initialization. + */ + public function __construct(ClientInterface $client, ?array $options = null) + { + $this->checkCapabilities($client); + + $this->options = $options ?: []; + $this->client = $client; + + $this->genericSubscribeInit('subscribe'); + $this->genericSubscribeInit('psubscribe'); + } + + /** + * Returns the underlying client instance used by the pub/sub iterator. + * + * @return ClientInterface + */ + public function getClient() + { + return $this->client; + } + + /** + * Checks if the client instance satisfies the required conditions needed to + * initialize a PUB/SUB consumer. + * + * @param ClientInterface $client Client instance used by the consumer. + * + * @throws NotSupportedException + */ + protected function checkCapabilities(ClientInterface $client) + { + if ($client->getConnection() instanceof ClusterInterface) { + throw new NotSupportedException( + 'Cannot initialize a PUB/SUB consumer over cluster connections.' + ); + } + + $commands = ['publish', 'subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe']; + + if (!$client->getCommandFactory()->supports(...$commands)) { + throw new NotSupportedException( + 'PUB/SUB commands are not supported by the current command factory.' + ); + } + } + + /** + * This method shares the logic to handle both SUBSCRIBE and PSUBSCRIBE. + * + * @param string $subscribeAction Type of subscription. + */ + protected function genericSubscribeInit($subscribeAction) + { + if (isset($this->options[$subscribeAction])) { + $this->$subscribeAction($this->options[$subscribeAction]); + } + } + + /** + * {@inheritdoc} + */ + protected function writeRequest($method, $arguments) + { + $this->client->getConnection()->writeRequest( + $this->client->createCommand($method, + Command::normalizeArguments($arguments) + ) + ); + } + + /** + * {@inheritdoc} + */ + protected function disconnect() + { + $this->client->disconnect(); + } + + /** + * {@inheritdoc} + */ + protected function getValue() + { + $response = $this->client->getConnection()->read(); + + switch ($response[0]) { + case self::SUBSCRIBE: + case self::UNSUBSCRIBE: + case self::PSUBSCRIBE: + case self::PUNSUBSCRIBE: + if ($response[2] === 0) { + $this->invalidate(); + } + // The missing break here is intentional as we must process + // subscriptions and unsubscriptions as standard messages. + // no break + + case self::MESSAGE: + return (object) [ + 'kind' => $response[0], + 'channel' => $response[1], + 'payload' => $response[2], + ]; + + case self::PMESSAGE: + return (object) [ + 'kind' => $response[0], + 'pattern' => $response[1], + 'channel' => $response[2], + 'payload' => $response[3], + ]; + + case self::PONG: + return (object) [ + 'kind' => $response[0], + 'payload' => $response[1], + ]; + + default: + throw new ClientException( + "Unknown message type '{$response[0]}' received in the PUB/SUB context." + ); + } + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/PubSub/DispatcherLoop.php b/plugins/cache-redis/Predis/PubSub/DispatcherLoop.php similarity index 85% rename from rainloop/v/0.0.0/app/libraries/Predis/PubSub/DispatcherLoop.php rename to plugins/cache-redis/Predis/PubSub/DispatcherLoop.php index 0d4a08ef72..b6f79cd3d3 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 <suppakilla@gmail.com> */ 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 @@ public function __construct(Consumer $pubsub) * * @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 @@ public function defaultCallback($callable = null) * 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 @@ public function attachCallback($channel, $callback) */ 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 @@ public function run() 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 @@ public function run() 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 0000000000..2af67b844c --- /dev/null +++ b/plugins/cache-redis/Predis/PubSub/RelayConsumer.php @@ -0,0 +1,114 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\PubSub; + +use Predis\NotSupportedException; + +/** + * Relay PUB/SUB consumer. + */ +class RelayConsumer extends Consumer +{ + /** + * Subscribes to the specified channels. + * + * @param string ...$channel One or more channel names. + * @param callable $callback The message callback. + */ + public function subscribe($channel) // @phpstan-ignore-line + { + $channels = func_get_args(); + $callback = array_pop($channels); + + $this->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 0000000000..d30c259d05 --- /dev/null +++ b/plugins/cache-redis/Predis/Replication/MissingMasterException.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Replication; + +use Predis\ClientException; + +/** + * Exception class that identifies when master is missing in a replication setup. + */ +class MissingMasterException extends ClientException +{ +} diff --git a/plugins/cache-redis/Predis/Replication/ReplicationStrategy.php b/plugins/cache-redis/Predis/Replication/ReplicationStrategy.php new file mode 100644 index 0000000000..f2c1bf56a4 --- /dev/null +++ b/plugins/cache-redis/Predis/Replication/ReplicationStrategy.php @@ -0,0 +1,292 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Replication; + +use Predis\Command\CommandInterface; +use Predis\NotSupportedException; + +/** + * Defines a strategy for master/slave replication. + */ +class ReplicationStrategy +{ + protected $disallowed; + protected $readonly; + protected $readonlySHA1; + protected $loadBalancing = true; + + public function __construct() + { + $this->disallowed = $this->getDisallowedOperations(); + $this->readonly = $this->getReadOnlyOperations(); + $this->readonlySHA1 = []; + } + + /** + * Returns if the specified command will perform a read-only operation + * on Redis or not. + * + * @param CommandInterface $command Command instance. + * + * @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." + ); + } + + if (isset($this->readonly[$id])) { + if (true === $readonly = $this->readonly[$id]) { + return true; + } + + return call_user_func($readonly, $command); + } + + if (($eval = $id === 'EVAL') || $id === 'EVALSHA') { + $argument = $command->getArgument(0); + $sha1 = $eval ? sha1(strval($argument)) : $argument; + + if (isset($this->readonlySHA1[$sha1])) { + if (true === $readonly = $this->readonlySHA1[$sha1]) { + return true; + } + + return call_user_func($readonly, $command); + } + } + + return false; + } + + /** + * Returns if the specified command is not allowed for execution in a master + * / slave replication context. + * + * @param CommandInterface $command Command instance. + * + * @return bool + */ + public function isDisallowedOperation(CommandInterface $command) + { + return isset($this->disallowed[$command->getId()]); + } + + /** + * 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 isBitfieldReadOnly(CommandInterface $command) + { + $arguments = $command->getArguments(); + $argc = count($arguments); + + 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; + } + + /** + * Marks a command as a read-only operation. + * + * When the behavior of a command can be decided only at runtime depending + * on its arguments, a callable object can be provided to dynamically check + * if the specified command performs a read or a write operation. + * + * @param string $commandID Command ID. + * @param mixed $readonly A boolean value or a callable object. + */ + public function setCommandReadOnly($commandID, $readonly = true) + { + $commandID = strtoupper($commandID); + + if ($readonly) { + $this->readonly[$commandID] = $readonly; + } else { + unset($this->readonly[$commandID]); + } + } + + /** + * Marks a Lua script for EVAL and EVALSHA as a read-only operation. When + * the behaviour of a script can be decided only at runtime depending on + * its arguments, a callable object can be provided to dynamically check + * if the passed instance of EVAL or EVALSHA performs write operations or + * not. + * + * @param string $script Body of the Lua script. + * @param mixed $readonly A boolean value or a callable object. + */ + public function setScriptReadOnly($script, $readonly = true) + { + $sha1 = sha1($script); + + if ($readonly) { + $this->readonlySHA1[$sha1] = $readonly; + } else { + unset($this->readonlySHA1[$sha1]); + } + } + + /** + * Returns the default list of disallowed commands. + * + * @return array + */ + protected function getDisallowedOperations() + { + return [ + 'SHUTDOWN' => true, + 'INFO' => true, + 'DBSIZE' => true, + 'LASTSAVE' => true, + 'CONFIG' => true, + 'MONITOR' => true, + 'SLAVEOF' => true, + 'SAVE' => true, + 'BGSAVE' => true, + 'BGREWRITEAOF' => true, + 'SLOWLOG' => true, + ]; + } + + /** + * Returns the default list of commands performing read-only operations. + * + * @return array + */ + protected function getReadOnlyOperations() + { + return [ + 'EXISTS' => true, + 'TYPE' => true, + 'KEYS' => true, + 'SCAN' => true, + 'RANDOMKEY' => true, + 'TTL' => true, + 'GET' => true, + 'MGET' => true, + 'SUBSTR' => true, + 'STRLEN' => true, + 'GETRANGE' => true, + 'GETBIT' => true, + 'LLEN' => true, + 'LRANGE' => true, + 'LINDEX' => true, + 'SCARD' => true, + 'SISMEMBER' => true, + 'SINTER' => true, + 'SUNION' => true, + 'SDIFF' => true, + 'SMEMBERS' => true, + 'SSCAN' => true, + 'SRANDMEMBER' => true, + 'ZRANGE' => true, + 'ZREVRANGE' => true, + 'ZRANGEBYSCORE' => true, + 'ZREVRANGEBYSCORE' => true, + 'ZCARD' => true, + 'ZSCORE' => true, + 'ZCOUNT' => true, + 'ZRANK' => true, + 'ZREVRANK' => true, + 'ZSCAN' => true, + 'ZLEXCOUNT' => true, + 'ZRANGEBYLEX' => true, + 'ZREVRANGEBYLEX' => true, + 'HGET' => true, + 'HMGET' => true, + 'HEXISTS' => true, + 'HLEN' => true, + 'HKEYS' => true, + 'HVALS' => true, + 'HGETALL' => true, + 'HSCAN' => true, + 'HSTRLEN' => true, + 'PING' => true, + 'AUTH' => true, + 'SELECT' => true, + 'ECHO' => true, + 'QUIT' => true, + 'OBJECT' => true, + 'BITCOUNT' => true, + 'BITPOS' => true, + 'TIME' => true, + 'PFCOUNT' => true, + '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 0000000000..968b7e2cde --- /dev/null +++ b/plugins/cache-redis/Predis/Replication/RoleException.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Replication; + +use Predis\CommunicationException; + +/** + * Exception class that identifies a role mismatch when connecting to node + * managed by redis-sentinel. + */ +class RoleException extends CommunicationException +{ +} diff --git a/plugins/cache-redis/Predis/Response/Error.php b/plugins/cache-redis/Predis/Response/Error.php new file mode 100644 index 0000000000..3a1c503436 --- /dev/null +++ b/plugins/cache-redis/Predis/Response/Error.php @@ -0,0 +1,58 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Response; + +/** + * Represents an error returned by Redis (-ERR responses) during the execution + * of a command on the server. + */ +class Error implements ErrorInterface +{ + private $message; + + /** + * @param string $message Error message returned by Redis + */ + public function __construct($message) + { + $this->message = $message; + } + + /** + * {@inheritdoc} + */ + public function getMessage() + { + return $this->message; + } + + /** + * {@inheritdoc} + */ + public function getErrorType() + { + [$errorType] = explode(' ', $this->getMessage(), 2); + + return $errorType; + } + + /** + * Converts the object to its string representation. + * + * @return string + */ + public function __toString() + { + return $this->getMessage(); + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Response/ErrorInterface.php b/plugins/cache-redis/Predis/Response/ErrorInterface.php similarity index 86% rename from rainloop/v/0.0.0/app/libraries/Predis/Response/ErrorInterface.php rename to plugins/cache-redis/Predis/Response/ErrorInterface.php index a4a4a02f79..ac3bb16e80 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * 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 <suppakilla@gmail.com> */ interface ErrorInterface extends ResponseInterface { diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulk.php rename to plugins/cache-redis/Predis/Response/Iterator/MultiBulk.php index b1d29241c7..09f4c08e5a 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Streamable multibulk response. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ class MultiBulk extends MultiBulkIterator { diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulkIterator.php rename to plugins/cache-redis/Predis/Response/Iterator/MultiBulkIterator.php index 5d328869b4..cbc6138594 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * * Always make sure that the whole iteration is consumed (or dropped) to prevent * protocol desynchronization issues. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ -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 @@ public function next() } /** - * {@inheritdoc} + * @return bool */ + #[ReturnTypeWillChange] public function valid() { return $this->position < $this->size; @@ -82,6 +89,7 @@ public function valid() * * @return int */ + #[ReturnTypeWillChange] public function count() { return $this->size; diff --git a/rainloop/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 rainloop/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulkTuple.php rename to plugins/cache-redis/Predis/Response/Iterator/MultiBulkTuple.php index 2b6f593c46..4761f0ec2b 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 <suppakilla@gmail.com> + * be iterator as $key => $value pairs. */ -class MultiBulkTuple extends MultiBulk implements \OuterIterator +class MultiBulkTuple extends MultiBulk implements OuterIterator { private $iterator; @@ -42,25 +46,26 @@ public function __construct(MultiBulk $iterator) * * @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 @@ protected function getValue() $v = $this->iterator->current(); $this->iterator->next(); - return array($k, $v); + return [$k, $v]; } } diff --git a/plugins/cache-redis/Predis/Response/ResponseInterface.php b/plugins/cache-redis/Predis/Response/ResponseInterface.php new file mode 100644 index 0000000000..d2f6828062 --- /dev/null +++ b/plugins/cache-redis/Predis/Response/ResponseInterface.php @@ -0,0 +1,20 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Response; + +/** + * Represents a complex response object from Redis. + */ +interface ResponseInterface +{ +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Response/ServerException.php b/plugins/cache-redis/Predis/Response/ServerException.php similarity index 82% rename from rainloop/v/0.0.0/app/libraries/Predis/Response/ServerException.php rename to plugins/cache-redis/Predis/Response/ServerException.php index 407dc5b764..305fe4091b 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Exception class that identifies server-side Redis errors. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ 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/plugins/cache-redis/Predis/Response/Status.php b/plugins/cache-redis/Predis/Response/Status.php new file mode 100644 index 0000000000..80cab93002 --- /dev/null +++ b/plugins/cache-redis/Predis/Response/Status.php @@ -0,0 +1,78 @@ +<?php + +/* + * This file is part of the Predis package. + * + * (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\Response; + +/** + * Represents a status response returned by Redis. + */ +class Status implements ResponseInterface +{ + private static $OK; + private static $QUEUED; + + private $payload; + + /** + * @param string $payload Payload of the status response as returned by Redis. + */ + public function __construct($payload) + { + $this->payload = $payload; + } + + /** + * Converts the response object to its string representation. + * + * @return string + */ + public function __toString() + { + return $this->payload; + } + + /** + * Returns the payload of status response. + * + * @return string + */ + public function getPayload() + { + return $this->payload; + } + + /** + * Returns an instance of a status response object. + * + * Common status responses such as OK or QUEUED are cached in order to lower + * the global memory usage especially when using pipelines. + * + * @param string $payload Status response payload. + * + * @return self + */ + public static function get($payload) + { + switch ($payload) { + case 'OK': + case 'QUEUED': + if (isset(self::$$payload)) { + return self::$$payload; + } + + return self::$$payload = new self($payload); + + default: + return new self($payload); + } + } +} diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Session/Handler.php b/plugins/cache-redis/Predis/Session/Handler.php similarity index 75% rename from rainloop/v/0.0.0/app/libraries/Predis/Session/Handler.php rename to plugins/cache-redis/Predis/Session/Handler.php index cecb9d5395..68c87378d9 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ * 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 <suppakilla@gmail.com> */ -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; @@ -48,23 +49,15 @@ public function __construct(ClientInterface $client, array $options = array()) */ public function register() { - if (PHP_VERSION_ID >= 50400) { - session_set_save_handler($this, true); - } else { - session_set_save_handler( - array($this, 'open'), - array($this, 'close'), - array($this, 'read'), - array($this, 'write'), - array($this, 'destroy'), - array($this, 'gc') - ); - } + session_set_save_handler($this, true); } /** - * {@inheritdoc} + * @param string $save_path + * @param string $session_id + * @return bool */ + #[ReturnTypeWillChange] public function open($save_path, $session_id) { // NOOP @@ -72,8 +65,9 @@ public function open($save_path, $session_id) } /** - * {@inheritdoc} + * @return bool */ + #[ReturnTypeWillChange] public function close() { // NOOP @@ -81,8 +75,10 @@ public function close() } /** - * {@inheritdoc} + * @param int $maxlifetime + * @return bool */ + #[ReturnTypeWillChange] public function gc($maxlifetime) { // NOOP @@ -90,8 +86,10 @@ public function gc($maxlifetime) } /** - * {@inheritdoc} + * @param string $session_id + * @return string */ + #[ReturnTypeWillChange] public function read($session_id) { if ($data = $this->client->get($session_id)) { @@ -100,9 +98,13 @@ public function read($session_id) 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); @@ -111,8 +113,10 @@ public function write($session_id, $session_data) } /** - * {@inheritdoc} + * @param string $session_id + * @return bool */ + #[ReturnTypeWillChange] public function destroy($session_id) { $this->client->del($session_id); diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Transaction/AbortedMultiExecException.php b/plugins/cache-redis/Predis/Transaction/AbortedMultiExecException.php similarity index 84% rename from rainloop/v/0.0.0/app/libraries/Predis/Transaction/AbortedMultiExecException.php rename to plugins/cache-redis/Predis/Transaction/AbortedMultiExecException.php index b36f38aac9..75fc0bb076 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Exception class that identifies a MULTI / EXEC transaction aborted by Redis. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ 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/rainloop/v/0.0.0/app/libraries/Predis/Transaction/MultiExec.php b/plugins/cache-redis/Predis/Transaction/MultiExec.php similarity index 81% rename from rainloop/v/0.0.0/app/libraries/Predis/Transaction/MultiExec.php rename to plugins/cache-redis/Predis/Transaction/MultiExec.php index 0cf1962da5..e7cf26944a 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 <suppakilla@gmail.com> */ class MultiExec implements ClientContextInterface { @@ -38,21 +44,21 @@ class MultiExec implements ClientContextInterface protected $commands; protected $exceptions = true; protected $attempts = 0; - protected $watchKeys = array(); + protected $watchKeys = []; protected $modeCAS = false; /** * @param ClientInterface $client Client instance used by the transaction. * @param array $options Initialization options. */ - public function __construct(ClientInterface $client, array $options = null) + public function __construct(ClientInterface $client, ?array $options = null) { $this->assertClient($client); $this->client = $client; $this->state = new MultiExecState(); - $this->configure($client, $options ?: array()); + $this->configure($client, $options ?: []); $this->reset(); } @@ -66,15 +72,15 @@ public function __construct(ClientInterface $client, array $options = null) */ 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 @@ protected function configure(ClientInterface $client, array $options) protected function reset() { $this->state->reset(); - $this->commands = new \SplQueue(); + $this->commands = new SplQueue(); } /** @@ -168,15 +174,30 @@ public function __call($method, $arguments) * @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 @@ protected function call($commandID, array $arguments = array()) * * @param CommandInterface $command Command instance. * + * @return $this|mixed * @throws AbortedMultiExecException * @throws CommunicationException - * - * @return $this|mixed */ public function executeCommand(CommandInterface $command) { @@ -207,6 +227,8 @@ public function executeCommand(CommandInterface $command) 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 @@ public function executeCommand(CommandInterface $command) * * @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 @@ public function multi() /** * 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 @@ public function exec() * * @param mixed $callable Callback for execution. * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * @throws ClientException */ private function checkBeforeExecution($callable) @@ -326,7 +346,7 @@ private function checkBeforeExecution($callable) 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 @@ private function checkBeforeExecution($callable) * * @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 @@ public function execute($callable = null) $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 @@ public function execute($callable = null) break; } while ($attempts-- > 0); - $response = array(); + $response = []; $commands = $this->commands; $size = count($execResponse); @@ -404,10 +425,20 @@ public function execute($callable = null) 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 @@ protected function executeTransactionBlock($callable) // NOOP } catch (ServerException $exception) { // NOOP - } catch (\Exception $exception) { + } catch (Exception $exception) { $this->discard(); } diff --git a/rainloop/v/0.0.0/app/libraries/Predis/Transaction/MultiExecState.php b/plugins/cache-redis/Predis/Transaction/MultiExecState.php similarity index 89% rename from rainloop/v/0.0.0/app/libraries/Predis/Transaction/MultiExecState.php rename to plugins/cache-redis/Predis/Transaction/MultiExecState.php index a0a8285295..c9be15c877 100644 --- a/rainloop/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 <suppakilla@gmail.com> + * (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 @@ /** * Utility class used to track the state of a MULTI / EXEC transaction. - * - * @author Daniele Alessandri <suppakilla@gmail.com> */ 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 0000000000..d737547068 --- /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 <<<LUA +math.randomseed(ARGV[1]) +local rnd = tostring(math.random()) +redis.call('lpush', KEYS[1], rnd) +return rnd +LUA; + } +} + +// Inject the script command in the current command factory: +$client = new Predis\Client($parameters, [ + 'commands' => [ + '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/plugins/cache-redis/Redis.php b/plugins/cache-redis/Redis.php new file mode 100644 index 0000000000..ea41a148c5 --- /dev/null +++ b/plugins/cache-redis/Redis.php @@ -0,0 +1,106 @@ +<?php + +/* + * This file is part of MailSo. + * + * (c) 2014 Usenko Timur + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace MailSo\Cache\Drivers; + +/** + * @category MailSo + * @package Cache + * @subpackage Drivers + */ +class Redis implements \MailSo\Cache\DriverInterface +{ + private int $iExpire; + + /** + * @var \Predis\Client|null + */ + private $oRedis; + + private string $sKeyPrefix; + + function __construct(string $sHost = '127.0.0.1', int $iPort = 6379, int $iExpire = 43200) + { + $this->iExpire = 0 < $iExpire ? $iExpire : 43200; + + $this->oRedis = null; + + try + { + $this->oRedis = new \Predis\Client(\strpos($sHost, ':/') ? $sHost : array( + 'host' => $sHost, + 'port' => $iPort + )); + + $this->oRedis->connect(); + + if (!$this->oRedis->isConnected()) { + $this->oRedis = null; + } + } + catch (\Throwable $oExc) + { + $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_]/', '_', $sKeyPrefix).'/'; + } + + public function Set(string $sKey, string $sValue) : bool + { + if (!$this->oRedis) { + return false; + } + + $sValue = $this->oRedis->setex($this->generateCachedKey($sKey), $this->iExpire, $sValue); + + return $sValue === true || $sValue == 'OK'; + } + + 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 : null; + } + + public function Delete(string $sKey) : void + { + if ($this->oRedis) { + $this->oRedis->del($this->generateCachedKey($sKey)); + } + } + + public function GC(int $iTimeToClearInHours = 24) : bool + { + if (0 === $iTimeToClearInHours && $this->oRedis) { + return $this->oRedis->flushdb(); + } + + return false; + } + + private function generateCachedKey(string $sKey) : string + { + return $this->sKeyPrefix.\sha1($sKey); + } +} diff --git a/plugins/cache-redis/index.php b/plugins/cache-redis/index.php new file mode 100644 index 0000000000..8387ebb756 --- /dev/null +++ b/plugins/cache-redis/index.php @@ -0,0 +1,58 @@ +<?php + +class CacheRedisPlugin extends \RainLoop\Plugins\AbstractPlugin +{ +// use \MailSo\Log\Inherit; + + const + NAME = 'Cache Redis', + VERSION = '2.36.2', + RELEASE = '2024-04-08', + REQUIRED = '2.36.0', + CATEGORY = 'Cache', + DESCRIPTION = 'Cache handler using Redis'; + + public function Init() : void + { + spl_autoload_register(function($sClassName){ + $file = __DIR__ . DIRECTORY_SEPARATOR . strtr($sClassName, '\\', DIRECTORY_SEPARATOR) . '.php'; + is_file($file) && include_once $file; + }); + if (\class_exists('Predis\Client')) { + $this->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 0000000000..61d8d222a4 --- /dev/null +++ b/plugins/change-password-cpanel/driver.php @@ -0,0 +1,121 @@ +<?php +/** + * TODO: convert to https://api.docs.cpanel.net/openapi/cpanel/operation/passwd_pop/ + * https://github.com/CpanelInc/xmlapi-php + */ + +use SnappyMail\SensitiveString; + +class ChangePasswordCPanelDriver +{ + const + NAME = 'cPanel', + DESCRIPTION = 'Change passwords in cPanel.'; + + private \RainLoop\Config\Plugin $oConfig; + + private \MailSo\Log\Logger $oLogger; + + function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger) + { + $this->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 0000000000..a3ee6138da --- /dev/null +++ b/plugins/change-password-cpanel/index.php @@ -0,0 +1,17 @@ +<?php + +class ChangePasswordCPanelPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Change Password cPanel', + VERSION = '2.36', + RELEASE = '2024-03-17', + REQUIRED = '2.36.0', + CATEGORY = 'Security', + DESCRIPTION = 'Extension to allow users to change their passwords through cPanel'; + + public function Supported() : string + { + return 'Use Change Password plugin'; + } +} diff --git a/plugins/change-password-cpanel/jsonapi.php b/plugins/change-password-cpanel/jsonapi.php new file mode 100644 index 0000000000..16e91adfd9 --- /dev/null +++ b/plugins/change-password-cpanel/jsonapi.php @@ -0,0 +1,339 @@ +<?php +/** + * cPanel XMLAPI Client Class + * + * This class allows for easy interaction with cPanel's XML-API allow functions within the XML-API to be called + * by calling funcions within this class + * + * LICENSE: + * + * Copyright (c) 2012, cPanel, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other materials provided with the distribution. + * * Neither the name of the cPanel, Inc. nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @category Cpanel + * @package xmlapi + * @copyright 2012 cPanel, Inc. + * @license http://sdk.cpanel.net/license/bsd.html + * @version Release: 1.0.13 + * @link http://twiki.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/XmlApi + * @since Class available since release 0.1 + */ + +namespace cPanel; + +class jsonapi +{ + // should debugging statements be printed? + private bool $debug = false; + + // The host to connect to + private string $host = '127.0.0.1'; + + // the port to connect to + private int $port = 2087; + + // should be the literal strings http or https + private string $protocol = 'https'; + + // literal strings hash or password + private ?string $auth_type = null; + + // the actual password or hash + private ?string $auth = null; + + // username to authenticate as + private ?string $user = null; + + // The HTTP Client to use + private string $http_client = 'curl'; + + public function __construct(string $host, ?string $user = null, ?string $password = null ) + { + if ( ( $user != null ) && ( \strlen( $user ) < 9 ) ) { + $this->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, '<html>') && + \preg_match('/HTTP-EQUIV[\s]?=[\s]?"refresh"/i', $response) && + \preg_match('/<meta [^>]+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 <html> is in the case of authentication error + // cPanel 11.25 fixes this issue, but if <html> is in the response, we'll error out. + + if (\stristr($response, '<html>') == 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-custom-sql/ChangePasswordCustomSqlDriver.php b/plugins/change-password-custom-sql/ChangePasswordCustomSqlDriver.php deleted file mode 100644 index 1a60346e2f..0000000000 --- a/plugins/change-password-custom-sql/ChangePasswordCustomSqlDriver.php +++ /dev/null @@ -1,237 +0,0 @@ -<?php - -class ChangePasswordCustomSqlDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $mHost = '127.0.0.1'; - - /** - * @var string - */ - private $mUser = ''; - - /** - * @var string - */ - private $mPass = ''; - - /** - * @var string - */ - private $mDatabase = ''; - - /** - * @var string - */ - private $mTable = ''; - - /** - * @var string - */ - private $mSql = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $mHost - * - * @return \ChangePasswordCustomSqlDriver - */ - public function SetmHost($mHost) - { - $this->mHost = $mHost; - return $this; - } - - /** - * @param string $mUser - * - * @return \ChangePasswordCustomSqlDriver - */ - public function SetmUser($mUser) - { - $this->mUser = $mUser; - return $this; - } - - /** - * @param string $mPass - * - * @return \ChangePasswordCustomSqlDriver - */ - public function SetmPass($mPass) - { - $this->mPass = $mPass; - return $this; - } - - /** - * @param string $mDatabase - * - * @return \ChangePasswordCustomSqlDriver - */ - public function SetmDatabase($mDatabase) - { - $this->mDatabase = $mDatabase; - return $this; - } - - /** - * @param string $mTable - * - * @return \ChangePasswordCustomSqlDriver - */ - public function SetmTable($mTable) - { - $this->mTable = $mTable; - return $this; - } - - /** - * @param string $mSql - * - * @return \ChangePasswordCustomSqlDriver - */ - public function SetmSql($mSql) - { - $this->mSql = $mSql; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \ChangePasswordCustomSqlDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email(); - } - - /** - * @param \RainLoop\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('Try to change password for '.$oAccount->Email()); - } - - $bResult = false; - - $dsn = 'mysql:host='.$this->mHost.';dbname='.$this->mDatabase.';charset=utf8'; - $options = array( - PDO::ATTR_EMULATE_PREPARES => true, - PDO::ATTR_PERSISTENT => true, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - ); - - try - { - $conn = new PDO($dsn,$this->mUser,$this->mPass,$options); - - //prepare SQL varaibles - $sEmail = $oAccount->Email(); - $sEmailUser = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail); - $sEmailDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail); - - // some variables cannot be prepared - $this->mSql = str_replace(array( - ':table' - ), array( - $this->mTable - ), $this->mSql); - - $placeholders = array( - ':email' => $sEmail, - ':oldpass' => $sPrevPassword, - ':newpass' => $sNewPassword, - ':domain' => $sEmailDomain, - ':username' => $sEmailUser - ); - - // we have to check that all placehoders are used in the query, passing any unused placeholders will generate an error - $used_placeholders = array(); - - foreach($placeholders as $placeholder => $value) { - if(preg_match_all('/'.$placeholder . '(?![a-zA-Z0-9\-])'.'/', $this->mSql) === 1) { - // backwards-compabitibility: remove single and double quotes around placeholders - $this->mSql = str_replace('`'.$placeholder.'`', $placeholder, $this->mSql); - $this->mSql = str_replace("'".$placeholder."'", $placeholder, $this->mSql); - $this->mSql = str_replace('"'.$placeholder.'"', $placeholder, $this->mSql); - $used_placeholders[$placeholder] = $value; - } - } - - $statement = $conn->prepare($this->mSql); - - // everything is ready (hopefully), bind the values - foreach($used_placeholders as $placeholder => $value) { - $statement->bindValue($placeholder, $value); - } - - // and execute - $mSqlReturn = $statement->execute(); - - /* can be used for debugging - ob_start(); - $statement->debugDumpParams(); - $r = ob_get_contents(); - ob_end_clean(); - $this->oLogger->Write($r); - */ - - if ($mSqlReturn == true) - { - $bResult = true; - if ($this->oLogger) - { - $this->oLogger->Write('Success! Password changed.'); - } - } - else - { - $bResult = false; - if ($this->oLogger) - { - $this->oLogger->Write('Something went wrong. Either current password is incorrect, or new password does not match criteria.'); - } - } - } - catch (\Exception $oException) - { - $bResult = false; - if ($this->oLogger) - { - $this->oLogger->WriteException($oException); - } - } - - return $bResult; - } -} diff --git a/plugins/change-password-custom-sql/LICENSE b/plugins/change-password-custom-sql/LICENSE deleted file mode 100644 index b5bc06b1be..0000000000 --- a/plugins/change-password-custom-sql/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Martin Vilimovsky (vilimovsky@mvcs.cz) - -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/change-password-custom-sql/README b/plugins/change-password-custom-sql/README deleted file mode 100644 index b35d488fb2..0000000000 --- a/plugins/change-password-custom-sql/README +++ /dev/null @@ -1 +0,0 @@ -You can use your own SQL (MySQL) statement (with wildcards) to change password \ No newline at end of file diff --git a/plugins/change-password-custom-sql/README.md b/plugins/change-password-custom-sql/README.md deleted file mode 100644 index df03331b81..0000000000 --- a/plugins/change-password-custom-sql/README.md +++ /dev/null @@ -1,10 +0,0 @@ -Rainloop change password custom mysql plugin -============================================ - -This plugin adds change password capability to Rainloop webmail by write your own SQL statement - -##### Installation is simple: - -1. Drop the change-password-custom-sql in the plugins directory (eg. _RainLoopDir_/data/data_xxxxx/_default/plugins/*) -2. In rainloop admin panel go to Plugins, and activate change-password-custom-sql. -3. Enter mysql details on the plugin config screen. diff --git a/plugins/change-password-custom-sql/VERSION b/plugins/change-password-custom-sql/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/change-password-custom-sql/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/change-password-custom-sql/index.php b/plugins/change-password-custom-sql/index.php deleted file mode 100644 index 692f86e7e5..0000000000 --- a/plugins/change-password-custom-sql/index.php +++ /dev/null @@ -1,53 +0,0 @@ -<?php - -class ChangePasswordCustomSqlPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - include_once __DIR__.'/ChangePasswordCustomSqlDriver.php'; - $oProvider = new ChangePasswordCustomSqlDriver(); - $oProvider - ->SetLogger($this->Manager()->Actions()->Logger()) - ->SetmHost($this->Config()->Get('plugin', 'mHost', '')) - ->SetmUser($this->Config()->Get('plugin', 'mUser', '')) - ->SetmPass($this->Config()->Get('plugin', 'mPass', '')) - ->SetmDatabase($this->Config()->Get('plugin', 'mDatabase', '')) - ->SetmTable($this->Config()->Get('plugin', 'mTable', '')) - ->SetmSql($this->Config()->Get('plugin', 'mSql', '')) - ; - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('mHost')->SetLabel('MySQL Host') - ->SetDefaultValue('127.0.0.1'), - \RainLoop\Plugins\Property::NewInstance('mUser')->SetLabel('MySQL User'), - \RainLoop\Plugins\Property::NewInstance('mPass')->SetLabel('MySQL Password') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD), - \RainLoop\Plugins\Property::NewInstance('mDatabase')->SetLabel('MySQL Database'), - \RainLoop\Plugins\Property::NewInstance('mTable')->SetLabel('MySQL Table'), - \RainLoop\Plugins\Property::NewInstance('mSql')->SetLabel('SQL statement') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) - ->SetDescription('SQL statement (allowed wildcards :table, :email, :oldpass, :newpass, :domain, :username). Use SQL functions for encryption.') - ->SetDefaultValue('UPDATE :table SET password = md5(:newpass) WHERE domain = :domain AND username = :username and oldpass = md5(:oldpass)') - ); - } -} diff --git a/plugins/change-password-cyberpanel/ChangePasswordCyberPanel.php b/plugins/change-password-cyberpanel/ChangePasswordCyberPanel.php deleted file mode 100644 index 150ed5edb6..0000000000 --- a/plugins/change-password-cyberpanel/ChangePasswordCyberPanel.php +++ /dev/null @@ -1,138 +0,0 @@ -<?php - -class ChangePasswordCyberPanel implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $mHost = '127.0.0.1'; - - /** - * @var string - */ - private $mUser = ''; - - /** - * @var string - */ - private $mPass = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $mHost - * - * @return \ChangePasswordCyberPanel - */ - public function SetmHost($mHost) - { - $this->mHost = $mHost; - return $this; - } - - /** - * @param string $mUser - * - * @return \ChangePasswordCyberPanel - */ - public function SetmUser($mUser) - { - $this->mUser = $mUser; - return $this; - } - - /** - * @param string $mPass - * - * @return \ChangePasswordCyberPanel - */ - public function SetmPass($mPass) - { - $this->mPass = $mPass; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \ChangePasswordCyberPanel - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email(); - } - - /** - * @param \RainLoop\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('Try to change password for '.$oAccount->Email()); - } - - $bResult = false; - $db = mysqli_connect($this->mHost, $this->mUser, $this->mPass, 'cyberpanel'); - - try - { - $sEmail = mysqli_real_escape_string($db, $oAccount->Email()); - $sEmailUser = mysqli_real_escape_string($db, \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail)); - $sEmailDomain = mysqli_real_escape_string($db, \MailSo\Base\Utils::GetDomainFromEmail($sEmail)); - - $password_check_query = "SELECT * FROM e_users WHERE emailOwner_id = '$sEmailDomain' AND email = '$sEmail'"; - $result = mysqli_query($db, $password_check_query); - $password_check = mysqli_fetch_assoc($result); - - if (password_verify($sPrevPassword, substr($password_check['password'], 7))) { - $hashed_password = mysqli_real_escape_string($db, '{CRYPT}'.password_hash($sNewPassword, PASSWORD_BCRYPT, ['cost' => 12,])); - $password_update_query = "UPDATE e_users SET password = '$hashed_password' WHERE emailOwner_id = '$sEmailDomain' AND email = '$sEmail'"; - mysqli_query($db, $password_update_query); - $bResult = true; - if ($this->oLogger) - { - $this->oLogger->Write('Success! The password was changed.'); - } - } else { - $bResult = false; - if ($this->oLogger) - { - $this->oLogger->Write('Something went wrong. Either the current password is incorrect or the new password does not meet the criteria.'); - } - } - } - catch (\Exception $oException) - { - $bResult = false; - if ($this->oLogger) - { - $this->oLogger->WriteException($oException); - } - } - - return $bResult; - } -} diff --git a/plugins/change-password-cyberpanel/LICENSE b/plugins/change-password-cyberpanel/LICENSE deleted file mode 100644 index 67b1540b27..0000000000 --- a/plugins/change-password-cyberpanel/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2019 David Forbush - -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/change-password-cyberpanel/README b/plugins/change-password-cyberpanel/README deleted file mode 100644 index 0d410bb230..0000000000 --- a/plugins/change-password-cyberpanel/README +++ /dev/null @@ -1 +0,0 @@ -This plugin allows you to change passwords of email accounts managed by CyberPanel web panel software diff --git a/plugins/change-password-cyberpanel/README.md b/plugins/change-password-cyberpanel/README.md deleted file mode 100644 index 6bf3ae2d0a..0000000000 --- a/plugins/change-password-cyberpanel/README.md +++ /dev/null @@ -1,10 +0,0 @@ -RainLoop CyberPanel Password Changing Plugin -============================================ - -This plugin adds password changing capability to RainLoop webmail for servers running CyberPanel web panel software. - -##### Installation is simple: - -1. Place the change-password-cyberpanel folder in the plugins directory (e.g. _RainLoopDir_/data/data_xxxxx/_default/plugins/*). -2. In RainLoop administration panel, go to Plugins and activate change-password-cyberpanel. -3. Enter CyberPanel's SQL user details on the plugin configuration screen. diff --git a/plugins/change-password-cyberpanel/VERSION b/plugins/change-password-cyberpanel/VERSION deleted file mode 100644 index 9459d4ba2a..0000000000 --- a/plugins/change-password-cyberpanel/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 diff --git a/plugins/change-password-cyberpanel/index.php b/plugins/change-password-cyberpanel/index.php deleted file mode 100644 index 7547c18a28..0000000000 --- a/plugins/change-password-cyberpanel/index.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php - -class ChangePasswordCyberPanelPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - include_once __DIR__.'/ChangePasswordCyberPanel.php'; - $oProvider = new ChangePasswordCyberPanel(); - $oProvider - ->SetLogger($this->Manager()->Actions()->Logger()) - ->SetmHost($this->Config()->Get('plugin', 'mHost', '')) - ->SetmUser($this->Config()->Get('plugin', 'mUser', '')) - ->SetmPass($this->Config()->Get('plugin', 'mPass', '')) - ; - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('mHost')->SetLabel('MySQL Host') - ->SetDefaultValue('127.0.0.1'), - \RainLoop\Plugins\Property::NewInstance('mUser')->SetLabel('MySQL User'), - \RainLoop\Plugins\Property::NewInstance('mPass')->SetLabel('MySQL Password') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) - ); - } -} diff --git a/plugins/change-password-example/ChangePasswordExampleDriver.php b/plugins/change-password-example/ChangePasswordExampleDriver.php deleted file mode 100644 index 1ffd3f3a9a..0000000000 --- a/plugins/change-password-example/ChangePasswordExampleDriver.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php - -class ChangePasswordExampleDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @param string $sAllowedEmails - * - * @return \ChangePasswordExampleDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \RainLoop\Model\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - $bResult = false; - - // TODO - - return $bResult; - } -} \ No newline at end of file diff --git a/plugins/change-password-example/LICENSE b/plugins/change-password-example/LICENSE deleted file mode 100644 index 271342337d..0000000000 --- a/plugins/change-password-example/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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 -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/change-password-example/VERSION b/plugins/change-password-example/VERSION deleted file mode 100644 index b123147e2a..0000000000 --- a/plugins/change-password-example/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 \ No newline at end of file diff --git a/plugins/change-password-example/index.php b/plugins/change-password-example/index.php deleted file mode 100644 index 7e6a6b13a3..0000000000 --- a/plugins/change-password-example/index.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php - -class ChangePasswordExamplePlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - include_once __DIR__.'/ChangePasswordExampleDriver.php'; - - $oProvider = new ChangePasswordExampleDriver(); - $oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))); - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} \ No newline at end of file diff --git a/plugins/change-password-froxlor/driver.php b/plugins/change-password-froxlor/driver.php new file mode 100644 index 0000000000..f7194edd86 --- /dev/null +++ b/plugins/change-password-froxlor/driver.php @@ -0,0 +1,109 @@ +<?php + +use SnappyMail\SensitiveString; + +class ChangePasswordFroxlorDriver +{ + const + NAME = 'Froxlor', + DESCRIPTION = 'Change passwords in Froxlor.'; + + /** + * @var \RainLoop\Config\Plugin + */ + private $oConfig = null; + + /** + * @var \MailSo\Log\Logger + */ + private $oLogger = null; + + function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger) + { + $this->oConfig = $oConfig; + $this->oLogger = $oLogger; + } + + public static function isSupported() : bool + { + return \class_exists('PDO', false) + // The PHP extension PDO (mysql) must be installed to use this plugin + && \in_array('mysql', \PDO::getAvailableDrivers()); + } + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('froxlor_dsn')->SetLabel('Froxlor PDO dsn') + ->SetDefaultValue('mysql:host=localhost;dbname=froxlor;charset=utf8'), + \RainLoop\Plugins\Property::NewInstance('froxlor_user')->SetLabel('User'), + \RainLoop\Plugins\Property::NewInstance('froxlor_password')->SetLabel('Password') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD), + \RainLoop\Plugins\Property::NewInstance('froxlor_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', 'froxlor_allowed_emails', ''))) { + return false; + } + + try + { + if ($this->oLogger) { + $this->oLogger->Write('froxlor: Try to change password for '.$oAccount->Email()); + } + + $oPdo = new \PDO( + $this->oConfig->Get('plugin', 'froxlor_dsn', ''), + $this->oConfig->Get('plugin', 'froxlor_user', ''), + $this->oConfig->Get('plugin', 'froxlor_password', ''), + array( + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION + ) + ); + + $oStmt = $oPdo->prepare('SELECT password_enc, id FROM mail_users WHERE username = ? LIMIT 1'); + + if ($oStmt->execute(array($oAccount->IncLogin()))) { + $aFetchResult = $oStmt->fetch(\PDO::FETCH_ASSOC); + if (!empty($aFetchResult['id'])) { + $sDbPassword = $aFetchResult['password_enc']; + $sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$')); + if (\crypt($oPrevPassword, $sDbSalt) === $sDbPassword) { + + $oStmt = $oPdo->prepare('UPDATE mail_users SET password_enc = ? WHERE id = ?'); + + return !!$oStmt->execute(array( + $this->cryptPassword($oNewPassword), + $aFetchResult['id'] + )); + } + } + } + } + catch (\Exception $oException) + { + if ($this->oLogger) { + $this->oLogger->WriteException($oException); + } + } + return false; + } + + private function cryptPassword(SensitiveString $oPassword) : string + { + if (\defined('CRYPT_SHA512') && CRYPT_SHA512) { + $sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$'; + } elseif (\defined('CRYPT_SHA256') && CRYPT_SHA256) { + $sSalt = '$5$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$'; + } else { + $sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$'; + } + return \crypt($oPassword, $sSalt); + } +} diff --git a/plugins/change-password-froxlor/index.php b/plugins/change-password-froxlor/index.php new file mode 100644 index 0000000000..339d7e937d --- /dev/null +++ b/plugins/change-password-froxlor/index.php @@ -0,0 +1,18 @@ +<?php + +class ChangePasswordFroxlorPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Change Password Froxlor', + AUTHOR = 'Euphonique', + VERSION = '2.36', + RELEASE = '2024-03-17', + REQUIRED = '2.36.0', + CATEGORY = 'Security', + DESCRIPTION = 'Extension to allow users to change their passwords through Froxlor'; + + public function Supported() : string + { + return 'Use Change Password plugin'; + } +} diff --git a/plugins/change-password-hestia/driver.php b/plugins/change-password-hestia/driver.php new file mode 100644 index 0000000000..847c792b75 --- /dev/null +++ b/plugins/change-password-hestia/driver.php @@ -0,0 +1,72 @@ +<?php + +use SnappyMail\SensitiveString; + +class ChangePasswordHestiaDriver +{ + const + NAME = 'Hestia', + DESCRIPTION = 'Change passwords in Hestia.'; + + /** + * @var \RainLoop\Config\Plugin + */ + private $oConfig = null; + + /** + * @var \MailSo\Log\Logger + */ + protected $oLogger = null; + + function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger) + { + $this->oConfig = $oConfig; + $this->oLogger = $oLogger; + } + + public static function isSupported() : bool + { + return true; + } + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('hestia_host')->SetLabel('Hestia Host') + ->SetDefaultValue('') + ->SetDescription('Ex: localhost or domain.com'), + \RainLoop\Plugins\Property::NewInstance('hestia_port')->SetLabel('Hestia Port') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) + ->SetDefaultValue(8083) + ); + } + + public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool + { + if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'hestia_allowed_emails', ''))) { + return false; + } + + $this->oLogger->Write("Hestia: Try to change password for {$oAccount->Email()}"); + + $sHost = $this->oConfig->Get('plugin', 'hestia_host'); + $sPort = $this->oConfig->Get('plugin', 'hestia_port'); + + $HTTP = \SnappyMail\HTTP\Request::factory(); + $postvars = array( + 'email' => $oAccount->Email(), + 'password' => (string) $oPrevPassword, + 'new' => (string) $oNewPassword, + ); + $response = $HTTP->doRequest('POST', 'https://'.$sHost.':'.$sPort.'/reset/mail/', \http_build_query($postvars)); + if (!$response) { + $this->oLogger->Write("Hestia[Error]: Response failed"); + return false; + } + if ('==ok==' != $response->body) { + $this->oLogger->Write("Hestia[Error]: Response: {$response->status} {$response->body}"); + return false; + } + return true; + } +} diff --git a/plugins/change-password-hestia/index.php b/plugins/change-password-hestia/index.php new file mode 100644 index 0000000000..2decd15487 --- /dev/null +++ b/plugins/change-password-hestia/index.php @@ -0,0 +1,18 @@ +<?php + +class ChangePasswordHestiaPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Change Password Hestia', + AUTHOR = 'Jaap Marcus', + VERSION = '2.36', + RELEASE = '2024-03-17', + REQUIRED = '2.36.0', + CATEGORY = 'Security', + DESCRIPTION = 'Extension to allow users to change their passwords through HestiaCP'; + + public function Supported() : string + { + return 'Use Change Password plugin'; + } +} diff --git a/plugins/change-password-hmailserver/driver.php b/plugins/change-password-hmailserver/driver.php new file mode 100644 index 0000000000..d16ccef363 --- /dev/null +++ b/plugins/change-password-hmailserver/driver.php @@ -0,0 +1,86 @@ +<?php + +use MailSo\Net\ConnectSettings; +use SnappyMail\SensitiveString; + +class ChangePasswordHMailServerDriver +{ + const + NAME = 'hMailServer', + DESCRIPTION = 'Change passwords using hMailServer. The PHP extension COM must be installed to use this plugin'; + + private + $oConfig = null; + + function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger) + { + $this->oConfig = $oConfig; + $this->oLogger = $oLogger; + } + + public static function isSupported() : bool + { + return \class_exists('COM'); + } + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('hmailserver_login')->SetLabel('Admin Login') + ->SetDefaultValue('Administrator'), + \RainLoop\Plugins\Property::NewInstance('hmailserver_password')->SetLabel('Admin Password') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) + ->SetDefaultValue(''), + \RainLoop\Plugins\Property::NewInstance('hmailserver_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', 'hmailserver_emails', ''))) { + return false; + } + + $this->oLogger && $this->oLogger->Write('hMailServer: Try to change password for '.$oAccount->Email()); + + $bResult = false; + + try + { + $oHmailApp = new \COM('hMailServer.Application'); + $oHmailApp->Connect(); + + if ($oHmailApp->Authenticate( + $this->oConfig->Get('plugin', 'hmailserver_login', ''), + $this->oConfig->Get('plugin', 'hmailserver_password', '') + )) { + $sEmail = $oAccount->Email(); + $sDomain = \MailSo\Base\Utils::getEmailAddressDomain($sEmail); + $oHmailDomain = $oHmailApp->Domains->ItemByName($sDomain); + if ($oHmailDomain) { + $oHmailAccount = $oHmailDomain->Accounts->ItemByAddress($sEmail); + if ($oHmailAccount) { + $oHmailAccount->Password = (string) $oNewPassword; + $oHmailAccount->Save(); + $bResult = true; + } else { + $this->oLogger && $this->oLogger->Write('hMailServer: Unknown account ('.$sEmail.')', \LOG_ERROR); + } + } else { + $this->oLogger && $this->oLogger->Write('hMailServer: Unknown domain ('.$sDomain.')', \LOG_ERROR); + } + } else { + $this->oLogger && $this->oLogger->Write('hMailServer: Auth error', \LOG_ERROR); + } + } + catch (\Exception $oException) + { + $this->oLogger && $this->oLogger->WriteException($oException); + } + + return $bResult; + } +} diff --git a/plugins/change-password-hmailserver/index.php b/plugins/change-password-hmailserver/index.php new file mode 100644 index 0000000000..8fa5180d82 --- /dev/null +++ b/plugins/change-password-hmailserver/index.php @@ -0,0 +1,18 @@ +<?php + +class ChangePasswordHMailServerPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Change Password hMailServer', + VERSION = '2.36', + RELEASE = '2024-03-17', + REQUIRED = '2.36.0', + CATEGORY = 'Security', + DESCRIPTION = 'Extension to allow users to change their passwords through hMailServer'; + + public function Supported() : string + { + return 'Activate in Change Password plugin' + . (\class_exists('COM') ? '' : '. And you must install PHP COM extension'); + } +} diff --git a/plugins/contacts-suggestions-example/LICENSE b/plugins/change-password-ispconfig/LICENSE similarity index 100% rename from plugins/contacts-suggestions-example/LICENSE rename to plugins/change-password-ispconfig/LICENSE diff --git a/plugins/change-password-ispconfig/driver.php b/plugins/change-password-ispconfig/driver.php new file mode 100644 index 0000000000..86783e796c --- /dev/null +++ b/plugins/change-password-ispconfig/driver.php @@ -0,0 +1,106 @@ +<?php + +use SnappyMail\SensitiveString; + +class ChangePasswordISPConfigDriver +{ + const + NAME = 'ISPConfig', + DESCRIPTION = 'Change passwords in ISPConfig.'; + + /** + * @var \RainLoop\Config\Plugin + */ + private $oConfig = null; + + /** + * @var \MailSo\Log\Logger + */ + private $oLogger = null; + + function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger) + { + $this->oConfig = $oConfig; + $this->oLogger = $oLogger; + } + + public static function isSupported() : bool + { + return \class_exists('PDO', false) + // The PHP extension PDO (mysql) must be installed to use this plugin + && \in_array('mysql', \PDO::getAvailableDrivers()); + } + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('ispconfig_dsn')->SetLabel('ISPConfig PDO dsn') + ->SetDefaultValue('mysql:host=localhost;dbname=dbispconfig;charset=utf8'), + \RainLoop\Plugins\Property::NewInstance('ispconfig_user')->SetLabel('User'), + \RainLoop\Plugins\Property::NewInstance('ispconfig_password')->SetLabel('Password') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD), + \RainLoop\Plugins\Property::NewInstance('ispconfig_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', 'ispconfig_allowed_emails', ''))) { + return false; + } + + try + { + if ($this->oLogger) { + $this->oLogger->Write('ISPConfig: Try to change password for '.$oAccount->Email()); + } + + $oPdo = new \PDO( + $this->oConfig->Get('plugin', 'ispconfig_dsn', ''), + $this->oConfig->Get('plugin', 'ispconfig_user', ''), + $this->oConfig->Get('plugin', 'ispconfig_password', ''), + array( + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION + ) + ); + + $oStmt = $oPdo->prepare('SELECT password, mailuser_id FROM mail_user WHERE login = ? LIMIT 1'); + if ($oStmt->execute(array($oAccount->IncLogin()))) { + $aFetchResult = $oStmt->fetch(\PDO::FETCH_ASSOC); + if (!empty($aFetchResult['mailuser_id'])) { + $sDbPassword = $aFetchResult['password']; + $sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$')); + if (\crypt($oPrevPassword, $sDbSalt) === $sDbPassword) { + $oStmt = $oPdo->prepare('UPDATE mail_user SET password = ? WHERE mailuser_id = ?'); + return !!$oStmt->execute(array( + $this->cryptPassword($oNewPassword), + $aFetchResult['mailuser_id'] + )); + } + } + } + } + catch (\Exception $oException) + { + if ($this->oLogger) { + $this->oLogger->WriteException($oException); + } + } + return false; + } + + private function cryptPassword(SensitiveString $oPassword) : string + { + if (\defined('CRYPT_SHA512') && CRYPT_SHA512) { + $sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$'; + } elseif (\defined('CRYPT_SHA256') && CRYPT_SHA256) { + $sSalt = '$5$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$'; + } else { + $sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$'; + } + return \crypt($oPassword, $sSalt); + } +} diff --git a/plugins/change-password-ispconfig/index.php b/plugins/change-password-ispconfig/index.php new file mode 100644 index 0000000000..5486e41ae4 --- /dev/null +++ b/plugins/change-password-ispconfig/index.php @@ -0,0 +1,17 @@ +<?php + +class ChangePasswordISPConfigPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Change Password ISPConfig', + VERSION = '2.36', + RELEASE = '2024-03-17', + REQUIRED = '2.36.0', + CATEGORY = 'Security', + DESCRIPTION = 'Extension to allow users to change their passwords through ISPConfig'; + + public function Supported() : string + { + return 'Use Change Password plugin'; + } +} diff --git a/plugins/change-password-mailcow/driver.php b/plugins/change-password-mailcow/driver.php new file mode 100644 index 0000000000..3a66b63c96 --- /dev/null +++ b/plugins/change-password-mailcow/driver.php @@ -0,0 +1,74 @@ +<?php + +class ChangePasswordMailcowDriver +{ + const + NAME = 'Mailcow', + DESCRIPTION = 'Driver to change the email account password on Mailcow.'; + + private $sHostName = ''; + private $sApiToken = ''; + + /** + * @var \MailSo\Log\Logger + */ + private $oLogger = null; + + function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger) + { + $this->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 0000000000..80a19c4069 --- /dev/null +++ b/plugins/change-password-mailcow/index.php @@ -0,0 +1,18 @@ +<?php + +class ChangePasswordMailcowPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Change Password Mailcow', + AUTHOR = 'Cuttlefish', + VERSION = '2.36', + RELEASE = '2024-07-25', + REQUIRED = '2.36.0', + CATEGORY = 'Security', + DESCRIPTION = 'Extension to allow users to change their passwords through Mailcow'; + + public function Supported() : string + { + return 'Use Change Password plugin'; + } +} diff --git a/plugins/custom-settings-tab/LICENSE b/plugins/change-password-poppassd/LICENSE similarity index 100% rename from plugins/custom-settings-tab/LICENSE rename to plugins/change-password-poppassd/LICENSE diff --git a/plugins/change-password-poppassd/driver.php b/plugins/change-password-poppassd/driver.php new file mode 100644 index 0000000000..5784dd8dea --- /dev/null +++ b/plugins/change-password-poppassd/driver.php @@ -0,0 +1,178 @@ +<?php + +use MailSo\Net\ConnectSettings; +use SnappyMail\SensitiveString; + +class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient +{ + const + NAME = 'Poppassd', + DESCRIPTION = 'Change passwords using Poppassd.'; + + private + $oConfig = null; + + function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger) + { + $this->oConfig = $oConfig; + $this->oLogger = $oLogger; + } + + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('poppassd_host')->SetLabel('POPPASSD Host') + ->SetDefaultValue('127.0.0.1'), + \RainLoop\Plugins\Property::NewInstance('poppassd_port')->SetLabel('POPPASSD Port') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) + ->SetDefaultValue(106), + \RainLoop\Plugins\Property::NewInstance('poppassd_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', 'poppassd_allowed_emails', ''))) { + return false; + } + + try + { + $oSettings = new ConnectSettings; + $oSettings->host = $this->oConfig->Get('plugin', 'poppassd_host', ''); + $oSettings->port = (int) $this->oConfig->Get('plugin', 'poppassd_port', 106); + $this->Connect($oSettings); + + if ($this->bIsLoggined) { + $this->writeLogException( + new \RuntimeException('Already authenticated for this session'), + \LOG_ERR, true); + } + + try + { + $this->sendRequestWithCheck('user', $oAccount->IncLogin(), true); + $this->sendRequestWithCheck('pass', $oPrevPassword, true); + } + catch (\Throwable $oException) + { + $this->writeLogException($oException, \LOG_NOTICE, true); + } + + $this->bIsLoggined = true; + + if ($this->bIsLoggined) { + $this->sendRequestWithCheck('newpass', $oNewPassword); + } else { + $this->writeLogException( + new \RuntimeException('Required login'), + \LOG_ERR, true); + } + + + $this->Disconnect(); + + return true; + } + catch (\Throwable $oException) + { + } + + return false; + } + + private + $bIsLoggined = false, + $iRequestTime = 0; + + public function Connect(ConnectSettings $oSettings) : void + { + $this->iRequestTime = \microtime(true); + parent::Connect($oSettings); + $this->validateResponse(); + } + + public function supportsAuthType(string $sasl_type) : bool + { + return true; + } + + public static function isSupported() : bool + { + return true; + } + + public function Logout() : void + { + if ($this->bIsLoggined) { + $this->sendRequestWithCheck('quit'); + } + $this->bIsLoggined = false; + } + + private function secureRequestParams($sCommand, $sAddToCommand) : ?string + { + if (\strlen($sAddToCommand)) { + switch (\strtolower($sCommand)) + { + case 'pass': + case 'newpass': + return '********'; + } + } + + return null; + } + + private function sendRequestWithCheck(string $sCommand, string $sAddToCommand = '', bool $bAuthRequestValidate = false) : void + { + $sCommand = \trim($sCommand); + if (!\strlen($sCommand)) { + $this->writeLogException(new \MailSo\Base\Exceptions\InvalidArgumentException(), \LOG_ERR, true); + } + + $this->IsConnected(true); + + $sRealCommand = $sCommand . (\strlen($sAddToCommand) ? ' '.$sAddToCommand : ''); + + $sFakeCommand = ''; + $sFakeAddToCommand = $this->secureRequestParams($sCommand, $sAddToCommand); + if (\strlen($sFakeAddToCommand)) { + $sFakeCommand = $sCommand . ' ' . $sFakeAddToCommand; + } + + $this->iRequestTime = \microtime(true); + $this->sendRaw($sRealCommand, true, $sFakeCommand); + + $this->validateResponse($bAuthRequestValidate); + } + + private function validateResponse(bool $bAuthRequestValidate = false) : self + { + $sResponseBuffer = $this->getNextBuffer(); + + $bResult = \preg_match($bAuthRequestValidate ? '/^[23]\d\d/' : '/^2\d\d/', \trim($sResponseBuffer)); + + if (!$bResult) { + // POP3 validation hack + $bResult = '+OK ' === \substr(\trim($sResponseBuffer), 0, 4); + } + + if (!$bResult) { + $this->writeLogException(new \MailSo\Base\Exceptions\Exception(), \LOG_WARNING, true); + } + + $this->writeLog((\microtime(true) - $this->iRequestTime), \LOG_DEBUG); + + return $this; + } + + function getLogName() : string + { + return 'POPPASSD'; + } +} diff --git a/plugins/change-password-poppassd/index.php b/plugins/change-password-poppassd/index.php new file mode 100644 index 0000000000..cad4573ec2 --- /dev/null +++ b/plugins/change-password-poppassd/index.php @@ -0,0 +1,17 @@ +<?php + +class ChangePasswordPoppassdPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Change Password Poppassd', + VERSION = '2.36', + RELEASE = '2024-03-17', + REQUIRED = '2.36.0', + CATEGORY = 'Security', + DESCRIPTION = 'Extension to allow users to change their passwords through Poppassd'; + + public function Supported() : string + { + return 'Use Change Password plugin'; + } +} diff --git a/plugins/ldap-change-password/LICENSE b/plugins/change-password/LICENSE similarity index 100% rename from plugins/ldap-change-password/LICENSE rename to plugins/change-password/LICENSE diff --git a/plugins/change-password/drivers/example.phps b/plugins/change-password/drivers/example.phps new file mode 100644 index 0000000000..d2a1cb2c9b --- /dev/null +++ b/plugins/change-password/drivers/example.phps @@ -0,0 +1,37 @@ +<?php + +class ChangePasswordDriverExample +{ + const + NAME = 'Example', + DESCRIPTION = 'Example driver to change the email account passwords.'; + + private + $sHostName = ''; + + /** + * @var \MailSo\Log\Logger + */ + private $oLogger = null; + + function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger) + { + $this->oLogger = $oLogger; +// $this->sHostName = $oConfig->Get('plugin', 'example_hostname', ''); + } + + public static function isSupported() : bool + { + return false; + } + + public static function configMapping() : array + { + return array(); + } + + public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool + { + return false; + } +} diff --git a/plugins/change-password/drivers/ldap.php b/plugins/change-password/drivers/ldap.php new file mode 100644 index 0000000000..f9dae8ff82 --- /dev/null +++ b/plugins/change-password/drivers/ldap.php @@ -0,0 +1,130 @@ +<?php + +use SnappyMail\SensitiveString; + +class ChangePasswordDriverLDAP +{ + const + NAME = 'LDAP', + DESCRIPTION = 'Change passwords in LDAP.'; + + private + $sLdapUri = 'ldap://localhost:389', + $bUseStartTLS = True, + $sUserDnFormat = '', + $sPasswordField = 'userPassword', + $sPasswordEncType = 'SHA'; + + /** + * @var \MailSo\Log\Logger + */ + private $oLogger = null; + + function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger) + { + $this->oLogger = $oLogger; + $this->sLdapUri = \trim($oConfig->Get('plugin', 'ldap_uri', '')); + $this->bUseStartTLS = (bool) \trim($oConfig->Get('plugin', 'ldap_use_start_tls', '')); + $this->sUserDnFormat = \trim($oConfig->Get('plugin', 'ldap_user_dn_format', '')); + $this->sPasswordField = \trim($oConfig->Get('plugin', 'ldap_password_field', '')); + $this->sPasswordEncType = \trim($oConfig->Get('plugin', 'ldap_password_enc_type', '')); + } + + public static function isSupported() : bool + { + // 'The LDAP PHP extension must be installed to use this plugin'; + return \function_exists('ldap_connect'); + } + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('ldap_uri')->SetLabel('LDAP URI') + ->SetDefaultValue('ldap://localhost:389') + ->SetDescription('LDAP server URI(s), space separated'), + \RainLoop\Plugins\Property::NewInstance('ldap_use_start_tls')->SetLabel('Use StartTLS') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(True), + \RainLoop\Plugins\Property::NewInstance('ldap_user_dn_format')->SetLabel('User DN format') + ->SetDescription('LDAP user dn format. Supported tokens: {email}, {email:user}, {email:domain}, {login}, {domain}, {domain:dc}, {imap:login}, {imap:host}, {imap:port}, {gecos}') + ->SetDefaultValue('uid={imap:login},ou=Users,{domain:dc}'), + \RainLoop\Plugins\Property::NewInstance('ldap_password_field')->SetLabel('Password field') + ->SetDefaultValue('userPassword'), + \RainLoop\Plugins\Property::NewInstance('ldap_password_enc_type')->SetLabel('Encryption type') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) + ->SetDefaultValue(array('SHA', 'SSHA', 'MD5', 'Crypt', 'Clear')) + ->SetDescription('In what way do you want the passwords to be encrypted?') + ); + } + + public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool + { + $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::getEmailAddressLocalPart($oAccount->Email()), + '{email:domain}' => $sDomain, + '{login}' => $oAccount->IncLogin(), + '{imap:login}' => $oAccount->IncLogin(), + '{imap:host}' => $oAccount->Domain()->ImapSettings()->host, + '{imap:port}' => $oAccount->Domain()->ImapSettings()->port, + '{gecos}' => \function_exists('posix_getpwnam') ? \posix_getpwnam($oAccount->IncLogin()) : '' + )); + + $oCon = \ldap_connect($this->sLdapUri); + if (!$oCon) { + return false; + } + + if (!\ldap_set_option($oCon, LDAP_OPT_PROTOCOL_VERSION, 3)) { + $this->oLogger->Write( + 'Failed to set LDAP Protocol version to 3, TLS not supported.', + \LOG_WARNING, + 'LDAP' + ); + } + + if ($this->bUseStartTLS && !@\ldap_start_tls($oCon)) + { + throw new \Exception('ldap_start_tls error '.\ldap_errno($oCon).': '.\ldap_error($oCon)); + } + + if (!\ldap_bind($oCon, $sUserDn, $oPrevPassword)) { + throw new \Exception('ldap_bind error '.\ldap_errno($oCon).': '.\ldap_error($oCon)); + } + + $sSshaSalt = ''; + $sPrefix = '{'.\strtoupper($this->sPasswordEncType).'}'; + $sEncodedNewPassword = $oNewPassword; + switch ($sPrefix) + { + case '{SSHA}': + $sSshaSalt = $this->getSalt(4); + case '{SHA}': + $sEncodedNewPassword = $sPrefix.\base64_encode(\hash('sha1', $oNewPassword.$sSshaSalt, true).$sSshaSalt); + break; + case '{MD5}': + $sEncodedNewPassword = $sPrefix.\base64_encode(\md5($oNewPassword, true)); + break; + case '{CRYPT}': + $sEncodedNewPassword = $sPrefix.\crypt($oNewPassword, $this->getSalt(2)); + break; + } + + $aEntry = array(); + $aEntry[$this->sPasswordField] = $sEncodedNewPassword; + + if (!\ldap_mod_replace($oCon, $sUserDn, $aEntry)) { + throw new \Exception('ldap_mod_replace error '.\ldap_errno($oCon).': '.\ldap_error($oCon)); + } + + return true; + } + + private function getSalt(int $iLength) : string + { + return \substr(\preg_replace('#+/=#', '', \base64_encode(\random_bytes($iLength))), 0, $iLength); + } +} diff --git a/plugins/change-password/drivers/pdo.php b/plugins/change-password/drivers/pdo.php new file mode 100644 index 0000000000..2051ed2cee --- /dev/null +++ b/plugins/change-password/drivers/pdo.php @@ -0,0 +1,120 @@ +<?php + +use SnappyMail\SensitiveString; + +class ChangePasswordDriverPDO +{ + const + NAME = 'PDO', + DESCRIPTION = 'Use your own SQL (PDO) statement (with wildcards).'; + + /** + * @var \RainLoop\Config\Plugin + */ + private $oConfig = null; + + /** + * @var \MailSo\Log\Logger + */ + private $oLogger = null; + + function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger) + { + $this->oConfig = $oConfig; + $this->oLogger = $oLogger; + } + + public static function isSupported() : bool + { + return \class_exists('PDO', false); + } + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('pdo_dsn')->SetLabel('DSN') + ->SetDefaultValue('mysql:host=localhost;dbname=snappymail;charset=utf8'), + \RainLoop\Plugins\Property::NewInstance('pdo_user')->SetLabel('User'), + \RainLoop\Plugins\Property::NewInstance('pdo_password')->SetLabel('Password') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD), + \RainLoop\Plugins\Property::NewInstance('pdo_sql')->SetLabel('Statement') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('SQL statement (allowed wildcards :email, :oldpass, :newpass, :domain, :username, :login_name).') + ->SetDefaultValue('UPDATE table SET password = :newpass WHERE (domain = :domain AND username = :username) OR loginname = :login_name'), + \RainLoop\Plugins\Property::NewInstance('pdo_encrypt')->SetLabel('Encryption') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) + ->SetDefaultValue(array('none', 'bcrypt', 'Argon2i', 'Argon2id', 'SHA256-CRYPT', 'SHA512-CRYPT')) + ->SetDescription('In what way do you want the passwords to be encrypted?'), + \RainLoop\Plugins\Property::NewInstance('pdo_encryptprefix')->SetLabel('Encrypt prefix') + ->SetDescription('Optional encrypted password prefix, like {ARGON2I} or {BLF-CRYPT} or {SHA512-CRYPT}'), + \RainLoop\Plugins\Property::NewInstance('pdo_mysql_ssl')->SetLabel('MySQL SSL connection') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(false), + \RainLoop\Plugins\Property::NewInstance('pdo_mysql_ssl_verify')->SetLabel('MySQL SSL verify server cert') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDescription('Verify that certificate\'s Common Name of SAN matches the database server\'s hostname.') + ->SetDefaultValue(false), + \RainLoop\Plugins\Property::NewInstance('pdo_mysql_ssl_ca')->SetLabel('MySQL SSL CA certificate file') + ->SetDescription('Path to a file containing the CA certificate used to sign the server certificate, or a CA bundle. Required for SSL/TLS connections to work.') + ->SetDefaultValue('/etc/pki/tls/certs/ca-bundle.crt') + ); + } + + public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool + { + try + { + $pdo_attr = array( + \PDO::ATTR_EMULATE_PREPARES => true, + \PDO::ATTR_PERSISTENT => true, + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + ); + if ($this->oConfig->Get('plugin', 'pdo_mysql_ssl', false)) { + $pdo_attr[\PDO::MYSQL_ATTR_SSL_CA] = $this->oConfig->Get('plugin', 'pdo_mysql_ssl_ca', ''); + $pdo_attr[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->oConfig->Get('plugin', 'pdo_mysql_ssl_verify', true); + } + + $conn = new \PDO( + $this->oConfig->Get('plugin', 'pdo_dsn', ''), + $this->oConfig->Get('plugin', 'pdo_user', ''), + $this->oConfig->Get('plugin', 'pdo_password', ''), + $pdo_attr + ); + + $sEmail = $oAccount->Email(); + $encrypt = $this->oConfig->Get('plugin', 'pdo_encrypt', ''); + $encrypt_prefix = $this->oConfig->Get('plugin', 'pdo_encryptprefix', ''); + + $placeholders = array( + ':email' => $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() + ); + + $sql = $this->oConfig->Get('plugin', 'pdo_sql', ''); + + $statement = $conn->prepare($sql); + + // we have to check that all placehoders are used in the query, passing any unused placeholders will generate an error + foreach ($placeholders as $placeholder => $value) { + if (\preg_match_all("/{$placeholder}(?![a-zA-Z0-9\-])/", $sql)) { + $statement->bindValue($placeholder, $value); + } + } + + // and execute + return !!$statement->execute(); + } + catch (\Exception $oException) + { + \SnappyMail\Log::error('change-password', $oException->getMessage()); + if ($this->oLogger) { + $this->oLogger->WriteException($oException); + } + } + return false; + } +} diff --git a/plugins/change-password/index.php b/plugins/change-password/index.php new file mode 100644 index 0000000000..d0d49bab70 --- /dev/null +++ b/plugins/change-password/index.php @@ -0,0 +1,269 @@ +<?php + +use RainLoop\Exceptions\ClientException; +use SnappyMail\SensitiveString; + +class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Change Password', + VERSION = '2.38', + RELEASE = '2024-04-22', + REQUIRED = '2.36.1', + CATEGORY = 'Security', + DESCRIPTION = 'Extension to allow users to change their passwords'; + + // \RainLoop\Notifications\ + const + CouldNotSaveNewPassword = 130, + CurrentPasswordIncorrect = 131, + NewPasswordShort = 132, + NewPasswordWeak = 133, + NewPasswordHibp = 134; + + public function Init() : void + { + $this->UseLangs(true); // start use langs folder + +// $this->addCss('style.css'); + $this->addJs('js/ChangePasswordUserSettings.js'); // add js file + $this->addJsonHook('ChangePassword', 'ChangePassword'); + $this->addTemplate('templates/SettingsChangePassword.html'); + } + + protected function getSupportedDrivers(bool $all = false) : iterable + { + if ($phar_file = \Phar::running()) { + $phar = new \Phar($phar_file, \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME); + foreach (new \RecursiveIteratorIterator($phar) as $file) { + if (\preg_match('#/drivers/([a-z]+)\\.php$#Di', $file, $m)) { + $class = 'ChangePasswordDriver' . $m[1]; + try + { + if ($all || $this->Config()->Get('plugin', "driver_{$m[1]}_enabled", false)) { + require_once $file; + if ($class::isSupported()) { + yield $m[1] => $class; + } + } + } + catch (\Throwable $oException) + { + \trigger_error("ERROR {$class}: " . $oException->getMessage()); + } + } + } + } else { + foreach (\glob(__DIR__ . '/drivers/*.php') as $file) { + $name = \basename($file, '.php'); + $class = 'ChangePasswordDriver' . $name; + try + { + if ($all || $this->Config()->Get('plugin', "driver_{$name}_enabled", false)) { + require_once $file; + if ($class::isSupported()) { + yield $name => $class; + } + } + } + catch (\Throwable $oException) + { + \trigger_error("ERROR {$class}: " . $oException->getMessage()); + } + } + } + + foreach (\glob(__DIR__ . '/../change-password-*', GLOB_ONLYDIR) as $file) { + $name = \str_replace('change-password-', '', \basename($file)); + $class = "ChangePassword{$name}Driver"; + $file .= '/driver.php'; + try + { + if (\is_readable($file) && ($all || $this->Config()->Get('plugin', "driver_{$name}_enabled", false))) { + require_once $file; + if ($class::isSupported()) { + yield $name => $class; + } + } + } + catch (\Throwable $oException) + { + \trigger_error("ERROR {$class}: " . $oException->getMessage()); + } + } + } + + public function Supported() : string + { + foreach ($this->getSupportedDrivers() as $class) { + return ''; + } + return 'There are no change-password drivers enabled'; + } + + public function configMapping() : array + { + $result = [ + \RainLoop\Plugins\Property::NewInstance('pass_min_length') + ->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') + ->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); + $props = [ + \RainLoop\Plugins\Property::NewInstance("driver_{$name}_enabled") + ->SetLabel('Enable ' . $class::NAME) + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDescription($class::DESCRIPTION) + ->SetDefaultValue(false), + \RainLoop\Plugins\Property::NewInstance("driver_{$name}_allowed_emails") + ->SetLabel('Allowed emails') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@example.net user2@example1.net *@example2.net') + ->SetDefaultValue('*') + ]; + $group->exchangeArray(\array_merge($props, \call_user_func("{$class}::configMapping"))); + $result[] = $group; + } + return $result; + } + + public function ChangePassword() + { + $oActions = $this->Manager()->Actions(); + $oAccount = $oActions->GetAccount(); + + if (!$oAccount->Email()) { + \trigger_error('ChangePassword failed: empty email address'); + throw new ClientException(static::CouldNotSaveNewPassword); + } + + $sPrevPassword = $this->jsonParam('PrevPassword'); + 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(); + foreach ($this->getSupportedDrivers() as $name => $class) { + if (\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $oConfig->Get('plugin', "driver_{$name}_allowed_emails"))) { + $name = $class::NAME; + $oLogger = $oActions->Logger(); + try + { + $oDriver = new $class( + $oConfig, + $oLogger + ); + if (!$oDriver->ChangePassword($oAccount, $oPrevPassword, $oNewPassword)) { + throw new ClientException(static::CouldNotSaveNewPassword); + } + $bResult = true; + if ($oLogger) { + $oLogger->Write("{$name} password changed for {$oAccount->Email()}"); + } + } + catch (\Throwable $oException) + { + \trigger_error("{$class} failed: {$oException->getMessage()}"); + if ($oLogger) { + $oLogger->Write("ERROR: {$name} password change for {$oAccount->Email()} failed"); + $oLogger->WriteException($oException); +// $oLogger->WriteException($oException, \LOG_WARNING, $name); + } + } + } + } + + if (!$bResult) { + \trigger_error("ChangePassword failed"); + throw new ClientException(static::CouldNotSaveNewPassword); + } + + $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, SensitiveString $password) + { + switch (\strtolower($algo)) + { + case 'argon2i': + return \password_hash($password, PASSWORD_ARGON2I); + + case 'argon2id': + return \password_hash($password, PASSWORD_ARGON2ID); + + case 'bcrypt': + return \password_hash($password, PASSWORD_BCRYPT); + + case 'sha256-crypt': + return \crypt($password,'$5$'.\substr(\base64_encode(\random_bytes(32)), 0, 16)); + + case 'sha512-crypt': + return \crypt($password,'$6$'.\substr(\base64_encode(\random_bytes(32)), 0, 16)); + + default: + break; + } + + return $password; + } + + private static function PasswordStrength(string $sPassword) : int + { + $i = \strlen($sPassword); + $max = \min(100, $i * 8); + $s = 0; + while (--$i) { + $s += ($sPassword[$i] != $sPassword[$i-1] ? 1 : -0.5); + } + $c = 0; + $re = [ '/[^0-9A-Za-z]+/', '/[0-9]+/', '/[A-Z]+/', '/[a-z]+/' ]; + foreach ($re as $regex) { + if (\preg_match_all($regex, $sPassword, $m)) { + ++$c; + foreach ($m[0] as $str) { + if (5 > \strlen($str)) { + ++$s; + } + } + } + } + + return \intval(\max(0, \min($max, $s * $c * 1.5))); + } + +} diff --git a/plugins/change-password/js/ChangePasswordUserSettings.js b/plugins/change-password/js/ChangePasswordUserSettings.js new file mode 100644 index 0000000000..8ec0ad048e --- /dev/null +++ b/plugins/change-password/js/ChangePasswordUserSettings.js @@ -0,0 +1,141 @@ + +(rl => { + + if (!rl) + { + return; + } + + let pw_re = [/[^0-9A-Za-z]+/g, /[0-9]+/g, /[A-Z]+/g, /[a-z]+/g], + getPassStrength = v => { + let m, + i = v.length, + max = Math.min(100, i * 8), + s = 0, + c = 0, + ii; + while (i--) { + s += (v[i] != v[i+1] ? 1 : -0.5); + } + for (i = 0; i < 4; ++i) { + m = v.match(pw_re[i]); + if (m) { + ++c; + for (ii = 0; ii < m.length; ++ii) { + if (5 > m[ii].length) { + ++s; + } + } + } + } + return Math.max(0, Math.min(max, s * c * 1.5)); + }; + + class ChangePasswordUserSettings + { + constructor() { + let minLength = rl.pluginSettingsGet('change-password', 'pass_min_length'); + let minStrength = rl.pluginSettingsGet('change-password', 'pass_min_strength'); + + this.changeProcess = ko.observable(false); + this.errorDescription = ko.observable(''); + this.passwordMismatch = ko.observable(false); + this.passwordUpdateError = ko.observable(false); + this.passwordUpdateSuccess = ko.observable(false); + this.currentPassword = ko.observable(''); + this.currentPasswordError = ko.observable(false); + this.newPassword = ko.observable(''); + this.newPassword2 = ko.observable(''); + this.pass_min_length = minLength; + + this.currentPassword.subscribe(() => this.resetUpdate(true)); + this.newPassword.subscribe(() => this.resetUpdate()); + this.newPassword2.subscribe(() => this.resetUpdate()); + + ko.decorateCommands(this, { + saveNewPasswordCommand: self => !self.changeProcess() + && '' !== self.currentPassword() + && self.newPassword().length >= minLength + && self.newPassword2() == self.newPassword() + && (!this.meter || this.meter.value >= minStrength) + }); + } + + submitForm(form) { + form.reportValidity() && this.saveNewPasswordCommand(); + } + + saveNewPasswordCommand() { + if (this.newPassword() !== this.newPassword2()) { + this.passwordMismatch(true); + this.errorDescription(rl.i18n('SETTINGS_CHANGE_PASSWORD/ERROR_PASSWORD_MISMATCH')); + } else { + this.reset(true); + rl.pluginRemoteRequest( + (iError, data) => { + this.reset(false); + if (iError) { + this.passwordUpdateError(true); + if (131 === iError) { + // Notification.CurrentPasswordIncorrect + this.currentPasswordError(true); + } + this.errorDescription((data && rl.i18n(data.ErrorMessageAdditional)) + || rl.i18n('NOTIFICATIONS/COULD_NOT_SAVE_NEW_PASSWORD')); + } else { + this.currentPassword(''); + this.newPassword(''); + this.newPassword2(''); + this.passwordUpdateSuccess(true); +/* + const refresh = rl.app.refresh; + rl.app.refresh = ()=>{}; + rl.setData(data.Result); + rl.app.refresh = refresh; +*/ + } + }, + 'ChangePassword', + { + 'PrevPassword': this.currentPassword(), + 'NewPassword': this.newPassword() + } + ); + } + } + + reset(change) { + this.changeProcess(change); + this.resetUpdate(); + this.currentPasswordError(false); + this.errorDescription(''); + } + + resetUpdate(current) { + this.passwordUpdateError(false); + this.passwordUpdateSuccess(false); + current ? this.currentPasswordError(false) : this.passwordMismatch(false); + } + + onBuild(dom) { + let meter = dom.querySelector('.new-password-meter'); + meter && this.newPassword.subscribe(value => meter.value = getPassStrength(value)); + this.meter = meter; + } + + onHide() { + this.reset(false); + this.currentPassword(''); + this.newPassword(''); + this.newPassword2(''); + } + } + + rl.addSettingsViewModel( + ChangePasswordUserSettings, + 'SettingsChangePassword', + 'GLOBAL/PASSWORD', + 'change-password' + ); + +})(window.rl); diff --git a/plugins/change-password/langs/de.ini b/plugins/change-password/langs/de.ini new file mode 100644 index 0000000000..71360c0006 --- /dev/null +++ b/plugins/change-password/langs/de.ini @@ -0,0 +1,13 @@ +[SETTINGS_CHANGE_PASSWORD] +LEGEND_CHANGE_PASSWORD = "Passwort ändern" +LABEL_CURRENT_PASSWORD = "Aktuelles Passwort" +LABEL_NEW_PASSWORD = "Neues Passwort" +LABEL_REPEAT_PASSWORD = "Neues Passwort bestätigen" +BUTTON_UPDATE_PASSWORD = "Neues Passwort setzen" +ERROR_PASSWORD_MISMATCH = "Passwörter stimmen nicht überein; versuchen Sie es bitte erneut" +[NOTIFICATIONS] +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 new file mode 100644 index 0000000000..4a4a6ea128 --- /dev/null +++ b/plugins/change-password/langs/en.ini @@ -0,0 +1,13 @@ +[SETTINGS_CHANGE_PASSWORD] +LEGEND_CHANGE_PASSWORD = "Change Password" +LABEL_CURRENT_PASSWORD = "Current password" +LABEL_NEW_PASSWORD = "New password" +LABEL_REPEAT_PASSWORD = "Confirm New Password" +BUTTON_UPDATE_PASSWORD = "Set New Password" +ERROR_PASSWORD_MISMATCH = "Passwords do not match, please try again" +[NOTIFICATIONS] +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 new file mode 100644 index 0000000000..4a4a6ea128 --- /dev/null +++ b/plugins/change-password/langs/en_GB.ini @@ -0,0 +1,13 @@ +[SETTINGS_CHANGE_PASSWORD] +LEGEND_CHANGE_PASSWORD = "Change Password" +LABEL_CURRENT_PASSWORD = "Current password" +LABEL_NEW_PASSWORD = "New password" +LABEL_REPEAT_PASSWORD = "Confirm New Password" +BUTTON_UPDATE_PASSWORD = "Set New Password" +ERROR_PASSWORD_MISMATCH = "Passwords do not match, please try again" +[NOTIFICATIONS] +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 new file mode 100644 index 0000000000..4a4a6ea128 --- /dev/null +++ b/plugins/change-password/langs/en_US.ini @@ -0,0 +1,13 @@ +[SETTINGS_CHANGE_PASSWORD] +LEGEND_CHANGE_PASSWORD = "Change Password" +LABEL_CURRENT_PASSWORD = "Current password" +LABEL_NEW_PASSWORD = "New password" +LABEL_REPEAT_PASSWORD = "Confirm New Password" +BUTTON_UPDATE_PASSWORD = "Set New Password" +ERROR_PASSWORD_MISMATCH = "Passwords do not match, please try again" +[NOTIFICATIONS] +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.ini b/plugins/change-password/langs/es.ini new file mode 100644 index 0000000000..47bc3ecee4 --- /dev/null +++ b/plugins/change-password/langs/es.ini @@ -0,0 +1,13 @@ +[SETTINGS_CHANGE_PASSWORD] +LEGEND_CHANGE_PASSWORD = "Cambiar contraseña" +LABEL_CURRENT_PASSWORD = "Contraseña actual" +LABEL_NEW_PASSWORD = "Nueva contraseña" +LABEL_REPEAT_PASSWORD = "Confirmar nueva contraseña" +BUTTON_UPDATE_PASSWORD = "Establecer nueva contraseña" +ERROR_PASSWORD_MISMATCH = "Las contraseñas no coinciden, por favor intente de nuevo" +[NOTIFICATIONS] +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.ini b/plugins/change-password/langs/fr.ini new file mode 100644 index 0000000000..ecb563f83a --- /dev/null +++ b/plugins/change-password/langs/fr.ini @@ -0,0 +1,13 @@ +[SETTINGS_CHANGE_PASSWORD] +LEGEND_CHANGE_PASSWORD = "Modifier le mot de passe" +LABEL_CURRENT_PASSWORD = "Mot de passe actuel" +LABEL_NEW_PASSWORD = "Nouveau mot de passe" +LABEL_REPEAT_PASSWORD = "Confirmation du nouveau mot de passe" +BUTTON_UPDATE_PASSWORD = "Enregistrer le nouveau mot de passe" +ERROR_PASSWORD_MISMATCH = "Les mots de passe ne correspondent pas, merci de réessayer" +[NOTIFICATIONS] +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.ini b/plugins/change-password/langs/it.ini new file mode 100644 index 0000000000..62507ce53d --- /dev/null +++ b/plugins/change-password/langs/it.ini @@ -0,0 +1,13 @@ +[SETTINGS_CHANGE_PASSWORD] +LEGEND_CHANGE_PASSWORD = "Cambia password" +LABEL_CURRENT_PASSWORD = "Password attuale" +LABEL_NEW_PASSWORD = "Nuova password" +LABEL_REPEAT_PASSWORD = "Conferma nuova password" +BUTTON_UPDATE_PASSWORD = "Imposta nuova password" +ERROR_PASSWORD_MISMATCH = "Le password non corrispondono, riprova" +[NOTIFICATIONS] +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.ini b/plugins/change-password/langs/nl.ini new file mode 100644 index 0000000000..9b6078e1da --- /dev/null +++ b/plugins/change-password/langs/nl.ini @@ -0,0 +1,13 @@ +[SETTINGS_CHANGE_PASSWORD] +LEGEND_CHANGE_PASSWORD = "Wachtwoord aanpassen" +LABEL_CURRENT_PASSWORD = "Huidig wachtwoord" +LABEL_NEW_PASSWORD = "Nieuw wachtwoord" +LABEL_REPEAT_PASSWORD = "Bevestig wachtwoord" +BUTTON_UPDATE_PASSWORD = "Wachtwoord wijzigen" +ERROR_PASSWORD_MISMATCH = "Wachtwoorden zijn niet gelijk, probeer opnieuw" +[NOTIFICATIONS] +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/uk.ini b/plugins/change-password/langs/uk.ini new file mode 100644 index 0000000000..83afa3df4d --- /dev/null +++ b/plugins/change-password/langs/uk.ini @@ -0,0 +1,13 @@ +[SETTINGS_CHANGE_PASSWORD] +LEGEND_CHANGE_PASSWORD = "Змінити пароль" +LABEL_CURRENT_PASSWORD = "Поточний пароль" +LABEL_NEW_PASSWORD = "Новий пароль" +LABEL_REPEAT_PASSWORD = "Підтвердьте новий пароль" +BUTTON_UPDATE_PASSWORD = "Змінити" +ERROR_PASSWORD_MISMATCH = "Паролі не збігаються, спробуйте ще раз" +[NOTIFICATIONS] +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/change-password/langs/zh.ini b/plugins/change-password/langs/zh.ini new file mode 100644 index 0000000000..9f902096b7 --- /dev/null +++ b/plugins/change-password/langs/zh.ini @@ -0,0 +1,13 @@ +[SETTINGS_CHANGE_PASSWORD] +LEGEND_CHANGE_PASSWORD = "修改密码" +LABEL_CURRENT_PASSWORD = "当前密码" +LABEL_NEW_PASSWORD = "新密码" +LABEL_REPEAT_PASSWORD = "再次输入新密码" +BUTTON_UPDATE_PASSWORD = "设置新密码" +ERROR_PASSWORD_MISMATCH = "密码不匹配,请再试一次" +[NOTIFICATIONS] +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/change-password/style.css b/plugins/change-password/style.css new file mode 100644 index 0000000000..09788f9812 --- /dev/null +++ b/plugins/change-password/style.css @@ -0,0 +1,5 @@ +.form-horizontal.change-password .control-group { + .control-label { + width: 160px; + } +} diff --git a/plugins/change-password/templates/SettingsChangePassword.html b/plugins/change-password/templates/SettingsChangePassword.html new file mode 100644 index 0000000000..69cda54b12 --- /dev/null +++ b/plugins/change-password/templates/SettingsChangePassword.html @@ -0,0 +1,41 @@ +<div class="b-settings-general g-ui-user-select-none"> + <form action="" spellcheck="false" data-bind="submit: submitForm" class="form-horizontal change-password"> + <div class="legend" data-i18n="SETTINGS_CHANGE_PASSWORD/LEGEND_CHANGE_PASSWORD"></div> + <div class="row"> + <div class="span6"> + <div class="control-group" data-bind="css: {'error': currentPasswordError}"> + <label class="control-label" data-i18n="SETTINGS_CHANGE_PASSWORD/LABEL_CURRENT_PASSWORD"></label> + <div class="controls"> + <input type="password" autocomplete="current-password" autocorrect="off" autocapitalize="off" + data-bind="textInput: currentPassword" /> + </div> + </div> + <div class="control-group" data-bind="css: {'error': passwordMismatch}"> + <label class="control-label" data-i18n="SETTINGS_CHANGE_PASSWORD/LABEL_NEW_PASSWORD"></label> + <div class="controls"> + <input style="margin:0" class="new-password" type="password" autocomplete="new-password" autocorrect="off" autocapitalize="off" + data-bind="textInput: newPassword, attr:{minlength:pass_min_length}" /> + <br/> + <meter style="width:210px" class="new-password-meter" min="0" low="35" optimum="85" high="70" max="100" value="0"></meter> + </div> + </div> + <div class="control-group" data-bind="css: {'error': passwordMismatch}"> + <label class="control-label" data-i18n="SETTINGS_CHANGE_PASSWORD/LABEL_REPEAT_PASSWORD"></label> + <div class="controls"> + <input type="password" autocomplete="new-password" autocorrect="off" autocapitalize="off" + data-bind="textInput: newPassword2" /> + </div> + </div> + <div class="control-group"> + <div class="controls"> + <button class="btn" data-bind="command: saveNewPasswordCommand, css: { 'btn-success': passwordUpdateSuccess, 'btn-danger': passwordUpdateError }"> + <i class="fontastic" data-bind="css: {'icon-spinner': changeProcess()}">🔑</i> + <span class="i18n" data-i18n="SETTINGS_CHANGE_PASSWORD/BUTTON_UPDATE_PASSWORD"></span> + </button> + </div> + </div> + </div> + <div class="span4 alert alert-error alert-null-left-margin" data-bind="visible: '' !== errorDescription(), text: errorDescription"></div> + </div> + </form> +</div> diff --git a/plugins/change-smtp-ehlo-message/VERSION b/plugins/change-smtp-ehlo-message/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/change-smtp-ehlo-message/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/change-smtp-ehlo-message/index.php b/plugins/change-smtp-ehlo-message/index.php index 696ea84b09..093566d6c5 100644 --- a/plugins/change-smtp-ehlo-message/index.php +++ b/plugins/change-smtp-ehlo-message/index.php @@ -2,24 +2,28 @@ class ChangeSmtpEhloMessagePlugin extends \RainLoop\Plugins\AbstractPlugin { - public function Init() + const + NAME = 'Change SMTP EHLO Message', + CATEGORY = 'General', + DESCRIPTION = 'Extension to enable custom SMTP EHLO messages'; + + public function Init() : void { - $this->addHook('filter.smtp-credentials', 'FilterSmtpCredentials'); + $this->addHook('smtp.before-connect', 'FilterSmtpCredentials'); } /** * @param \RainLoop\Model\Account $oAccount * @param array $aSmtpCredentials */ - public function FilterSmtpCredentials($oAccount, &$aSmtpCredentials) + public function FilterSmtpCredentials(\RainLoop\Model\Account $oAccount, + \MailSo\Smtp\SmtpClient $oSmtpClient, + array &$aSmtpCredentials) { - if ($oAccount instanceof \RainLoop\Model\Account && \is_array($aSmtpCredentials)) - { - // Default: - // $aSmtpCredentials['Ehlo'] = \MailSo\Smtp\SmtpClient::EhloHelper(); - // - // or write your custom php - $aSmtpCredentials['Ehlo'] = 'localhost'; - } + // Default: + // $aSmtpCredentials['Ehlo'] = \MailSo\Smtp\SmtpClient::EhloHelper(); + // + // or write your custom php + $aSmtpCredentials['Ehlo'] = 'localhost'; } } diff --git a/plugins/compact-composer/css/composer.css b/plugins/compact-composer/css/composer.css new file mode 100644 index 0000000000..c2afc31c6c --- /dev/null +++ b/plugins/compact-composer/css/composer.css @@ -0,0 +1,97 @@ +/* Prevent collapse of empty elements so that the caret can be placed in there */ +.CompactComposer *:empty::before { + content: "\200B"; +} + +.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 0000000000..c9a1be1f5d --- /dev/null +++ b/plugins/compact-composer/index.php @@ -0,0 +1,22 @@ +<?php + +class CompactComposerPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Compact Composer', + AUTHOR = 'Sergey Mosin', + URL = 'https://github.com/the-djmaze/snappymail/pull/1466', + VERSION = '1.0.6', + RELEASE = '2024-08-08', + REQUIRED = '2.34.0', + LICENSE = 'AGPL v3', + DESCRIPTION = 'WYSIWYG editor with a compact toolbar'; + + public function Init(): void + { + $this->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 0000000000..15ff25d036 --- /dev/null +++ b/plugins/compact-composer/js/CompactComposer.js @@ -0,0 +1,1002 @@ +/* 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') { + // add visible binding to the label + const bodyLabel = vm.querySelector('.tabs label[for="tab-body"]'); + bodyLabel.dataset.bind = 'visible: canMailvelope'; + // re-apply the binding + const labelNext = bodyLabel.nextElementSibling; + ko.removeNode(bodyLabel); + labelNext.parentElement.insertBefore(bodyLabel, labelNext); + ko.applyBindingAccessorsToNode(bodyLabel, null, vm); + + // Now move the attachments tab to the bottom of the screen + const area = vm.querySelector('.tabs .attachmentAreaParent'); + vm.querySelector('.tabs input[value="attachments"]').remove(); + vm.querySelector('.tabs label[for="tab-attachments"]').remove(); + area.querySelector('.no-attachments-desc').remove(); + area.classList.add('compact'); + vm.viewModelDom.append(area); + // Add and re-apply the bindings for the attachment-place + const place = area.querySelector('.b-attachment-place'); + ko.removeNode(place); + area.insertBefore(place, area.firstElementChild); + place.dataset.bind = 'visible: addAttachmentEnabled(), css: {dragAndDropOver: dragAndDropVisible}'; + ko.applyBindingAccessorsToNode(place, null, vm); + // 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 + // 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(/^(<div>\s*<br\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(/<span[^>]*>\s*<\/span>/gi, '') + .trim(); + tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove()); + return tpl.content; + } + }, + + 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', (event) => { + // https://github.com/fastmail/Squire?tab=readme-ov-file#addeventlistener + // The content that will be inserted is available as either the fragment property, or the text property for plain text, on the detail property of the event. You can modify this text/fragment in your event handler to change what will be pasted + tpl.innerHTML = rl.Utils.cleanHtml(event.detail.html).html; + event.detail.fragment = tpl.content; + }); + 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.<string,number>} + */ + 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 <select> + sizeSelectedIndex = fontSizeAction.items.indexOf(fontSize); + } + fontSizeAction.element.selectedIndex = sizeSelectedIndex; + + ignoreNextSelectEvent = true; + }); + + squire.addEventListener('select', e => { + if (ignoreNextSelectEvent) { + ignoreNextSelectEvent = false; + return; + } + + if (e.detail.range.collapsed) { + return; + } + + this.indicators.forEach((indicator) => { + indicator.element.classList.toggle('active', indicator.selectors.some(selector => squire.hasFormat(selector))); + }); + }); + + /* + squire.addEventListener('cursor', e => { + console.dir({cursor:e.range}); + }); + squire.addEventListener('select', e => { + console.dir({select:e.range}); + }); + */ + } + + /** + * @param {Squire} squire + * @param {HTMLDivElement} toolbar + * @returns {Array} + * @private + */ + makeActions(squire, toolbar) { + + const clr = this.makeClr(); + const doClr = name => input => { + // https://github.com/the-djmaze/snappymail/issues/826 + clr.style.left = (input.offsetLeft + input.parentNode.offsetLeft) + 'px'; + clr.style.width = input.offsetWidth + 'px'; + + // firefox does not call "onchange" for #000 if we use clr.value='' + clr.value = '#00ff0c'; + clr.onchange = () => { + switch (name) { + case 'color': + squire.setTextColor(clr.value); + break; + case 'backgroundColor': + squire.setHighlightColor(clr.value); + break; + default: + console.error('invalid name:', name); + } + }; + // Chrome 110+ https://github.com/the-djmaze/snappymail/issues/1199 +// clr.oninput = () => squire.setStyle({[name]:clr.value}); + setTimeout(() => clr.click(), 1); + }; + toolbar.append(clr); + + const browseImage = createElement('input'); + browseImage.type = 'file'; + browseImage.accept = 'image/*'; + browseImage.style.display = 'none'; + browseImage.onchange = () => { + if (browseImage.files.length) { + let reader = new FileReader(); + reader.readAsDataURL(browseImage.files[0]); + reader.onloadend = () => reader.result && squire.insertImage(reader.result); + } + }; + + const actions = [ + { + type: 'group', + items: [ + { + type: 'select', + label: 'Font', + cmd: s => squire.setFontFace(s.value), + prop: 'fontFamily', + items: { + 'sans-serif': { + Arial: '\'Nimbus Sans L\', \'Liberation sans\', \'Arial Unicode MS\', Arial, Helvetica, Garuda, Utkal, FreeSans, sans-serif', + Tahoma: '\'Luxi Sans\', Tahoma, Loma, Geneva, Meera, sans-serif', + Trebuchet: '\'DejaVu Sans Condensed\', Trebuchet, \'Trebuchet MS\', sans-serif', + Lucida: '\'Lucida Sans Unicode\', \'Lucida Sans\', \'DejaVu Sans\', \'Bitstream Vera Sans\', \'DejaVu LGC Sans\', sans-serif', + Verdana: '\'DejaVu Sans\', Verdana, Geneva, \'Bitstream Vera Sans\', \'DejaVu LGC Sans\', sans-serif' + }, + monospace: { + Courier: '\'Liberation Mono\', \'Courier New\', FreeMono, Courier, monospace', + Lucida: '\'DejaVu Sans Mono\', \'DejaVu LGC Sans Mono\', \'Bitstream Vera Sans Mono\', \'Lucida Console\', Monaco, monospace' + }, + sans: { + Times: '\'Nimbus Roman No9 L\', \'Times New Roman\', Times, FreeSerif, serif', + Palatino: '\'Bitstream Charter\', \'Palatino Linotype\', Palatino, Palladio, \'URW Palladio L\', \'Book Antiqua\', Times, serif', + Georgia: '\'URW Palladio L\', Georgia, Times, serif' + } + } + }, + { + type: 'select', + label: 'Font size', + cmd: s => squire.setFontSize(s.value), + prop: 'fontSize', + items: ['11px', '13px', '16px', '20px', '24px', '30px'], + defaultValueIndex: 2 + } + ] + }, + { + type: 'menu', + label: 'Colors', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m8 7.75c0.713 0 1.25-0.559 1.25-1.25 0-0.691-0.54-1.25-1.25-1.25-0.71 0-1.25 0.556-1.25 1.25 0 0.694 0.537 1.25 1.25 1.25zm6.5 3c0.713 0 1.25-0.559 1.25-1.25 0-0.691-0.54-1.25-1.25-1.25s-1.25 0.556-1.25 1.25c0 0.694 0.537 1.25 1.25 1.25zm-9 0c0.713 0 1.25-0.559 1.25-1.25 0-0.691-0.54-1.25-1.25-1.25s-1.25 0.556-1.25 1.25c0 0.694 0.537 1.25 1.25 1.25zm4.5 7.25c-4.47 0-8-3.63-8-8 0-4.81 3.97-8 8.17-8 4.12 0 7.83 3.02 7.83 7.21 0 2.83-2.2 4.79-4.79 4.79h-1.42c-0.277 0-0.417 0.2-0.417 0.375 0 0.208 0.104 0.382 0.312 0.521 0.208 0.139 0.312 0.507 0.312 1.1 0 1.09-0.858 2-2 2zm2-10.2c0.713 0 1.25-0.559 1.25-1.25s-0.54-1.25-1.25-1.25-1.25 0.556-1.25 1.25 0.537 1.25 1.25 1.25zm-2 8.75c0.477 0 0.737-0.739 0.188-1.08-0.226-0.142-0.312-0.514-0.312-1.04 0-1.18 0.934-1.88 1.9-1.88h1.44c2.09-0.032 3.27-1.65 3.29-3.29 0-3.19-2.79-5.71-6.33-5.71-3.74 0-6.67 2.89-6.67 6.5 0 3.49 2.68 6.45 6.5 6.5z"/></svg>', + items: [ + { + type: 'menu_item', + label: 'Text Color', + cmd: doClr('color'), + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 18v-3h14v3zm2.35-5 3.75-10h1.79l3.75 10h-1.73l-0.896-2.56h-4.02l-0.917 2.56zm3.15-4h3l-1.46-4.04h-0.0833z"/></svg>' + }, + { + type: 'menu_item', + label: 'Background Color', + cmd: doClr('backgroundColor'), + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m7.22 16.6-4.87-4.85q-0.166-0.166-0.26-0.364-0.0936-0.198-0.0936-0.427t0.0936-0.447q0.0936-0.218 0.26-0.385l4.6-4.6-2.52-2.52 1.06-1.06 8.18 8.18q0.166 0.166 0.25 0.375 0.0832 0.208 0.0832 0.437 0 0.229-0.0832 0.437-0.0832 0.208-0.25 0.375l-4.85 4.85q-0.166 0.166-0.375 0.26-0.208 0.0936-0.437 0.0936-0.229-0.0208-0.427-0.104-0.198-0.0832-0.364-0.25zm0.791-10-4.35 4.35v-0.0208 0.0208h8.7v-0.0208 0.0208zm8.18 10.3q-0.77 0-1.3-0.52-0.531-0.52-0.531-1.29 0-0.499 0.229-0.967 0.229-0.468 0.541-0.884l1.06-1.33 1.04 1.33q0.291 0.416 0.531 0.884 0.239 0.468 0.239 0.967 0 0.77-0.531 1.29-0.531 0.52-1.28 0.52z"/></svg>' + } + ] + }, + { + type: 'group', + items: [ + { + type: 'button', + label: 'Bold', + cmd: () => this.doAction('bold', 'B'), + key: 'B', + matches: 'B,STRONT', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m5.53 16v-12h4.75q1.4 0 2.57 0.861 1.18 0.861 1.18 2.39 0 1.06-0.469 1.66t-0.885 0.865q0.542 0.249 1.17 0.895 0.625 0.646 0.625 1.9 0 1.9-1.4 2.67-1.4 0.771-2.62 0.771zm2.65-2.46h2.18q1.01 0 1.21-0.51t0.208-0.74q0-0.229-0.219-0.74-0.219-0.51-1.28-0.51h-2.1zm0-4.83h1.94q0.688 0 1.01-0.365 0.323-0.365 0.323-0.781 0-0.5-0.356-0.812-0.356-0.312-0.923-0.312h-1.99z"/></svg>' + }, + { + type: 'button', + label: 'Italic', + cmd: () => this.doAction('italic', 'I'), + key: 'I', + matches: 'I,EM', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m4.5 16v-2h3.33l2.58-8h-3.42v-2h8.5v2h-3.08l-2.58 8h3.17v2z"/></svg>' + }, + { + type: 'button', + label: 'Underline', + cmd: () => this.doAction('underline', 'U'), + key: 'U', + matches: 'U', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m5 17v-1.5h10v1.5zm5-3q-2 0-3.09-1.24t-1.09-3.28v-6.48h2.03v6.61q0 1.1 0.551 1.79 0.551 0.688 1.61 0.688 1.06 0 1.61-0.688 0.55-0.688 0.55-1.79v-6.61h2.02v6.48q0 2.04-1.09 3.28t-3.09 1.24z"/></svg>' + } + ] + }, + { + type: 'group', + items: [ + { + type: 'button', + label: 'Ordered List', + cmd: () => this.doList('OL'), + key: 'Shift + 8', + matches: 'OL', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 17v-1h2v-0.5h-1v-1h1v-0.5h-2v-1h2.5q0.212 0 0.356 0.144t0.144 0.356v1q0 0.212-0.144 0.356t-0.356 0.144q0.212 0 0.356 0.144t0.144 0.356v1q0 0.212-0.144 0.356t-0.356 0.144zm0-5v-2q0-0.212 0.144-0.356t0.356-0.144h1.5v-0.5h-2v-1h2.5q0.212 0 0.356 0.144t0.144 0.356v1.5q0 0.212-0.144 0.356t-0.356 0.144h-1.5v0.5h2v1zm1-5v-3h-1v-1h2v4zm3.5 8v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5z"/></svg>' + }, + { + type: 'button', + label: 'List', + cmd: () => this.doList('UL'), + key: 'Shift + 9', + matches: 'UL', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m7.5 15v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5zm-3 9.25q-0.621 0-1.06-0.442-0.438-0.442-0.438-1.06 0-0.621 0.442-1.06 0.442-0.438 1.06-0.438 0.621 0 1.06 0.442 0.438 0.442 0.438 1.06 0 0.621-0.442 1.06-0.442 0.438-1.06 0.438zm0-4.25q-0.621 0-1.06-0.442-0.438-0.442-0.438-1.06 0-0.621 0.442-1.06 0.442-0.438 1.06-0.438 0.621 0 1.06 0.442 0.438 0.442 0.438 1.06 0 0.621-0.442 1.06-0.442 0.438-1.06 0.438zm0-4.25q-0.621 0-1.06-0.442-0.438-0.442-0.438-1.06 0-0.621 0.442-1.06 0.442-0.438 1.06-0.438 0.621 0 1.06 0.442 0.438 0.442 0.438 1.06 0 0.621-0.442 1.06-0.442 0.438-1.06 0.438z"/></svg>' + }, + { + type: 'button', + label: 'Decrease Indent', + cmd: () => this.changeLevel('decrease'), + key: ']', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 17v-1.5h14v1.5zm6-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm-6-3.12v-1.5h14v1.5zm4 8.5v-6l-4 3z"/></svg>' + }, + { + type: 'button', + label: 'Increase Indent', + cmd: () => this.changeLevel('increase'), + key: '[', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 17v-1.5h14v1.5zm6-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm-6-3.12v-1.5h14v1.5zm0 8.5v-6l4 3z"/></svg>' + } + ] + }, + { + type: 'menu', + rightEdge: true, + label: 'Insert Image', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m15 1v2h-2v2h2v2h2v-2h2v-2h-2v-2zm-12 2c-1.09 0-2 0.909-2 2v10c0 1.09 0.909 2 2 2h14c1.09 0 2-0.909 2-2v-5h-1.75v5.25h-14.5v-10.5h7.25v-1.75zm9 6-3 4-2-3-3 4h12z"/></svg>', + items: [ + { + type: 'menu_item', + label: 'Image File', + cmd: () => browseImage.click(), + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m7 11h8l-2.62-3.5-1.88 2.5-1.37-1.83zm-4.5 6q-0.604 0-1.05-0.448t-0.448-1.05v-10h1.5v10h13.5v1.5zm3-3q-0.604 0-1.05-0.448-0.448-0.448-0.448-1.05v-9q0-0.619 0.448-1.06t1.05-0.441h3.52l2 2h5.48q0.619 0 1.06 0.441 0.441 0.441 0.441 1.06v7q0 0.604-0.441 1.05-0.441 0.448-1.06 0.448zm0-1.5h11v-7h-6.08l-2-2h-2.92zm0 0v-9z"/></svg>' + }, + { + type: 'menu_item', + label: 'From URL', + cmd: () => { + //TODO: check is if an IMG node is in range already + const src = prompt('Image', 'https://'); + if (src) { + this.squire.insertImage(src); + } + }, + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m11.9 8h-1.88c-0.692 0-1.28-0.244-1.77-0.732-0.488-0.488-0.731-1.08-0.731-1.77s0.244-1.28 0.731-1.77 1.08-0.729 1.77-0.729h1.88v0.938h-1.88c-0.434 0-0.803 0.152-1.11 0.456s-0.456 0.673-0.456 1.11 0.152 0.803 0.456 1.11 0.607 0.456 1.11 0.456h1.88zm-1.25-2.03v-0.938h3.75v0.938zm2.5 2.03v-0.938h1.88c0.434 0 0.803-0.152 1.11-0.456s0.456-0.673 0.456-1.11-0.152-0.803-0.456-1.11-0.673-0.456-1.11-0.456h-1.88v-0.938h1.88c0.692 0 1.28 0.244 1.77 0.732 0.488 0.488 0.731 1.08 0.731 1.77 0 0.692-0.244 1.28-0.731 1.77-0.488 0.486-1.08 0.729-1.77 0.729zm3.38 2v5.5c0 0.403-0.147 0.753-0.441 1.05s-0.647 0.448-1.06 0.448h-11c-0.412 0-0.766-0.149-1.06-0.448s-0.441-0.649-0.441-1.05v-9.25c0-0.403 0.147-0.753 0.441-1.05s0.649-0.407 1.06-0.448h1.5v1.5h-1.5v9.25h11v-5.5zm-11.5 4h9l-3-4-2.25 3-1.5-2z"/></svg>' + } + ] + }, + { + // this is a special case: we move the "attach" button group to the toolbar + // TODO: there is probably a better way of doing this in the template + // TODO: move Encrypt/Sign button group ? + type: 'move_parent', + label: 'Attach File', + id: 'composeUploadButton', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m12.5 9.5v-4.5h1.5v4.5zm-3.5 5.44c-1.02-0.272-1.5-1.15-1.5-2.02v-7.92h1.5zm0.5 3.06c-2.59 0-4.5-2.11-4.5-4.65v-8.1c0-1.95 1.55-3.25 3.25-3.25 1.85 0 3.25 1.51 3.25 3.4v6.35h-1.5v-6.5c0-1.09-0.883-1.75-1.75-1.75-1.06 0-1.75 0.906-1.75 1.79v8.21c0 2.63 3.33 4.15 5.25 1.96v1.94c-0.706 0.428-1.56 0.604-2.25 0.604zm3.75-1v-2.25h-2.25v-1.5h2.25v-2.25h1.5v2.25h2.25v1.5h-2.25v2.25z"/></svg>' + }, + { + type: 'menu_more', + label: 'More', + rightEdge: true, + showInPlainMode: true, + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m2 4v2h16v-2zm0 5v2h16v-2zm0 5v2h16v-2z"/></svg>', + items: [ + { + type: 'menu_item', + label: 'Undo', + cmd: () => squire.undo(), + key: 'Z', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m4.82 9.3c1.45-1.26 3.32-2.03 5.4-2.03 3.64 0 6.71 2.37 7.79 5.65l-1.85 0.61c-0.821-2.49-3.17-4.3-5.94-4.3-1.52 0-2.92 0.563-4 1.47l2.83 2.83h-7.04v-7.04z"/></svg>' + }, + { + type: 'menu_item', + label: 'Redo', + cmd: () => squire.redo(), + key: 'Y', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m15.2 9.3c-1.45-1.26-3.32-2.03-5.4-2.03-3.64 0-6.71 2.37-7.79 5.65l1.85 0.61c0.821-2.49 3.17-4.3 5.94-4.3 1.52 0 2.92 0.563 4 1.47l-2.83 2.83h7.04v-7.04z"/></svg>' + }, + { + type: 'menu_item', + label: 'Blockquote', + cmd: () => { + if (!['UL', 'OL', 'BLOCKQUOTE'].some(listTag => this.squire.hasFormat(listTag))) { + this.changeLevel('increase'); + } + }, + matches: 'BLOCKQUOTE', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m4 3c-0.554 0-1 0.446-1 1s0.446 1 1 1h12c0.554 0 1-0.446 1-1s-0.446-1-1-1h-12zm0 6c-0.554 0-1 0.446-1 1v6c0 0.554 0.446 1 1 1s1-0.446 1-1v-6c0-0.554-0.446-1-1-1zm5 0c-0.554 0-1 0.446-1 1s0.446 1 1 1h7c0.554 0 1-0.446 1-1s-0.446-1-1-1h-7zm0 6c-0.554 0-1 0.446-1 1s0.446 1 1 1h7c0.554 0 1-0.446 1-1s-0.446-1-1-1h-7z"/></svg>' + }, + { + type: 'menu_item', + label: 'Link', + cmd: () => { + /** @type {Range} range */ + const range = this.squire.getSelection(); + let linkNode; + if (range.collapsed || range.startContainer.parentNode === range.endContainer.parentNode) { + const root = this.squire.getRoot(); + for (let node = range.startContainer; node !== root; node = node.parentNode) { + if (node.tagName === 'A') { + range.selectNode(node); + linkNode = node; + break; + } + } + } + const url = prompt('Link', linkNode?.href || 'https://'); + if (url != null) { + if (url.length) { + if (range.collapsed === false) { + // squire breaks the wrapping node, so if we have a <b> + // inside the selection it will create something like this: + // <a>t</a><b><a>ex</a></b><a>t</a> and we don't want that + // TODO: this could be more elegant + this.squire.removeAllFormatting(range); + } + this.squire.makeLink(url); + } else if (linkNode) { + this.squire.removeLink(); + } + } + }, + matches: 'A', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m15.4 16.7c-0.509 0.521-1.13 0.781-1.87 0.781-0.735 0-1.36-0.256-1.87-0.771l-1.91-1.91c-0.515-0.515-0.771-1.14-0.771-1.87 0-0.743 0.267-1.38 0.801-1.9l-0.809-0.809c-0.525 0.533-1.16 0.801-1.91 0.801-0.735 0-1.36-0.255-1.87-0.763l-1.9-1.89c-0.521-0.509-0.781-1.13-0.781-1.87-6e-7 -0.735 0.255-1.36 0.763-1.87l1.34-1.35c0.509-0.521 1.13-0.781 1.87-0.781 0.735 1.6e-4 1.36 0.256 1.87 0.771l1.91 1.91c0.515 0.515 0.772 1.14 0.772 1.87 0 0.739-0.265 1.37-0.792 1.9l0.809 0.809c0.524-0.527 1.16-0.792 1.9-0.792 0.735 0 1.36 0.255 1.87 0.763l1.9 1.89c0.521 0.509 0.781 1.13 0.781 1.87 0 0.735-0.255 1.36-0.763 1.87zm-1.25-1.24 1.34-1.35c0.165-0.178 0.248-0.385 0.248-0.624 0-0.245-0.0862-0.455-0.258-0.626l-1.9-1.89c-0.172-0.172-0.38-0.257-0.625-0.257-0.249-3.1e-5 -0.466 0.0966-0.653 0.286l0.577 0.577c0.353 0.353 0.353 0.922 0 1.28-0.353 0.353-0.922 0.353-1.28 0l-0.577-0.577c-0.184 0.182-0.278 0.401-0.278 0.654 0 0.251 0.0824 0.459 0.248 0.624l1.91 1.91c0.172 0.171 0.38 0.257 0.625 0.257 0.239 0 0.444-0.0849 0.615-0.257zm-6.43-6.5-0.577-0.577c-0.353-0.353-0.353-0.922 0-1.28 0.353-0.353 0.922-0.353 1.28 0l0.575 0.575c0.184-0.18 0.279-0.394 0.279-0.643-6e-7 -0.245-0.0849-0.454-0.257-0.625l-1.91-1.91c-0.172-0.172-0.38-0.257-0.625-0.257-0.239 0-0.444 0.0852-0.615 0.257l-1.34 1.35c-0.159 0.172-0.238 0.379-0.238 0.624 2e-7 0.251 0.0811 0.46 0.247 0.625l1.9 1.89c0.172 0.172 0.38 0.257 0.625 0.257 0.253-9e-7 0.473-0.0991 0.661-0.295z"/></svg>' + }, + { + type: 'menu_item', + label: 'Strikethrough', + cmd: () => this.doAction('strikethrough', 'S'), + key: 'Shift + 7', + matches: 'S', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m10.1 15.9q-1.5 0-2.62-0.875t-1.5-2.31l1.69-0.688q0.333 1.06 0.969 1.6 0.635 0.542 1.51 0.542 0.937 0 1.49-0.448 0.552-0.448 0.552-1.2 0-0.333-0.125-0.615t-0.375-0.51h2.15q0.104 0.229 0.135 0.49t0.0312 0.615q0 1.5-1.08 2.45-1.08 0.948-2.81 0.948zm-8.12-6v-1.5h16v1.5zm8-6q1.33 0 2.21 0.562t1.42 1.79l-1.62 0.708q-0.229-0.625-0.76-1.01-0.531-0.385-1.2-0.385-0.771 0-1.28 0.375-0.51 0.375-0.552 0.958h-1.81q0.0417-1.31 1.06-2.16 1.02-0.844 2.54-0.844z"/></svg>' + }, + { + type: 'menu_item', + label: 'Superscript', + cmd: () => this.doAction('superscript', 'SUP'), + key: 'Shift + 6', + matches: 'SUP', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m13.4 8v-1.99q0-0.424 0.288-0.715 0.288-0.291 0.712-0.292h1v-1h-2v-1h2q0.425 0 0.712 0.287 0.288 0.286 0.288 0.71v0.997q0 0.424-0.288 0.715-0.288 0.292-0.712 0.292h-1v1h2v1zm-9.38 8 3.31-5.21-3.08-4.79h1.89l2.21 3.56h0.0833l2.21-3.56h1.9l-3.1 4.79 3.33 5.21h-1.9l-2.44-3.88h-0.0833l-2.44 3.88z"/></svg>' + }, + { + type: 'menu_item', + label: 'Subscript', + cmd: () => this.doAction('subscript', 'SUB'), + key: 'Shift + 5', + matches: 'SUB', + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m13.4 18v-1.99q0-0.424 0.288-0.715 0.288-0.292 0.712-0.292h1v-1h-2v-1h2q0.425 0 0.712 0.287 0.288 0.286 0.288 0.71v0.997q0 0.424-0.288 0.715-0.288 0.292-0.712 0.292h-1v1h2v1zm-9.38-3 3.31-5.21-3.08-4.79h1.89l2.21 3.56h0.0833l2.21-3.56h1.9l-3.1 4.79 3.33 5.21h-1.9l-2.44-3.88h-0.0833l-2.44 3.88z"/></svg>' + }, + { + type: 'menu_item', + label: 'Left to Right', + cmd: () => squire.setTextDirection('ltr'), + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m8 12v-4q-1.25 0-2.12-0.875-0.875-0.875-0.875-2.12t0.875-2.12q0.875-0.875 2.11-0.875h6.01v1.5h-1.5v8.5h-1.5v-8.5h-1.5v8.5zm6 6-1.06-1.06 1.19-1.19h-11.1v-1.5h11.1l-1.19-1.19 1.06-1.06 3 3zm-6-11.5v-3q-0.625 0-1.06 0.442-0.438 0.442-0.438 1.06 0 0.621 0.441 1.06 0.441 0.437 1.06 0.437z"/></svg>' + }, + { + type: 'menu_item', + label: 'Right to Left', + cmd: () => squire.setTextDirection('rtl'), + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m6 18-3-3 3-3 1.06 1.06-1.19 1.19h11.1v1.5h-11.1l1.19 1.19zm2-6v-4q-1.25 0-2.12-0.875-0.875-0.875-0.875-2.12t0.875-2.12q0.875-0.875 2.11-0.875h6.01v1.5h-1.5v8.5h-1.5v-8.5h-1.5v8.5zm0-5.5v-3q-0.625 0-1.06 0.442-0.438 0.442-0.438 1.06 0 0.621 0.441 1.06 0.441 0.437 1.06 0.437z"/></svg>' + }, + { + type: 'menu_item', + label: 'HTML Mode', + id: 'menu-item-mode-wysiwyg', + cmd: () => this.setModeCmd('wysiwyg'), + showInPlainMode: true, + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m2 2v3h1v-2h2v-1zm13 0v1h2v2h1v-3zm-9 3v10h2v-4h4v4h2v-10h-2v4h-4v-4zm-4 10v3h3v-1h-2v-2zm15 0v2h-2v1h3v-3z"/></svg>' + }, + { + type: 'menu_item', + label: 'Edit Source', + id: 'menu-item-mode-source', + cmd: () => this.setModeCmd('source'), + showInPlainMode: true, + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m12 2.83c-0.478-0.138-0.976 0.141-1.11 0.619l-3.6 12.6c-0.138 0.478 0.141 0.976 0.619 1.11 0.478 0.138 0.976-0.141 1.11-0.619l3.6-12.6c0.138-0.478-0.141-0.976-0.619-1.11zm2.27 4.65 2.51 2.51-2.51 2.51c-0.352 0.352-0.352 0.923 0 1.27 0.352 0.352 0.923 0.352 1.27 0l3.15-3.15c0.352-0.352 0.352-0.923 0-1.27l-3.15-3.15c-0.352-0.352-0.923-0.352-1.27-0.00141-0.35 0.35-0.35 0.921 0.00141 1.27zm-8.63-1.27c-0.352-0.352-0.923-0.352-1.27 0l-3.15 3.15c-0.352 0.352-0.352 0.923 0 1.27l3.15 3.15c0.352 0.352 0.923 0.352 1.27 0 0.352-0.352 0.352-0.923 0-1.27l-2.51-2.51 2.51-2.51c0.352-0.352 0.352-0.923 0-1.27z"/></svg>' + }, + { + type: 'menu_item', + label: 'Plain Text Mode', + id: 'menu-item-mode-plain', + cmd: () => this.setModeCmd('plain'), + showInPlainMode: true, + icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m2 2v3h1v-2h2v-1zm13 0v1h2v2h1v-3zm-9 3v2h3v8h2v-8h3v-2zm-4 10v3h3v-1h-2v-2zm15 0v2h-2v1h3v-3z"/></svg>' + } + ] + } + ]; +// // clear: { +// // removeStyle: { +// // html: '⎚', +// // cmd: () => squire.setStyle() +// // } +// // } + + dispatchEvent(new CustomEvent('squire2-toolbar', { + detail: { + squire: this, + actions: actions + } + })); + this.indicators = this.addActionsToParent(actions, toolbar); + return actions; + } + + /** + * @private + */ + makeClr() { + /**@type {HTMLInputElement} clr*/ + const clr = createElement('input'); + clr.type = 'color'; + // Chrome https://github.com/the-djmaze/snappymail/issues/1199 + let clrid = 'squire-colors', + colorlist = doc.getElementById(clrid), + add = hex => colorlist.append(new Option(hex)); + if (!colorlist) { + colorlist = createElement('datalist'); + colorlist.id = clrid; + // Color blind safe Tableau 10 by Maureen Stone + add('#4E79A7'); + add('#F28E2B'); + add('#E15759'); + add('#76B7B2'); + add('#59A14F'); + add('#EDC948'); + add('#B07AA1'); + add('#FF9DA7'); + add('#9C755F'); + add('#BAB0AC'); + doc.body.append(colorlist); + } + clr.setAttribute('list', clrid); + return clr; + } + + /** + * @param {Array} items + * @param {HTMLElement} parent + * @private + */ + addActionsToParent(items, parent) { + const indicators = []; + items.forEach(item => { + let element, event; + switch (item.type) { + case 'group': + const group = createElement('div'); + group.className = 'btn-group'; + if (!item.showInPlainMode) { + group.className += ' squire-html-mode-item'; + } + if (item.items) { + indicators.push(...this.addActionsToParent(item.items, group)); + } + parent.append(group); + return indicators; + case 'menu': + case 'menu_more': + const menuWrap = createElement('div'); + menuWrap.className = 'btn-group dropdown squire-toolbar-menu-wrap'; + menuWrap.title = item.label; + if (!item.showInPlainMode) { + menuWrap.className += ' squire-html-mode-item'; + } + const menuBtn = createElement('button'); + menuBtn.type = 'button'; + menuBtn.className = 'btn dropdown-toggle'; + if (item.icon !== '') { + menuBtn.innerHTML = item.icon; + menuBtn.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon'); + } else { + menuBtn.className += ' fontastic'; + menuBtn.textContent = '☰'; + } + menuWrap.appendChild(menuBtn); + + const menu = createElement('ul'); + menu.className = 'dropdown-menu squire-toolbar-menu'; + if (item.rightEdge) { + menu.className += ' right-edge'; + } + menu.setAttribute('role', 'menu'); + + if (item.items) { + indicators.push(...this.addActionsToParent(item.items, menu)); + } + menuWrap.appendChild(menu); + parent.append(menuWrap); + ko.applyBindingAccessorsToNode(menuWrap, { registerBootstrapDropdown: true }); + item.element = menuWrap; + return indicators; + case 'move_parent': + // we only move into main composer not the signature composer + if (this.container.className.indexOf('e-signature-place') === -1) { + element = doc.getElementById(item.id); + if (element) { + element.className = 'btn'; + if (item.icon) { + element.innerHTML = item.icon; + element.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon'); + } + if (item.label) { + element.title = item.label; + } + element.parentElement.className += ' ' + item.id + '-parent'; + parent.append(element.parentElement); + } + } + return []; + case 'button': + element = createElement('button'); + element.type = 'button'; + element.className = 'btn'; + element.innerHTML = item.icon; + element.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon'); + event = 'click'; + break; + case 'select': + element = createElement('select'); + element.className = 'btn'; + element.innerHTML = item.icon; + event = 'input'; + if (Array.isArray(item.items)) { + item.items.forEach(value => { + value = Array.isArray(value) ? value : [value, value]; + const option = new Option(value[0], value[1]); + option.style[item.prop] = value[1]; + element.append(option); + }); + } else { + Object.entries(item.items).forEach(([label, options]) => { + const optgroup = createElement('optgroup'); + optgroup.label = label; + Object.entries(options).forEach(([text, value]) => { + const option = new Option(text, value); + option.style[item.prop] = value; + optgroup.append(option); + }); + element.append(optgroup); + }); + } + if (item.defaultValueIndex) { + element.selectedIndex = item.defaultValueIndex; + } + item.element = element; + break; + case 'menu_item': + element = createElement('li'); + element.className = 'squire-toolbar-menu-item'; + if (!item.showInPlainMode) { + element.className += ' squire-html-mode-item'; + } + element.innerHTML = item.icon + '<span>' + item.label + '</span>'; + element.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon squire-toolbar-menu-item-icon'); + event = 'click'; + break; + } + + element.title = item.label + (item.key ? ' (' + ctrlKey + item.key + ')' : ''); + element.tabIndex = -1; + element.addEventListener(event, () => item.cmd(element)); + if (item.id) { + element.id = item.id; + } + if (item.matches) { + indicators.push({ + element: element, + selectors: item.matches.split(',') + }); + } + parent.append(element); + }); + return indicators; + } + + /** + * Plugins might add their own pathChange listeners therefore they should + * use this utility function. @see example below + * @param {Object} eventDetail detail of pathChange event + * @returns {Map<any, any>} + */ + buildTokensMap(eventDetail) { + if (!eventDetail.tokensMap) { + const tokensMap = new Map(); + if (eventDetail.path !== '(selection)') { + window.parsel.tokenize(eventDetail.path).forEach(token => { + if (token.type === 'type') { + // token.name is a tag like B, I, UL, etc... + tokensMap.set(token.name, '1'); + } else if (token.name === 'fontFamily') { + // token.value can be a string like '"LucidaSansUnicode","DejaVuSans","BitstreamVeraSans",sans-serif' + tokensMap.set('__font_family__', token.value); + } else if (token.name === 'fontSize') { + // token.value can be a string like '24px' or 'Large' + tokensMap.set('__font_size__', token.value); + } + }); + } else { + tokensMap.set('__selection__', '1'); + } + eventDetail.tokensMap = tokensMap; + } + return eventDetail.tokensMap; + } + + doAction(name, tag) { + if (tag && this.squire.hasFormat(tag)) { + // ex: bold -> removeBold + name = 'remove' + name.charAt(0).toUpperCase() + name.slice(1); + } + this.squire[name](); + } + + doList(type) { + if (this.squire.hasFormat(type)) { + this.squire.removeList(); + return; + } + if (type === 'UL') { + this.squire.makeUnorderedList(); + } else if (type === 'OL') { + this.squire.makeOrderedList(); + } + } + + changeLevel(incDec) { + const type = ['UL', 'OL'].some(listTag => this.squire.hasFormat(listTag)) + ? 'List' + : 'Quote'; + this.squire[incDec + type + 'Level'](); + } + + /* + testPresenceinSelection(format, validation) { + return validation.test(this.squire.getPath()) || this.squire.hasFormat(format); + } + */ + + setModeCmd(mode) { + this.setMode(mode); + setTimeout(() => this.focus(), 1); + } + + setMode(mode) { + if (this.mode !== mode) { + let cl = this.container.classList, + source = 'source' === this.mode; + cl.remove('squire2-mode-' + this.mode); + if ('plain' === mode) { + this.plain.value = htmlToPlain(source ? this.plain.value : this.squire.getHTML(), true); + this.toolbar.classList.add('mode-plain'); + } else if ('source' === mode) { + this.plain.value = this.squire.getHTML(); + this.toolbar.classList.add('mode-plain'); + } else { + this.setData(source ? this.plain.value : plainToHtml(this.plain.value, true)); + mode = 'wysiwyg'; + this.toolbar.classList.remove('mode-plain'); + } + + doc.getElementById('menu-item-mode-' + this.mode)?.classList.remove('active'); + doc.getElementById('menu-item-mode-' + mode).classList.add('active'); + + this.mode = mode; + cl.add('squire2-mode-' + mode); + this.onModeChange?.(); + } + } + + on(type, fn) { + if ('mode' === type) { + this.onModeChange = fn; + } else { + this.squire.addEventListener(type, fn); + this.plain.addEventListener(type, fn); + } + } + + execCommand(cmd, cfg) { + if ('insertSignature' === cmd) { + cfg = Object.assign({ + clearCache: false, + isHtml: false, + insertBefore: false, + signature: '' + }, cfg); + + if (cfg.clearCache) { + this._prev_txt_sig = null; + } else try { + const signature = cfg.isHtml ? htmlToPlain(cfg.signature) : cfg.signature; + if ('plain' === this.mode) { + let + text = this.plain.value, + prevSignature = this._prev_txt_sig; + if (prevSignature) { + text = text.replace(prevSignature, '').trim(); + } + this.plain.value = cfg.insertBefore ? '\n\n' + signature + '\n\n' + text : text + '\n\n' + signature; + } else { + const squire = this.squire, + root = squire.getRoot(), + div = createElement('div'); + div.className = 'rl-signature'; + div.innerHTML = cfg.isHtml ? cfg.signature : plainToHtml(cfg.signature); + root.querySelectorAll('div.rl-signature').forEach(node => node.remove()); + cfg.insertBefore ? root.prepend(div) : root.append(div); + // Move cursor above signature + for (let i = 0; i < 2; i++) { + const divbr = createElement('div'); + divbr.append(createElement('br')); + div.before(divbr); + } + } + this._prev_txt_sig = signature; + } catch (e) { + console.error(e); + } + } + } + + getData() { + return 'source' === this.mode ? this.plain.value : trimLines(this.squire.getHTML()); + } + + setData(html) { +// this.plain.value = html; + const squire = this.squire; + squire.setHTML(trimLines(html)); + const node = squire.getRoot(), + range = squire.getSelection(); + range.setStart(node, 0); + range.setEnd(node, 0); + squire.setSelection(range); + } + + getPlainData() { + return this.plain.value; + } + + setPlainData(text) { + this.plain.value = text; + } + + blur() { + this.squire.blur(); + } + + focus() { + if ('wysiwyg' === this.mode) { + this.squire.focus(); + } else { + this.plain.focus(); + this.plain.setSelectionRange(0, 0); + } + } + } +})(window); diff --git a/plugins/compact-composer/js/parsel.js b/plugins/compact-composer/js/parsel.js new file mode 100644 index 0000000000..bb207bcceb --- /dev/null +++ b/plugins/compact-composer/js/parsel.js @@ -0,0 +1,413 @@ +var parsel = (function (exports) { + 'use strict'; + + const TOKENS = { + attribute: /\[\s*(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)\s*(?:(?<operator>\W?=)\s*(?<value>.+?)\s*(\s(?<caseSensitive>[iIsS]))?\s*)?\]/gu, + id: /#(?<name>[-\w\P{ASCII}]+)/gu, + class: /\.(?<name>[-\w\P{ASCII}]+)/gu, + comma: /\s*,\s*/g, + combinator: /\s*[\s>+~]\s*/g, + 'pseudo-element': /::(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu, + 'pseudo-class': /:(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu, + universal: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?\*/gu, + type: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)/gu, // this must be last + }; + const TRIM_TOKENS = new Set(['combinator', 'comma']); + const RECURSIVE_PSEUDO_CLASSES = new Set([ + 'not', + 'is', + 'where', + 'has', + 'matches', + '-moz-any', + '-webkit-any', + 'nth-child', + 'nth-last-child', + ]); + const nthChildRegExp = /(?<index>[\dn+-]+)\s+of\s+(?<subtree>.+)/; + const RECURSIVE_PSEUDO_CLASSES_ARGS = { + 'nth-child': nthChildRegExp, + 'nth-last-child': nthChildRegExp, + }; + const getArgumentPatternByType = (type) => { + switch (type) { + case 'pseudo-element': + case 'pseudo-class': + return new RegExp(TOKENS[type].source.replace('(?<argument>¶*)', '(?<argument>.*)'), 'gu'); + default: + return TOKENS[type]; + } + }; + function gobbleParens(text, offset) { + let nesting = 0; + let result = ''; + for (; offset < text.length; offset++) { + const char = text[offset]; + switch (char) { + case '(': + ++nesting; + break; + case ')': + --nesting; + break; + } + result += char; + if (nesting === 0) { + return result; + } + } + return result; + } + function tokenizeBy(text, grammar = TOKENS) { + if (!text) { + return []; + } + const tokens = [text]; + for (const [type, pattern] of Object.entries(grammar)) { + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + if (typeof token !== 'string') { + continue; + } + pattern.lastIndex = 0; + const match = pattern.exec(token); + if (!match) { + continue; + } + const from = match.index - 1; + const args = []; + const content = match[0]; + const before = token.slice(0, from + 1); + if (before) { + args.push(before); + } + args.push({ + ...match.groups, + type, + content, + }); + const after = token.slice(from + content.length + 1); + if (after) { + args.push(after); + } + tokens.splice(i, 1, ...args); + } + } + let offset = 0; + for (const token of tokens) { + switch (typeof token) { + case 'string': + throw new Error(`Unexpected sequence ${token} found at index ${offset}`); + case 'object': + offset += token.content.length; + token.pos = [offset - token.content.length, offset]; + if (TRIM_TOKENS.has(token.type)) { + token.content = token.content.trim() || ' '; + } + break; + } + } + return tokens; + } + const STRING_PATTERN = /(['"])([^\\\n]+?)\1/g; + const ESCAPE_PATTERN = /\\./g; + function tokenize(selector, grammar = TOKENS) { + // Prevent leading/trailing whitespaces from being interpreted as combinators + selector = selector.trim(); + if (selector === '') { + return []; + } + const replacements = []; + // Replace escapes with placeholders. + selector = selector.replace(ESCAPE_PATTERN, (value, offset) => { + replacements.push({ value, offset }); + return '\uE000'.repeat(value.length); + }); + // Replace strings with placeholders. + selector = selector.replace(STRING_PATTERN, (value, quote, content, offset) => { + replacements.push({ value, offset }); + return `${quote}${'\uE001'.repeat(content.length)}${quote}`; + }); + // Replace parentheses with placeholders. + { + let pos = 0; + let offset; + while ((offset = selector.indexOf('(', pos)) > -1) { + const value = gobbleParens(selector, offset); + replacements.push({ value, offset }); + selector = `${selector.substring(0, offset)}(${'¶'.repeat(value.length - 2)})${selector.substring(offset + value.length)}`; + pos = offset + value.length; + } + } + // Now we have no nested structures and we can parse with regexes + const tokens = tokenizeBy(selector, grammar); + // Replace placeholders in reverse order. + const changedTokens = new Set(); + for (const replacement of replacements.reverse()) { + for (const token of tokens) { + const { offset, value } = replacement; + if (!(token.pos[0] <= offset && + offset + value.length <= token.pos[1])) { + continue; + } + const { content } = token; + const tokenOffset = offset - token.pos[0]; + token.content = + content.slice(0, tokenOffset) + + value + + content.slice(tokenOffset + value.length); + if (token.content !== content) { + changedTokens.add(token); + } + } + } + // Update changed tokens. + for (const token of changedTokens) { + const pattern = getArgumentPatternByType(token.type); + if (!pattern) { + throw new Error(`Unknown token type: ${token.type}`); + } + pattern.lastIndex = 0; + const match = pattern.exec(token.content); + if (!match) { + throw new Error(`Unable to parse content for ${token.type}: ${token.content}`); + } + Object.assign(token, match.groups); + } + return tokens; + } + /** + * Convert a flat list of tokens into a tree of complex & compound selectors + */ + function nestTokens(tokens, { list = true } = {}) { + if (list && tokens.find((t) => t.type === 'comma')) { + const selectors = []; + const temp = []; + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].type === 'comma') { + if (temp.length === 0) { + throw new Error('Incorrect comma at ' + i); + } + selectors.push(nestTokens(temp, { list: false })); + temp.length = 0; + } + else { + temp.push(tokens[i]); + } + } + if (temp.length === 0) { + throw new Error('Trailing comma'); + } + else { + selectors.push(nestTokens(temp, { list: false })); + } + return { type: 'list', list: selectors }; + } + for (let i = tokens.length - 1; i >= 0; i--) { + let token = tokens[i]; + if (token.type === 'combinator') { + let left = tokens.slice(0, i); + let right = tokens.slice(i + 1); + return { + type: 'complex', + combinator: token.content, + left: nestTokens(left), + right: nestTokens(right), + }; + } + } + switch (tokens.length) { + case 0: + throw new Error('Could not build AST.'); + case 1: + // If we're here, there are no combinators, so it's just a list. + return tokens[0]; + default: + return { + type: 'compound', + list: [...tokens], // clone to avoid pointers messing up the AST + }; + } + } + /** + * Traverse an AST in depth-first order + */ + function* flatten(node, + /** + * @internal + */ + parent) { + switch (node.type) { + case 'list': + for (let child of node.list) { + yield* flatten(child, node); + } + break; + case 'complex': + yield* flatten(node.left, node); + yield* flatten(node.right, node); + break; + case 'compound': + yield* node.list.map((token) => [token, node]); + break; + default: + yield [node, parent]; + } + } + /** + * Traverse an AST (or part thereof), in depth-first order + */ + function walk(node, visit, + /** + * @internal + */ + parent) { + if (!node) { + return; + } + for (const [token, ast] of flatten(node, parent)) { + visit(token, ast); + } + } + /** + * Parse a CSS selector + * + * @param selector - The selector to parse + * @param options.recursive - Whether to parse the arguments of pseudo-classes like :is(), :has() etc. Defaults to true. + * @param options.list - Whether this can be a selector list (A, B, C etc). Defaults to true. + */ + function parse(selector, { recursive = true, list = true } = {}) { + const tokens = tokenize(selector); + if (!tokens) { + return; + } + const ast = nestTokens(tokens, { list }); + if (!recursive) { + return ast; + } + for (const [token] of flatten(ast)) { + if (token.type !== 'pseudo-class' || !token.argument) { + continue; + } + if (!RECURSIVE_PSEUDO_CLASSES.has(token.name)) { + continue; + } + let argument = token.argument; + const childArg = RECURSIVE_PSEUDO_CLASSES_ARGS[token.name]; + if (childArg) { + const match = childArg.exec(argument); + if (!match) { + continue; + } + Object.assign(token, match.groups); + argument = match.groups['subtree']; + } + if (!argument) { + continue; + } + Object.assign(token, { + subtree: parse(argument, { + recursive: true, + list: true, + }), + }); + } + return ast; + } + /** + * Converts the given list or (sub)tree to a string. + */ + function stringify(listOrNode) { + let tokens; + if (Array.isArray(listOrNode)) { + tokens = listOrNode; + } + else { + tokens = [...flatten(listOrNode)].map(([token]) => token); + } + return tokens.map(token => token.content).join(''); + } + /** + * To convert the specificity array to a number + */ + function specificityToNumber(specificity, base) { + base = base || Math.max(...specificity) + 1; + return (specificity[0] * (base << 1) + specificity[1] * base + specificity[2]); + } + /** + * Calculate specificity of a selector. + * + * If the selector is a list, the max specificity is returned. + */ + function specificity(selector) { + let ast = selector; + if (typeof ast === 'string') { + ast = parse(ast, { recursive: true }); + } + if (!ast) { + return []; + } + if (ast.type === 'list' && 'list' in ast) { + let base = 10; + const specificities = ast.list.map((ast) => { + const sp = specificity(ast); + base = Math.max(base, ...specificity(ast)); + return sp; + }); + const numbers = specificities.map((ast) => specificityToNumber(ast, base)); + return specificities[numbers.indexOf(Math.max(...numbers))]; + } + const ret = [0, 0, 0]; + for (const [token] of flatten(ast)) { + switch (token.type) { + case 'id': + ret[0]++; + break; + case 'class': + case 'attribute': + ret[1]++; + break; + case 'pseudo-element': + case 'type': + ret[2]++; + break; + case 'pseudo-class': + if (token.name === 'where') { + break; + } + if (!RECURSIVE_PSEUDO_CLASSES.has(token.name) || + !token.subtree) { + ret[1]++; + break; + } + const sub = specificity(token.subtree); + sub.forEach((s, i) => (ret[i] += s)); + // :nth-child() & :nth-last-child() add (0, 1, 0) to the specificity of their most complex selector + if (token.name === 'nth-child' || + token.name === 'nth-last-child') { + ret[1]++; + } + } + } + return ret; + } + + exports.RECURSIVE_PSEUDO_CLASSES = RECURSIVE_PSEUDO_CLASSES; + exports.RECURSIVE_PSEUDO_CLASSES_ARGS = RECURSIVE_PSEUDO_CLASSES_ARGS; + exports.TOKENS = TOKENS; + exports.TRIM_TOKENS = TRIM_TOKENS; + exports.flatten = flatten; + exports.gobbleParens = gobbleParens; + exports.parse = parse; + exports.specificity = specificity; + exports.specificityToNumber = specificityToNumber; + exports.stringify = stringify; + exports.tokenize = tokenize; + exports.tokenizeBy = tokenizeBy; + exports.walk = walk; + + Object.defineProperty(exports, '__esModule', { value: true }); + + return exports; + +}({})); diff --git a/plugins/compact-composer/js/squire-raw.js b/plugins/compact-composer/js/squire-raw.js new file mode 100644 index 0000000000..512387eaae --- /dev/null +++ b/plugins/compact-composer/js/squire-raw.js @@ -0,0 +1,4120 @@ +"use strict"; +// v2.3.2 +(() => { + // source/node/TreeIterator.ts + var SHOW_ELEMENT = 1; + var SHOW_TEXT = 4; + var SHOW_ELEMENT_OR_TEXT = 5; + var always = () => true; + var TreeIterator = class { + constructor(root, nodeType, filter) { + this.root = root; + this.currentNode = root; + this.nodeType = nodeType; + this.filter = filter || always; + } + isAcceptableNode(node) { + const nodeType = node.nodeType; + const nodeFilterType = nodeType === Node.ELEMENT_NODE ? SHOW_ELEMENT : nodeType === Node.TEXT_NODE ? SHOW_TEXT : 0; + return !!(nodeFilterType & this.nodeType) && this.filter(node); + } + nextNode() { + const root = this.root; + let current = this.currentNode; + let node; + while (true) { + node = current.firstChild; + while (!node && current) { + if (current === root) { + break; + } + node = current.nextSibling; + if (!node) { + current = current.parentNode; + } + } + if (!node) { + return null; + } + if (this.isAcceptableNode(node)) { + this.currentNode = node; + return node; + } + current = node; + } + } + previousNode() { + const root = this.root; + let current = this.currentNode; + let node; + while (true) { + if (current === root) { + return null; + } + node = current.previousSibling; + if (node) { + while (current = node.lastChild) { + node = current; + } + } else { + node = current.parentNode; + } + if (!node) { + return null; + } + if (this.isAcceptableNode(node)) { + this.currentNode = node; + return node; + } + current = node; + } + } + // Previous node in post-order. + previousPONode() { + const root = this.root; + let current = this.currentNode; + let node; + while (true) { + node = current.lastChild; + while (!node && current) { + if (current === root) { + break; + } + node = current.previousSibling; + if (!node) { + current = current.parentNode; + } + } + if (!node) { + return null; + } + if (this.isAcceptableNode(node)) { + this.currentNode = node; + return node; + } + current = node; + } + } + }; + + // source/Constants.ts + var ELEMENT_NODE = 1; + var TEXT_NODE = 3; + var DOCUMENT_FRAGMENT_NODE = 11; + var ZWS = "\u200B"; + var ua = navigator.userAgent; + var isMac = /Mac OS X/.test(ua); + var isWin = /Windows NT/.test(ua); + var isIOS = /iP(?:ad|hone|od)/.test(ua) || isMac && !!navigator.maxTouchPoints; + var isAndroid = /Android/.test(ua); + var isGecko = /Gecko\//.test(ua); + var isLegacyEdge = /Edge\//.test(ua); + var isWebKit = !isLegacyEdge && /WebKit\//.test(ua); + var ctrlKey = isMac || isIOS ? "Meta-" : "Ctrl-"; + var cantFocusEmptyTextNodes = isWebKit; + var supportsInputEvents = "onbeforeinput" in document && "inputType" in new InputEvent("input"); + var notWS = /[^ \t\r\n]/; + + // source/node/Category.ts + var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/; + var leafNodeNames = /* @__PURE__ */ new Set(["BR", "HR", "IFRAME", "IMG", "INPUT"]); + var UNKNOWN = 0; + var INLINE = 1; + var BLOCK = 2; + var CONTAINER = 3; + var cache = /* @__PURE__ */ new WeakMap(); + var resetNodeCategoryCache = () => { + cache = /* @__PURE__ */ new WeakMap(); + }; + var isLeaf = (node) => { + return leafNodeNames.has(node.nodeName); + }; + var getNodeCategory = (node) => { + switch (node.nodeType) { + case TEXT_NODE: + return INLINE; + case ELEMENT_NODE: + case DOCUMENT_FRAGMENT_NODE: + if (cache.has(node)) { + return cache.get(node); + } + break; + default: + return UNKNOWN; + } + let nodeCategory; + if (!Array.from(node.childNodes).every(isInline)) { + nodeCategory = CONTAINER; + } else if (inlineNodeNames.test(node.nodeName)) { + nodeCategory = INLINE; + } else { + nodeCategory = BLOCK; + } + cache.set(node, nodeCategory); + return nodeCategory; + }; + var isInline = (node) => { + return getNodeCategory(node) === INLINE; + }; + var isBlock = (node) => { + return getNodeCategory(node) === BLOCK; + }; + var isContainer = (node) => { + return getNodeCategory(node) === CONTAINER; + }; + + // source/node/Node.ts + var createElement = (tag, props, children) => { + const el = document.createElement(tag); + if (props instanceof Array) { + children = props; + props = null; + } + if (props) { + for (const attr in props) { + const value = props[attr]; + if (value !== void 0) { + el.setAttribute(attr, value); + } + } + } + if (children) { + children.forEach((node) => el.appendChild(node)); + } + return el; + }; + var areAlike = (node, node2) => { + if (isLeaf(node)) { + return false; + } + if (node.nodeType !== node2.nodeType || node.nodeName !== node2.nodeName) { + return false; + } + if (node instanceof HTMLElement && node2 instanceof HTMLElement) { + return node.nodeName !== "A" && node.className === node2.className && node.style.cssText === node2.style.cssText; + } + return true; + }; + var hasTagAttributes = (node, tag, attributes) => { + if (node.nodeName !== tag) { + return false; + } + for (const attr in attributes) { + if (!("getAttribute" in node) || node.getAttribute(attr) !== attributes[attr]) { + return false; + } + } + return true; + }; + var getNearest = (node, root, tag, attributes) => { + while (node && node !== root) { + if (hasTagAttributes(node, tag, attributes)) { + return node; + } + node = node.parentNode; + } + return null; + }; + var getNodeBeforeOffset = (node, offset) => { + let children = node.childNodes; + while (offset && node instanceof Element) { + node = children[offset - 1]; + children = node.childNodes; + offset = children.length; + } + return node; + }; + var getNodeAfterOffset = (node, offset) => { + let returnNode = node; + if (returnNode instanceof Element) { + const children = returnNode.childNodes; + if (offset < children.length) { + returnNode = children[offset]; + } else { + while (returnNode && !returnNode.nextSibling) { + returnNode = returnNode.parentNode; + } + if (returnNode) { + returnNode = returnNode.nextSibling; + } + } + } + return returnNode; + }; + var getLength = (node) => { + return node instanceof Element || node instanceof DocumentFragment ? node.childNodes.length : node instanceof CharacterData ? node.length : 0; + }; + var empty = (node) => { + const frag = document.createDocumentFragment(); + let child = node.firstChild; + while (child) { + frag.appendChild(child); + child = node.firstChild; + } + return frag; + }; + var detach = (node) => { + const parent = node.parentNode; + if (parent) { + parent.removeChild(node); + } + return node; + }; + var replaceWith = (node, node2) => { + const parent = node.parentNode; + if (parent) { + parent.replaceChild(node2, node); + } + }; + + // source/node/Whitespace.ts + var notWSTextNode = (node) => { + return node instanceof Element ? node.nodeName === "BR" : ( + // okay if data is 'undefined' here. + notWS.test(node.data) + ); + }; + var isLineBreak = (br, isLBIfEmptyBlock) => { + let block = br.parentNode; + while (isInline(block)) { + block = block.parentNode; + } + const walker = new TreeIterator( + block, + SHOW_ELEMENT_OR_TEXT, + notWSTextNode + ); + walker.currentNode = br; + return !!walker.nextNode() || isLBIfEmptyBlock && !walker.previousNode(); + }; + var removeZWS = (root, keepNode) => { + const walker = new TreeIterator(root, SHOW_TEXT); + let textNode; + let index; + while (textNode = walker.nextNode()) { + while ((index = textNode.data.indexOf(ZWS)) > -1 && // eslint-disable-next-line no-unmodified-loop-condition + (!keepNode || textNode.parentNode !== keepNode)) { + if (textNode.length === 1) { + let node = textNode; + let parent = node.parentNode; + while (parent) { + parent.removeChild(node); + walker.currentNode = parent; + if (!isInline(parent) || getLength(parent)) { + break; + } + node = parent; + parent = node.parentNode; + } + break; + } else { + textNode.deleteData(index, 1); + } + } + } + }; + + // source/range/Boundaries.ts + var START_TO_START = 0; + var START_TO_END = 1; + var END_TO_END = 2; + var END_TO_START = 3; + var isNodeContainedInRange = (range, node, partial) => { + const nodeRange = document.createRange(); + nodeRange.selectNode(node); + if (partial) { + const nodeEndBeforeStart = range.compareBoundaryPoints(END_TO_START, nodeRange) > -1; + const nodeStartAfterEnd = range.compareBoundaryPoints(START_TO_END, nodeRange) < 1; + return !nodeEndBeforeStart && !nodeStartAfterEnd; + } else { + const nodeStartAfterStart = range.compareBoundaryPoints(START_TO_START, nodeRange) < 1; + const nodeEndBeforeEnd = range.compareBoundaryPoints(END_TO_END, nodeRange) > -1; + return nodeStartAfterStart && nodeEndBeforeEnd; + } + }; + var moveRangeBoundariesDownTree = (range) => { + let { startContainer, startOffset, endContainer, endOffset } = range; + while (!(startContainer instanceof Text)) { + let child = startContainer.childNodes[startOffset]; + if (!child || isLeaf(child)) { + if (startOffset) { + child = startContainer.childNodes[startOffset - 1]; + if (child instanceof Text) { + let textChild = child; + let prev; + while (!textChild.length && (prev = textChild.previousSibling) && prev instanceof Text) { + textChild.remove(); + textChild = prev; + } + startContainer = textChild; + startOffset = textChild.data.length; + } + } + break; + } + startContainer = child; + startOffset = 0; + } + if (endOffset) { + while (!(endContainer instanceof Text)) { + const child = endContainer.childNodes[endOffset - 1]; + if (!child || isLeaf(child)) { + if (child && child.nodeName === "BR" && !isLineBreak(child, false)) { + endOffset -= 1; + continue; + } + break; + } + endContainer = child; + endOffset = getLength(endContainer); + } + } else { + while (!(endContainer instanceof Text)) { + const child = endContainer.firstChild; + if (!child || isLeaf(child)) { + break; + } + endContainer = child; + } + } + range.setStart(startContainer, startOffset); + range.setEnd(endContainer, endOffset); + }; + var moveRangeBoundariesUpTree = (range, startMax, endMax, root) => { + let startContainer = range.startContainer; + let startOffset = range.startOffset; + let endContainer = range.endContainer; + let endOffset = range.endOffset; + let parent; + if (!startMax) { + startMax = range.commonAncestorContainer; + } + if (!endMax) { + endMax = startMax; + } + while (!startOffset && startContainer !== startMax && startContainer !== root) { + parent = startContainer.parentNode; + startOffset = Array.from(parent.childNodes).indexOf( + startContainer + ); + startContainer = parent; + } + while (true) { + if (endContainer === endMax || endContainer === root) { + break; + } + if (endContainer.nodeType !== TEXT_NODE && endContainer.childNodes[endOffset] && endContainer.childNodes[endOffset].nodeName === "BR" && !isLineBreak(endContainer.childNodes[endOffset], false)) { + endOffset += 1; + } + if (endOffset !== getLength(endContainer)) { + break; + } + parent = endContainer.parentNode; + endOffset = Array.from(parent.childNodes).indexOf(endContainer) + 1; + endContainer = parent; + } + range.setStart(startContainer, startOffset); + range.setEnd(endContainer, endOffset); + }; + var moveRangeBoundaryOutOf = (range, tag, root) => { + let parent = getNearest(range.endContainer, root, tag); + if (parent && (parent = parent.parentNode)) { + const clone = range.cloneRange(); + moveRangeBoundariesUpTree(clone, parent, parent, root); + if (clone.endContainer === parent) { + range.setStart(clone.endContainer, clone.endOffset); + range.setEnd(clone.endContainer, clone.endOffset); + } + } + return range; + }; + + // source/node/MergeSplit.ts + var fixCursor = (node) => { + let fixer = null; + if (node instanceof Text) { + return node; + } + if (isInline(node)) { + let child = node.firstChild; + if (cantFocusEmptyTextNodes) { + while (child && child instanceof Text && !child.data) { + node.removeChild(child); + child = node.firstChild; + } + } + if (!child) { + if (cantFocusEmptyTextNodes) { + fixer = document.createTextNode(ZWS); + } else { + fixer = document.createTextNode(""); + } + } + } else if ((node instanceof Element || node instanceof DocumentFragment) && !node.querySelector("BR")) { + fixer = createElement("BR"); + let parent = node; + let child; + while ((child = parent.lastElementChild) && !isInline(child)) { + parent = child; + } + node = parent; + } + if (fixer) { + try { + node.appendChild(fixer); + } catch (error) { + } + } + return node; + }; + var fixContainer = (container, root) => { + let wrapper = null; + Array.from(container.childNodes).forEach((child) => { + const isBR = child.nodeName === "BR"; + if (!isBR && isInline(child)) { + if (!wrapper) { + wrapper = createElement("DIV"); + } + wrapper.appendChild(child); + } else if (isBR || wrapper) { + if (!wrapper) { + wrapper = createElement("DIV"); + } + fixCursor(wrapper); + if (isBR) { + container.replaceChild(wrapper, child); + } else { + container.insertBefore(wrapper, child); + } + wrapper = null; + } + if (isContainer(child)) { + fixContainer(child, root); + } + }); + if (wrapper) { + container.appendChild(fixCursor(wrapper)); + } + return container; + }; + var split = (node, offset, stopNode, root) => { + if (node instanceof Text && node !== stopNode) { + if (typeof offset !== "number") { + throw new Error("Offset must be a number to split text node!"); + } + if (!node.parentNode) { + throw new Error("Cannot split text node with no parent!"); + } + return split(node.parentNode, node.splitText(offset), stopNode, root); + } + let nodeAfterSplit = typeof offset === "number" ? offset < node.childNodes.length ? node.childNodes[offset] : null : offset; + const parent = node.parentNode; + if (!parent || node === stopNode || !(node instanceof Element)) { + return nodeAfterSplit; + } + const clone = node.cloneNode(false); + while (nodeAfterSplit) { + const next = nodeAfterSplit.nextSibling; + clone.appendChild(nodeAfterSplit); + nodeAfterSplit = next; + } + if (node instanceof HTMLOListElement && getNearest(node, root, "BLOCKQUOTE")) { + clone.start = (+node.start || 1) + node.childNodes.length - 1; + } + fixCursor(node); + fixCursor(clone); + parent.insertBefore(clone, node.nextSibling); + return split(parent, clone, stopNode, root); + }; + var _mergeInlines = (node, fakeRange) => { + const children = node.childNodes; + let l = children.length; + const frags = []; + while (l--) { + const child = children[l]; + const prev = l ? children[l - 1] : null; + if (prev && isInline(child) && areAlike(child, prev)) { + if (fakeRange.startContainer === child) { + fakeRange.startContainer = prev; + fakeRange.startOffset += getLength(prev); + } + if (fakeRange.endContainer === child) { + fakeRange.endContainer = prev; + fakeRange.endOffset += getLength(prev); + } + if (fakeRange.startContainer === node) { + if (fakeRange.startOffset > l) { + fakeRange.startOffset -= 1; + } else if (fakeRange.startOffset === l) { + fakeRange.startContainer = prev; + fakeRange.startOffset = getLength(prev); + } + } + if (fakeRange.endContainer === node) { + if (fakeRange.endOffset > l) { + fakeRange.endOffset -= 1; + } else if (fakeRange.endOffset === l) { + fakeRange.endContainer = prev; + fakeRange.endOffset = getLength(prev); + } + } + detach(child); + if (child instanceof Text) { + prev.appendData(child.data); + } else { + frags.push(empty(child)); + } + } else if (child instanceof Element) { + let frag; + while (frag = frags.pop()) { + child.appendChild(frag); + } + _mergeInlines(child, fakeRange); + } + } + }; + var mergeInlines = (node, range) => { + const element = node instanceof Text ? node.parentNode : node; + if (element instanceof Element) { + const fakeRange = { + startContainer: range.startContainer, + startOffset: range.startOffset, + endContainer: range.endContainer, + endOffset: range.endOffset + }; + _mergeInlines(element, fakeRange); + range.setStart(fakeRange.startContainer, fakeRange.startOffset); + range.setEnd(fakeRange.endContainer, fakeRange.endOffset); + } + }; + var mergeWithBlock = (block, next, range, root) => { + let container = next; + let parent; + let offset; + while ((parent = container.parentNode) && parent !== root && parent instanceof Element && parent.childNodes.length === 1) { + container = parent; + } + detach(container); + offset = block.childNodes.length; + const last = block.lastChild; + if (last && last.nodeName === "BR") { + block.removeChild(last); + offset -= 1; + } + block.appendChild(empty(next)); + range.setStart(block, offset); + range.collapse(true); + mergeInlines(block, range); + }; + var mergeContainers = (node, root) => { + const prev = node.previousSibling; + const first = node.firstChild; + const isListItem = node.nodeName === "LI"; + if (isListItem && (!first || !/^[OU]L$/.test(first.nodeName))) { + return; + } + if (prev && areAlike(prev, node)) { + if (!isContainer(prev)) { + if (isListItem) { + const block = createElement("DIV"); + block.appendChild(empty(prev)); + prev.appendChild(block); + } else { + return; + } + } + detach(node); + const needsFix = !isContainer(node); + prev.appendChild(empty(node)); + if (needsFix) { + fixContainer(prev, root); + } + if (first) { + mergeContainers(first, root); + } + } else if (isListItem) { + const block = createElement("DIV"); + node.insertBefore(block, first); + fixCursor(block); + } + }; + + // source/Clean.ts + var styleToSemantic = { + "font-weight": { + regexp: /^bold|^700/i, + replace() { + return createElement("B"); + } + }, + "font-style": { + regexp: /^italic/i, + replace() { + return createElement("I"); + } + }, + "font-family": { + regexp: notWS, + replace(classNames, family) { + return createElement("SPAN", { + class: classNames.fontFamily, + style: "font-family:" + family + }); + } + }, + "font-size": { + regexp: notWS, + replace(classNames, size) { + return createElement("SPAN", { + class: classNames.fontSize, + style: "font-size:" + size + }); + } + }, + "text-decoration": { + regexp: /^underline/i, + replace() { + return createElement("U"); + } + } + }; + var replaceStyles = (node, _, config) => { + const style = node.style; + let newTreeBottom; + let newTreeTop; + for (const attr in styleToSemantic) { + const converter = styleToSemantic[attr]; + const css = style.getPropertyValue(attr); + if (css && converter.regexp.test(css)) { + const el = converter.replace(config.classNames, css); + if (el.nodeName === node.nodeName && el.className === node.className) { + continue; + } + if (!newTreeTop) { + newTreeTop = el; + } + if (newTreeBottom) { + newTreeBottom.appendChild(el); + } + newTreeBottom = el; + node.style.removeProperty(attr); + } + } + if (newTreeTop && newTreeBottom) { + newTreeBottom.appendChild(empty(node)); + if (node.style.cssText) { + node.appendChild(newTreeTop); + } else { + replaceWith(node, newTreeTop); + } + } + return newTreeBottom || node; + }; + var replaceWithTag = (tag) => { + return (node, parent) => { + const el = createElement(tag); + const attributes = node.attributes; + for (let i = 0, l = attributes.length; i < l; i += 1) { + const attribute = attributes[i]; + el.setAttribute(attribute.name, attribute.value); + } + parent.replaceChild(el, node); + el.appendChild(empty(node)); + return el; + }; + }; + var fontSizes = { + "1": "10", + "2": "13", + "3": "16", + "4": "18", + "5": "24", + "6": "32", + "7": "48" + }; + var stylesRewriters = { + STRONG: replaceWithTag("B"), + EM: replaceWithTag("I"), + INS: replaceWithTag("U"), + STRIKE: replaceWithTag("S"), + SPAN: replaceStyles, + FONT: (node, parent, config) => { + const font = node; + const face = font.face; + const size = font.size; + let color = font.color; + const classNames = config.classNames; + let fontSpan; + let sizeSpan; + let colorSpan; + let newTreeBottom; + let newTreeTop; + if (face) { + fontSpan = createElement("SPAN", { + class: classNames.fontFamily, + style: "font-family:" + face + }); + newTreeTop = fontSpan; + newTreeBottom = fontSpan; + } + if (size) { + sizeSpan = createElement("SPAN", { + class: classNames.fontSize, + style: "font-size:" + fontSizes[size] + "px" + }); + if (!newTreeTop) { + newTreeTop = sizeSpan; + } + if (newTreeBottom) { + newTreeBottom.appendChild(sizeSpan); + } + newTreeBottom = sizeSpan; + } + if (color && /^#?([\dA-F]{3}){1,2}$/i.test(color)) { + if (color.charAt(0) !== "#") { + color = "#" + color; + } + colorSpan = createElement("SPAN", { + class: classNames.color, + style: "color:" + color + }); + if (!newTreeTop) { + newTreeTop = colorSpan; + } + if (newTreeBottom) { + newTreeBottom.appendChild(colorSpan); + } + newTreeBottom = colorSpan; + } + if (!newTreeTop || !newTreeBottom) { + newTreeTop = newTreeBottom = createElement("SPAN"); + } + parent.replaceChild(newTreeTop, font); + newTreeBottom.appendChild(empty(font)); + return newTreeBottom; + }, + TT: (node, parent, config) => { + const el = createElement("SPAN", { + class: config.classNames.fontFamily, + style: 'font-family:menlo,consolas,"courier new",monospace' + }); + parent.replaceChild(el, node); + el.appendChild(empty(node)); + return el; + } + }; + var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|IGCAPTION|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|COL(?:GROUP)?|UL)$/; + var blacklist = /^(?:HEAD|META|STYLE)/; + var cleanTree = (node, config, preserveWS) => { + const children = node.childNodes; + let nonInlineParent = node; + while (isInline(nonInlineParent)) { + nonInlineParent = nonInlineParent.parentNode; + } + const walker = new TreeIterator( + nonInlineParent, + SHOW_ELEMENT_OR_TEXT + ); + for (let i = 0, l = children.length; i < l; i += 1) { + let child = children[i]; + const nodeName = child.nodeName; + const rewriter = stylesRewriters[nodeName]; + if (child instanceof HTMLElement) { + const childLength = child.childNodes.length; + if (rewriter) { + child = rewriter(child, node, config); + } else if (blacklist.test(nodeName)) { + node.removeChild(child); + i -= 1; + l -= 1; + continue; + } else if (!allowedBlock.test(nodeName) && !isInline(child)) { + i -= 1; + l += childLength - 1; + node.replaceChild(empty(child), child); + continue; + } + if (childLength) { + cleanTree(child, config, preserveWS || nodeName === "PRE"); + } + } else { + if (child instanceof Text) { + let data = child.data; + const startsWithWS = !notWS.test(data.charAt(0)); + const endsWithWS = !notWS.test(data.charAt(data.length - 1)); + if (preserveWS || !startsWithWS && !endsWithWS) { + continue; + } + if (startsWithWS) { + walker.currentNode = child; + let sibling; + while (sibling = walker.previousPONode()) { + if (sibling.nodeName === "IMG" || sibling instanceof Text && notWS.test(sibling.data)) { + break; + } + if (!isInline(sibling)) { + sibling = null; + break; + } + } + data = data.replace(/^[ \t\r\n]+/g, sibling ? " " : ""); + } + if (endsWithWS) { + walker.currentNode = child; + let sibling; + while (sibling = walker.nextNode()) { + if (sibling.nodeName === "IMG" || sibling instanceof Text && notWS.test(sibling.data)) { + break; + } + if (!isInline(sibling)) { + sibling = null; + break; + } + } + data = data.replace(/[ \t\r\n]+$/g, sibling ? " " : ""); + } + if (data) { + child.data = data; + continue; + } + } + node.removeChild(child); + i -= 1; + l -= 1; + } + } + return node; + }; + var removeEmptyInlines = (node) => { + const children = node.childNodes; + let l = children.length; + while (l--) { + const child = children[l]; + if (child instanceof Element && !isLeaf(child)) { + removeEmptyInlines(child); + if (isInline(child) && !child.firstChild) { + node.removeChild(child); + } + } else if (child instanceof Text && !child.data) { + node.removeChild(child); + } + } + }; + var cleanupBRs = (node, root, keepForBlankLine) => { + const brs = node.querySelectorAll("BR"); + const brBreaksLine = []; + let l = brs.length; + for (let i = 0; i < l; i += 1) { + brBreaksLine[i] = isLineBreak(brs[i], keepForBlankLine); + } + while (l--) { + const br = brs[l]; + const parent = br.parentNode; + if (!parent) { + continue; + } + if (!brBreaksLine[l]) { + detach(br); + } else if (!isInline(parent)) { + fixContainer(parent, root); + } + } + }; + var escapeHTML = (text) => { + return text.split("&").join("&").split("<").join("<").split(">").join(">").split('"').join("""); + }; + + // source/node/Block.ts + var getBlockWalker = (node, root) => { + const walker = new TreeIterator(root, SHOW_ELEMENT, isBlock); + walker.currentNode = node; + return walker; + }; + var getPreviousBlock = (node, root) => { + const block = getBlockWalker(node, root).previousNode(); + return block !== root ? block : null; + }; + var getNextBlock = (node, root) => { + const block = getBlockWalker(node, root).nextNode(); + return block !== root ? block : null; + }; + var isEmptyBlock = (block) => { + return !block.textContent && !block.querySelector("IMG"); + }; + + // source/range/Block.ts + var getStartBlockOfRange = (range, root) => { + const container = range.startContainer; + let block; + if (isInline(container)) { + block = getPreviousBlock(container, root); + } else if (container !== root && container instanceof HTMLElement && isBlock(container)) { + block = container; + } else { + const node = getNodeBeforeOffset(container, range.startOffset); + block = getNextBlock(node, root); + } + return block && isNodeContainedInRange(range, block, true) ? block : null; + }; + var getEndBlockOfRange = (range, root) => { + const container = range.endContainer; + let block; + if (isInline(container)) { + block = getPreviousBlock(container, root); + } else if (container !== root && container instanceof HTMLElement && isBlock(container)) { + block = container; + } else { + let node = getNodeAfterOffset(container, range.endOffset); + if (!node || !root.contains(node)) { + node = root; + let child; + while (child = node.lastChild) { + node = child; + } + } + block = getPreviousBlock(node, root); + } + return block && isNodeContainedInRange(range, block, true) ? block : null; + }; + var isContent = (node) => { + return node instanceof Text ? notWS.test(node.data) : node.nodeName === "IMG"; + }; + var rangeDoesStartAtBlockBoundary = (range, root) => { + const startContainer = range.startContainer; + const startOffset = range.startOffset; + let nodeAfterCursor; + if (startContainer instanceof Text) { + const text = startContainer.data; + for (let i = startOffset; i > 0; i -= 1) { + if (text.charAt(i - 1) !== ZWS) { + return false; + } + } + nodeAfterCursor = startContainer; + } else { + nodeAfterCursor = getNodeAfterOffset(startContainer, startOffset); + if (nodeAfterCursor && !root.contains(nodeAfterCursor)) { + nodeAfterCursor = null; + } + if (!nodeAfterCursor) { + nodeAfterCursor = getNodeBeforeOffset(startContainer, startOffset); + if (nodeAfterCursor instanceof Text && nodeAfterCursor.length) { + return false; + } + } + } + const block = getStartBlockOfRange(range, root); + if (!block) { + return false; + } + const contentWalker = new TreeIterator( + block, + SHOW_ELEMENT_OR_TEXT, + isContent + ); + contentWalker.currentNode = nodeAfterCursor; + return !contentWalker.previousNode(); + }; + var rangeDoesEndAtBlockBoundary = (range, root) => { + const endContainer = range.endContainer; + const endOffset = range.endOffset; + let currentNode; + if (endContainer instanceof Text) { + const text = endContainer.data; + const length = text.length; + for (let i = endOffset; i < length; i += 1) { + if (text.charAt(i) !== ZWS) { + return false; + } + } + currentNode = endContainer; + } else { + currentNode = getNodeBeforeOffset(endContainer, endOffset); + } + const block = getEndBlockOfRange(range, root); + if (!block) { + return false; + } + const contentWalker = new TreeIterator( + block, + SHOW_ELEMENT_OR_TEXT, + isContent + ); + contentWalker.currentNode = currentNode; + return !contentWalker.nextNode(); + }; + var expandRangeToBlockBoundaries = (range, root) => { + const start = getStartBlockOfRange(range, root); + const end = getEndBlockOfRange(range, root); + let parent; + if (start && end) { + parent = start.parentNode; + range.setStart(parent, Array.from(parent.childNodes).indexOf(start)); + parent = end.parentNode; + range.setEnd(parent, Array.from(parent.childNodes).indexOf(end) + 1); + } + }; + + // source/range/InsertDelete.ts + function createRange(startContainer, startOffset, endContainer, endOffset) { + const range = document.createRange(); + range.setStart(startContainer, startOffset); + if (endContainer && typeof endOffset === "number") { + range.setEnd(endContainer, endOffset); + } else { + range.setEnd(startContainer, startOffset); + } + return range; + } + var insertNodeInRange = (range, node) => { + let { startContainer, startOffset, endContainer, endOffset } = range; + let children; + if (startContainer instanceof Text) { + const parent = startContainer.parentNode; + children = parent.childNodes; + if (startOffset === startContainer.length) { + startOffset = Array.from(children).indexOf(startContainer) + 1; + if (range.collapsed) { + endContainer = parent; + endOffset = startOffset; + } + } else { + if (startOffset) { + const afterSplit = startContainer.splitText(startOffset); + if (endContainer === startContainer) { + endOffset -= startOffset; + endContainer = afterSplit; + } else if (endContainer === parent) { + endOffset += 1; + } + startContainer = afterSplit; + } + startOffset = Array.from(children).indexOf( + startContainer + ); + } + startContainer = parent; + } else { + children = startContainer.childNodes; + } + const childCount = children.length; + if (startOffset === childCount) { + startContainer.appendChild(node); + } else { + startContainer.insertBefore(node, children[startOffset]); + } + if (startContainer === endContainer) { + endOffset += children.length - childCount; + } + range.setStart(startContainer, startOffset); + range.setEnd(endContainer, endOffset); + }; + var extractContentsOfRange = (range, common, root) => { + const frag = document.createDocumentFragment(); + if (range.collapsed) { + return frag; + } + if (!common) { + common = range.commonAncestorContainer; + } + if (common instanceof Text) { + common = common.parentNode; + } + const startContainer = range.startContainer; + const startOffset = range.startOffset; + let endContainer = split(range.endContainer, range.endOffset, common, root); + let endOffset = 0; + let node = split(startContainer, startOffset, common, root); + while (node && node !== endContainer) { + const next = node.nextSibling; + frag.appendChild(node); + node = next; + } + node = endContainer && endContainer.previousSibling; + if (node && node instanceof Text && endContainer instanceof Text) { + endOffset = node.length; + node.appendData(endContainer.data); + detach(endContainer); + endContainer = node; + } + range.setStart(startContainer, startOffset); + if (endContainer) { + range.setEnd(endContainer, endOffset); + } else { + range.setEnd(common, common.childNodes.length); + } + fixCursor(common); + return frag; + }; + var getAdjacentInlineNode = (iterator, method, node) => { + iterator.currentNode = node; + let nextNode; + while (nextNode = iterator[method]()) { + if (nextNode instanceof Text || isLeaf(nextNode)) { + return nextNode; + } + if (!isInline(nextNode)) { + return null; + } + } + return null; + }; + var deleteContentsOfRange = (range, root) => { + const startBlock = getStartBlockOfRange(range, root); + let endBlock = getEndBlockOfRange(range, root); + const needsMerge = startBlock !== endBlock; + if (startBlock && endBlock) { + moveRangeBoundariesDownTree(range); + moveRangeBoundariesUpTree(range, startBlock, endBlock, root); + } + const frag = extractContentsOfRange(range, null, root); + moveRangeBoundariesDownTree(range); + if (needsMerge) { + endBlock = getEndBlockOfRange(range, root); + if (startBlock && endBlock && startBlock !== endBlock) { + mergeWithBlock(startBlock, endBlock, range, root); + } + } + if (startBlock) { + fixCursor(startBlock); + } + const child = root.firstChild; + if (!child || child.nodeName === "BR") { + fixCursor(root); + if (root.firstChild) { + range.selectNodeContents(root.firstChild); + } + } + range.collapse(true); + const startContainer = range.startContainer; + const startOffset = range.startOffset; + const iterator = new TreeIterator(root, SHOW_ELEMENT_OR_TEXT); + let afterNode = startContainer; + let afterOffset = startOffset; + if (!(afterNode instanceof Text) || afterOffset === afterNode.data.length) { + afterNode = getAdjacentInlineNode(iterator, "nextNode", afterNode); + afterOffset = 0; + } + let beforeNode = startContainer; + let beforeOffset = startOffset - 1; + if (!(beforeNode instanceof Text) || beforeOffset === -1) { + beforeNode = getAdjacentInlineNode( + iterator, + "previousPONode", + afterNode || (startContainer instanceof Text ? startContainer : startContainer.childNodes[startOffset] || startContainer) + ); + if (beforeNode instanceof Text) { + beforeOffset = beforeNode.data.length; + } + } + let node = null; + let offset = 0; + if (afterNode instanceof Text && afterNode.data.charAt(afterOffset) === " " && rangeDoesStartAtBlockBoundary(range, root)) { + node = afterNode; + offset = afterOffset; + } else if (beforeNode instanceof Text && beforeNode.data.charAt(beforeOffset) === " ") { + if (afterNode instanceof Text && afterNode.data.charAt(afterOffset) === " " || rangeDoesEndAtBlockBoundary(range, root)) { + node = beforeNode; + offset = beforeOffset; + } + } + if (node) { + node.replaceData(offset, 1, "\xA0"); + } + range.setStart(startContainer, startOffset); + range.collapse(true); + return frag; + }; + var insertTreeFragmentIntoRange = (range, frag, root) => { + const firstInFragIsInline = frag.firstChild && isInline(frag.firstChild); + let node; + fixContainer(frag, root); + node = frag; + while (node = getNextBlock(node, root)) { + fixCursor(node); + } + if (!range.collapsed) { + deleteContentsOfRange(range, root); + } + moveRangeBoundariesDownTree(range); + range.collapse(false); + const stopPoint = getNearest(range.endContainer, root, "BLOCKQUOTE") || root; + let block = getStartBlockOfRange(range, root); + let blockContentsAfterSplit = null; + const firstBlockInFrag = getNextBlock(frag, frag); + const replaceBlock = !firstInFragIsInline && !!block && isEmptyBlock(block); + if (block && firstBlockInFrag && !replaceBlock && // Don't merge table cells or PRE elements into block + !getNearest(firstBlockInFrag, frag, "PRE") && !getNearest(firstBlockInFrag, frag, "TABLE")) { + moveRangeBoundariesUpTree(range, block, block, root); + range.collapse(true); + let container = range.endContainer; + let offset = range.endOffset; + cleanupBRs(block, root, false); + if (isInline(container)) { + const nodeAfterSplit = split( + container, + offset, + getPreviousBlock(container, root) || root, + root + ); + container = nodeAfterSplit.parentNode; + offset = Array.from(container.childNodes).indexOf( + nodeAfterSplit + ); + } + if ( + /*isBlock( container ) && */ + offset !== getLength(container) + ) { + blockContentsAfterSplit = document.createDocumentFragment(); + while (node = container.childNodes[offset]) { + blockContentsAfterSplit.appendChild(node); + } + } + mergeWithBlock(container, firstBlockInFrag, range, root); + offset = Array.from(container.parentNode.childNodes).indexOf( + container + ) + 1; + container = container.parentNode; + range.setEnd(container, offset); + } + if (getLength(frag)) { + if (replaceBlock && block) { + range.setEndBefore(block); + range.collapse(false); + detach(block); + } + moveRangeBoundariesUpTree(range, stopPoint, stopPoint, root); + let nodeAfterSplit = split( + range.endContainer, + range.endOffset, + stopPoint, + root + ); + const nodeBeforeSplit = nodeAfterSplit ? nodeAfterSplit.previousSibling : stopPoint.lastChild; + stopPoint.insertBefore(frag, nodeAfterSplit); + if (nodeAfterSplit) { + range.setEndBefore(nodeAfterSplit); + } else { + range.setEnd(stopPoint, getLength(stopPoint)); + } + block = getEndBlockOfRange(range, root); + moveRangeBoundariesDownTree(range); + const container = range.endContainer; + const offset = range.endOffset; + if (nodeAfterSplit && isContainer(nodeAfterSplit)) { + mergeContainers(nodeAfterSplit, root); + } + nodeAfterSplit = nodeBeforeSplit && nodeBeforeSplit.nextSibling; + if (nodeAfterSplit && isContainer(nodeAfterSplit)) { + mergeContainers(nodeAfterSplit, root); + } + range.setEnd(container, offset); + } + if (blockContentsAfterSplit && block) { + const tempRange = range.cloneRange(); + fixCursor(blockContentsAfterSplit); + mergeWithBlock(block, blockContentsAfterSplit, tempRange, root); + range.setEnd(tempRange.endContainer, tempRange.endOffset); + } + moveRangeBoundariesDownTree(range); + }; + + // source/range/Contents.ts + var getTextContentsOfRange = (range) => { + if (range.collapsed) { + return ""; + } + const startContainer = range.startContainer; + const endContainer = range.endContainer; + const walker = new TreeIterator( + range.commonAncestorContainer, + SHOW_ELEMENT_OR_TEXT, + (node2) => { + return isNodeContainedInRange(range, node2, true); + } + ); + walker.currentNode = startContainer; + let node = startContainer; + let textContent = ""; + let addedTextInBlock = false; + let value; + if (!(node instanceof Element) && !(node instanceof Text) || !walker.filter(node)) { + node = walker.nextNode(); + } + while (node) { + if (node instanceof Text) { + value = node.data; + if (value && /\S/.test(value)) { + if (node === endContainer) { + value = value.slice(0, range.endOffset); + } + if (node === startContainer) { + value = value.slice(range.startOffset); + } + textContent += value; + addedTextInBlock = true; + } + } else if (node.nodeName === "BR" || addedTextInBlock && !isInline(node)) { + textContent += "\n"; + addedTextInBlock = false; + } + node = walker.nextNode(); + } + textContent = textContent.replace(/ /g, " "); + return textContent; + }; + + // source/Clipboard.ts + var indexOf = Array.prototype.indexOf; + var extractRangeToClipboard = (event, range, root, removeRangeFromDocument, toCleanHTML, toPlainText, plainTextOnly) => { + const clipboardData = event.clipboardData; + if (isLegacyEdge || !clipboardData) { + return false; + } + let text = toPlainText ? "" : getTextContentsOfRange(range); + const startBlock = getStartBlockOfRange(range, root); + const endBlock = getEndBlockOfRange(range, root); + let copyRoot = root; + if (startBlock === endBlock && (startBlock == null ? void 0 : startBlock.contains(range.commonAncestorContainer))) { + copyRoot = startBlock; + } + let contents; + if (removeRangeFromDocument) { + contents = deleteContentsOfRange(range, root); + } else { + range = range.cloneRange(); + moveRangeBoundariesDownTree(range); + moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root); + contents = range.cloneContents(); + } + let parent = range.commonAncestorContainer; + if (parent instanceof Text) { + parent = parent.parentNode; + } + while (parent && parent !== copyRoot) { + const newContents = parent.cloneNode(false); + newContents.appendChild(contents); + contents = newContents; + parent = parent.parentNode; + } + let html; + if (contents.childNodes.length === 1 && contents.childNodes[0] instanceof Text) { + text = contents.childNodes[0].data.replace(/ /g, " "); + plainTextOnly = true; + } else { + const node = createElement("DIV"); + node.appendChild(contents); + html = node.innerHTML; + if (toCleanHTML) { + html = toCleanHTML(html); + } + } + if (toPlainText && html !== void 0) { + text = toPlainText(html); + } + if (isWin) { + text = text.replace(/\r?\n/g, "\r\n"); + } + if (!plainTextOnly && html && text !== html) { + html = "<!-- squire -->" + html; + clipboardData.setData("text/html", html); + } + clipboardData.setData("text/plain", text); + event.preventDefault(); + return true; + }; + var _onCut = function(event) { + const range = this.getSelection(); + const root = this._root; + if (range.collapsed) { + event.preventDefault(); + return; + } + this.saveUndoState(range); + const handled = extractRangeToClipboard( + event, + range, + root, + true, + this._config.willCutCopy, + this._config.toPlainText, + false + ); + if (!handled) { + setTimeout(() => { + try { + this._ensureBottomLine(); + } catch (error) { + this._config.didError(error); + } + }, 0); + } + this.setSelection(range); + }; + var _onCopy = function(event) { + extractRangeToClipboard( + event, + this.getSelection(), + this._root, + false, + this._config.willCutCopy, + this._config.toPlainText, + false + ); + }; + var _monitorShiftKey = function(event) { + this._isShiftDown = event.shiftKey; + }; + var _onPaste = function(event) { + const clipboardData = event.clipboardData; + const items = clipboardData == null ? void 0 : clipboardData.items; + const choosePlain = this._isShiftDown; + let hasRTF = false; + let hasImage = false; + let plainItem = null; + let htmlItem = null; + if (items) { + let l = items.length; + while (l--) { + const item = items[l]; + const type = item.type; + if (type === "text/html") { + htmlItem = item; + } else if (type === "text/plain" || type === "text/uri-list") { + plainItem = item; + } else if (type === "text/rtf") { + hasRTF = true; + } else if (/^image\/.*/.test(type)) { + hasImage = true; + } + } + if (hasImage && !(hasRTF && htmlItem)) { + event.preventDefault(); + this.fireEvent("pasteImage", { + clipboardData + }); + return; + } + if (!isLegacyEdge) { + event.preventDefault(); + if (htmlItem && (!choosePlain || !plainItem)) { + htmlItem.getAsString((html) => { + this.insertHTML(html, true); + }); + } else if (plainItem) { + plainItem.getAsString((text) => { + let isLink = false; + const range2 = this.getSelection(); + if (!range2.collapsed && notWS.test(range2.toString())) { + const match = this.linkRegExp.exec(text); + isLink = !!match && match[0].length === text.length; + } + if (isLink) { + this.makeLink(text); + } else { + this.insertPlainText(text, true); + } + }); + } + return; + } + } + const types = clipboardData == null ? void 0 : clipboardData.types; + if (!isLegacyEdge && types && (indexOf.call(types, "text/html") > -1 || !isGecko && indexOf.call(types, "text/plain") > -1 && indexOf.call(types, "text/rtf") < 0)) { + event.preventDefault(); + let data; + if (!choosePlain && (data = clipboardData.getData("text/html"))) { + this.insertHTML(data, true); + } else if ((data = clipboardData.getData("text/plain")) || (data = clipboardData.getData("text/uri-list"))) { + this.insertPlainText(data, true); + } + return; + } + const body = document.body; + const range = this.getSelection(); + const startContainer = range.startContainer; + const startOffset = range.startOffset; + const endContainer = range.endContainer; + const endOffset = range.endOffset; + let pasteArea = createElement("DIV", { + contenteditable: "true", + style: "position:fixed; overflow:hidden; top:0; right:100%; width:1px; height:1px;" + }); + body.appendChild(pasteArea); + range.selectNodeContents(pasteArea); + this.setSelection(range); + setTimeout(() => { + try { + let html = ""; + let next = pasteArea; + let first; + while (pasteArea = next) { + next = pasteArea.nextSibling; + detach(pasteArea); + first = pasteArea.firstChild; + if (first && first === pasteArea.lastChild && first instanceof HTMLDivElement) { + pasteArea = first; + } + html += pasteArea.innerHTML; + } + this.setSelection( + createRange( + startContainer, + startOffset, + endContainer, + endOffset + ) + ); + if (html) { + this.insertHTML(html, true); + } + } catch (error) { + this._config.didError(error); + } + }, 0); + }; + var _onDrop = function(event) { + if (!event.dataTransfer) { + return; + } + const types = event.dataTransfer.types; + let l = types.length; + let hasPlain = false; + let hasHTML = false; + while (l--) { + switch (types[l]) { + case "text/plain": + hasPlain = true; + break; + case "text/html": + hasHTML = true; + break; + default: + return; + } + } + if (hasHTML || hasPlain && this.saveUndoState) { + this.saveUndoState(); + } + }; + + // source/keyboard/Enter.ts + var Enter = (self, event, range) => { + event.preventDefault(); + self.splitBlock(event.shiftKey, range); + }; + + // source/keyboard/KeyHelpers.ts + var afterDelete = (self, range) => { + try { + if (!range) { + range = self.getSelection(); + } + let node = range.startContainer; + if (node instanceof Text) { + node = node.parentNode; + } + let parent = node; + while (isInline(parent) && (!parent.textContent || parent.textContent === ZWS)) { + node = parent; + parent = node.parentNode; + } + if (node !== parent) { + range.setStart( + parent, + Array.from(parent.childNodes).indexOf(node) + ); + range.collapse(true); + parent.removeChild(node); + if (!isBlock(parent)) { + parent = getPreviousBlock(parent, self._root) || self._root; + } + fixCursor(parent); + moveRangeBoundariesDownTree(range); + } + if (node === self._root && (node = node.firstChild) && node.nodeName === "BR") { + detach(node); + } + self._ensureBottomLine(); + self.setSelection(range); + self._updatePath(range, true); + } catch (error) { + self._config.didError(error); + } + }; + var detachUneditableNode = (node, root) => { + let parent; + while (parent = node.parentNode) { + if (parent === root || parent.isContentEditable) { + break; + } + node = parent; + } + detach(node); + }; + var linkifyText = (self, textNode, offset) => { + if (getNearest(textNode, self._root, "A")) { + return; + } + const data = textNode.data || ""; + const searchFrom = Math.max( + data.lastIndexOf(" ", offset - 1), + data.lastIndexOf("\xA0", offset - 1) + ) + 1; + const searchText = data.slice(searchFrom, offset); + const match = self.linkRegExp.exec(searchText); + if (match) { + const selection = self.getSelection(); + self._docWasChanged(); + self._recordUndoState(selection); + self._getRangeAndRemoveBookmark(selection); + const index = searchFrom + match.index; + const endIndex = index + match[0].length; + const needsSelectionUpdate = selection.startContainer === textNode; + const newSelectionOffset = selection.startOffset - endIndex; + if (index) { + textNode = textNode.splitText(index); + } + const defaultAttributes = self._config.tagAttributes.a; + const link = createElement( + "A", + Object.assign( + { + href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : "http://" + match[1] : "mailto:" + match[0] + }, + defaultAttributes + ) + ); + link.textContent = data.slice(index, endIndex); + textNode.parentNode.insertBefore(link, textNode); + textNode.data = data.slice(endIndex); + if (needsSelectionUpdate) { + selection.setStart(textNode, newSelectionOffset); + selection.setEnd(textNode, newSelectionOffset); + } + self.setSelection(selection); + } + }; + + // source/keyboard/Backspace.ts + var Backspace = (self, event, range) => { + const root = self._root; + self._removeZWS(); + self.saveUndoState(range); + if (!range.collapsed) { + event.preventDefault(); + deleteContentsOfRange(range, root); + afterDelete(self, range); + } else if (rangeDoesStartAtBlockBoundary(range, root)) { + event.preventDefault(); + const startBlock = getStartBlockOfRange(range, root); + if (!startBlock) { + return; + } + let current = startBlock; + fixContainer(current.parentNode, root); + const previous = getPreviousBlock(current, root); + if (previous) { + if (!previous.isContentEditable) { + detachUneditableNode(previous, root); + return; + } + mergeWithBlock(previous, current, range, root); + current = previous.parentNode; + while (current !== root && !current.nextSibling) { + current = current.parentNode; + } + if (current !== root && (current = current.nextSibling)) { + mergeContainers(current, root); + } + self.setSelection(range); + } else if (current) { + if (getNearest(current, root, "UL") || getNearest(current, root, "OL")) { + self.decreaseListLevel(range); + return; + } else if (getNearest(current, root, "BLOCKQUOTE")) { + self.removeQuote(range); + return; + } + self.setSelection(range); + self._updatePath(range, true); + } + } else { + moveRangeBoundariesDownTree(range); + const text = range.startContainer; + const offset = range.startOffset; + const a = text.parentNode; + if (text instanceof Text && a instanceof HTMLAnchorElement && offset && a.href.includes(text.data)) { + text.deleteData(offset - 1, 1); + self.setSelection(range); + self.removeLink(); + event.preventDefault(); + } else { + self.setSelection(range); + setTimeout(() => { + afterDelete(self); + }, 0); + } + } + }; + + // source/keyboard/Delete.ts + var Delete = (self, event, range) => { + const root = self._root; + let current; + let next; + let originalRange; + let cursorContainer; + let cursorOffset; + let nodeAfterCursor; + self._removeZWS(); + self.saveUndoState(range); + if (!range.collapsed) { + event.preventDefault(); + deleteContentsOfRange(range, root); + afterDelete(self, range); + } else if (rangeDoesEndAtBlockBoundary(range, root)) { + event.preventDefault(); + current = getStartBlockOfRange(range, root); + if (!current) { + return; + } + fixContainer(current.parentNode, root); + next = getNextBlock(current, root); + if (next) { + if (!next.isContentEditable) { + detachUneditableNode(next, root); + return; + } + mergeWithBlock(current, next, range, root); + next = current.parentNode; + while (next !== root && !next.nextSibling) { + next = next.parentNode; + } + if (next !== root && (next = next.nextSibling)) { + mergeContainers(next, root); + } + self.setSelection(range); + self._updatePath(range, true); + } + } else { + originalRange = range.cloneRange(); + moveRangeBoundariesUpTree(range, root, root, root); + cursorContainer = range.endContainer; + cursorOffset = range.endOffset; + if (cursorContainer instanceof Element) { + nodeAfterCursor = cursorContainer.childNodes[cursorOffset]; + if (nodeAfterCursor && nodeAfterCursor.nodeName === "IMG") { + event.preventDefault(); + detach(nodeAfterCursor); + moveRangeBoundariesDownTree(range); + afterDelete(self, range); + return; + } + } + self.setSelection(originalRange); + setTimeout(() => { + afterDelete(self); + }, 0); + } + }; + + // source/keyboard/Tab.ts + var Tab = (self, event, range) => { + const root = self._root; + self._removeZWS(); + if (range.collapsed && rangeDoesStartAtBlockBoundary(range, root)) { + let node = getStartBlockOfRange(range, root); + let parent; + while (parent = node.parentNode) { + if (parent.nodeName === "UL" || parent.nodeName === "OL") { + event.preventDefault(); + self.increaseListLevel(range); + break; + } + node = parent; + } + } + }; + var ShiftTab = (self, event, range) => { + const root = self._root; + self._removeZWS(); + if (range.collapsed && rangeDoesStartAtBlockBoundary(range, root)) { + const node = range.startContainer; + if (getNearest(node, root, "UL") || getNearest(node, root, "OL")) { + event.preventDefault(); + self.decreaseListLevel(range); + } + } + }; + + // source/keyboard/Space.ts + var Space = (self, event, range) => { + var _a; + let node; + const root = self._root; + self._recordUndoState(range); + self._getRangeAndRemoveBookmark(range); + if (!range.collapsed) { + deleteContentsOfRange(range, root); + self._ensureBottomLine(); + self.setSelection(range); + self._updatePath(range, true); + } else if (rangeDoesEndAtBlockBoundary(range, root)) { + const block = getStartBlockOfRange(range, root); + if (block && block.nodeName !== "PRE") { + const text = (_a = block.textContent) == null ? void 0 : _a.trimEnd().replace(ZWS, ""); + if (text === "*" || text === "1.") { + event.preventDefault(); + self.insertPlainText(" ", false); + self._docWasChanged(); + self.saveUndoState(range); + const walker = new TreeIterator(block, SHOW_TEXT); + let textNode; + while (textNode = walker.nextNode()) { + detach(textNode); + } + if (text === "*") { + self.makeUnorderedList(); + } else { + self.makeOrderedList(); + } + return; + } + } + } + node = range.endContainer; + if (range.endOffset === getLength(node)) { + do { + if (node.nodeName === "A") { + range.setStartAfter(node); + break; + } + } while (!node.nextSibling && (node = node.parentNode) && node !== root); + } + if (self._config.addLinks) { + const linkRange = range.cloneRange(); + moveRangeBoundariesDownTree(linkRange); + const textNode = linkRange.startContainer; + const offset = linkRange.startOffset; + setTimeout(() => { + linkifyText(self, textNode, offset); + }, 0); + } + self.setSelection(range); + }; + + // source/keyboard/KeyHandlers.ts + var _onKey = function(event) { + if (event.defaultPrevented || event.isComposing) { + return; + } + let key = event.key; + let modifiers = ""; + const code = event.code; + if (/^Digit\d$/.test(code)) { + key = code.slice(-1); + } + if (key !== "Backspace" && key !== "Delete") { + if (event.altKey) { + modifiers += "Alt-"; + } + if (event.ctrlKey) { + modifiers += "Ctrl-"; + } + if (event.metaKey) { + modifiers += "Meta-"; + } + if (event.shiftKey) { + modifiers += "Shift-"; + } + } + if (isWin && event.shiftKey && key === "Delete") { + modifiers += "Shift-"; + } + key = modifiers + key; + const range = this.getSelection(); + if (this._keyHandlers[key]) { + this._keyHandlers[key](this, event, range); + } else if (!range.collapsed && !event.ctrlKey && !event.metaKey && key.length === 1) { + this.saveUndoState(range); + deleteContentsOfRange(range, this._root); + this._ensureBottomLine(); + this.setSelection(range); + this._updatePath(range, true); + } + }; + var keyHandlers = { + "Backspace": Backspace, + "Delete": Delete, + "Tab": Tab, + "Shift-Tab": ShiftTab, + " ": Space, + "ArrowLeft"(self) { + self._removeZWS(); + }, + "ArrowRight"(self, event, range) { + self._removeZWS(); + const root = self.getRoot(); + if (rangeDoesEndAtBlockBoundary(range, root)) { + moveRangeBoundariesDownTree(range); + let node = range.endContainer; + do { + if (node.nodeName === "CODE") { + let next = node.nextSibling; + if (!(next instanceof Text)) { + const textNode = document.createTextNode("\xA0"); + node.parentNode.insertBefore(textNode, next); + next = textNode; + } + range.setStart(next, 1); + self.setSelection(range); + event.preventDefault(); + break; + } + } while (!node.nextSibling && (node = node.parentNode) && node !== root); + } + } + }; + if (!supportsInputEvents) { + keyHandlers.Enter = Enter; + keyHandlers["Shift-Enter"] = Enter; + } + if (!isMac && !isIOS) { + keyHandlers.PageUp = (self) => { + self.moveCursorToStart(); + }; + keyHandlers.PageDown = (self) => { + self.moveCursorToEnd(); + }; + } + var mapKeyToFormat = (tag, remove) => { + remove = remove || null; + return (self, event) => { + event.preventDefault(); + const range = self.getSelection(); + if (self.hasFormat(tag, null, range)) { + self.changeFormat(null, { tag }, range); + } else { + self.changeFormat({ tag }, remove, range); + } + }; + }; + keyHandlers[ctrlKey + "b"] = mapKeyToFormat("B"); + keyHandlers[ctrlKey + "i"] = mapKeyToFormat("I"); + keyHandlers[ctrlKey + "u"] = mapKeyToFormat("U"); + keyHandlers[ctrlKey + "Shift-7"] = mapKeyToFormat("S"); + keyHandlers[ctrlKey + "Shift-5"] = mapKeyToFormat("SUB", { tag: "SUP" }); + keyHandlers[ctrlKey + "Shift-6"] = mapKeyToFormat("SUP", { tag: "SUB" }); + keyHandlers[ctrlKey + "Shift-8"] = (self, event) => { + event.preventDefault(); + const path = self.getPath(); + if (!/(?:^|>)UL/.test(path)) { + self.makeUnorderedList(); + } else { + self.removeList(); + } + }; + keyHandlers[ctrlKey + "Shift-9"] = (self, event) => { + event.preventDefault(); + const path = self.getPath(); + if (!/(?:^|>)OL/.test(path)) { + self.makeOrderedList(); + } else { + self.removeList(); + } + }; + keyHandlers[ctrlKey + "["] = (self, event) => { + event.preventDefault(); + const path = self.getPath(); + if (/(?:^|>)BLOCKQUOTE/.test(path) || !/(?:^|>)[OU]L/.test(path)) { + self.decreaseQuoteLevel(); + } else { + self.decreaseListLevel(); + } + }; + keyHandlers[ctrlKey + "]"] = (self, event) => { + event.preventDefault(); + const path = self.getPath(); + if (/(?:^|>)BLOCKQUOTE/.test(path) || !/(?:^|>)[OU]L/.test(path)) { + self.increaseQuoteLevel(); + } else { + self.increaseListLevel(); + } + }; + keyHandlers[ctrlKey + "d"] = (self, event) => { + event.preventDefault(); + self.toggleCode(); + }; + keyHandlers[ctrlKey + "z"] = (self, event) => { + event.preventDefault(); + self.undo(); + }; + keyHandlers[ctrlKey + "y"] = // Depending on platform, the Shift may cause the key to come through as + // upper case, but sometimes not. Just add both as shortcuts — the browser + // will only ever fire one or the other. + keyHandlers[ctrlKey + "Shift-z"] = keyHandlers[ctrlKey + "Shift-Z"] = (self, event) => { + event.preventDefault(); + self.redo(); + }; + + // source/Editor.ts + var Squire = class { + constructor(root, config) { + /** + * Subscribing to these events won't automatically add a listener to the + * document node, since these events are fired in a custom manner by the + * editor code. + */ + this.customEvents = /* @__PURE__ */ new Set([ + "pathChange", + "select", + "input", + "pasteImage", + "undoStateChange" + ]); + // --- + this.startSelectionId = "squire-selection-start"; + this.endSelectionId = "squire-selection-end"; + /* + linkRegExp = new RegExp( + // Only look on boundaries + '\\b(?:' + + // Capture group 1: URLs + '(' + + // Add links to URLS + // Starts with: + '(?:' + + // http(s):// or ftp:// + '(?:ht|f)tps?:\\/\\/' + + // or + '|' + + // www. + 'www\\d{0,3}[.]' + + // or + '|' + + // foo90.com/ + '[a-z0-9][a-z0-9.\\-]*[.][a-z]{2,}\\/' + + ')' + + // Then we get one or more: + '(?:' + + // Run of non-spaces, non ()<> + '[^\\s()<>]+' + + // or + '|' + + // balanced parentheses (one level deep only) + '\\([^\\s()<>]+\\)' + + ')+' + + // And we finish with + '(?:' + + // Not a space or punctuation character + '[^\\s?&`!()\\[\\]{};:\'".,<>«»“”‘’]' + + // or + '|' + + // Balanced parentheses. + '\\([^\\s()<>]+\\)' + + ')' + + // Capture group 2: Emails + ')|(' + + // Add links to emails + '[\\w\\-.%+]+@(?:[\\w\\-]+\\.)+[a-z]{2,}\\b' + + // Allow query parameters in the mailto: style + '(?:' + + '[?][^&?\\s]+=[^\\s?&`!()\\[\\]{};:\'".,<>«»“”‘’]+' + + '(?:&[^&?\\s]+=[^\\s?&`!()\\[\\]{};:\'".,<>«»“”‘’]+)*' + + ')?' + + '))', + 'i' + ); + */ + this.linkRegExp = /\b(?:((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9][a-z0-9.\-]*[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:[^\s?&`!()\[\]{};:'".,<>«»“”‘’]|\([^\s()<>]+\)))|([\w\-.%+]+@(?:[\w\-]+\.)+[a-z]{2,}\b(?:[?][^&?\s]+=[^\s?&`!()\[\]{};:'".,<>«»“”‘’]+(?:&[^&?\s]+=[^\s?&`!()\[\]{};:'".,<>«»“”‘’]+)*)?))/i; + this.tagAfterSplit = { + DT: "DD", + DD: "DT", + LI: "LI", + PRE: "PRE" + }; + this._root = root; + this._config = this._makeConfig(config); + this._isFocused = false; + this._lastSelection = createRange(root, 0); + this._willRestoreSelection = false; + this._mayHaveZWS = false; + this._lastAnchorNode = null; + this._lastFocusNode = null; + this._path = ""; + this._events = /* @__PURE__ */ new Map(); + this._undoIndex = -1; + this._undoStack = []; + this._undoStackLength = 0; + this._isInUndoState = false; + this._ignoreChange = false; + this._ignoreAllChanges = false; + this.addEventListener("selectionchange", this._updatePathOnEvent); + this.addEventListener("blur", this._enableRestoreSelection); + this.addEventListener("mousedown", this._disableRestoreSelection); + this.addEventListener("touchstart", this._disableRestoreSelection); + this.addEventListener("focus", this._restoreSelection); + this.addEventListener("blur", this._removeZWS); + this._isShiftDown = false; + this.addEventListener("cut", _onCut); + this.addEventListener("copy", _onCopy); + this.addEventListener("paste", _onPaste); + this.addEventListener("drop", _onDrop); + this.addEventListener( + "keydown", + _monitorShiftKey + ); + this.addEventListener("keyup", _monitorShiftKey); + this.addEventListener("keydown", _onKey); + this._keyHandlers = Object.create(keyHandlers); + const mutation = new MutationObserver(() => this._docWasChanged()); + mutation.observe(root, { + childList: true, + attributes: true, + characterData: true, + subtree: true + }); + this._mutation = mutation; + root.setAttribute("contenteditable", "true"); + this.addEventListener( + "beforeinput", + this._beforeInput + ); + this.setHTML(""); + } + destroy() { + this._events.forEach((_, type) => { + this.removeEventListener(type); + }); + this._mutation.disconnect(); + this._undoIndex = -1; + this._undoStack = []; + this._undoStackLength = 0; + } + _makeConfig(userConfig) { + const config = { + blockTag: "DIV", + blockAttributes: null, + tagAttributes: {}, + classNames: { + color: "color", + fontFamily: "font", + fontSize: "size", + highlight: "highlight" + }, + undo: { + documentSizeThreshold: -1, + // -1 means no threshold + undoLimit: -1 + // -1 means no limit + }, + addLinks: true, + willCutCopy: null, + toPlainText: null, + sanitizeToDOMFragment: (html) => { + const frag = DOMPurify.sanitize(html, { + ALLOW_UNKNOWN_PROTOCOLS: true, + WHOLE_DOCUMENT: false, + RETURN_DOM: true, + RETURN_DOM_FRAGMENT: true, + FORCE_BODY: false + }); + return frag ? document.importNode(frag, true) : document.createDocumentFragment(); + }, + didError: (error) => console.log(error) + }; + if (userConfig) { + Object.assign(config, userConfig); + config.blockTag = config.blockTag.toUpperCase(); + } + return config; + } + setKeyHandler(key, fn) { + this._keyHandlers[key] = fn; + return this; + } + _beforeInput(event) { + switch (event.inputType) { + case "insertLineBreak": + event.preventDefault(); + this.splitBlock(true); + break; + case "insertParagraph": + event.preventDefault(); + this.splitBlock(false); + break; + case "insertOrderedList": + event.preventDefault(); + this.makeOrderedList(); + break; + case "insertUnoderedList": + event.preventDefault(); + this.makeUnorderedList(); + break; + case "historyUndo": + event.preventDefault(); + this.undo(); + break; + case "historyRedo": + event.preventDefault(); + this.redo(); + break; + case "formatBold": + event.preventDefault(); + this.bold(); + break; + case "formaItalic": + event.preventDefault(); + this.italic(); + break; + case "formatUnderline": + event.preventDefault(); + this.underline(); + break; + case "formatStrikeThrough": + event.preventDefault(); + this.strikethrough(); + break; + case "formatSuperscript": + event.preventDefault(); + this.superscript(); + break; + case "formatSubscript": + event.preventDefault(); + this.subscript(); + break; + case "formatJustifyFull": + case "formatJustifyCenter": + case "formatJustifyRight": + case "formatJustifyLeft": { + event.preventDefault(); + let alignment = event.inputType.slice(13).toLowerCase(); + if (alignment === "full") { + alignment = "justify"; + } + this.setTextAlignment(alignment); + break; + } + case "formatRemove": + event.preventDefault(); + this.removeAllFormatting(); + break; + case "formatSetBlockTextDirection": { + event.preventDefault(); + let dir = event.data; + if (dir === "null") { + dir = null; + } + this.setTextDirection(dir); + break; + } + case "formatBackColor": + event.preventDefault(); + this.setHighlightColor(event.data); + break; + case "formatFontColor": + event.preventDefault(); + this.setTextColor(event.data); + break; + case "formatFontName": + event.preventDefault(); + this.setFontFace(event.data); + break; + } + } + // --- Events + handleEvent(event) { + this.fireEvent(event.type, event); + } + fireEvent(type, detail) { + let handlers = this._events.get(type); + if (/^(?:focus|blur)/.test(type)) { + const isFocused = this._root === document.activeElement; + if (type === "focus") { + if (!isFocused || this._isFocused) { + return this; + } + this._isFocused = true; + } else { + if (isFocused || !this._isFocused) { + return this; + } + this._isFocused = false; + } + } + if (handlers) { + const event = detail instanceof Event ? detail : new CustomEvent(type, { + detail + }); + handlers = handlers.slice(); + for (const handler of handlers) { + try { + if ("handleEvent" in handler) { + handler.handleEvent(event); + } else { + handler.call(this, event); + } + } catch (error) { + this._config.didError(error); + } + } + } + return this; + } + addEventListener(type, fn) { + let handlers = this._events.get(type); + let target = this._root; + if (!handlers) { + handlers = []; + this._events.set(type, handlers); + if (!this.customEvents.has(type)) { + if (type === "selectionchange") { + target = document; + } + target.addEventListener(type, this, true); + } + } + handlers.push(fn); + return this; + } + removeEventListener(type, fn) { + const handlers = this._events.get(type); + let target = this._root; + if (handlers) { + if (fn) { + let l = handlers.length; + while (l--) { + if (handlers[l] === fn) { + handlers.splice(l, 1); + } + } + } else { + handlers.length = 0; + } + if (!handlers.length) { + this._events.delete(type); + if (!this.customEvents.has(type)) { + if (type === "selectionchange") { + target = document; + } + target.removeEventListener(type, this, true); + } + } + } + return this; + } + // --- Focus + focus() { + this._root.focus({ preventScroll: true }); + return this; + } + blur() { + this._root.blur(); + return this; + } + // --- Selection and bookmarking + _enableRestoreSelection() { + this._willRestoreSelection = true; + } + _disableRestoreSelection() { + this._willRestoreSelection = false; + } + _restoreSelection() { + if (this._willRestoreSelection) { + this.setSelection(this._lastSelection); + } + } + // --- + _removeZWS() { + if (!this._mayHaveZWS) { + return; + } + removeZWS(this._root); + this._mayHaveZWS = false; + } + _saveRangeToBookmark(range) { + let startNode = createElement("INPUT", { + id: this.startSelectionId, + type: "hidden" + }); + let endNode = createElement("INPUT", { + id: this.endSelectionId, + type: "hidden" + }); + let temp; + insertNodeInRange(range, startNode); + range.collapse(false); + insertNodeInRange(range, endNode); + if (startNode.compareDocumentPosition(endNode) & Node.DOCUMENT_POSITION_PRECEDING) { + startNode.id = this.endSelectionId; + endNode.id = this.startSelectionId; + temp = startNode; + startNode = endNode; + endNode = temp; + } + range.setStartAfter(startNode); + range.setEndBefore(endNode); + } + _getRangeAndRemoveBookmark(range) { + const root = this._root; + const start = root.querySelector("#" + this.startSelectionId); + const end = root.querySelector("#" + this.endSelectionId); + if (start && end) { + let startContainer = start.parentNode; + let endContainer = end.parentNode; + const startOffset = Array.from(startContainer.childNodes).indexOf( + start + ); + let endOffset = Array.from(endContainer.childNodes).indexOf(end); + if (startContainer === endContainer) { + endOffset -= 1; + } + start.remove(); + end.remove(); + if (!range) { + range = document.createRange(); + } + range.setStart(startContainer, startOffset); + range.setEnd(endContainer, endOffset); + mergeInlines(startContainer, range); + if (startContainer !== endContainer) { + mergeInlines(endContainer, range); + } + if (range.collapsed) { + startContainer = range.startContainer; + if (startContainer instanceof Text) { + endContainer = startContainer.childNodes[range.startOffset]; + if (!endContainer || !(endContainer instanceof Text)) { + endContainer = startContainer.childNodes[range.startOffset - 1]; + } + if (endContainer && endContainer instanceof Text) { + range.setStart(endContainer, 0); + range.collapse(true); + } + } + } + } + return range || null; + } + getSelection() { + const selection = window.getSelection(); + const root = this._root; + let range = null; + if (this._isFocused && selection && selection.rangeCount) { + range = selection.getRangeAt(0).cloneRange(); + const startContainer = range.startContainer; + const endContainer = range.endContainer; + if (startContainer && isLeaf(startContainer)) { + range.setStartBefore(startContainer); + } + if (endContainer && isLeaf(endContainer)) { + range.setEndBefore(endContainer); + } + } + if (range && root.contains(range.commonAncestorContainer)) { + this._lastSelection = range; + } else { + range = this._lastSelection; + if (!document.contains(range.commonAncestorContainer)) { + range = null; + } + } + if (!range) { + range = createRange(root.firstElementChild || root, 0); + } + return range; + } + setSelection(range) { + this._lastSelection = range; + if (!this._isFocused) { + this._enableRestoreSelection(); + } else { + const selection = window.getSelection(); + if (selection) { + if ("setBaseAndExtent" in Selection.prototype) { + selection.setBaseAndExtent( + range.startContainer, + range.startOffset, + range.endContainer, + range.endOffset + ); + } else { + selection.removeAllRanges(); + selection.addRange(range); + } + } + } + return this; + } + // --- + _moveCursorTo(toStart) { + const root = this._root; + const range = createRange(root, toStart ? 0 : root.childNodes.length); + moveRangeBoundariesDownTree(range); + this.setSelection(range); + return this; + } + moveCursorToStart() { + return this._moveCursorTo(true); + } + moveCursorToEnd() { + return this._moveCursorTo(false); + } + // --- + getCursorPosition() { + const range = this.getSelection(); + let rect = range.getBoundingClientRect(); + if (rect && !rect.top) { + this._ignoreChange = true; + const node = createElement("SPAN"); + node.textContent = ZWS; + insertNodeInRange(range, node); + rect = node.getBoundingClientRect(); + const parent = node.parentNode; + parent.removeChild(node); + mergeInlines(parent, range); + } + return rect; + } + // --- Path + getPath() { + return this._path; + } + _updatePathOnEvent() { + if (this._isFocused) { + this._updatePath(this.getSelection()); + } + } + _updatePath(range, force) { + const anchor = range.startContainer; + const focus = range.endContainer; + let newPath; + if (force || anchor !== this._lastAnchorNode || focus !== this._lastFocusNode) { + this._lastAnchorNode = anchor; + this._lastFocusNode = focus; + newPath = anchor && focus ? anchor === focus ? this._getPath(focus) : "(selection)" : ""; + if (this._path !== newPath || anchor !== focus) { + this._path = newPath; + this.fireEvent("pathChange", { + path: newPath + }); + } + } + this.fireEvent(range.collapsed ? "cursor" : "select", { + range + }); + } + _getPath(node) { + const root = this._root; + const config = this._config; + let path = ""; + if (node && node !== root) { + const parent = node.parentNode; + path = parent ? this._getPath(parent) : ""; + if (node instanceof HTMLElement) { + const id = node.id; + const classList = node.classList; + const classNames = Array.from(classList).sort(); + const dir = node.dir; + const styleNames = config.classNames; + path += (path ? ">" : "") + node.nodeName; + if (id) { + path += "#" + id; + } + if (classNames.length) { + path += "."; + path += classNames.join("."); + } + if (dir) { + path += "[dir=" + dir + "]"; + } + if (classList.contains(styleNames.highlight)) { + path += "[backgroundColor=" + node.style.backgroundColor.replace(/ /g, "") + "]"; + } + if (classList.contains(styleNames.color)) { + path += "[color=" + node.style.color.replace(/ /g, "") + "]"; + } + if (classList.contains(styleNames.fontFamily)) { + path += "[fontFamily=" + node.style.fontFamily.replace(/ /g, "") + "]"; + } + if (classList.contains(styleNames.fontSize)) { + path += "[fontSize=" + node.style.fontSize + "]"; + } + } + } + return path; + } + // --- History + modifyDocument(modificationFn) { + const mutation = this._mutation; + if (mutation) { + if (mutation.takeRecords().length) { + this._docWasChanged(); + } + mutation.disconnect(); + } + this._ignoreAllChanges = true; + modificationFn(); + this._ignoreAllChanges = false; + if (mutation) { + mutation.observe(this._root, { + childList: true, + attributes: true, + characterData: true, + subtree: true + }); + this._ignoreChange = false; + } + return this; + } + _docWasChanged() { + resetNodeCategoryCache(); + this._mayHaveZWS = true; + if (this._ignoreAllChanges) { + return; + } + if (this._ignoreChange) { + this._ignoreChange = false; + return; + } + if (this._isInUndoState) { + this._isInUndoState = false; + this.fireEvent("undoStateChange", { + canUndo: true, + canRedo: false + }); + } + this.fireEvent("input"); + } + /** + * Leaves bookmark. + */ + _recordUndoState(range, replace) { + const isInUndoState = this._isInUndoState; + if (!isInUndoState || replace) { + let undoIndex = this._undoIndex + 1; + const undoStack = this._undoStack; + const undoConfig = this._config.undo; + const undoThreshold = undoConfig.documentSizeThreshold; + const undoLimit = undoConfig.undoLimit; + if (undoIndex < this._undoStackLength) { + undoStack.length = this._undoStackLength = undoIndex; + } + if (range) { + this._saveRangeToBookmark(range); + } + if (isInUndoState) { + return this; + } + const html = this._getRawHTML(); + if (replace) { + undoIndex -= 1; + } + if (undoThreshold > -1 && html.length * 2 > undoThreshold) { + if (undoLimit > -1 && undoIndex > undoLimit) { + undoStack.splice(0, undoIndex - undoLimit); + undoIndex = undoLimit; + this._undoStackLength = undoLimit; + } + } + undoStack[undoIndex] = html; + this._undoIndex = undoIndex; + this._undoStackLength += 1; + this._isInUndoState = true; + } + return this; + } + saveUndoState(range) { + if (!range) { + range = this.getSelection(); + } + this._recordUndoState(range, this._isInUndoState); + this._getRangeAndRemoveBookmark(range); + return this; + } + undo() { + if (this._undoIndex !== 0 || !this._isInUndoState) { + this._recordUndoState(this.getSelection(), false); + this._undoIndex -= 1; + this._setRawHTML(this._undoStack[this._undoIndex]); + const range = this._getRangeAndRemoveBookmark(); + if (range) { + this.setSelection(range); + } + this._isInUndoState = true; + this.fireEvent("undoStateChange", { + canUndo: this._undoIndex !== 0, + canRedo: true + }); + this.fireEvent("input"); + } + return this.focus(); + } + redo() { + const undoIndex = this._undoIndex; + const undoStackLength = this._undoStackLength; + if (undoIndex + 1 < undoStackLength && this._isInUndoState) { + this._undoIndex += 1; + this._setRawHTML(this._undoStack[this._undoIndex]); + const range = this._getRangeAndRemoveBookmark(); + if (range) { + this.setSelection(range); + } + this.fireEvent("undoStateChange", { + canUndo: true, + canRedo: undoIndex + 2 < undoStackLength + }); + this.fireEvent("input"); + } + return this.focus(); + } + // --- Get and set data + getRoot() { + return this._root; + } + _getRawHTML() { + return this._root.innerHTML; + } + _setRawHTML(html) { + const root = this._root; + root.innerHTML = html; + let node = root; + const child = node.firstChild; + if (!child || child.nodeName === "BR") { + const block = this.createDefaultBlock(); + if (child) { + node.replaceChild(block, child); + } else { + node.appendChild(block); + } + } else { + while (node = getNextBlock(node, root)) { + fixCursor(node); + } + } + this._ignoreChange = true; + return this; + } + getHTML(withBookmark) { + let range; + if (withBookmark) { + range = this.getSelection(); + this._saveRangeToBookmark(range); + } + const html = this._getRawHTML().replace(/\u200B/g, ""); + if (withBookmark) { + this._getRangeAndRemoveBookmark(range); + } + return html; + } + setHTML(html) { + const frag = this._config.sanitizeToDOMFragment(html, this); + const root = this._root; + cleanTree(frag, this._config); + cleanupBRs(frag, root, false); + fixContainer(frag, root); + let node = frag; + let child = node.firstChild; + if (!child || child.nodeName === "BR") { + const block = this.createDefaultBlock(); + if (child) { + node.replaceChild(block, child); + } else { + node.appendChild(block); + } + } else { + while (node = getNextBlock(node, root)) { + fixCursor(node); + } + } + this._ignoreChange = true; + while (child = root.lastChild) { + root.removeChild(child); + } + root.appendChild(frag); + this._undoIndex = -1; + this._undoStack.length = 0; + this._undoStackLength = 0; + this._isInUndoState = false; + const range = this._getRangeAndRemoveBookmark() || createRange(root.firstElementChild || root, 0); + this.saveUndoState(range); + this.setSelection(range); + this._updatePath(range, true); + return this; + } + /** + * Insert HTML at the cursor location. If the selection is not collapsed + * insertTreeFragmentIntoRange will delete the selection so that it is + * replaced by the html being inserted. + */ + insertHTML(html, isPaste) { + const config = this._config; + let frag = config.sanitizeToDOMFragment(html, this); + const range = this.getSelection(); + this.saveUndoState(range); + try { + const root = this._root; + if (config.addLinks) { + this.addDetectedLinks(frag, frag); + } + cleanTree(frag, this._config); + cleanupBRs(frag, root, false); + removeEmptyInlines(frag); + frag.normalize(); + let node = frag; + while (node = getNextBlock(node, frag)) { + fixCursor(node); + } + let doInsert = true; + if (isPaste) { + const event = new CustomEvent("willPaste", { + cancelable: true, + detail: { + html, + fragment: frag + } + }); + this.fireEvent("willPaste", event); + frag = event.detail.fragment; + doInsert = !event.defaultPrevented; + } + if (doInsert) { + insertTreeFragmentIntoRange(range, frag, root); + range.collapse(false); + moveRangeBoundaryOutOf(range, "A", root); + this._ensureBottomLine(); + } + this.setSelection(range); + this._updatePath(range, true); + if (isPaste) { + this.focus(); + } + } catch (error) { + this._config.didError(error); + } + return this; + } + insertElement(el, range) { + if (!range) { + range = this.getSelection(); + } + range.collapse(true); + if (isInline(el)) { + insertNodeInRange(range, el); + range.setStartAfter(el); + } else { + const root = this._root; + const startNode = getStartBlockOfRange( + range, + root + ); + let splitNode = startNode || root; + let nodeAfterSplit = null; + while (splitNode !== root && !splitNode.nextSibling) { + splitNode = splitNode.parentNode; + } + if (splitNode !== root) { + const parent = splitNode.parentNode; + nodeAfterSplit = split( + parent, + splitNode.nextSibling, + root, + root + ); + } + if (startNode && isEmptyBlock(startNode)) { + detach(startNode); + } + root.insertBefore(el, nodeAfterSplit); + const blankLine = this.createDefaultBlock(); + root.insertBefore(blankLine, nodeAfterSplit); + range.setStart(blankLine, 0); + range.setEnd(blankLine, 0); + moveRangeBoundariesDownTree(range); + } + this.focus(); + this.setSelection(range); + this._updatePath(range); + return this; + } + insertImage(src, attributes) { + const img = createElement( + "IMG", + Object.assign( + { + src + }, + attributes + ) + ); + this.insertElement(img); + return img; + } + insertPlainText(plainText, isPaste) { + const range = this.getSelection(); + if (range.collapsed && getNearest(range.startContainer, this._root, "PRE")) { + const startContainer = range.startContainer; + let offset = range.startOffset; + let textNode; + if (!startContainer || !(startContainer instanceof Text)) { + const text = document.createTextNode(""); + startContainer.insertBefore( + text, + startContainer.childNodes[offset] + ); + textNode = text; + offset = 0; + } else { + textNode = startContainer; + } + let doInsert = true; + if (isPaste) { + const event = new CustomEvent("willPaste", { + cancelable: true, + detail: { + text: plainText + } + }); + this.fireEvent("willPaste", event); + plainText = event.detail.text; + doInsert = !event.defaultPrevented; + } + if (doInsert) { + textNode.insertData(offset, plainText); + range.setStart(textNode, offset + plainText.length); + range.collapse(true); + } + this.setSelection(range); + return this; + } + const lines = plainText.split("\n"); + const config = this._config; + const tag = config.blockTag; + const attributes = config.blockAttributes; + const closeBlock = "</" + tag + ">"; + let openBlock = "<" + tag; + for (const attr in attributes) { + openBlock += " " + attr + '="' + escapeHTML(attributes[attr]) + '"'; + } + openBlock += ">"; + for (let i = 0, l = lines.length; i < l; i += 1) { + let line = lines[i]; + line = escapeHTML(line).replace(/ (?=(?: |$))/g, " "); + if (i) { + line = openBlock + (line || "<BR>") + closeBlock; + } + lines[i] = line; + } + return this.insertHTML(lines.join(""), isPaste); + } + getSelectedText(range) { + return getTextContentsOfRange(range || this.getSelection()); + } + // --- Inline formatting + /** + * Extracts the font-family and font-size (if any) of the element + * holding the cursor. If there's a selection, returns an empty object. + */ + getFontInfo(range) { + const fontInfo = { + color: void 0, + backgroundColor: void 0, + fontFamily: void 0, + fontSize: void 0 + }; + if (!range) { + range = this.getSelection(); + } + moveRangeBoundariesDownTree(range); + let seenAttributes = 0; + let element = range.commonAncestorContainer; + if (range.collapsed || element instanceof Text) { + if (element instanceof Text) { + element = element.parentNode; + } + while (seenAttributes < 4 && element) { + const style = element.style; + if (style) { + const color = style.color; + if (!fontInfo.color && color) { + fontInfo.color = color; + seenAttributes += 1; + } + const backgroundColor = style.backgroundColor; + if (!fontInfo.backgroundColor && backgroundColor) { + fontInfo.backgroundColor = backgroundColor; + seenAttributes += 1; + } + const fontFamily = style.fontFamily; + if (!fontInfo.fontFamily && fontFamily) { + fontInfo.fontFamily = fontFamily; + seenAttributes += 1; + } + const fontSize = style.fontSize; + if (!fontInfo.fontSize && fontSize) { + fontInfo.fontSize = fontSize; + seenAttributes += 1; + } + } + element = element.parentNode; + } + } + return fontInfo; + } + /** + * Looks for matching tag and attributes, so won't work if <strong> + * instead of <b> etc. + */ + hasFormat(tag, attributes, range) { + tag = tag.toUpperCase(); + if (!attributes) { + attributes = {}; + } + if (!range) { + range = this.getSelection(); + } + if (!range.collapsed && range.startContainer instanceof Text && range.startOffset === range.startContainer.length && range.startContainer.nextSibling) { + range.setStartBefore(range.startContainer.nextSibling); + } + if (!range.collapsed && range.endContainer instanceof Text && range.endOffset === 0 && range.endContainer.previousSibling) { + range.setEndAfter(range.endContainer.previousSibling); + } + const root = this._root; + const common = range.commonAncestorContainer; + if (getNearest(common, root, tag, attributes)) { + return true; + } + if (common instanceof Text) { + return false; + } + const walker = new TreeIterator(common, SHOW_TEXT, (node2) => { + return isNodeContainedInRange(range, node2, true); + }); + let seenNode = false; + let node; + while (node = walker.nextNode()) { + if (!getNearest(node, root, tag, attributes)) { + return false; + } + seenNode = true; + } + return seenNode; + } + changeFormat(add, remove, range, partial) { + if (!range) { + range = this.getSelection(); + } + this.saveUndoState(range); + if (remove) { + range = this._removeFormat( + remove.tag.toUpperCase(), + remove.attributes || {}, + range, + partial + ); + } + if (add) { + range = this._addFormat( + add.tag.toUpperCase(), + add.attributes || {}, + range + ); + } + this.setSelection(range); + this._updatePath(range, true); + return this.focus(); + } + _addFormat(tag, attributes, range) { + const root = this._root; + if (range.collapsed) { + const el = fixCursor(createElement(tag, attributes)); + insertNodeInRange(range, el); + const focusNode = el.firstChild || el; + const focusOffset = focusNode instanceof Text ? focusNode.length : 0; + range.setStart(focusNode, focusOffset); + range.collapse(true); + let block = el; + while (isInline(block)) { + block = block.parentNode; + } + removeZWS(block, el); + } else { + const walker = new TreeIterator( + range.commonAncestorContainer, + SHOW_ELEMENT_OR_TEXT, + (node) => { + return (node instanceof Text || node.nodeName === "BR" || node.nodeName === "IMG") && isNodeContainedInRange(range, node, true); + } + ); + let { startContainer, startOffset, endContainer, endOffset } = range; + walker.currentNode = startContainer; + if (!(startContainer instanceof Element) && !(startContainer instanceof Text) || !walker.filter(startContainer)) { + const next = walker.nextNode(); + if (!next) { + return range; + } + startContainer = next; + startOffset = 0; + } + do { + let node = walker.currentNode; + const needsFormat = !getNearest(node, root, tag, attributes); + if (needsFormat) { + if (node === endContainer && node.length > endOffset) { + node.splitText(endOffset); + } + if (node === startContainer && startOffset) { + node = node.splitText(startOffset); + if (endContainer === startContainer) { + endContainer = node; + endOffset -= startOffset; + } else if (endContainer === startContainer.parentNode) { + endOffset += 1; + } + startContainer = node; + startOffset = 0; + } + const el = createElement(tag, attributes); + replaceWith(node, el); + el.appendChild(node); + } + } while (walker.nextNode()); + range = createRange( + startContainer, + startOffset, + endContainer, + endOffset + ); + } + return range; + } + _removeFormat(tag, attributes, range, partial) { + this._saveRangeToBookmark(range); + let fixer; + if (range.collapsed) { + if (cantFocusEmptyTextNodes) { + fixer = document.createTextNode(ZWS); + } else { + fixer = document.createTextNode(""); + } + insertNodeInRange(range, fixer); + } + let root = range.commonAncestorContainer; + while (isInline(root)) { + root = root.parentNode; + } + const startContainer = range.startContainer; + const startOffset = range.startOffset; + const endContainer = range.endContainer; + const endOffset = range.endOffset; + const toWrap = []; + const examineNode = (node, exemplar) => { + if (isNodeContainedInRange(range, node, false)) { + return; + } + let child; + let next; + if (!isNodeContainedInRange(range, node, true)) { + if (!(node instanceof HTMLInputElement) && (!(node instanceof Text) || node.data)) { + toWrap.push([exemplar, node]); + } + return; + } + if (node instanceof Text) { + if (node === endContainer && endOffset !== node.length) { + toWrap.push([exemplar, node.splitText(endOffset)]); + } + if (node === startContainer && startOffset) { + node.splitText(startOffset); + toWrap.push([exemplar, node]); + } + } else { + for (child = node.firstChild; child; child = next) { + next = child.nextSibling; + examineNode(child, exemplar); + } + } + }; + const formatTags = Array.from( + root.getElementsByTagName(tag) + ).filter((el) => { + return isNodeContainedInRange(range, el, true) && hasTagAttributes(el, tag, attributes); + }); + if (!partial) { + formatTags.forEach((node) => { + examineNode(node, node); + }); + } + toWrap.forEach(([el, node]) => { + el = el.cloneNode(false); + replaceWith(node, el); + el.appendChild(node); + }); + formatTags.forEach((el) => { + replaceWith(el, empty(el)); + }); + if (cantFocusEmptyTextNodes && fixer) { + fixer = fixer.parentNode; + let block = fixer; + while (block && isInline(block)) { + block = block.parentNode; + } + if (block) { + removeZWS(block, fixer); + } + } + this._getRangeAndRemoveBookmark(range); + if (fixer) { + range.collapse(false); + } + mergeInlines(root, range); + return range; + } + // --- + bold() { + return this.changeFormat({ tag: "B" }); + } + removeBold() { + return this.changeFormat(null, { tag: "B" }); + } + italic() { + return this.changeFormat({ tag: "I" }); + } + removeItalic() { + return this.changeFormat(null, { tag: "I" }); + } + underline() { + return this.changeFormat({ tag: "U" }); + } + removeUnderline() { + return this.changeFormat(null, { tag: "U" }); + } + strikethrough() { + return this.changeFormat({ tag: "S" }); + } + removeStrikethrough() { + return this.changeFormat(null, { tag: "S" }); + } + subscript() { + return this.changeFormat({ tag: "SUB" }, { tag: "SUP" }); + } + removeSubscript() { + return this.changeFormat(null, { tag: "SUB" }); + } + superscript() { + return this.changeFormat({ tag: "SUP" }, { tag: "SUB" }); + } + removeSuperscript() { + return this.changeFormat(null, { tag: "SUP" }); + } + // --- + makeLink(url, attributes) { + const range = this.getSelection(); + if (range.collapsed) { + let protocolEnd = url.indexOf(":") + 1; + if (protocolEnd) { + while (url[protocolEnd] === "/") { + protocolEnd += 1; + } + } + insertNodeInRange( + range, + document.createTextNode(url.slice(protocolEnd)) + ); + } + attributes = Object.assign( + { + href: url + }, + this._config.tagAttributes.a, + attributes + ); + return this.changeFormat( + { + tag: "A", + attributes + }, + { + tag: "A" + }, + range + ); + } + removeLink() { + return this.changeFormat( + null, + { + tag: "A" + }, + this.getSelection(), + true + ); + } + addDetectedLinks(searchInNode, root) { + const walker = new TreeIterator( + searchInNode, + SHOW_TEXT, + (node2) => !getNearest(node2, root || this._root, "A") + ); + const linkRegExp = this.linkRegExp; + const defaultAttributes = this._config.tagAttributes.a; + let node; + while (node = walker.nextNode()) { + const parent = node.parentNode; + let data = node.data; + let match; + while (match = linkRegExp.exec(data)) { + const index = match.index; + const endIndex = index + match[0].length; + if (index) { + parent.insertBefore( + document.createTextNode(data.slice(0, index)), + node + ); + } + const child = createElement( + "A", + Object.assign( + { + href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : "http://" + match[1] : "mailto:" + match[0] + }, + defaultAttributes + ) + ); + child.textContent = data.slice(index, endIndex); + parent.insertBefore(child, node); + node.data = data = data.slice(endIndex); + } + } + return this; + } + // --- + setFontFace(name) { + const className = this._config.classNames.fontFamily; + return this.changeFormat( + name ? { + tag: "SPAN", + attributes: { + class: className, + style: "font-family: " + name + ", sans-serif;" + } + } : null, + { + tag: "SPAN", + attributes: { class: className } + } + ); + } + setFontSize(size) { + const className = this._config.classNames.fontSize; + return this.changeFormat( + size ? { + tag: "SPAN", + attributes: { + class: className, + style: "font-size: " + (typeof size === "number" ? size + "px" : size) + } + } : null, + { + tag: "SPAN", + attributes: { class: className } + } + ); + } + setTextColor(color) { + const className = this._config.classNames.color; + return this.changeFormat( + color ? { + tag: "SPAN", + attributes: { + class: className, + style: "color:" + color + } + } : null, + { + tag: "SPAN", + attributes: { class: className } + } + ); + } + setHighlightColor(color) { + const className = this._config.classNames.highlight; + return this.changeFormat( + color ? { + tag: "SPAN", + attributes: { + class: className, + style: "background-color:" + color + } + } : null, + { + tag: "SPAN", + attributes: { class: className } + } + ); + } + // --- Block formatting + _ensureBottomLine() { + const root = this._root; + const last = root.lastElementChild; + if (!last || last.nodeName !== this._config.blockTag || !isBlock(last)) { + root.appendChild(this.createDefaultBlock()); + } + } + createDefaultBlock(children) { + const config = this._config; + return fixCursor( + createElement(config.blockTag, config.blockAttributes, children) + ); + } + splitBlock(lineBreakOnly, range) { + if (!range) { + range = this.getSelection(); + } + const root = this._root; + let block; + let parent; + let node; + let nodeAfterSplit; + this._recordUndoState(range); + this._removeZWS(); + this._getRangeAndRemoveBookmark(range); + if (!range.collapsed) { + deleteContentsOfRange(range, root); + } + if (this._config.addLinks) { + moveRangeBoundariesDownTree(range); + const textNode = range.startContainer; + const offset2 = range.startOffset; + setTimeout(() => { + linkifyText(this, textNode, offset2); + }, 0); + } + block = getStartBlockOfRange(range, root); + if (block && (parent = getNearest(block, root, "PRE"))) { + moveRangeBoundariesDownTree(range); + node = range.startContainer; + const offset2 = range.startOffset; + if (!(node instanceof Text)) { + node = document.createTextNode(""); + parent.insertBefore(node, parent.firstChild); + } + if (!lineBreakOnly && node instanceof Text && (node.data.charAt(offset2 - 1) === "\n" || rangeDoesStartAtBlockBoundary(range, root)) && (node.data.charAt(offset2) === "\n" || rangeDoesEndAtBlockBoundary(range, root))) { + node.deleteData(offset2 && offset2 - 1, offset2 ? 2 : 1); + nodeAfterSplit = split( + node, + offset2 && offset2 - 1, + root, + root + ); + node = nodeAfterSplit.previousSibling; + if (!node.textContent) { + detach(node); + } + node = this.createDefaultBlock(); + nodeAfterSplit.parentNode.insertBefore(node, nodeAfterSplit); + if (!nodeAfterSplit.textContent) { + detach(nodeAfterSplit); + } + range.setStart(node, 0); + } else { + node.insertData(offset2, "\n"); + fixCursor(parent); + if (node.length === offset2 + 1) { + range.setStartAfter(node); + } else { + range.setStart(node, offset2 + 1); + } + } + range.collapse(true); + this.setSelection(range); + this._updatePath(range, true); + this._docWasChanged(); + return this; + } + if (!block || lineBreakOnly || /^T[HD]$/.test(block.nodeName)) { + moveRangeBoundaryOutOf(range, "A", root); + insertNodeInRange(range, createElement("BR")); + range.collapse(false); + this.setSelection(range); + this._updatePath(range, true); + return this; + } + if (parent = getNearest(block, root, "LI")) { + block = parent; + } + if (isEmptyBlock(block)) { + if (getNearest(block, root, "UL") || getNearest(block, root, "OL")) { + this.decreaseListLevel(range); + return this; + } else if (getNearest(block, root, "BLOCKQUOTE")) { + this.replaceWithBlankLine(range); + return this; + } + } + node = range.startContainer; + const offset = range.startOffset; + let splitTag = this.tagAfterSplit[block.nodeName]; + nodeAfterSplit = split( + node, + offset, + block.parentNode, + this._root + ); + const config = this._config; + let splitProperties = null; + if (!splitTag) { + splitTag = config.blockTag; + splitProperties = config.blockAttributes; + } + if (!hasTagAttributes(nodeAfterSplit, splitTag, splitProperties)) { + block = createElement(splitTag, splitProperties); + if (nodeAfterSplit.dir) { + block.dir = nodeAfterSplit.dir; + } + replaceWith(nodeAfterSplit, block); + block.appendChild(empty(nodeAfterSplit)); + nodeAfterSplit = block; + } + removeZWS(block); + removeEmptyInlines(block); + fixCursor(block); + while (nodeAfterSplit instanceof Element) { + let child = nodeAfterSplit.firstChild; + let next; + if (nodeAfterSplit.nodeName === "A" && (!nodeAfterSplit.textContent || nodeAfterSplit.textContent === ZWS)) { + child = document.createTextNode(""); + replaceWith(nodeAfterSplit, child); + nodeAfterSplit = child; + break; + } + while (child && child instanceof Text && !child.data) { + next = child.nextSibling; + if (!next || next.nodeName === "BR") { + break; + } + detach(child); + child = next; + } + if (!child || child.nodeName === "BR" || child instanceof Text) { + break; + } + nodeAfterSplit = child; + } + range = createRange(nodeAfterSplit, 0); + this.setSelection(range); + this._updatePath(range, true); + return this; + } + forEachBlock(fn, mutates, range) { + if (!range) { + range = this.getSelection(); + } + if (mutates) { + this.saveUndoState(range); + } + const root = this._root; + let start = getStartBlockOfRange(range, root); + const end = getEndBlockOfRange(range, root); + if (start && end) { + do { + if (fn(start) || start === end) { + break; + } + } while (start = getNextBlock(start, root)); + } + if (mutates) { + this.setSelection(range); + this._updatePath(range, true); + } + return this; + } + modifyBlocks(modify, range) { + if (!range) { + range = this.getSelection(); + } + this._recordUndoState(range, this._isInUndoState); + const root = this._root; + expandRangeToBlockBoundaries(range, root); + moveRangeBoundariesUpTree(range, root, root, root); + const frag = extractContentsOfRange(range, root, root); + if (!range.collapsed) { + let node = range.endContainer; + if (node === root) { + range.collapse(false); + } else { + while (node.parentNode !== root) { + node = node.parentNode; + } + range.setStartBefore(node); + range.collapse(true); + } + } + insertNodeInRange(range, modify.call(this, frag)); + if (range.endOffset < range.endContainer.childNodes.length) { + mergeContainers( + range.endContainer.childNodes[range.endOffset], + root + ); + } + mergeContainers( + range.startContainer.childNodes[range.startOffset], + root + ); + this._getRangeAndRemoveBookmark(range); + this.setSelection(range); + this._updatePath(range, true); + return this; + } + // --- + setTextAlignment(alignment) { + this.forEachBlock((block) => { + const className = block.className.split(/\s+/).filter((klass) => { + return !!klass && !/^align/.test(klass); + }).join(" "); + if (alignment) { + block.className = className + " align-" + alignment; + block.style.textAlign = alignment; + } else { + block.className = className; + block.style.textAlign = ""; + } + }, true); + return this.focus(); + } + setTextDirection(direction) { + this.forEachBlock((block) => { + if (direction) { + block.dir = direction; + } else { + block.removeAttribute("dir"); + } + }, true); + return this.focus(); + } + // --- + _getListSelection(range, root) { + let list = range.commonAncestorContainer; + let startLi = range.startContainer; + let endLi = range.endContainer; + while (list && list !== root && !/^[OU]L$/.test(list.nodeName)) { + list = list.parentNode; + } + if (!list || list === root) { + return null; + } + if (startLi === list) { + startLi = startLi.childNodes[range.startOffset]; + } + if (endLi === list) { + endLi = endLi.childNodes[range.endOffset]; + } + while (startLi && startLi.parentNode !== list) { + startLi = startLi.parentNode; + } + while (endLi && endLi.parentNode !== list) { + endLi = endLi.parentNode; + } + return [list, startLi, endLi]; + } + increaseListLevel(range) { + if (!range) { + range = this.getSelection(); + } + const root = this._root; + const listSelection = this._getListSelection(range, root); + if (!listSelection) { + return this.focus(); + } + let [list, startLi, endLi] = listSelection; + if (!startLi || startLi === list.firstChild) { + return this.focus(); + } + this._recordUndoState(range, this._isInUndoState); + const type = list.nodeName; + let newParent = startLi.previousSibling; + let listAttrs; + let next; + if (newParent.nodeName !== type) { + listAttrs = this._config.tagAttributes[type.toLowerCase()]; + newParent = createElement(type, listAttrs); + list.insertBefore(newParent, startLi); + } + do { + next = startLi === endLi ? null : startLi.nextSibling; + newParent.appendChild(startLi); + } while (startLi = next); + next = newParent.nextSibling; + if (next) { + mergeContainers(next, root); + } + this._getRangeAndRemoveBookmark(range); + this.setSelection(range); + this._updatePath(range, true); + return this.focus(); + } + decreaseListLevel(range) { + if (!range) { + range = this.getSelection(); + } + const root = this._root; + const listSelection = this._getListSelection(range, root); + if (!listSelection) { + return this.focus(); + } + let [list, startLi, endLi] = listSelection; + if (!startLi) { + startLi = list.firstChild; + } + if (!endLi) { + endLi = list.lastChild; + } + this._recordUndoState(range, this._isInUndoState); + let next; + let insertBefore = null; + if (startLi) { + let newParent = list.parentNode; + insertBefore = !endLi.nextSibling ? list.nextSibling : split(list, endLi.nextSibling, newParent, root); + if (newParent !== root && newParent.nodeName === "LI") { + newParent = newParent.parentNode; + while (insertBefore) { + next = insertBefore.nextSibling; + endLi.appendChild(insertBefore); + insertBefore = next; + } + insertBefore = list.parentNode.nextSibling; + } + const makeNotList = !/^[OU]L$/.test(newParent.nodeName); + do { + next = startLi === endLi ? null : startLi.nextSibling; + list.removeChild(startLi); + if (makeNotList && startLi.nodeName === "LI") { + startLi = this.createDefaultBlock([empty(startLi)]); + } + newParent.insertBefore(startLi, insertBefore); + } while (startLi = next); + } + if (!list.firstChild) { + detach(list); + } + if (insertBefore) { + mergeContainers(insertBefore, root); + } + this._getRangeAndRemoveBookmark(range); + this.setSelection(range); + this._updatePath(range, true); + return this.focus(); + } + _makeList(frag, type) { + const walker = getBlockWalker(frag, this._root); + const tagAttributes = this._config.tagAttributes; + const listAttrs = tagAttributes[type.toLowerCase()]; + const listItemAttrs = tagAttributes.li; + let node; + while (node = walker.nextNode()) { + if (node.parentNode instanceof HTMLLIElement) { + node = node.parentNode; + walker.currentNode = node.lastChild; + } + if (!(node instanceof HTMLLIElement)) { + const newLi = createElement("LI", listItemAttrs); + if (node.dir) { + newLi.dir = node.dir; + } + const prev = node.previousSibling; + if (prev && prev.nodeName === type) { + prev.appendChild(newLi); + detach(node); + } else { + replaceWith(node, createElement(type, listAttrs, [newLi])); + } + newLi.appendChild(empty(node)); + walker.currentNode = newLi; + } else { + node = node.parentNode; + const tag = node.nodeName; + if (tag !== type && /^[OU]L$/.test(tag)) { + replaceWith( + node, + createElement(type, listAttrs, [empty(node)]) + ); + } + } + } + return frag; + } + makeUnorderedList() { + this.modifyBlocks((frag) => this._makeList(frag, "UL")); + return this.focus(); + } + makeOrderedList() { + this.modifyBlocks((frag) => this._makeList(frag, "OL")); + return this.focus(); + } + removeList() { + this.modifyBlocks((frag) => { + const lists = frag.querySelectorAll("UL, OL"); + const items = frag.querySelectorAll("LI"); + const root = this._root; + for (let i = 0, l = lists.length; i < l; i += 1) { + const list = lists[i]; + const listFrag = empty(list); + fixContainer(listFrag, root); + replaceWith(list, listFrag); + } + for (let i = 0, l = items.length; i < l; i += 1) { + const item = items[i]; + if (isBlock(item)) { + replaceWith(item, this.createDefaultBlock([empty(item)])); + } else { + fixContainer(item, root); + replaceWith(item, empty(item)); + } + } + return frag; + }); + return this.focus(); + } + // --- + increaseQuoteLevel(range) { + this.modifyBlocks( + (frag) => createElement( + "BLOCKQUOTE", + this._config.tagAttributes.blockquote, + [frag] + ), + range + ); + return this.focus(); + } + decreaseQuoteLevel(range) { + this.modifyBlocks((frag) => { + Array.from(frag.querySelectorAll("blockquote")).filter((el) => { + return !getNearest(el.parentNode, frag, "BLOCKQUOTE"); + }).forEach((el) => { + replaceWith(el, empty(el)); + }); + return frag; + }, range); + return this.focus(); + } + removeQuote(range) { + this.modifyBlocks((frag) => { + Array.from(frag.querySelectorAll("blockquote")).forEach( + (el) => { + replaceWith(el, empty(el)); + } + ); + return frag; + }, range); + return this.focus(); + } + replaceWithBlankLine(range) { + this.modifyBlocks( + () => this.createDefaultBlock([ + createElement("INPUT", { + id: this.startSelectionId, + type: "hidden" + }), + createElement("INPUT", { + id: this.endSelectionId, + type: "hidden" + }) + ]), + range + ); + return this.focus(); + } + // --- + code() { + const range = this.getSelection(); + if (range.collapsed || isContainer(range.commonAncestorContainer)) { + this.modifyBlocks((frag) => { + const root = this._root; + const output = document.createDocumentFragment(); + const blockWalker = getBlockWalker(frag, root); + let node; + while (node = blockWalker.nextNode()) { + let nodes = node.querySelectorAll("BR"); + const brBreaksLine = []; + let l = nodes.length; + for (let i = 0; i < l; i += 1) { + brBreaksLine[i] = isLineBreak(nodes[i], false); + } + while (l--) { + const br = nodes[l]; + if (!brBreaksLine[l]) { + detach(br); + } else { + replaceWith(br, document.createTextNode("\n")); + } + } + nodes = node.querySelectorAll("CODE"); + l = nodes.length; + while (l--) { + replaceWith(nodes[l], empty(nodes[l])); + } + if (output.childNodes.length) { + output.appendChild(document.createTextNode("\n")); + } + output.appendChild(empty(node)); + } + const textWalker = new TreeIterator(output, SHOW_TEXT); + while (node = textWalker.nextNode()) { + node.data = node.data.replace(/ /g, " "); + } + output.normalize(); + return fixCursor( + createElement("PRE", this._config.tagAttributes.pre, [ + output + ]) + ); + }, range); + this.focus(); + } else { + this.changeFormat( + { + tag: "CODE", + attributes: this._config.tagAttributes.code + }, + null, + range + ); + } + return this; + } + removeCode() { + const range = this.getSelection(); + const ancestor = range.commonAncestorContainer; + const inPre = getNearest(ancestor, this._root, "PRE"); + if (inPre) { + this.modifyBlocks((frag) => { + const root = this._root; + const pres = frag.querySelectorAll("PRE"); + let l = pres.length; + while (l--) { + const pre = pres[l]; + const walker = new TreeIterator(pre, SHOW_TEXT); + let node; + while (node = walker.nextNode()) { + let value = node.data; + value = value.replace(/ (?= )/g, "\xA0"); + const contents = document.createDocumentFragment(); + let index; + while ((index = value.indexOf("\n")) > -1) { + contents.appendChild( + document.createTextNode(value.slice(0, index)) + ); + contents.appendChild(createElement("BR")); + value = value.slice(index + 1); + } + node.parentNode.insertBefore(contents, node); + node.data = value; + } + fixContainer(pre, root); + replaceWith(pre, empty(pre)); + } + return frag; + }, range); + this.focus(); + } else { + this.changeFormat(null, { tag: "CODE" }, range); + } + return this; + } + toggleCode() { + if (this.hasFormat("PRE") || this.hasFormat("CODE")) { + this.removeCode(); + } else { + this.code(); + } + return this; + } + // --- + _removeFormatting(root, clean) { + for (let node = root.firstChild, next; node; node = next) { + next = node.nextSibling; + if (isInline(node)) { + if (node instanceof Text || node.nodeName === "BR" || node.nodeName === "IMG") { + clean.appendChild(node); + continue; + } + } else if (isBlock(node)) { + clean.appendChild( + this.createDefaultBlock([ + this._removeFormatting( + node, + document.createDocumentFragment() + ) + ]) + ); + continue; + } + this._removeFormatting(node, clean); + } + return clean; + } + removeAllFormatting(range) { + if (!range) { + range = this.getSelection(); + } + if (range.collapsed) { + return this.focus(); + } + const root = this._root; + let stopNode = range.commonAncestorContainer; + while (stopNode && !isBlock(stopNode)) { + stopNode = stopNode.parentNode; + } + if (!stopNode) { + expandRangeToBlockBoundaries(range, root); + stopNode = root; + } + if (stopNode instanceof Text) { + return this.focus(); + } + this.saveUndoState(range); + moveRangeBoundariesUpTree(range, stopNode, stopNode, root); + const startContainer = range.startContainer; + let startOffset = range.startOffset; + const endContainer = range.endContainer; + let endOffset = range.endOffset; + const formattedNodes = document.createDocumentFragment(); + const cleanNodes = document.createDocumentFragment(); + const nodeAfterSplit = split(endContainer, endOffset, stopNode, root); + let nodeInSplit = split(startContainer, startOffset, stopNode, root); + let nextNode; + while (nodeInSplit !== nodeAfterSplit) { + nextNode = nodeInSplit.nextSibling; + formattedNodes.appendChild(nodeInSplit); + nodeInSplit = nextNode; + } + this._removeFormatting(formattedNodes, cleanNodes); + cleanNodes.normalize(); + nodeInSplit = cleanNodes.firstChild; + nextNode = cleanNodes.lastChild; + if (nodeInSplit) { + stopNode.insertBefore(cleanNodes, nodeAfterSplit); + const childNodes = Array.from(stopNode.childNodes); + startOffset = childNodes.indexOf(nodeInSplit); + endOffset = nextNode ? childNodes.indexOf(nextNode) + 1 : 0; + } else if (nodeAfterSplit) { + const childNodes = Array.from(stopNode.childNodes); + startOffset = childNodes.indexOf(nodeAfterSplit); + endOffset = startOffset; + } + range.setStart(stopNode, startOffset); + range.setEnd(stopNode, endOffset); + mergeInlines(stopNode, range); + moveRangeBoundariesDownTree(range); + this.setSelection(range); + this._updatePath(range, true); + return this.focus(); + } + }; + + // source/Legacy.ts + window.Squire2 = Squire; +})(); diff --git a/plugins/compact-composer/screenshots/Screenshot1.png b/plugins/compact-composer/screenshots/Screenshot1.png new file mode 100644 index 0000000000..8e12904dfc Binary files /dev/null and b/plugins/compact-composer/screenshots/Screenshot1.png differ diff --git a/plugins/compact-composer/screenshots/Screenshot2.png b/plugins/compact-composer/screenshots/Screenshot2.png new file mode 100644 index 0000000000..81faa38895 Binary files /dev/null and b/plugins/compact-composer/screenshots/Screenshot2.png differ diff --git a/plugins/compact-composer/screenshots/Screenshot3.png b/plugins/compact-composer/screenshots/Screenshot3.png new file mode 100644 index 0000000000..e8911800bc Binary files /dev/null and b/plugins/compact-composer/screenshots/Screenshot3.png differ diff --git a/plugins/compact-composer/screenshots/Screenshot4.png b/plugins/compact-composer/screenshots/Screenshot4.png new file mode 100644 index 0000000000..72c4f85968 Binary files /dev/null and b/plugins/compact-composer/screenshots/Screenshot4.png differ diff --git a/plugins/compact-composer/screenshots/Screenshot5.png b/plugins/compact-composer/screenshots/Screenshot5.png new file mode 100644 index 0000000000..85aa6d66f5 Binary files /dev/null and b/plugins/compact-composer/screenshots/Screenshot5.png differ diff --git a/plugins/compact-composer/screenshots/Screenshot6.png b/plugins/compact-composer/screenshots/Screenshot6.png new file mode 100644 index 0000000000..e2d140e4e6 Binary files /dev/null and b/plugins/compact-composer/screenshots/Screenshot6.png differ diff --git a/plugins/contact-group-excel-paste/VERSION b/plugins/contact-group-excel-paste/VERSION deleted file mode 100644 index ceab6e11ec..0000000000 --- a/plugins/contact-group-excel-paste/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.1 \ No newline at end of file diff --git a/plugins/contact-group-excel-paste/index.php b/plugins/contact-group-excel-paste/index.php index 81fc9037cd..3ad0089b31 100644 --- a/plugins/contact-group-excel-paste/index.php +++ b/plugins/contact-group-excel-paste/index.php @@ -2,7 +2,12 @@ class ContactGroupExcelPastePlugin extends \RainLoop\Plugins\AbstractPlugin { - public function Init() + const + NAME = '', + CATEGORY = 'General', + DESCRIPTION = ''; + + public function Init() : void { $this->addJs('js/excel_contact_group.js'); } diff --git a/plugins/contacts-suggestions-example/ContactsExampleSuggestions.php b/plugins/contacts-suggestions-example/ContactsExampleSuggestions.php deleted file mode 100644 index 05fcc2dfee..0000000000 --- a/plugins/contacts-suggestions-example/ContactsExampleSuggestions.php +++ /dev/null @@ -1,21 +0,0 @@ -<?php - -class ContactsExampleSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions -{ - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sQuery - * @param int $iLimit = 20 - * - * @return array - */ - public function Process($oAccount, $sQuery, $iLimit = 20) - { - $aResult = array( - array($oAccount->Email(), ''), - array('email@domain.com', 'name') - ); - - return $aResult; - } -} diff --git a/plugins/contacts-suggestions-example/VERSION b/plugins/contacts-suggestions-example/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/contacts-suggestions-example/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/contacts-suggestions-example/index.php b/plugins/contacts-suggestions-example/index.php deleted file mode 100644 index bb9bf2e73a..0000000000 --- a/plugins/contacts-suggestions-example/index.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php - -class ContactsSuggestionsExamplePlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $mResult - */ - public function MainFabrica($sName, &$mResult) - { - switch ($sName) - { - case 'suggestions': - - if (!\is_array($mResult)) - { - $mResult = array(); - } - - include_once __DIR__.'/ContactsExampleSuggestions.php'; - $mResult[] = new ContactsExampleSuggestions(); - break; - } - } -} \ No newline at end of file diff --git a/plugins/cpanel-change-password/CpanelChangePasswordDriver.php b/plugins/cpanel-change-password/CpanelChangePasswordDriver.php deleted file mode 100644 index 310dfe27a9..0000000000 --- a/plugins/cpanel-change-password/CpanelChangePasswordDriver.php +++ /dev/null @@ -1,188 +0,0 @@ -<?php - -class CpanelChangePasswordDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sHost = ''; - - /** - * @var int - */ - private $iPost = 2087; - - /** - * @var bool - */ - private $bSsl = true; - - /** - * @var string - */ - private $sUser = ''; - - /** - * @var string - */ - private $sPassword = ''; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sHost - * @param int $iPost - * @param bool $bSsl - * @param string $sUser - * @param string $sPassword - * - * @return \CpanelChangePasswordDriver - */ - public function SetConfig($sHost, $iPost, $bSsl, $sUser, $sPassword) - { - $this->sHost = $sHost; - $this->iPost = $iPost; - $this->bSsl = !!$bSsl; - $this->sUser = $sUser; - $this->sPassword = $sPassword; - - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \CpanelChangePasswordDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \CpanelChangePasswordDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Model\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('Try to change password for '.$oAccount->Email()); - } - - if (!\class_exists('xmlapi')) - { - include_once __DIR__.'/xmlapi.php'; - } - - $bResult = false; - if (!empty($this->sHost) && 0 < $this->iPost && - 0 < \strlen($this->sUser) && 0 < \strlen($this->sPassword) && - $oAccount && \class_exists('xmlapi')) - { - $sEmail = $oAccount->Email(); - $sEmailUser = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail); - $sEmailDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail); - - $sHost = $this->sHost; - $sHost = \str_replace('{user:domain}', $sEmailDomain, $sHost); - - $sUser = $this->sUser; - $sUser = \str_replace('{user:email}', $sEmail, $sUser); - $sUser = \str_replace('{user:login}', $sEmailUser, $sUser); - - $sPassword = $this->sPassword; - $sPassword = \str_replace('{user:password}', $oAccount->Password(), $sPassword); - - try - { - $oXmlApi = new \xmlapi($sHost); - $oXmlApi->set_port($this->iPost); - $oXmlApi->set_protocol($this->bSsl ? 'https' : 'http'); - $oXmlApi->set_debug(false); - $oXmlApi->set_output('json'); -// $oXmlApi->set_http_client('fopen'); - $oXmlApi->set_http_client('curl'); - $oXmlApi->password_auth($sUser, $sPassword); - - $aArgs = array( - 'email' => $sEmailUser, - 'domain' => $sEmailDomain, - 'password' => $sNewPassword - ); - - $sResult = $oXmlApi->api2_query($sUser, 'Email', 'passwdpop', $aArgs); - if ($sResult) - { - if ($this->oLogger) - { - $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) - { - $this->oLogger->Write('CPANEL: '.$sResult, \MailSo\Log\Enumerations\Type::ERROR); - } - } - catch (\Exception $oException) - { - if ($this->oLogger) - { - $this->oLogger->WriteException($oException); - } - } - } - else - { - if ($this->oLogger) - { - $this->oLogger->Write('CPANEL: Incorrent configuration data', \MailSo\Log\Enumerations\Type::ERROR); - } - } - - return $bResult; - } -} \ No newline at end of file diff --git a/plugins/cpanel-change-password/LICENSE b/plugins/cpanel-change-password/LICENSE deleted file mode 100644 index 271342337d..0000000000 --- a/plugins/cpanel-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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 -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/cpanel-change-password/VERSION b/plugins/cpanel-change-password/VERSION deleted file mode 100644 index a58941b07a..0000000000 --- a/plugins/cpanel-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.3 \ No newline at end of file diff --git a/plugins/cpanel-change-password/index.php b/plugins/cpanel-change-password/index.php deleted file mode 100644 index 5f2602bdba..0000000000 --- a/plugins/cpanel-change-password/index.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php - -class CpanelChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - $sHost = \trim($this->Config()->Get('plugin', 'host', '')); - $iPost = (int) $this->Config()->Get('plugin', 'port', 2087); - $sUser = (string) $this->Config()->Get('plugin', 'user', ''); - $sPassword = (string) $this->Config()->Get('plugin', 'password', ''); - $bSsl = (bool) $this->Config()->Get('plugin', 'ssl', false); - - if (!empty($sHost) && 0 < $iPost && 0 < \strlen($sUser) && 0 < \strlen($sPassword)) - { - include_once __DIR__.'/CpanelChangePasswordDriver.php'; - - $oProvider = new CpanelChangePasswordDriver(); - $oProvider->SetLogger($this->Manager()->Actions()->Logger()); - $oProvider->SetConfig($sHost, $iPost, $bSsl, $sUser, $sPassword); - $oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))); - } - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('host')->SetLabel('cPanel Host') - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('port')->SetLabel('cPanel Port') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) - ->SetDefaultValue(2087), - \RainLoop\Plugins\Property::NewInstance('ssl')->SetLabel('Use SSL') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) - ->SetDefaultValue(false), - \RainLoop\Plugins\Property::NewInstance('user')->SetLabel('cPanel User') - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('password')->SetLabel('cPanel Password') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} \ No newline at end of file diff --git a/plugins/cpanel-change-password/xmlapi.php b/plugins/cpanel-change-password/xmlapi.php deleted file mode 100644 index 88b85c8268..0000000000 --- a/plugins/cpanel-change-password/xmlapi.php +++ /dev/null @@ -1,2490 +0,0 @@ -<?php -/** -* cPanel XMLAPI Client Class -* -* This class allows for easy interaction with cPanel's XML-API allow functions within the XML-API to be called -* by calling funcions within this class -* -* LICENSE: -* -* Copyright (c) 2012, cPanel, Inc. -* All rights reserved. -* -* Redistribution and use in source and binary forms, with or without modification, are permitted provided -* that the following conditions are met: -* -* * Redistributions of source code must retain the above copyright notice, this list of conditions and the -* following disclaimer. -* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -* following disclaimer in the documentation and/or other materials provided with the distribution. -* * Neither the name of the cPanel, Inc. nor the names of its contributors may be used to endorse or promote -* products derived from this software without specific prior written permission. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -* POSSIBILITY OF SUCH DAMAGE. -* -* Version: 1.0.13 -* Last updated: 19 November 2012 -* -* Changes -* -* 1.0.13: -* Tidy -* -* 1.0.12: -* github#2 - [Bugfix]: typo related to environment variable XMLAPI_USE_SSL -* -* 1.0.11: -* [Feature]: Remove value requirement for park()'s 'topdomain' argument -* (Case 51116) -* -* 1.0.10: -* github#1 - [Bugfix]: setresellerpackagelimits() does not properly prepare -* input arguments for query (Case 51076) -* -* 1.0.9: -* added input argument to servicestatus method which allows single service -* filtering (Case 50804) -* -* 1.0.8: -* correct unpark bug as reported by Randall Kent -* -* 1.0.7: -* Corrected typo for setrellerlimits where xml_query incorrectly called xml-api's setresellerips -* -* 1.0.6: -* Changed 'user' URL parameter for API1/2 calls to 'cpanel_xmlapi_user'/'cpanel_jsonapi_user' to resolve conflicts with API2 functions that use 'user' as a parameter -* Relocated exmaple script to Example subdirectory -* Modified example scripts to take remote server IP and root password from environment variables REMOTE_HOST and REMOTE_PASSWORD, respectively -* Created subdirectory Tests for PHPUnit tests -* Add PHPUnit test BasicParseTest.php -* -* 1.0.5: -* fix bug where api1_query and api2_query would not return JSON data -* -* 1.0.4: -* set_port will now convert non-int values to ints -* -* 1.0.3: -* Fixed issue with set_auth_type using incorrect logic for determining acceptable auth types -* Suppress non-UTF8 encoding when using curl -* -* 1.0.2: -* Increased curl buffer size to 128kb from 16kb -* Fix double encoding issue in terminateresellers() -* -* 1.0.1: -* Fixed use of wrong variable name in curl error checking -* adjust park() to use api2 rather than API1 -* -* 1.0 -* Added in 11.25 functions -* Changed the constructor to allow for either the "DEFINE" config setting method or using parameters -* Removed used of the gui setting -* Added fopen support -* Added auto detection for fopen or curl (uses curl by default) -* Added ability to return in multiple formats: associative array, simplexml, xml, json -* Added PHP Documentor documentation for all necessary functions -* Changed submission from GET to POST -* -* -* @copyright 2012 cPanel, Inc -* @license http://sdk.cpanel.net/license/bsd.html -* @version 1.0.13 -* @link http://twiki.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/XmlApi -* @since File available since release 0.1 -**/ - -/** -* The base XML-API class -* -* The XML-API class allows for easy execution of cPanel XML-API calls. The goal of this project is to create -* an open source library that can be used for multiple types of applications. This class relies on PHP5 compiled -* with both curl and simplexml support. -* -* Making Calls with this class are done in the following steps: -* -* 1.) Instaniating the class: -* $xmlapi = new xmlapi($host); -* -* 2.) Setting access credentials within the class via either set_password or set_hash: -* $xmlapi->set_hash("username", $accessHash); -* $xmlapi->set_password("username", "password"); -* -* 3.) Execute a function -* $xmlapi->listaccts(); -* -* @category Cpanel -* @package xmlapi -* @copyright 2012 cPanel, Inc. -* @license http://sdk.cpanel.net/license/bsd.html -* @version Release: 1.0.13 -* @link http://twiki.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/XmlApi -* @since Class available since release 0.1 -**/ - -class xmlapi -{ - // should debugging statements be printed? - private $debug = false; - - // The host to connect to - private $host = '127.0.0.1'; - - // the port to connect to - private $port = '2087'; - - // should be the literal strings http or https - private $protocol = 'https'; - - // output that should be given by the xml-api - private $output = 'simplexml'; - - // literal strings hash or password - private $auth_type = null; - - // the actual password or hash - private $auth = null; - - // username to authenticate as - private $user = null; - - // The HTTP Client to use - - private $http_client = 'curl'; - - /** - * Instantiate the XML-API Object - * All parameters to this function are optional and can be set via the accessor functions or constants - * This defaults to password auth, however set_hash can be used to use hash authentication - * - * @param string $host The host to perform queries on - * @param string $user The username to authenticate as - * @param string $password The password to authenticate with - * @return Xml_Api object - */ - public function __construct($host = null, $user = null, $password = null ) - { - // Check if debugging must be enabled - if ( (defined('XMLAPI_DEBUG')) && (XMLAPI_DEBUG == '1') ) { - $this->debug = true; - } - - // Check if raw xml output must be enabled - if ( (defined('XMLAPI_RAW_XML')) && (XMLAPI_RAW_XML == '1') ) { - $this->raw_xml = true; - } - - /** - * Authentication - * This can either be passed at this point or by using the set_hash or set_password functions - **/ - - if ( ( defined('XMLAPI_USER') ) && ( strlen(XMLAPI_USER) > 0 ) ) { - $this->user = XMLAPI_USER; - - // set the authtype to pass and place the password in $this->pass - if ( ( defined('XMLAPI_PASS') ) && ( strlen(XMLAPI_PASS) > 0 ) ) { - $this->auth_type = 'pass'; - $this->auth = XMLAPI_PASS; - } - - // set the authtype to hash and place the hash in $this->auth - if ( ( defined('XMLAPI_HASH') ) && ( strlen(XMLAPI_HASH) > 0 ) ) { - $this->auth_type = 'hash'; - $this->auth = preg_replace("/(\n|\r|\s)/", '', XMLAPI_HASH); - } - - // Throw warning if XMLAPI_HASH and XMLAPI_PASS are defined - if ( ( ( defined('XMLAPI_HASH') ) && ( strlen(XMLAPI_HASH) > 0 ) ) - && ( ( defined('XMLAPI_PASS') ) && ( strlen(XMLAPI_PASS) > 0 ) ) ) { - error_log('warning: both XMLAPI_HASH and XMLAPI_PASS are defined, defaulting to XMLAPI_HASH'); - } - - - // Throw a warning if XMLAPI_HASH and XMLAPI_PASS are undefined and XMLAPI_USER is defined - if ( !(defined('XMLAPI_HASH') ) || !defined('XMLAPI_PASS') ) { - error_log('warning: XMLAPI_USER set but neither XMLAPI_HASH or XMLAPI_PASS have not been defined'); - } - - } - - if ( ( $user != null ) && ( strlen( $user ) < 9 ) ) { - $this->user = $user; - } - - if ($password != null) { - $this->set_password($password); - } - - /** - * Connection - * - * $host/XMLAPI_HOST should always be equal to either the IP of the server or it's hostname - */ - - // Set the host, error if not defined - if ($host == null) { - if ( (defined('XMLAPI_HOST')) && (strlen(XMLAPI_HOST) > 0) ) { - $this->host = XMLAPI_HOST; - } else { - throw new Exception("No host defined"); - } - } else { - $this->host = $host; - } - - // disabling SSL is probably a bad idea.. just saying. - if ( defined('XMLAPI_USE_SSL' ) && (XMLAPI_USE_SSL == '0' ) ) { - $this->protocol = "http"; - } - - // 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'); - } - - } - - /** - * Accessor Functions - **/ - /** - * Return whether the debug option is set within the object - * - * @return boolean - * @see set_debug() - */ - public function get_debug() - { - return $this->debug; - } - - /** - * Turn on debug mode - * - * Enabling this option will cause this script to print debug information such as - * the queries made, the response XML/JSON and other such pertinent information. - * Calling this function without any parameters will enable debug mode. - * - * @param bool $debug turn on or off debug mode - * @see get_debug() - */ - public function set_debug( $debug = 1 ) - { - $this->debug = $debug; - } - - /** - * Get the host being connected to - * - * This function will return the host being connected to - * @return string host - * @see set_host() - */ - public function get_host() - { - return $this->host; - } - - /** - * Set the host to query - * - * Setting this will set the host to be queried - * @param string $host The host to query - * @see get_host() - */ - public function set_host( $host ) - { - $this->host = $host; - } - - /** - * Get the port to connect to - * - * This will return which port the class is connecting to - * @return int $port - * @see set_port() - */ - public function get_port() - { - return $this->port; - } - - /** - * Set the port to connect to - * - * This will allow a user to define which port needs to be connected to. - * The default port set within the class is 2087 (WHM-SSL) however other ports are optional - * this function will automatically set the protocol to http if the port is equal to: - * - 2082 - * - 2086 - * - 2095 - * - 80 - * @param int $port the port to connect to - * @see set_protocol() - * @see get_port() - */ - public function set_port( $port ) - { - if ( !is_int( $port ) ) { - $port = intval($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; - } - - /** - * Return the protocol being used to query - * - * This will return the protocol being connected to - * @return string - * @see set_protocol() - */ - public function get_protocol() - { - return $this->protocol; - } - - /** - * Set the protocol to use to query - * - * This will allow you to set the protocol to query cpsrvd with. The only to acceptable values - * to be passed to this function are 'http' or 'https'. Anything else will cause the class to throw - * an Exception. - * @param string $proto the protocol to use to connect to cpsrvd - * @see get_protocol() - */ - public function set_protocol( $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; - } - - /** - * Return what format calls with be returned in - * - * This function will return the currently set output format - * @see set_output() - * @return string - */ - public function get_output() - { - return $this->output; - } - - /** - * Set the output format for call functions - * - * This class is capable of returning data in numerous formats including: - * - json - * - xml - * - {@link http://php.net/simplexml SimpleXML} - * - {@link http://us.php.net/manual/en/language.types.array.php Associative Arrays} - * - * These can be set by passing this class any of the following values: - * - json - return JSON string - * - xml - return XML string - * - simplexml - return SimpleXML object - * - array - Return an associative array - * - * Passing any value other than these to this class will cause an Exception to be thrown. - * @param string $output the output type to be set - * @see get_output() - */ - public function set_output( $output ) - { - if ($output != 'json' && $output != 'xml' && $output != 'array' && $output != 'simplexml') { - throw new Exception('json, xml, array and simplexml are the only allowed values for set_output'); - } - $this->output = $output; - } - - /** - * Return the auth_type being used - * - * This function will return a string containing the auth type in use - * @return string auth type - * @see set_auth_type() - */ - public function get_auth_type() - { - return $this->auth_type; - } - - /** - * Set the auth type - * - * This class is capable of authenticating with both hash auth and password auth - * This function will allow you to manually set which auth_type you are using. - * - * the only accepted parameters for this function are "hash" and "pass" anything else will cuase - * an exception to be thrown - * - * @see set_password() - * @see set_hash() - * @see get_auth_type() - * @param string auth_type the auth type to be set - */ - public function set_auth_type( $auth_type ) - { - if ($auth_type != 'hash' && $auth_type != 'pass') { - throw new Exception('the only two allowable auth types arehash and path'); - } - $this->auth_type = $auth_type; - } - - /** - * Set the password to be autenticated with - * - * This will set the password to be authenticated with, the auth_type will be automatically adjusted - * when this function is used - * - * @param string $pass the password to authenticate with - * @see set_hash() - * @see set_auth_type() - * @see set_user() - */ - public function set_password( $pass ) - { - $this->auth_type = 'pass'; - $this->auth = $pass; - } - - /** - * Set the hash to authenticate with - * - * This will set the hash to authenticate with, the auth_type will automatically be set when this function - * is used. This function will automatically strip the newlines from the hash. - * @param string $hash the hash to autenticate with - * @see set_password() - * @see set_auth_type() - * @see set_user() - */ - public function set_hash( $hash ) - { - $this->auth_type = 'hash'; - $this->auth = preg_replace("/(\n|\r|\s)/", '', $hash); - } - - /** - * Return the user being used for authtication - * - * This will return the username being authenticated against. - * - * @return string - */ - public function get_user() - { - return $this->user; - } - - /** - * Set the user to authenticate against - * - * This will set the user being authenticated against. - * @param string $user username - * @see set_password() - * @see set_hash() - * @see get_user() - */ - public function set_user( $user ) - { - $this->user = $user; - } - - /** - * Set the user and hash to be used for authentication - * - * This function will allow one to set the user AND hash to be authenticated with - * - * @param string $user username - * @param string $hash WHM Access Hash - * @see set_hash() - * @see set_user() - */ - public function hash_auth( $user, $hash ) - { - $this->set_hash( $hash ); - $this->set_user( $user ); - } - - /** - * Set the user and password to be used for authentication - * - * This function will allow one to set the user AND password to be authenticated with - * @param string $user username - * @param string $pass password - * @see set_pass() - * @see set_user() - */ - public function password_auth( $user, $pass ) - { - $this->set_password( $pass ); - $this->set_user( $user ); - } - - /** - * Return XML format - * - * this function will cause call functions to return XML format, this is the same as doing: - * set_output('xml') - * - * @see set_output() - */ - public function return_xml() - { - $this->set_output('xml'); - } - - /** - * Return simplexml format - * - * this function will cause all call functions to return simplexml format, this is the same as doing: - * set_output('simplexml') - * - * @see set_output() - */ - public function return_object() - { - $this->set_output('simplexml'); - } - - /** - * Set the HTTP client to use - * - * This class is capable of two types of HTTP Clients: - * - curl - * - fopen - * - * When using allow url fopen the class will use get_file_contents to perform the query - * The only two acceptable parameters for this function are 'curl' and 'fopen'. - * This will default to fopen, however if allow_url_fopen is disabled inside of php.ini - * it will switch to curl - * - * @param string client The http client to use - * @see get_http_client() - */ - - public function set_http_client( $client ) - { - if ( ( $client != 'curl' ) && ( $client != 'fopen' ) ) { - throw new Exception('only curl and fopen and allowed http clients'); - } - $this->http_client = $client; - } - - /** - * Get the HTTP Client in use - * - * This will return a string containing the HTTP client currently in use - * - * @see set_http_client() - * @return string - */ - public function get_http_client() - { - return $this->http_client; - } - - /* - * Query Functions - * -- - * This is where the actual calling of the XML-API, building API1 & API2 calls happens - */ - - /** - * 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 xmlapi_query( $function, $vars = array() ) - { - // Check to make sure all the data needed to perform the query is in place - if (!$function) { - throw new Exception('xmlapi_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 = '/xml-api/'; - - if ($this->output == 'json') { - $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, '<html>') && - preg_match('/HTTP-EQUIV[\s]?=[\s]?"refresh"/i', $response) && - preg_match('/<meta [^>]+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 <html> is in the case of authentication error - // cPanel 11.25 fixes this issue, but if <html> is in the response, we'll error out. - - if (stristr($response, '<html>') == 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; - } - - - // perform simplexml transformation (array relies on this) - if ( ($this->output == 'simplexml') || $this->output == 'array') { - $response = simplexml_load_string($response, null, LIBXML_NOERROR | LIBXML_NOWARNING); - if (!$response) { - error_log("Some error message here"); - - return; - } - if ($this->debug) { - error_log("SimpleXML var_dump:\n" . print_r($response, true)); - } - } - - // perform array tranformation - if ($this->output == 'array') { - $response = $this->unserialize_xml($response); - if ($this->debug) { - error_log("Associative Array var_dump:\n" . print_r($response, true)); - } - } - - 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); - } - - - /* - * Convert simplexml to associative arrays - * - * This function will convert simplexml to associative arrays. - */ - private function unserialize_xml($input, $callback = null, $recurse = false) - { - // Get input, loading an xml string with simplexml if its the top level of recursion - $data = ( (!$recurse) && is_string($input) ) ? simplexml_load_string($input) : $input; - // Convert SimpleXMLElements to array - if ($data instanceof SimpleXMLElement) { - $data = (array) $data; - } - // Recurse into arrays - if (is_array($data)) { - foreach ($data as &$item) { - $item = $this->unserialize_xml($item, $callback, true); - } - } - // Run callback and return - return (!is_array($data) && is_callable($callback)) ? call_user_func($callback, $data) : $data; - } - - - /* TO DO: - Implement API1 and API2 query functions!!!!! - */ - /** - * Call an API1 function - * - * This function allows you to call API1 from within the XML-API, This allowes a user to peform actions - * such as adding ftp accounts, etc - * - * @param string $user The username of the account to perform API1 actions on - * @param string $module The module of the API1 call to use - * @param string $function The function of the API1 call - * @param array $args The arguments for the API1 function, this should be a non-associative array - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/CallingAPIFunctions XML API Call documentation - * @link http://docs.cpanel.net/twiki/bin/view/DeveloperResources/ApiRef/WebHome API1 & API2 Call documentation - * @link http://docs.cpanel.net/twiki/bin/view/DeveloperResources/ApiBasics/CallingApiOne API1 Documentation - */ - public function api1_query($user, $module, $function, $args = array() ) - { - if ( !isset($module) || !isset($function) || !isset($user) ) { - error_log("api1_query requires that a module and function are passed to it"); - - return false; - } - - if (!is_array($args)) { - error_log('api1_query requires that it is passed an array as the 4th parameter'); - - return false; - } - - $cpuser = 'cpanel_xmlapi_user'; - $module_type = 'cpanel_xmlapi_module'; - $func_type = 'cpanel_xmlapi_func'; - $api_type = 'cpanel_xmlapi_apiversion'; - - if ( $this->get_output() == 'json' ) { - $cpuser = 'cpanel_jsonapi_user'; - $module_type = 'cpanel_jsonapi_module'; - $func_type = 'cpanel_jsonapi_func'; - $api_type = 'cpanel_jsonapi_apiversion'; - } - - $call = array( - $cpuser => $user, - $module_type => $module, - $func_type => $function, - $api_type => '1' - ); - for ($int = 0; $int < count($args); $int++) { - $call['arg-' . $int] = $args[$int]; - } - - return $this->xmlapi_query('cpanel', $call); - } - - /** - * Call an API2 Function - * - * This function allows you to call an API2 function, this is the modern API for cPanel and should be used in preference over - * API1 when possible - * - * @param string $user The username of the account to perform API2 actions on - * @param string $module The module of the API2 call to use - * @param string $function The function of the API2 call - * @param array $args An associative array containing the arguments for the API2 call - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/CallingAPIFunctions XML API Call documentation - * @link http://docs.cpanel.net/twiki/bin/view/DeveloperResources/ApiRef/WebHome API1 & API2 Call documentation - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ApiTwo Legacy API2 Documentation - * @link http://docs.cpanel.net/twiki/bin/view/DeveloperResources/ApiBasics/CallingApiTwo API2 Documentation - */ - - 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; - } - - $cpuser = 'cpanel_xmlapi_user'; - $module_type = 'cpanel_xmlapi_module'; - $func_type = 'cpanel_xmlapi_func'; - $api_type = 'cpanel_xmlapi_apiversion'; - - if ( $this->get_output() == 'json' ) { - $cpuser = 'cpanel_jsonapi_user'; - $module_type = 'cpanel_jsonapi_module'; - $func_type = 'cpanel_jsonapi_func'; - $api_type = 'cpanel_jsonapi_apiversion'; - } - - $args[$cpuser] = $user; - $args[$module_type] = $module; - $args[$func_type] = $function; - $args[$api_type] = '2'; - - return $this->xmlapi_query('cpanel', $args); - } - - #### - # XML API Functions - #### - - /** - * Return a list of available XML-API calls - * - * This function will return an array containing all applications available within the XML-API - * - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ListAvailableCalls XML API Call documentation - */ - public function applist() - { - return $this->xmlapi_query('applist'); - } - - #### - # Account functions - #### - - /** - * Create a cPanel Account - * - * This function will allow one to create an account, the $acctconf parameter requires that the follow - * three associations are defined: - * - username - * - password - * - domain - * - * Failure to prive these will cause an error to be logged. Any other key/value pairs as defined by the createaccount call - * documentation are allowed parameters for this call. - * - * @param array $acctconf - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/CreateAccount XML API Call documentation - */ - - public function createacct($acctconf) - { - if (!is_array($acctconf)) { - error_log("createacct requires that first parameter passed to it is an array"); - - return false; - } - if (!isset($acctconf['username']) || !isset($acctconf['password']) || !isset($acctconf['domain'])) { - error_log("createacct requires that username, password & domain elements are in the array passed to it"); - - return false; - } - - return $this->xmlapi_query('createacct', $acctconf); - } - - /** - * Change a cPanel Account's Password - * - * This function will allow you to change the password of a cpanel account - * - * @param string $username The username to change the password of - * @param string $pass The new password for the cPanel Account - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ChangePassword XML API Call documentation - */ - public function passwd($username, $pass) - { - if (!isset($username) || !isset($pass)) { - error_log("passwd requires that an username and password are passed to it"); - - return false; - } - - return $this->xmlapi_query('passwd', array('user' => $username, 'pass' => $pass)); - } - - /** - * Limit an account's monthly bandwidth usage - * - * This function will set an account's bandwidth limit. - * - * @param string $username The username of the cPanel account to modify - * @param int $bwlimit The new bandwidth limit in megabytes - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/LimitBandwidth XML API Call documentation - */ - public function limitbw($username, $bwlimit) - { - if (!isset($username) || !isset($bwlimit)) { - error_log("limitbw requires that an username and bwlimit are passed to it"); - - return false; - } - - return $this->xmlapi_query('limitbw', array('user' => $username, 'bwlimit' => $bwlimit)); - } - - /** - * List accounts on Server - * - * This call will return a list of account on a server, either no parameters or both parameters may be passed to this function. - * - * @param string $searchtype Type of account search to use, allowed values: domain, owner, user, ip or package - * @param string $search the string to search against - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ListAccounts XML API Call documentation - */ - public function listaccts($searchtype = null, $search = null) - { - if ($search) { - return $this->xmlapi_query('listaccts', array('searchtype' => $searchtype, 'search' => $search )); - } - - return $this->xmlapi_query('listaccts'); - } - - /** - * Modify a cPanel account - * - * This call will allow you to change limitations and information about an account. See the XML API call documentation for a list of - * acceptable values for args. - * - * @param string $username The username to modify - * @param array $args the new values for the modified account (see {@link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ModifyAccount modifyacct documentation}) - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ModifyAccount XML API Call documentation - */ - public function modifyacct($username, $args = array()) - { - if (!isset($username)) { - error_log("modifyacct requires that username is passed to it"); - - return false; - } - $args['user'] = $username; - if (sizeof($args) < 2) { - error_log("modifyacct requires that at least one attribute is passed to it"); - - return false; - } - - return $this->xmlapi_query('modifyacct', $args); - } - - /** - * Edit a cPanel Account's Quota - * - * This call will allow you to change a cPanel account's quota - * - * @param string $username The username of the account to modify the quota. - * @param int $quota the new quota in megabytes - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/EditQuota XML API Call documentation - */ - public function editquota($username, $quota) - { - if (!isset($username) || !isset($quota)) { - error_log("editquota requires that an username and quota are passed to it"); - - return false; - } - - return $this->xmlapi_query('editquota', array('user' => $username, 'quota' => $quota)); - } - - /** - * Return a summary of the account's information - * - * This call will return a brief report of information about an account, such as: - * - Disk Limit - * - Disk Used - * - Domain - * - Account Email - * - Theme - * - Start Data - * - * Please see the XML API Call documentation for more information on what is returned by this call - * - * @param string $username The username to retrieve a summary of - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ShowAccountInformation XML API Call documenation - */ - public function accountsummary($username) - { - if (!isset($username)) { - error_log("accountsummary requires that an username is passed to it"); - - return false; - } - - return $this->xmlapi_query('accountsummary', array('user' => $username)); - } - - /** - * Suspend a User's Account - * - * This function will suspend the specified cPanel users account. - * The $reason parameter is optional, but can contain a string of any length - * - * @param string $username The username to suspend - * @param string $reason The reason for the suspension - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/SuspendAccount XML API Call documentation - */ - public function suspendacct($username, $reason = null) - { - if (!isset($username)) { - error_log("suspendacct requires that an username is passed to it"); - - return false; - } - if ($reason) { - return $this->xmlapi_query('suspendacct', array('user' => $username, 'reason' => $reason )); - } - - return $this->xmlapi_query('suspendacct', array('user' => $username)); - } - - /** - * List suspended accounts on a server - * - * This function will return an array containing all the suspended accounts on a server - * - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ListSuspended XML API Call documentation - */ - public function listsuspended() - { - return $this->xmlapi_query('listsuspended'); - } - - /** - * Remove an Account - * - * This XML API call will remove an account on the server - * The $keepdns parameter is optional, when enabled this will leave the DNS zone on the server - * - * @param string $username The usename to delete - * @param bool $keepdns When pass a true value, the DNS zone will be retained - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/TerminateAccount - */ - public function removeacct($username, $keepdns = false) - { - if (!isset($username)) { - error_log("removeacct requires that a username is passed to it"); - - return false; - } - if ($keepdns) { - return $this->xmlapi_query('removeacct', array('user' => $username, 'keepdns' => '1')); - } - - return $this->xmlapi_query('removeacct', array('user' => $username)); - } - - /** - * Unsuspend an Account - * - * This XML API call will unsuspend an account - * - * @param string $username The username to unsuspend - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/UnsuspendAcount XML API Call documentation - */ - public function unsuspendacct($username) - { - if (!isset($username)) { - error_log("unsuspendacct requires that a username is passed to it"); - - return false; - } - - return $this->xmlapi_query('unsuspendacct', array('user' => $username)); - } - - /** - * Change an Account's Package - * - * This XML API will change the package associated account. - * - * @param string $username the username to change the package of - * @param string $pkg The package to change the account to. - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ChangePackage XML API Call documentation - */ - public function changepackage($username, $pkg) - { - if (!isset($username) || !isset($pkg)) { - error_log("changepackage requires that username and pkg are passed to it"); - - return false; - } - - return $this->xmlapi_query('changepackage', array('user' => $username, 'pkg' => $pkg)); - } - - /** - * Return the privileges a reseller has in WHM - * - * This will return a list of the privileges that a reseller has to WHM - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ViewPrivileges XML API Call documentation - */ - public function myprivs() - { - return $this->xmlapi_query('myprivs'); - } - - - /** - * Display Data about a Virtual Host - * - * This function will return information about a specific domain. This data is essentially a representation of the data - * Contained in the httpd.conf VirtualHost for the domain. - * - * @return mixed - * @param string $domain The domain to fetch information for - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/DomainUserData - */ - - public function domainuserdata( $domain ) - { - if (!isset( $domain ) ) { - error_log("domainuserdata requires that domain is passed to it"); - - return false; - } - - return $this->xmlapi_query("domainuserdata", array( 'domain' => $domain ) ); - } - - /** - * Change a site's IP Address - * - * This function will allow you to change the IP address that a domain listens on. - * In order to properly call this function Either $user or $domain parameters must be defined - * @param string $ip The $ip address to change the account or domain to - * @param string $user The username to change the IP of - * @param string $domain The domain to change the IP of - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/SetSiteIp XML API Call documentation - */ - public function setsiteip ( $ip, $user = null, $domain = null ) - { - if ( !isset($ip) ) { - error_log("setsiteip requires that ip is passed to it"); - - return false; - } - - if ($user == null && $domain == null) { - error_log("setsiteip requires that either domain or user is passed to it"); - - return false; - } - - if ($user == null) { - return $this->xmlapi_query( "setsiteip", array( "ip" => $ip, "domain" => $domain ) ); - } else { - return $this->xmlapi_query( "setsiteip", array( "ip" => $ip, "user" => $user ) ); - } - } - - #### - # DNS Functions - #### - - // This API function lets you create a DNS zone. - /** - * Add a DNS Zone - * - * This XML API function will create a DNS Zone. This will use the "standard" template when - * creating the zone. - * - * @param string $domain The DNS Domain that you wish to create a zone for - * @param string $ip The IP you want the domain to resolve to - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/AddDNSZone XML API Call documentation - */ - public function adddns($domain, $ip) - { - if (!isset($domain) || !isset($ip)) { - error_log("adddns require that domain, ip are passed to it"); - - return false; - } - - return $this->xmlapi_query('adddns', array('domain' => $domain, 'ip' => $ip)); - } - - /** - * Add a record to a zone - * - * This will append a record to a DNS Zone. The $args argument to this function - * must be an associative array containing information about the DNS zone, please - * see the XML API Call documentation for more info - * - * @param string $zone The DNS zone that you want to add the record to - * @param array $args Associative array representing the record to be added - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/AddZoneRecord XML API Call documentation - */ - public function addzonerecord( $zone, $args ) - { - if (!is_array($args)) { - error_log("addzonerecord requires that $args passed to it is an array"); - - return; - } - - $args['zone'] = $zone; - - return $this->xmlapi_query('addzonerecord', $args); - } - - /** - * Edit a Zone Record - * - * This XML API Function will allow you to edit an existing DNS Zone Record. - * This works by passing in the line number of the record you wish to edit. - * Line numbers can be retrieved with dumpzone() - * - * @param string $zone The zone to edit - * @param int $line The line number of the zone to edit - * @param array $args An associative array representing the zone record - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/EditZoneRecord XML API Call documentation - * @see dumpzone() - */ - - public function editzonerecord( $zone, $line, $args ) - { - if (!is_array($args)) { - error_log("editzone requires that $args passed to it is an array"); - - return; - } - - $args['domain'] = $zone; - $args['Line'] = $line; - - return $this->xmlapi_query('editzonerecord', $args); - } - - /** - * Retrieve a DNS Record - * - * This function will return a data structure representing a DNS record, to - * retrieve all lines see dumpzone. - * @param string $zone The zone that you want to retrieve a record from - * @param string $line The line of the zone that you want to retrieve - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/GetZoneRecord XML API Call documentation - */ - public function getzonerecord( $zone, $line ) - { - return $this->xmlapi_query('getzonerecord', array( 'domain' => $zone, 'Line' => $line ) ); - } - - /** - * Remove a DNS Zone - * - * This function will remove a DNS Zone from the server - * - * @param string $domain The domain to be remove - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/DeleteDNSZone XML API Call documentation - */ - public function killdns($domain) - { - if (!isset($domain)) { - error_log("killdns requires that domain is passed to it"); - - return false; - } - - return $this->xmlapi_query('killdns', array('domain' => $domain)); - } - - /** - * Return a List of all DNS Zones on the server - * - * This XML API function will return an array containing all the DNS Zones on the server - * - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ListDNSZone XML API Call documentation - */ - public function listzones() - { - return $this->xmlapi_query('listzones'); - } - - /** - * Return all records in a zone - * - * This function will return all records within a zone. - * @param string $domain The domain to return the records from. - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ListOneZone XML API Call documentation - * @see editdnsrecord() - * @see getdnsrecord() - */ - public function dumpzone($domain) - { - if (!isset($domain)) { - error_log("dumpzone requires that a domain is passed to it"); - - return false; - } - - return $this->xmlapi_query('dumpzone', array('domain' => $domain)); - } - - /** - * Return a Nameserver's IP - * - * This function will return a nameserver's IP - * - * @param string $nameserver The nameserver to lookup - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/LookupIP XML API Call documentation - */ - public function lookupnsip($nameserver) - { - if (!isset($nameserver)) { - error_log("lookupnsip requres that a nameserver is passed to it"); - - return false; - } - - return $this->xmlapi_query('lookupnsip', array('nameserver' => $nameserver)); - } - - /** - * Remove a line from a zone - * - * This function will remove the specified line from a zone - * @param string $zone The zone to remove a line from - * @param int $line The line to remove from the zone - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/RemoveZone XML API Call documentation - */ - public function removezonerecord($zone, $line) - { - if ( !isset($zone) || !isset($line) ) { - error_log("removezone record requires that a zone and line number is passed to it"); - - return false; - } - - return $this->xmlapi_query('removezonerecord', array('zone' => $zone, 'Line' => $line) ); - } - - /** - * Reset a zone - * - * This function will reset a zone removing all custom records. Subdomain records will be readded by scanning the userdata datastore. - * @param string $domain the domain name of the zone to reset - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ResetZone XML API Call documentation - */ - public function resetzone($domain) - { - if ( !isset($domain) ) { - error_log("resetzone requires that a domain name is passed to it"); - - return false; - } - - return $this->xmlapi_query('resetzone', array('domain' => $domain)); - } - - #### - # Package Functions - #### - - /** - * Add a new package - * - * This function will allow you to add a new package - * This function should be passed an associative array containing elements that define package parameters. - * These variables map directly to the parameters for the XML-API Call, please refer to the link below for a complete - * list of possible variable. The "name" element is required. - * @param array $pkg an associative array containing package parameters - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/AddPackage XML API Call documentation - */ - public function addpkg($pkg) - { - if (!isset($pkg['name'])) { - error_log("addpkg requires that name is defined in the array passed to it"); - - return false; - } - - return $this->xmlapi_query('addpkg', $pkg); - } - - /** - * Remove a package - * - * This function allow you to delete a package - * @param string $pkgname The package you wish to delete - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/DeletePackage XML API Call documentation - */ - public function killpkg($pkgname) - { - if (!isset($pkgname)) { - error_log("killpkg requires that the package name is passed to it"); - - return false; - } - - return $this->xmlapi_query('killpkg', array('pkg' => $pkgname)); - } - - /** - * Edit a package - * - * This function allows you to change a package's paremeters. This is passed an associative array defining - * the parameters for the package. The keys within this array map directly to the XML-API call, please see the link - * below for a list of possible keys within this package. The name element is required. - * @param array $pkg An associative array containing new parameters for the package - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/EditPackage XML API Call documentation - */ - public function editpkg($pkg) - { - if (!isset($pkg['name'])) { - error_log("editpkg requires that name is defined in the array passed to it"); - - return false; - } - - return $this->xmlapi_query('editpkg', $pkg); - } - - /** - * List Packages - * - * This function will list all packages available to the user - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ListPackages XML API Call documentation - */ - public function listpkgs() - { - return $this->xmlapi_query('listpkgs'); - } - - #### - # Reseller functions - #### - - /** - * Make a user a reseller - * - * This function will allow you to mark an account as having reseller privileges - * @param string $username The username of the account you wish to add reseller privileges to - * @param int $makeowner Boolean 1 or 0 defining whether the account should own itself or not - * @see setacls() - * @see setresellerlimits() - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/AddResellerPrivileges XML API Call documentation - */ - public function setupreseller($username, $makeowner = true) - { - if (!isset($username)) { - error_log("setupreseller requires that username is passed to it"); - - return false; - } - if ($makeowner) { - return $this->xmlapi_query('setupreseller', array('user' => $username, 'makeowner' => '1')); - } - - return $this->xmlapi_query('setupreseller', array('user' => $username, 'makeowner' => '0')); - } - - /** - * Create a New ACL List - * - * This function allows you to create a new privilege set for reseller accounts. This is passed an - * Associative Array containing the configuration information for this variable. Please see the XML API Call documentation - * For more information. "acllist" is a required element within this array - * @param array $acl an associative array describing the parameters for the ACL to be create - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/CreateResellerACLList XML API Call documentation - */ - public function saveacllist($acl) - { - if (!isset($acl['acllist'])) { - error_log("saveacllist requires that acllist is defined in the array passed to it"); - - return false; - } - - return $this->xmlapi_query('saveacllist', $acl); - } - - - /** - * List available saved ACLs - * - * This function will return a list of Saved ACLs for reseller accounts - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ListCurrentResellerACLLists XML API Call documentation - */ - public function listacls() - { - return $this->xmlapi_query('listacls'); - } - - /** - * List Resellers - * - * This function will return a list of resellers on the server - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ListResellerAccounts XML API Call documentation - */ - public function listresellers() - { - return $this->xmlapi_query('listresellers'); - } - - /** - * Get a reseller's statistics - * - * This function will return general information on a reseller and all it's account individually such as disk usage and bandwidth usage - * - * @param string $username The reseller to be checked - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ListResellersAccountsInformation XML API Call documentation - */ - public function resellerstats($username) - { - if (!isset($username)) { - error_log("resellerstats requires that a username is passed to it"); - - return false; - } - - return $this->xmlapi_query('resellerstats', array('reseller' => $username)); - } - - /** - * Remove Reseller Privileges - * - * This function will remove an account's reseller privileges, this does not remove the account. - * - * @param string $username The username to remove reseller privileges from - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/RemoveResellerPrivileges XML API Call documentation - */ - public function unsetupreseller($username) - { - if (!isset($username)) { - error_log("unsetupreseller requires that a username is passed to it"); - - return false; - } - - return $this->xmlapi_query('unsetupreseller', array('user' => $username)); - } - - /** - * Set a reseller's privileges - * - * This function will allow you to set what parts of WHM a reseller has access to. This is passed an associative array - * containing the privleges that this reseller should have access to. These map directly to the parameters passed to the XML API Call - * Please view the XML API Call documentation for more information. "reseller" is the only required element within this array - * @param array $acl An associative array containing all the ACL information for the reseller - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/SetResellersACLList XML API Call documentation - */ - public function setacls($acl) - { - if (!isset($acl['reseller'])) { - error_log("setacls requires that reseller is defined in the array passed to it"); - - return false; - } - - return $this->xmlapi_query('setacls', $acl); - } - - /** - * Terminate a Reseller's Account - * - * This function will terminate a reseller's account and all accounts owned by the reseller - * - * @param string $reseller the name of the reseller to terminate - * @param boolean $terminatereseller Passing this as true will terminate the the reseller's account as well as all the accounts owned by the reseller - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/TerminateResellerandAccounts XML API Call documentation - * - **/ - public function terminatereseller($reseller, $terminatereseller = true) - { - if (!isset($reseller)) { - error_log("terminatereseller requires that username is passed to it"); - - return false; - } - $verify = 'I understand this will irrevocably remove all the accounts owned by the reseller ' . $reseller; - if ($terminatereseller) { - return $this->xmlapi_query('terminatereseller', array('reseller' => $reseller, 'terminatereseller' => '1', 'verify' => $verify)); - } - - return $this->xmlapi_query('terminatereseller', array('reseller' => $reseller, 'terminatereseller' => '0', 'verify' => $verify)); - } - - /** - * Set a reseller's dedicated IP addresses - * - * This function will set a reseller's dedicated IP addresses. If an IP is not passed to this function, - * it will reset the reseller to use the server's main shared IP address. - * @param string $user The username of the reseller to change dedicated IPs for - * @param string $ip The IP to assign to the reseller, this can be a comma-seperated list of IPs to allow for multiple IP addresses - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/SetResellerIps XML API Call documentation - */ - public function setresellerips($user, $ip = null) - { - if (!isset($user) ) { - error_log("setresellerips requires that a username is passed to it"); - - return false; - } - $params = array("user" => $user); - if ($ip != null) { - $params['ip'] = $ip; - } - - return $this->xmlapi_query('setresellerips',$params); - } - - /** - * Set Accounting Limits for a reseller account - * - * This function allows you to define limits for reseller accounts not included with in access control such as - * the number of accounts a reseller is allowed to create, the amount of disk space to use. - * This function is passed an associative array defining these limits, these map directly to the parameters for the XML API - * Call, please refer to the XML API Call documentation for more information. The only required parameters is "user" - * - * @param array $reseller_cfg An associative array containing configuration information for the specified reseller - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/SetResellerLimits XML API Call documentation - * - */ - public function setresellerlimits( $reseller_cfg ) - { - if ( !isset($reseller_cfg['user'] ) ) { - error_log("setresellerlimits requires that a user is defined in the array passed to it"); - - return false; - } - - return $this->xmlapi_query('setresellerlimits',$reseller_cfg); - } - - /** - * Set a reseller's main IP - * - * This function will allow you to set a reseller's main IP. By default all accounts created by this reseller - * will be created on this IP - * @param string $reseller the username of the reseller to change the main IP of - * @param string $ip The ip you would like this reseller to create accounts on by default - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/SetResellerMainIp XML API Call documentation - */ - public function setresellermainip($reseller, $ip) - { - if ( !isset($reseller) || !isset($ip) ) { - error_log("setresellermainip requires that an reseller and ip are passed to it"); - - return false; - } - - return $this->xmlapi_query("setresellermainip", array('user' => $reseller, 'ip' => $ip)); - } - - /** - * Set reseller package limits - * - * This function allows you to define which packages a reseller has access to use - * @param string $user The reseller you wish to define package limits for - * @param boolean $no_limit Whether or not you wish this reseller to have packages limits - * @param string $package if $no_limit is false, then the package you wish to modify privileges for - * @param boolean $allowed if $no_limit is false, then defines if the reseller should have access to the package or not - * @param int $number if $no_limit is false, then defines the number of account a reseller can create of a specific package - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/SetResellerPkgLimit XML API Call documentation - */ - public function setresellerpackagelimits($user, $no_limit, $package = null, $allowed = null, $number = null) - { - if (!isset($user) || !isset($no_limit) ) { - error_log("setresellerpackagelimits requires that a username and no_limit are passed to it by default"); - - return false; - } - if ($no_limit) { - return $this->xmlapi_query("setresellerpackagelimits", array( 'user' => $user, "no_limit" => '1') ); - } else { - if ( is_null($package) || is_null($allowed) ) { - error_log('setresellerpackagelimits requires that package and allowed are passed to it if no_limit eq 0'); - - return false; - } - $params = array( - 'user' => $user, - 'no_limit' => '0', - 'package' => $package, - ); - if ($allowed) { - $params['allowed'] = 1; - } else { - $params['allowed'] = 0; - } - if ( !is_null($number) ) { - $params['number'] = $number; - } - - return $this->xmlapi_query('setresellerpackagelimits', $params); - } - } - - /** - * Suspend a reseller and all accounts owned by a reseller - * - * This function, when called will suspend a reseller account and all account owned by said reseller - * @param string $reseller The reseller account to be suspended - * @param string $reason (optional) The reason for suspending the reseller account - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/SuspendReseller XML API Call documentation - */ - public function suspendreseller($reseller, $reason = null) - { - if (!isset($reseller) ) { - error_log("suspendreseller requires that the reseller's username is passed to it"); - - return false; - } - $params = array("user" => $reseller); - if ($reason) { - $params['reason'] = $reason; - } - - return $this->xmlapi_query('suspendreseller', $params); - } - - - /** - * Unsuspend a Reseller Account - * - * This function will unsuspend a reseller account and all accounts owned by the reseller in question - * @param string $user The username of the reseller to be unsuspended - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/UnsuspendReseller XML API Call documentation - */ - public function unsuspendreseller($user) - { - if (!isset($user) ) { - error_log("unsuspendreseller requires that a username is passed to it"); - - return false; - } - - return $this->xmlapi_query('unsuspendreseller', array('user' => $user)); - } - - /** - * Get the number of accounts owned by a reseller - * - * This function will return the number of accounts owned by a reseller account, along with information such as the number of active, suspended and accounting limits - * @param string $user The username of the reseller to get account information from - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/AcctCounts XML API Call documentation - */ - public function acctcounts($user) - { - if (!isset($user)) { - error_log('acctcounts requires that a username is passed to it'); - - return false; - } - - return $this->xmlapi_query('acctcounts', array('user' => $user) ); - } - - /** - * Set a reseller's nameservers - * - * This function allows you to change the nameservers that account created by a specific reseller account will use. - * If this function is not passed a $nameservers parameter, it will reset the nameservers for the reseller to the servers's default - * @param string $user The username of the reseller account to grab reseller accounts from - * @param string $nameservers A comma seperate list of nameservers - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/SetResellerNameservers XML API Call documentation - */ - public function setresellernameservers($user, $nameservers = null) - { - if (!isset($user)) { - error_log("setresellernameservers requires that a username is passed to it"); - - return false; - } - $params = array('user' => $user); - if ($nameservers) { - $params['nameservers'] = $nameservers; - } - - return $this->xmlapi_query('setresellernameservers', $params); - } - - #### - # Server information - #### - - /** - * Get a server's hostname - * - * This function will return a server's hostname - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/DisplayServerHostname XML API Call documentation - */ - public function gethostname() - { - return $this->xmlapi_query('gethostname'); - } - - /** - * Get the version of cPanel running on the server - * - * This function will return the version of cPanel/WHM running on the remote system - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/DisplaycPanelWHMVersion XML API Call documentation - */ - public function version() - { - return $this->xmlapi_query('version'); - } - - - /** - * Get Load Average - * - * This function will return the loadavg of the remote system - * - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/LoadAvg XML API Call documentation - */ - public function loadavg() - { - return $this->xmlapi_query('loadavg'); - } - - /** - * Get a list of languages on the remote system - * - * This function will return a list of available langauges for the cPanel interface - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/GetLangList XML API Call documentation - * - */ - public function getlanglist() - { - return $this->xmlapi_query('getlanglist'); - } - - #### - # Server administration - #### - - /** - * Reboot server - * - * This function will reboot the server - * @param boolean $force This will determine if the server should be given a graceful or forceful reboot - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/RebootServer XML API Call documentation - */ - public function reboot($force = false) - { - if ($force) { - return $this->xmlapi_query('reboot', array('force' => '1')); - } - - return $this->xmlapi_query('reboot'); - } - - /** - * Add an IP to a server - * - * This function will add an IP alias to your server - * @param string $ip The IP to be added - * @param string $netmask The netmask of the IP to be added - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/AddIPAddress XML API Call documentation - */ - public function addip($ip, $netmask) - { - if (!isset($ip) || !isset($netmask)) { - error_log("addip requires that an IP address and Netmask are passed to it"); - - return false; - } - - return $this->xmlapi_query('addip', array('ip' => $ip, 'netmask' => $netmask)); - } - - // This function allows you to delete an IP address from your server. - /** - * Delete an IP from a server - * - * Remove an IP from the server - * @param string $ip The IP to remove - * @param string $ethernetdev The ethernet device that the IP is bound to - * @param bool $skipifshutdown Whether the function should remove the IP even if the ethernet interface is down - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/DeleteIPAddress XML API Call documentation - */ - public function delip($ip, $ethernetdev = null, $skipifshutdown = false) - { - $args = array(); - if (!isset($ip)) { - error_log("delip requires that an IP is defined in the array passed to it"); - - return false; - } - $args['ip'] = $ip; - if ($ethernetdev) { - $args['ethernetdev'] = $ethernetdev; - } - $args['skipifshutdown'] = ($skipifshutdown) ? '1' : '0'; - - return $this->xmlapi_query('delip', $args); - } - - /** - * List IPs - * - * This should return a list of IPs on a server - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/DeleteIPAddress XML API Call documentation - */ - public function listips() - { - return $this->xmlapi_query('listips'); - } - - /** - * Set Hostname - * - * This function will allow you to set the hostname of the server - * @param string $hostname the hostname that should be assigned to the serve - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/SetHostname XML API Call documentation - */ - public function sethostname($hostname) - { - if (!isset($hostname)) { - error_log("sethostname requires that hostname is passed to it"); - - return false; - } - - return $this->xmlapi_query('sethostname', array('hostname' => $hostname)); - } - - /** - * Set the resolvers used by the server - * - * This function will set the resolvers in /etc/resolv.conf for the server - * @param string $nameserver1 The IP of the first nameserver to use - * @param string $nameserver2 The IP of the second namesever to use - * @param string $nameserver3 The IP of the third nameserver to use - * @param string $nameserver4 The IP of the forth nameserver to use - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/SetResolvers XML API Call documentation - */ - public function setresolvers($nameserver1, $nameserver2 = null, $nameserver3 = null) - { - $args = array(); - if (!isset($nameserver1)) { - error_log("setresolvers requires that nameserver1 is defined in the array passed to it"); - - return false; - } - $args['nameserver1'] = $nameserver1; - if ($nameserver2) { - $args['nameserver2'] = $nameserver2; - } - if ($nameserver3) { - $args['nameserver3'] = $nameserver3; - } - - return $this->xmlapi_query('setresolvers', $args); - } - - /** - * Display bandwidth Usage - * - * This function will return all bandwidth usage information for the server, - * The arguments for this can be passed in via an associative array, the elements of this array map directly to the - * parameters of the call, please see the XML API Call documentation for more information - * @param array $args The configuration for what bandwidth information to display - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ShowBw XML API Call documentation - */ - public function showbw($args = null) - { - if (is_array($args)) { - return $this->xmlapi_query('showbw', $args); - } - - return $this->xmlapi_query('showbw'); - } - - public function nvset($key, $value) - { - if (!isset($key) || !isset($value)) { - error_log("nvset requires that key and value are passed to it"); - - return false; - } - - return $this->xmlapi_query('nvset', array('key' => $key, 'value' => $value)); - } - - // This function allows you to retrieve and view a non-volatile variable's value. - public function nvget($key) - { - if (!isset($key)) { - error_log("nvget requires that key is passed to it"); - - return false; - } - - return $this->xmlapi_query('nvget', array('key' => $key)); - } - - #### - # Service functions - #### - - /** - * Restart a Service - * - * This function allows you to restart a service on the server - * @param string $service the service that you wish to restart please view the XML API Call documentation for acceptable values to this parameters - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/RestartService XML API Call documentation - */ - public function restartsrv($service) - { - if (!isset($service)) { - error_log("restartsrv requires that service is passed to it"); - - return false; - } - - return $this->xmlapi_query('restartservice', array('service' => $service)); - } - - /** - * Service Status - * - * This function will return the status of all services on the and whether they are running or not - * @param array $args A single service to filter for. - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ServiceStatus XML API Call documentation - */ - public function servicestatus($args=array()) - { - if (!empty($args) && !is_array($args)) { - $args = array('service'=>$args); - } elseif (!is_array($args)) { - $args = array(); - } - - return $this->xmlapi_query('servicestatus', $args); - } - - /** - * Configure A Service - * - * This function will allow you to enabled or disable services along with their monitoring by chkservd - * @param string $service The service to be monitored - * @param bool $enabled Whether the service should be enabled or not - * @param bool $monitored Whether the service should be monitored or not - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ConfigureService XML API Call documentation - */ - public function configureservice($service, $enabled = true, $monitored = true) - { - if (!isset($service)) { - error_log("configure service requires that a service is passed to it"); - - return false; - } - $params = array('service' => $service); - - if ($enabled) { - $params['enabled'] = 1; - } else { - $params['enabled'] = 0; - } - - if ($monitored) { - $params['monitored'] = 1; - } else { - $params['monitored'] = 0; - } - - return $this->xmlapi_query('configureservice', $params); - - } - - #### - # SSL functions - #### - - /** - * Display information on an SSL host - * - * This function will return information on an SSL Certificate, CSR, cabundle and SSL key for a specified domain - * @param array $args Configuration information for the SSL certificate, please see XML API Call documentation for required values - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/FetchSSL XML API Call documentation - */ - public function fetchsslinfo($args) - { - if ( (isset($args['domain']) && isset($args['crtdata'])) || (!isset($args['domain']) && !isset($args['crtdata'])) ) { - error_log("fetchsslinfo requires domain OR crtdata is passed to it"); - } - if (isset($args['crtdata'])) { - // crtdata must be URL-encoded! - $args['crtdata'] = urlencode(trim($args['crtdata'])); - } - - return $this->xmlapi_query('fetchsslinfo', $args); - } - - /** - * Generate an SSL Certificate - * - * This function will generate an SSL Certificate, the arguments for this map directly to the call for the XML API call. Please consult the XML API Call documentation for more information - * @param array $args the configuration for the SSL Certificate being generated - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/GenerateSSL XML API Call documentation - */ - public function generatessl($args) - { - if (!isset($args['xemail']) || !isset($args['host']) || !isset($args['country']) || !isset($args['state']) || !isset($args['city']) || !isset($args['co']) || !isset($args['cod']) || !isset($args['email']) || !isset($args['pass'])) { - error_log("generatessl requires that xemail, host, country, state, city, co, cod, email and pass are defined in the array passed to it"); - - return false; - } - - return $this->xmlapi_query('generatessl', $args); - } - - /** - * Install an SSL certificate - * - * This function will allow you to install an SSL certificate that is uploaded via the $argument parameter to this call. The arguments for this call map directly to the parameters for the XML API call, - * please consult the XML API Call documentation for more information. - * @param array $args The configuration for the SSL certificate - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/InstallSSL XML API Call documentation - */ - public function installssl($args) - { - if (!isset($args['user']) || !isset($args['domain']) || !isset($args['cert']) || !isset($args['key']) || !isset($args['cab']) || !isset($args['ip'])) { - error_log("installssl requires that user, domain, cert, key, cab and ip are defined in the array passed to it"); - - return false; - } - - return $this->xmlapi_query('installssl', $args); - } - - /** - * List SSL Certs - * - * This function will list all SSL certificates installed on the server - * @return mixed - * @link http://docs.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/ListSSL XML API Call documentation - */ - public function listcrts() - { - return $this->xmlapi_query('listcrts'); - } - - #### - # cPanel API1 functions - # Note: A cPanel account username is required - # Some cPanel features must be enabled to be able to use some function (f.e. park, unpark) - #### - - // This API1 function adds a emailaccount for a specific user. - public function addpop($username, $args) - { - if (!isset($username) || !isset($args)) { - error_log("addpop requires that a user and args are passed to it"); - - return false; - } - if (is_array($args) && (sizeof($args) < 3)) { - error_log("addpop requires that args at least contains an email_username, email_password and email_domain"); - - return false; - } - - return $this->api1_query($username, 'Email', 'addpop', $args); - } - - // This API function displays a list of all parked domains for a specific user. - public function park($username, $newdomain, $topdomain) - { - $args = array(); - if ( (!isset($username)) && (!isset($newdomain)) ) { - error_log("park requires that a username and new domain are passed to it"); - - return false; - } - $args['domain'] = $newdomain; - if ($topdomain) { - $args['topdomain'] = $topdomain; - } - - return $this->api2_query($username, 'Park', 'park', $args); - } - - // This API function displays a list of all parked domains for a specific user. - public function unpark($username, $domain) - { - $args = array(); - if ( (!isset($username)) && (!isset($domain)) ) { - error_log("unpark requires that a username and domain are passed to it"); - - return false; - } - $args['domain'] = $domain; - - return $this->api2_query($username, 'Park', 'unpark', $args); - } - - #### - # cPanel API2 functions - # Note: A cPanel account username is required - # Some cPanel features must be enabled to be able to use some function - #### - - // This API2 function allows you to view the diskusage of a emailaccount. - public function getdiskusage($username, $args) - { - if (!isset($username) || !isset($args)) { - error_log("getdiskusage requires that a username and args are passed to it"); - - return false; - } - if (is_array($args) && (!isset($args['domain']) || !isset($args['login']))) { - error_log("getdiskusage requires that args at least contains an email_domain and email_username"); - - return false; - } - - return $this->api2_query($username, 'Email', 'getdiskusage', $args); - } - - // This API2 function allows you to list ftp-users associated with a cPanel account including disk information. - public function listftpwithdisk($username) - { - if (!isset($username)) { - error_log("listftpwithdisk requires that user is passed to it"); - - return false; - } - - return $this->api2_query($username, 'Ftp', 'listftpwithdisk'); - } - - // This API2 function allows you to list ftp-users associated with a cPanel account. - public function listftp($username) - { - if (!isset($username)) { - error_log("listftp requires that user is passed to it"); - - return false; - } - - return $this->api2_query($username, 'Ftp', 'listftp'); - } - - // This API function displays a list of all parked domains for a specific user. - public function listparkeddomains($username, $domain = null) - { - $args = array(); - if (!isset($username)) { - error_log("listparkeddomains requires that a user is passed to it"); - - return false; - } - if (isset($domain)) { - $args['regex'] = $domain; - - return $this->api2_query($username, 'Park', 'listparkeddomains', $args); - } - - return $this->api2_query($username, 'Park', 'listparkeddomains'); - } - - // This API function displays a list of all addon domains for a specific user. - public function listaddondomains($username, $domain = null) - { - $args = array(); - if (!isset($username)) { - error_log("listaddondomains requires that a user is passed to it"); - - return false; - } - if (isset($domain)) { - $args['regex'] = $domain; - - return $this->api2_query($username, 'AddonDomain', 'listaddondomains', $args); - } - - return $this->api2_query($username, 'Park', 'listaddondomains'); - } - - // This API function displays a list of all selected stats for a specific user. - public function stat($username, $args = null) - { - if ( (!isset($username)) || (!isset($args)) ) { - error_log("stat requires that a username and options are passed to it"); - - return false; - } - if (is_array($args)) { - $display = ''; - foreach ($args as $value) { - $display .= $value . '|'; - } - $values['display'] = substr($display, 0, -1); - } else { - $values['display'] = substr($args, 0, -1); - } - - return $this->api2_query($username, 'StatsBar', 'stat', $values); - } - -} diff --git a/plugins/custom-admin-settings-tab/VERSION b/plugins/custom-admin-settings-tab/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/custom-admin-settings-tab/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/custom-admin-settings-tab/index.php b/plugins/custom-admin-settings-tab/index.php deleted file mode 100644 index ef405a7c8c..0000000000 --- a/plugins/custom-admin-settings-tab/index.php +++ /dev/null @@ -1,29 +0,0 @@ -<?php - -class CustomAdminSettingsTabPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - /** - * @return void - */ - public function Init() - { - $this->UseLangs(true); // start use langs folder - - $this->addJs('js/CustomAdminSettings.js', true); // add js file - - $this->addAjaxHook('AjaxAdminGetData', 'AjaxAdminGetData'); - - $this->addTemplate('templates/PluginCustomAdminSettingnTab.html', true); - } - - /** - * @return array - */ - public function AjaxAdminGetData() - { - return $this->ajaxResponse(__FUNCTION__, array( - 'PHP' => phpversion() - )); - } -} - diff --git a/plugins/custom-admin-settings-tab/js/CustomAdminSettings.js b/plugins/custom-admin-settings-tab/js/CustomAdminSettings.js deleted file mode 100644 index 2060512116..0000000000 --- a/plugins/custom-admin-settings-tab/js/CustomAdminSettings.js +++ /dev/null @@ -1,41 +0,0 @@ - -(function () { - - if (!window.rl) - { - return; - } - - /** - * @constructor - */ - function CustomAdminSettings() - { - this.php = ko.observable(''); - - this.loading = ko.observable(false); - } - - CustomAdminSettings.prototype.onBuild = function () // special function - { - var self = this; - - this.loading(true); - - window.rl.pluginRemoteRequest(function (sResult, oData) { - - self.loading(false); - - if (window.rl.Enums.StorageResultType.Success === sResult && oData && oData.Result) - { - self.php(oData.Result.PHP || ''); - } - - }, 'AjaxAdminGetData'); - - }; - - window.rl.addSettingsViewModelForAdmin(CustomAdminSettings, 'PluginCustomAdminSettingnTab', - 'SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN/TAB_NAME', 'custom'); - -}()); \ No newline at end of file diff --git a/plugins/custom-admin-settings-tab/langs/en.ini b/plugins/custom-admin-settings-tab/langs/en.ini deleted file mode 100644 index 7dc2831b31..0000000000 --- a/plugins/custom-admin-settings-tab/langs/en.ini +++ /dev/null @@ -1,4 +0,0 @@ -[SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN] -TAB_NAME = "Custom Plugin Tab" -LEGEND_CUSTOM = "Custom Plugin Tab (Example)" -LABEL_PHP_VERSION = "PHP version" \ No newline at end of file diff --git a/plugins/custom-admin-settings-tab/langs/ru.ini b/plugins/custom-admin-settings-tab/langs/ru.ini deleted file mode 100644 index 71fcee2323..0000000000 --- a/plugins/custom-admin-settings-tab/langs/ru.ini +++ /dev/null @@ -1,4 +0,0 @@ -[SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN] -TAB_NAME = "Плагин" -LEGEND_CUSTOM = "Плагин (Пример)" -LABEL_PHP_VERSION = "PHP версия" \ No newline at end of file diff --git a/plugins/custom-admin-settings-tab/templates/PluginCustomAdminSettingnTab.html b/plugins/custom-admin-settings-tab/templates/PluginCustomAdminSettingnTab.html deleted file mode 100644 index cab9229a7b..0000000000 --- a/plugins/custom-admin-settings-tab/templates/PluginCustomAdminSettingnTab.html +++ /dev/null @@ -1,17 +0,0 @@ -<div> - <div class="form-horizontal"> - <div class="legend"> - <span class="i18n" data-i18n="SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN/LEGEND_CUSTOM"></span> -     - <i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: loading"></i> - </div> - <div class="control-group"> - <label class="control-label"> - <span class="i18n" data-i18n="SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN/LABEL_PHP_VERSION"></span> - </label> - <div class="controls" style="padding-top: 5px"> - <b data-bind="text: php"></b> - </div> - </div> - </div> -</div> \ No newline at end of file diff --git a/plugins/custom-auth-example/LICENSE b/plugins/custom-auth-example/LICENSE deleted file mode 100644 index 271342337d..0000000000 --- a/plugins/custom-auth-example/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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 -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/custom-auth-example/VERSION b/plugins/custom-auth-example/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/custom-auth-example/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/custom-auth-example/index.php b/plugins/custom-auth-example/index.php deleted file mode 100644 index 1b0cfc6fcb..0000000000 --- a/plugins/custom-auth-example/index.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php - -class CustomAuthExamplePlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('filter.login-credentials', 'FilterLoginСredentials'); - } - - /** - * @param string $sEmail - * @param string $sLogin - * @param string $sPassword - * - * @throws \RainLoop\Exceptions\ClientException - */ - public function FilterLoginСredentials(&$sEmail, &$sLogin, &$sPassword) - { - // Your custom php logic - // You may change login credentials - if ('demo@rainloop.net' === $sEmail) - { - $sEmail = 'user@rainloop.net'; - $sLogin = 'user@rainloop.net'; - $sPassword = 'super-puper-password'; - } - else - { - // or throw auth exeption - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AuthError); - // or - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountNotAllowed); - // or - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DomainNotAllowed); - } - } -} diff --git a/plugins/custom-login-mapping/README b/plugins/custom-login-mapping/README deleted file mode 100644 index e260527b27..0000000000 --- a/plugins/custom-login-mapping/README +++ /dev/null @@ -1,8 +0,0 @@ -Plugin which allows you to set up custom username (login) by email address. - -Some mail servers use arbitrary usernames for logging in to the email service. -This plugin replaces pre-@ email address with the predefined username for logging in. - -Usage: <user email>:<username> -Example: bob.jackson@mydomain.com:mydomain3 -By doing the above, users can log in with "bob.jackson@mydomain.com" instead of "mydomain3@mydomain.com" diff --git a/plugins/custom-login-mapping/VERSION b/plugins/custom-login-mapping/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/custom-login-mapping/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/custom-login-mapping/index.php b/plugins/custom-login-mapping/index.php index 8104265b37..e7d287b5ff 100644 --- a/plugins/custom-login-mapping/index.php +++ b/plugins/custom-login-mapping/index.php @@ -2,52 +2,55 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin { - public function Init() + const + NAME = 'Custom Login Mapping', + VERSION = '2.3', + RELEASE = '2024-09-21', + REQUIRED = '2.36.1', + CATEGORY = 'Login', + DESCRIPTION = 'Enables custom usernames by email address.'; + + public function Init() : void { - $this->addHook('filter.login-credentials', 'FilterLoginСredentials'); + // Happens only at DoLogin | ServiceSso | DoAccountSetup + $this->addHook('login.credentials', 'FilterLoginCredentials'); } /** * @param string $sEmail - * @param string $sLogin + * @param string $sImapUser * @param string $sPassword + * @param string $sSmtpUser * * @throws \RainLoop\Exceptions\ClientException */ - public function FilterLoginСredentials(&$sEmail, &$sLogin, &$sPassword) + public function FilterLoginCredentials(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser) { $sMapping = \trim($this->Config()->Get('plugin', 'mapping', '')); - if (!empty($sMapping)) - { + if (!empty($sMapping)) { $aLines = \explode("\n", \preg_replace('/[\r\n\t\s]+/', "\n", $sMapping)); - foreach ($aLines as $sLine) - { - if (false !== strpos($sLine, ':')) - { - $aData = \explode(':', $sLine, 2); - if (is_array($aData) && !empty($aData[0]) && isset($aData[1])) - { - $aData = \array_map('trim', $aData); - if ($sEmail === $aData[0] && 0 < strlen($aData[1])) - { - $sLogin = $aData[1]; - } + foreach ($aLines as $sLine) { + $aData = \explode(':', $sLine, 3); + if (\is_array($aData) && isset($aData[1]) && \trim($aData[0]) === $sEmail) { + $aData = \array_map('trim', $aData); + if (\strlen($aData[1])) { + $sImapUser = $aData[1]; + } + if (isset($aData[2]) && \strlen($aData[2])) { + $sSmtpUser = $aData[2]; } } } } } - /** - * @return array - */ - public function configMapping() + protected function configMapping() : array { return array( \RainLoop\Plugins\Property::NewInstance('mapping')->SetLabel('Mapping') ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) - ->SetDescription('email:login mapping') - ->SetDefaultValue("user@domain.com:user.bob\nadmin@domain.com:user.john") + ->SetDescription('email:imap-login:smtp-login mapping') + ->SetDefaultValue("user@domain.com:imapuser.bob:smtpuser.bob\nadmin@domain.com:imapuser.john") ); } } diff --git a/plugins/custom-settings-tab/VERSION b/plugins/custom-settings-tab/VERSION deleted file mode 100644 index b123147e2a..0000000000 --- a/plugins/custom-settings-tab/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 \ No newline at end of file diff --git a/plugins/custom-settings-tab/index.php b/plugins/custom-settings-tab/index.php deleted file mode 100644 index 67cb2332a1..0000000000 --- a/plugins/custom-settings-tab/index.php +++ /dev/null @@ -1,57 +0,0 @@ -<?php - -class CustomSettingsTabPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - /** - * @return void - */ - public function Init() - { - $this->UseLangs(true); // start use langs folder - - $this->addJs('js/CustomUserSettings.js'); // add js file - - $this->addAjaxHook('AjaxGetCustomUserData', 'AjaxGetCustomUserData'); - $this->addAjaxHook('AjaxSaveCustomUserData', 'AjaxSaveCustomUserData'); - - $this->addTemplate('templates/PluginCustomSettingsTab.html'); - } - - /** - * @return array - */ - public function AjaxGetCustomUserData() - { - $aSettings = $this->getUserSettings(); - - $sUserFacebook = isset($aSettings['UserFacebook']) ? $aSettings['UserFacebook'] : ''; - $sUserSkype = isset($aSettings['UserSkype']) ? $aSettings['UserSkype'] : ''; - - // or get user's data from your custom storage ( DB / LDAP / ... ). - - \sleep(1); - return $this->ajaxResponse(__FUNCTION__, array( - 'UserFacebook' => $sUserFacebook, - 'UserSkype' => $sUserSkype - )); - } - - /** - * @return array - */ - public function AjaxSaveCustomUserData() - { - $sUserFacebook = $this->ajaxParam('UserFacebook'); - $sUserSkype = $this->ajaxParam('UserSkype'); - - // or put user's data to your custom storage ( DB / LDAP / ... ). - - \sleep(1); - return $this->ajaxResponse(__FUNCTION__, $this->saveUserSettings(array( - 'UserFacebook' => $sUserFacebook, - 'UserSkype' => $sUserSkype - ))); - } - -} - diff --git a/plugins/custom-settings-tab/js/CustomUserSettings.js b/plugins/custom-settings-tab/js/CustomUserSettings.js deleted file mode 100644 index 16920d40f8..0000000000 --- a/plugins/custom-settings-tab/js/CustomUserSettings.js +++ /dev/null @@ -1,78 +0,0 @@ - -(function () { - - if (!window.rl) - { - return; - } - - /** - * @constructor - */ - function CustomUserSettings() - { - this.userSkype = ko.observable(''); - this.userFacebook = ko.observable(''); - - this.loading = ko.observable(false); - this.saving = ko.observable(false); - - this.savingOrLoading = ko.computed(function () { - return this.loading() || this.saving(); - }, this); - } - - CustomUserSettings.prototype.customAjaxSaveData = function () - { - var self = this; - - if (this.saving()) - { - return false; - } - - this.saving(true); - - window.rl.pluginRemoteRequest(function (sResult, oData) { - - self.saving(false); - - if (window.rl.Enums.StorageResultType.Success === sResult && oData && oData.Result) - { - // true - } - else - { - // false - } - - }, 'AjaxSaveCustomUserData', { - 'UserSkype': this.userSkype(), - 'UserFacebook': this.userFacebook() - }); - }; - - CustomUserSettings.prototype.onBuild = function () // special function - { - var self = this; - - this.loading(true); - - window.rl.pluginRemoteRequest(function (sResult, oData) { - - self.loading(false); - - if (window.rl.Enums.StorageResultType.Success === sResult && oData && oData.Result) - { - self.userSkype(oData.Result.UserSkype || ''); - self.userFacebook(oData.Result.UserFacebook || ''); - } - - }, 'AjaxGetCustomUserData'); - - }; - - window.rl.addSettingsViewModel(CustomUserSettings, 'PluginCustomSettingsTab', - 'SETTINGS_CUSTOM_PLUGIN/TAB_NAME', 'custom'); - -}()); \ No newline at end of file diff --git a/plugins/custom-settings-tab/langs/en.ini b/plugins/custom-settings-tab/langs/en.ini deleted file mode 100644 index 1f3e63378f..0000000000 --- a/plugins/custom-settings-tab/langs/en.ini +++ /dev/null @@ -1,6 +0,0 @@ -[SETTINGS_CUSTOM_PLUGIN] -TAB_NAME = "Custom Plugin" -LEGEND_CUSTOM = "Custom Plugin (Example)" -LABEL_SKYPE = "Skype" -LABEL_FACEBOOK = "Facebook" -BUTTON_SAVE = "Save" \ No newline at end of file diff --git a/plugins/custom-settings-tab/langs/ru.ini b/plugins/custom-settings-tab/langs/ru.ini deleted file mode 100644 index 6b275600c9..0000000000 --- a/plugins/custom-settings-tab/langs/ru.ini +++ /dev/null @@ -1,6 +0,0 @@ -[SETTINGS_CUSTOM_PLUGIN] -TAB_NAME = "Плагин" -LEGEND_CUSTOM = "Плагин (Пример)" -LABEL_SKYPE = "Skype" -LABEL_FACEBOOK = "Facebook" -BUTTON_SAVE = "Сохранить" \ No newline at end of file diff --git a/plugins/custom-settings-tab/templates/PluginCustomSettingsTab.html b/plugins/custom-settings-tab/templates/PluginCustomSettingsTab.html deleted file mode 100644 index 31d4eb82be..0000000000 --- a/plugins/custom-settings-tab/templates/PluginCustomSettingsTab.html +++ /dev/null @@ -1,34 +0,0 @@ -<div> - <div class="form-horizontal"> - <div class="legend"> - <span class="i18n" data-i18n="SETTINGS_CUSTOM_PLUGIN/LEGEND_CUSTOM"></span> -     - <i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: loading"></i> - </div> - <div class="control-group"> - <label class="control-label"> - <span class="i18n" data-i18n="SETTINGS_CUSTOM_PLUGIN/LABEL_SKYPE"></span> - </label> - <div class="controls"> - <input type="text" data-bind="value: userSkype, enable: !savingOrLoading()" /> - </div> - </div> - <div class="control-group"> - <label class="control-label"> - <span class="i18n" data-i18n="SETTINGS_CUSTOM_PLUGIN/LABEL_FACEBOOK"></span> - </label> - <div class="controls"> - <input type="text" data-bind="value: userFacebook, enable: !savingOrLoading()" /> - </div> - </div> - <div class="control-group"> - <div class="controls"> - <button class="btn" data-bind="click: customAjaxSaveData, enable: !savingOrLoading()"> - <i data-bind="css: {'icon-floppy': !saving(), 'icon-spinner animated': saving()}" class="icon-floppy"></i> -    - <span class="i18n" data-i18n="SETTINGS_CUSTOM_PLUGIN/BUTTON_SAVE"></span> - </button> - </div> - </div> - </div> -</div> \ No newline at end of file diff --git a/plugins/custom-system-folders/README b/plugins/custom-system-folders/README deleted file mode 100644 index 38b29fe962..0000000000 --- a/plugins/custom-system-folders/README +++ /dev/null @@ -1 +0,0 @@ -custom-sytem-folders \ No newline at end of file diff --git a/plugins/custom-system-folders/VERSION b/plugins/custom-system-folders/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/custom-system-folders/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/custom-system-folders/index.php b/plugins/custom-system-folders/index.php index 92d69ecb5d..64d9b6013d 100644 --- a/plugins/custom-system-folders/index.php +++ b/plugins/custom-system-folders/index.php @@ -2,6 +2,11 @@ class CustomSystemFoldersPlugin extends \RainLoop\Plugins\AbstractPlugin { + const + NAME = 'Custom System Folders', + CATEGORY = 'General', + DESCRIPTION = 'Set custom sytem folders'; + /** * @var string */ @@ -27,7 +32,7 @@ class CustomSystemFoldersPlugin extends \RainLoop\Plugins\AbstractPlugin */ private $sArchiveFolder = ''; - public function Init() + public function Init() : void { $this->sSentFolder = \trim($this->Config()->Get('plugin', 'sent_folder', '')); $this->sDraftsFolder = \trim($this->Config()->Get('plugin', 'drafts_folder', '')); @@ -121,7 +126,7 @@ public function FilterSystemFoldersNames($oAccount, &$aSystemFolderNames) /** * @return array */ - public function configMapping() + protected function configMapping() : array { return array( \RainLoop\Plugins\Property::NewInstance('sent_folder')->SetLabel('Sent') @@ -136,4 +141,4 @@ public function configMapping() ->SetDefaultValue('Archive') ); } -} \ No newline at end of file +} diff --git a/plugins/demo-account/VERSION b/plugins/demo-account/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/demo-account/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/demo-account/demo.js b/plugins/demo-account/demo.js new file mode 100644 index 0000000000..97edc3357c --- /dev/null +++ b/plugins/demo-account/demo.js @@ -0,0 +1,24 @@ +(rl => { + if (rl) { +/* + addEventListener('rl-view-model', e => { + const view = e.detail; + if (view && 'Login' === view.viewModelTemplateID) { + view.email(rl.settings.get('DemoEmail')); + view.password('DemoPassword'); + } + }); +*/ + addEventListener('rl-vm-visible', e => { + const view = e.detail; + if (view && 'PopupsCompose' === view.viewModelTemplateID) { + view.to('<' + rl.settings.get('Email') + '>'); +// view.to(view.from()); + } + if (view && 'PopupsAsk' === view.viewModelTemplateID) { + view.passphrase('demo'); + } + }); + } + +})(window.rl); diff --git a/plugins/demo-account/index.php b/plugins/demo-account/index.php index 74266b16ca..74a8cc675d 100644 --- a/plugins/demo-account/index.php +++ b/plugins/demo-account/index.php @@ -2,14 +2,23 @@ class DemoAccountPlugin extends \RainLoop\Plugins\AbstractPlugin { + const + NAME = 'Demo Account Extension', + CATEGORY = 'Login', + REQUIRED = '2.23', + DESCRIPTION = 'Extension to enable a demo account'; + /** * @return void */ - public function Init() + public function Init() : void { + $this->addJs('demo.js'); + $this->addHook('filter.app-data', 'FilterAppData'); $this->addHook('filter.action-params', 'FilterActionParams'); - $this->addHook('ajax.action-pre-call', 'AjaxActionPreCall'); + $this->addHook('json.before-accountsetup', 'BeforeAccountSetup'); + $this->addHook('json.after-accountsandidentities', 'AfterAccountsAndIdentities'); $this->addHook('filter.send-message', 'FilterSendMessage'); $this->addHook('main.fabrica', 'MainFabrica'); } @@ -17,13 +26,15 @@ public function Init() /** * @return array */ - protected function configMapping() + protected function configMapping() : array { return array( \RainLoop\Plugins\Property::NewInstance('email')->SetLabel('Demo Email') ->SetDefaultValue('demo@domain.com'), \RainLoop\Plugins\Property::NewInstance('password')->SetLabel('Demo Password') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) + ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD), + \RainLoop\Plugins\Property::NewInstance('recipient_delimiter')->SetLabel('recipient_delimiter') + ->SetDefaultValue(''), ); } @@ -32,10 +43,9 @@ protected function configMapping() */ public function FilterAppData($bAdmin, &$aResult) { - if (!$bAdmin && \is_array($aResult) && isset($aResult['Auth']) && !$aResult['Auth']) - { + if (!$bAdmin && \is_array($aResult) && empty($aResult['Auth'])) { $aResult['DevEmail'] = $this->Config()->Get('plugin', 'email', $aResult['DevEmail']); - $aResult['DevPassword'] = APP_DUMMY; + $aResult['DevPassword'] = '********'; } } @@ -44,13 +54,23 @@ public function FilterAppData($bAdmin, &$aResult) */ public function FilterActionParams($sMethodName, &$aActionParams) { - if ('DoLogin' === $sMethodName && isset($aActionParams['Email']) && isset($aActionParams['Password'])) - { - if ($this->Config()->Get('plugin', 'email') === $aActionParams['Email']) - { - $aActionParams['Password'] = $this->Config()->Get('plugin', 'password'); + if ('DoLogin' === $sMethodName + && isset($aActionParams['Email']) + && isset($aActionParams['Password']) + && $this->Config()->Get('plugin', 'email') === $aActionParams['Email']) { + $aActionParams['Password'] = $this->Config()->Get('plugin', 'password'); + } + else if ('DoFolderCreate' === $sMethodName || 'DoFolderRename' === $sMethodName) { + // Block spam https://github.com/the-djmaze/snappymail/issues/371 + $latin = transliterator_transliterate('Any-Latin; Latin-ASCII; Lower()', $aActionParams['folder']); + if (false !== \strpos($latin, 'nigger')) { + \error_log("blocked {$sMethodName} {$aActionParams['folder']}"); + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoAccountError); } } + else if ('DoFolderClear' === $sMethodName || 'DoMessageDelete' === $sMethodName) { + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoAccountError); + } } /** @@ -58,25 +78,137 @@ public function FilterActionParams($sMethodName, &$aActionParams) * * @return bool */ - public function isDemoAccount($oAccount) + private function isDemoAccount() { + $oAccount = $this->Manager()->Actions()->GetAccount(); return ($oAccount && $oAccount->Email() === $this->Config()->Get('plugin', 'email')); } - public function AjaxActionPreCall($sAction) + public function BeforeAccountSetup() { - if ('AccountSetup' === $sAction && - $this->isDemoAccount($this->Manager()->Actions()->GetAccount())) - { - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoAccountError); + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoAccountError); + } + + public function AfterAccountsAndIdentities(&$aResponse) + { + foreach ($aResponse['Result']['Identities'] as &$idenity) { + $idenity['smimeKey'] = '-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIJnDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQImwuT42kTeZYCAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECAkQTF6SBnEGBIIJSJ5CyPT/qR4E +Aba2sN0JzyHPb5AB4EaE8cJNZkLayE24AVaA6GAiO+d4UR8gr7wDhmWIYaigf9XM +DOqg4AIqsvBW5zwK+3Fiv6jZj37zWiSoKx2M9bq9DvSXloliJNkm1ZV07bwx8KNC +FYAE06YqJr9cIhzu3f6ijH+eGUat/G11WukGIHASTraRRhzmV51cbUNEpFgJsqFt +WOTkyUEJolpJquigLoIA2GTTps1HlsLlSWU41Y6EMYsyRCwxE8myn/XDkTAVr9OZ +psJOrGnnvbyoD2rdGxfBHMMxFREyapCa8xsLMYHDyDcqZbZk9Qe3UQA2EQg5hVWd +6Wx2yRA5O54MteP7Z/4jOgE4XOOWILX3S905yAQBR942WEN7LG3pcNDPrYo8F+W4 +JM7/VnyUW7AP2f37r7rJUuZKYfL47ZiU7EoisO7sWcWHjPJTe4IPi42db/djmgLH +ufE07XJnNfwBRsJRQhNGsCeh0/pYkpCGOGhbqjyduIbmoQLjMw3K+sTNRNUgcyE0 +jMkEOEvSIE5HZq7wbH7nRMRpZC2q4fsSYZpu6726UOQ2mP96JRXp7o8WZfxQGOQl +J6G8oFfZoJsujCwyRYON0L2H9CF90mhFkt9QY8k/hoTonrmFsse8Jh4Pxl+PzBd4 +Mc9MMgfy8A0Yu54R2gGizd/gbo2ZP+/rKOOTe02Q5JQerrX8YB2U2htTbQMP+Evh +Z9+ElcqMxyI+1OC1yXDq6weDCuugB87F9UCRHy33wGz/1u5h/mMssFMb8lNe6x9c +S9R/J6ylWvMseUmtxES2fqT6/CymJUnnCItB7Jb0GqR9XpZZwHlZAKg82hnvRFDD +S55CvnPMOEkm7dnA/njnuYPakm7/dbe2YXHBAx/FzepKPEY2xabPo3MX2lALPpiO +oDF8GkSejlzOsO1hnJTRY76y7Mi3N3DwfzdawBbLABXygGAGcPeYeq7cOWvXy9In +K8fckONLzjsVmKDSpUmoLv/OhvFoTfC7Rq9VWzza28VTGSrSRLN9c1t3ykISxOhv +0TQUXhJAv2He4nCGclxBi9sOLJFpuOpOenD+mMrhUYdqgNTeZG6y7VvjeXX2y3x+ +sDde0WBVZ+rwYw+Z8kFQ9sNhyhmgar/g7y1uZoon7J7nQhkRDXK7e598XQxJBZgo +3IPMTIMFW6lwQCS9LkQL+d7ZYdR1NTkfHkWIqPJV13JbFVPvGIFfLX6MDSlvq90R +u06X4FovZZgYRhGIAt/MWnOvrYlm/hvmrDI4MPyYW5zJcZ2zzVzLmckR6fZrGHoE +uW7lKAFxsEZgw8IdVFrxNSerc2S+XycWEDRSFvwuWNFcSFNeOYHEPHgA8+o8boeH +nVjEtGPL3WqbVqq/dStlo7Xq62S7p7HBoQ3clGASq1pfU7la7f6sWZws1CyMD3lD +a8hUVMOJs3FmlC3iRpLnfY7iCrb9RIiW8jvpCwi0HOgiin0C8f1dC8v/dN5fLOvv +T1FZ/B531ClmLO2FhIb3tM2HSeWFNNQALjlY47ODFFvr28cIMDcBonjxl7Yezckw +W4lb2Ts7ZtQwrv0VcdvcVyt+8dZmvfCHoomWl9f7V1zwpeiB6j+zt9phh4fQaa3C +uEnsNcU9+Qaorf1e5qBWGV8l99gOvPXuuEP9pARyCKR2QQSltplWYDgYbYENdxdU +MxhL9a/zimyWbQTZyIyaka5gxwXRQjag7/9veScvDEA9bs5LAfvZ1bzNdZFvBTRY +drHYWnsbhAqsIT+TjD+boWmTRVBlvvhea549qX2igMD8zqytqICQyLvPyTpm+E51 +ba/qs4+ljbOPjbin2tji+uHGpYYEm4WsxV28KbyH1pUoMZZPEDj/abbtw5vxlPgp +4+OaTTt8fmxlIZrf6gOXLqDZ9K/x5or7up2wP/tAQ4FqxFDw0CvIIePHgD5QE8dL +LqxKeCn/1cCgXOiYh7H3Qe9NniU5m7Ot6Whv3WFIPl6TRS3CR15xCZBqtKYq9fdg +Y6i37spEBuDkQAa8BXwit3QCvHqRn/j+hnMLGmzyVvlLMvbUFXa2DY39uso1uz9v +VI/IT8Nnq4qKGafD1ubkL2RcNYk2d0Kr2q3qtobl5QksmRpLuFGw0z3rzwjPFGRv +jT1YT+ATKuoS0rG+LYTLRXxuJFeUniYFXDurnRWnT7z2pO6bRs5/ZSkZLaTyF+aH +9MOEIEJcbRz0vrFdqHNCyVHKcRap7HJE+KnWj1Qq9j22dndU5RoYKzP5MTybUvzf +888ZlSgDaauRgLhCBXUJ1QgB0Ox0AzYAeSrIhxS2U05FGemETuKJbpd/WI2leGRK +LnZkOTJAUYOJ+kUo4XF4ClT5iXTa0ii05PhifsE2ayp6JVDUYZ32dF4mR8yoRzbt +pv2iI+kRuz/tV81/xd8R4XKD/Jd0QAk2wPFli/FL9cC2qtSvTxwKFuxzzfLFGVYp +NeW3qlMSnKEHD7J/m/Nf41pIhws9G2npnyNd1Ir6zLgC1/ONkpyD1i4gGJfMcWda +Xw+64j42hQ+5lRMB2aWRwllI7FWOmgBxfGyqImg9ImzIbZ7GlJ335ZBo4hE8k+CJ +RpP7jlfW+oF24bTApihgvla93qqmiiwA0gTnibiJMKLFcG4EInQ8GM3yvA6oQy+2 +4K82/tXlKBJ+PpaBse6g40hF4M+5Ceo8UM6CEy5CYsHRbWi1OpQE4qoAwJHcRvcJ +aLsKANDAfp/Sd0zoFPWe2k65bW1G35LMeC8UbzYuk0GDIuSNWTuLuQ5tWsP4Xu34 +lwun/ikdreo2AxiaBXebJugO6VbCrnBmidYyg9Qn6kQd7ZceVU0cwMbsbJVHM0bX +IhKKQIW8OTE4z6nNMjuusjAgRvO1LzKU9nteCRp2xzRFeZvPTD+K5v1N1FngMUy/ +13zGR82WGk9VmuTNaR12FAFP6DILbaIxPJJxo9QkYHkQSoNNZU9lqiYerWH7EQDX +4qX1WPP7OtE2PX75ouCSphsNVVr7tBmR6ACayFEGZKNKCkeL4qjJW4qOdVBEW4uU +ae62FiKwbl1VF7CXrtH5QibCmGG5vxG/zzxwngZIlXMwwUjh9yRnQo8Q9yXtSB1y +c0y853GguCJ9xidDPRBJpdFZfEndlvcRjW/XPIHsB/tLAHX5NsboJkmjt9orB+CK +/ZYVVu8GG0wUcrvQizogAg== +-----END ENCRYPTED PRIVATE KEY----- +'; + $idenity['smimeCertificate'] = '-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIBADANBgkqhkiG9w0BAQsFADCBhTELMAkGA1UEBhMCTkwx +EDAOBgNVBAgMB1V0cmVjaHQxEDAOBgNVBAcMB1V0cmVjaHQxEzARBgNVBAoMClNu +YXBweU1haWwxFjAUBgNVBAMMDXNuYXBweW1haWwuZXUxJTAjBgkqhkiG9w0BCQEW +FnNlY3VyaXR5QHNuYXBweW1haWwuZXUwHhcNMjQwMjE5MjI1OTExWhcNMjcwNTE5 +MjI1OTExWjA9MRgwFgYDVQQDDA9TbmFwcHlNYWlsIERlbW8xITAfBgkqhkiG9w0B +CQEWEmRlbW9Ac25hcHB5bWFpbC5ldTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAM0jsuMlM2DCEXQ2gxMNHui3ZRjmyR/kvEq+36YBnowq3fGKysn1XZ7O +0V24olaKUcV4YE4BjORAShzv7yH2TvTkCLgutbpFTisooLab/urcjNcwwLeuTc+E +vZ1YArTb8CUdJpoQ6b/NJsiF0srQYGwG0p/P/kw7Is5YZ179LhGeTd4KJ6jLM2Si +4mo3FQPFNL0VEeVsvQtyUFr+3D/eHvgS1CRvb+Z0Zdfw8ssSL2ihLnDBaLxOiPTG +u7shAmPK55n9hZ8N4phezLx11y/pEYcZHeQSIjTIvIHtuyVYMKubNWgHz42mzkIi +AGj5tX/+33e/yRNGpC83q2gbm19AjzhUiyAF3b2CswKZyrDSEX0+Rq2Q//pGXsJ2 +lPJg/JwqVNyQyu4LTphIC/jJPp7K7L/HBS0seakRj+OMGYhnHDEXzolGM3L6j7E1 +Fj9MAiEmzmwaB11HGx7aB1thCX4mMsYWbzFnZDbK12pFGQ8lmcA4MSkBICSxNAJO +5SSKmnFafvZH2sOuEzefWseLjpCwxzWdPG4yDD889dBiGF7XBH3H3FIt3SOXyCDz +4iG3uWgU0XqNaMd8sMaVY4jFXPTMMvATFgUbqBjaS9kATrcdvxlFTqxqdWemLMwa +YS4D0C709ckD3JrBVBgZXBz6EMwJzqv6Et5l6y+SPfIQuzNcuGi3AgMBAAGjgZ8w +gZwwDwYDVR0TAQH/BAUwAwEB/zALBgNVHQ8EBAMCBeAwHQYDVR0lBBYwFAYIKwYB +BQUHAwIGCCsGAQUFBwMEMB0GA1UdEQQWMBSBEmRlbW9Ac25hcHB5bWFpbC5ldTAd +BgNVHQ4EFgQU2UETBx2y4K72pJsm05QdQIzuZXgwHwYDVR0jBBgwFoAUjOzkbdLt +2/b/OqOz3jYT2GtjCDUwDQYJKoZIhvcNAQELBQADggIBAK9b4TiBzKCGb8d4LFfH +PIBGbqB77NhAcOtP+V4rxV60pqLpqn2hk6c6T78yMedFQw6/idEPG1v1XgoCQxnV +s2PmKjVYG6WDqTEPZKgFbw2VpkJEwn7UivL9GZ0VeKHxVSIjSuhkhUKXHC/h7JCu +Lm3+EOwmwlk2kZeC7ADVoCzLYj/F0eeDjb+LR+gSyYdDBYbCjZGIbFZpb88pwNKo +lIYOXwh1TpEZOIfDXvpnp4UBEJ6IX4rhhzCBsxGNH+JbRnP1+pvENDE8B9Ax7MHU +qxFxnO2vw3HUsU2WdiX0NuF4xiXwIZm+JsMQETdTAHQ6EhLGFzU5PukrwRiHsBEw +T5f/bmBegerGRr0NkY46bih77IBoR0QU5GIlNAp3ZgIW8x9JKWrhrXdoTEGt42XY +iA9ugxQHD9RplA2zirgXwWhsUAsSRt9ocEsrZKOnxX/449X/UyQxAbO3FS7kzSCd +2OGsAM2dvpj7bRxcmFbB6eGvEHC/mZ02IKmEqKDUWYTcmHZnFMnTbcFnVFD+cMV4 +B032HeRqxgxjV9fZlDRwsINOfO6laPXVWaYBIZ2+h/MEzA4SlDN4MpikZWgGpbJi +09bN5c6Jra0ltGZKO/KJZsPG8PlZ30yRZytzLM6QuuL6KzTfcnMaOJts7rxn/BTt +r7HREQ/4hof+B0bTZCma/l0n +-----END CERTIFICATE----- +'; } } - public function FilterSendMessage(&$oMessage) + public function FilterSendMessage($oMessage) { - if ($oMessage && $this->isDemoAccount($this->Manager()->Actions()->GetAccount())) - { - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoSendMessageError); + if ($oMessage && $this->isDemoAccount()) { + $recipient_delimiter = $this->Config()->Get('plugin', 'recipient_delimiter'); + $regex = '/^' . \preg_quote($this->Config()->Get('plugin', 'email')) . '$/D'; + if ($recipient_delimiter) { + $regex = \str_replace('@', '('.\preg_quote($recipient_delimiter).'.+)?@', $regex); + } + foreach ($oMessage->GetTo() as $oEmail) { + if (!\preg_match($regex, $oEmail->GetEmail())) { + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoSendMessageError); + } + } + foreach ($oMessage->GetCc() ?: [] as $oEmail) { + if (!\preg_match($regex, $oEmail->GetEmail())) { + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoSendMessageError); + } + } + foreach ($oMessage->GetBcc() ?: [] as $oEmail) { + if (!\preg_match($regex, $oEmail->GetEmail())) { + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoSendMessageError); + } + } +// throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoSendMessageError); } } @@ -86,22 +218,10 @@ public function FilterSendMessage(&$oMessage) */ public function MainFabrica($sName, &$oDriver) { - switch ($sName) - { - case 'storage': - case 'storage-local': - if (\class_exists('\\RainLoop\\Providers\\Storage\\TemproryApcStorage') && - \function_exists('apc_store')) - { - $oAccount = $this->Manager()->Actions()->GetAccount(); - if ($this->isDemoAccount($oAccount)) - { - $oDriver = new \RainLoop\Providers\Storage\TemproryApcStorage(APP_PRIVATE_DATA.'storage', - $sName === 'storage-local'); - } - } - break; + if ('storage' === $sName || 'storage-local' === $sName) { + require_once __DIR__ . '/storage.php'; + $oDriver = new \DemoStorage(APP_PRIVATE_DATA.'storage', $sName === 'storage-local'); + $oDriver->setDemoEmail($this->Config()->Get('plugin', 'email')); } } } - diff --git a/plugins/demo-account/storage.php b/plugins/demo-account/storage.php new file mode 100644 index 0000000000..511bf42268 --- /dev/null +++ b/plugins/demo-account/storage.php @@ -0,0 +1,78 @@ +<?php + +use RainLoop\Providers\Storage\Enumerations\StorageType; + +class DemoStorage extends \RainLoop\Providers\Storage\FileStorage +{ + private static $gc_done; + + /** + * @param \RainLoop\Model\Account|string|null $mAccount + */ + public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false) : string + { + $sEmail = ''; + if ($mAccount instanceof \RainLoop\Model\MainAccount) { + $sEmail = $mAccount->Email(); + } else if (\is_string($mAccount)) { + $sEmail = $mAccount; + } + if ($sEmail != $this->sDemoEmail) { + return parent::GenerateFilePath($mAccount, $iStorageType, $bMkDir); + } + + $sDataPath = "{$this->sDataPath}/demo"; + + // Garbage collection + if (!static::$gc_done) { + static::$gc_done = true; + if (!\random_int(0, \max(50, \ini_get('session.gc_divisor')))) { + \MailSo\Base\Utils::RecTimeDirRemove($sDataPath, 3600 * 3); // 3 hours + } + } + + // $_COOKIE['smtoken'] + if (empty($_COOKIE['smctoken'])) { + \SnappyMail\Cookies::set('smctoken', \base64_encode(\random_bytes(16)), 0, false); + } + $sDataPath .= '/' . \MailSo\Base\Utils::SecureFileName($_COOKIE['smctoken']); + if (!\is_dir($sDataPath) && \mkdir($sDataPath, 0700, true)) { + \file_put_contents("{$sDataPath}/settings",'{"RemoveColors":true,"ListInlineAttachments":true,"listGrouped":true}'); + \file_put_contents("{$sDataPath}/settings_local",'{"UseThreads":true}'); + if (\mkdir($sDataPath.'/.gnupg/private-keys-v1.d', 0700, true)) { + // AES + \file_put_contents("{$sDataPath}/.gnupg/private-keys-v1.d/3106F4281F98D820114228FEF16B5BA0D78AA005.key",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/private-keys-v1.d/3106F4281F98D820114228FEF16B5BA0D78AA005.key")); + \file_put_contents("{$sDataPath}/.gnupg/private-keys-v1.d/82CA239C482423D364BFD6DFC3E400B3B98AD66F.key",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/private-keys-v1.d/82CA239C482423D364BFD6DFC3E400B3B98AD66F.key")); + // ECC + \file_put_contents("{$sDataPath}/.gnupg/private-keys-v1.d/5A1A6C7310D0508C68E8E74F15068301E83FD1AE.key",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/private-keys-v1.d/5A1A6C7310D0508C68E8E74F15068301E83FD1AE.key")); + \file_put_contents("{$sDataPath}/.gnupg/private-keys-v1.d/886921A7E06BE56F8E8C51797BB476BB26DF21BF.key",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/private-keys-v1.d/886921A7E06BE56F8E8C51797BB476BB26DF21BF.key")); + + \file_put_contents("{$sDataPath}/.gnupg/pubring.kbx",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/pubring.kbx")); + \file_put_contents("{$sDataPath}/.gnupg/trustdb.gpg",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/trustdb.gpg")); + } + } + + if (StorageType::SIGN_ME === $iStorageType) { + $sDataPath .= '/.sign_me'; + } else if (StorageType::SESSION === $iStorageType) { + $sDataPath .= '/.sessions'; + } else if (StorageType::PGP === $iStorageType) { + $sDataPath .= '/.pgp'; + $sDataPath = "{$this->sDataPath}/demo.pgp/.pgp"; + $bMkDir = true; + } + + if ($bMkDir && !\is_dir($sDataPath) && !\mkdir($sDataPath, 0700, true)) + { + throw new \RainLoop\Exceptions\Exception('Can\'t make storage directory "'.$sDataPath.'"'); + } + + return $sDataPath . '/'; + } + + private $sDemoEmail; + public function setDemoEmail(string $sEmail) + { + $this->sDemoEmail = $sEmail; + } +} diff --git a/plugins/directadmin-change-password/DirectAdminChangePasswordDriver.php b/plugins/directadmin-change-password/DirectAdminChangePasswordDriver.php deleted file mode 100644 index b65bdb60f6..0000000000 --- a/plugins/directadmin-change-password/DirectAdminChangePasswordDriver.php +++ /dev/null @@ -1,152 +0,0 @@ -<?php - -class DirectAdminChangePasswordDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sHost = ''; - - /** - * @var string - */ - private $iPort = 2222; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sHost - * @param int $iPort - * - * @return \DirectAdminChangePasswordDriver - */ - public function SetConfig($sHost, $iPort) - { - $this->sHost = $sHost; - $this->iPort = $iPort; - - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \DirectAdminChangePasswordDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \DirectAdminChangePasswordDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('DirectAdmin: Try to change password for '.$oAccount->Email()); - } - - $bResult = false; - if (!empty($this->sHost) && 0 < $this->iPort && $oAccount) - { - $sEmail = \trim(\strtolower($oAccount->Email())); - - $sHost = \trim($this->sHost); - $sHost = \str_replace('{user:host-imap}', $oAccount->Domain()->IncHost(), $sHost); - $sHost = \str_replace('{user:host-smtp}', $oAccount->Domain()->OutHost(), $sHost); - $sHost = \str_replace('{user:domain}', \MailSo\Base\Utils::GetDomainFromEmail($sEmail), $sHost); - $sHost = \rtrim($this->sHost, '/\\'); - - if (!\preg_match('/^http[s]?:\/\//i', $sHost)) - { - $sHost = 'http://'.$sHost; - } - - $sUrl = $sHost.':'.$this->iPort.'/CMD_CHANGE_EMAIL_PASSWORD'; - - $iCode = 0; - $oHttp = \MailSo\Base\Http::SingletonInstance(); - - if ($this->oLogger) - { - $this->oLogger->Write('DirectAdmin[Api Request]:'.$sUrl); - } - - $mResult = $oHttp->SendPostRequest($sUrl, - array( - 'email' => $sEmail, - 'oldpassword' => $sPrevPassword, - 'password1' => $sNewPassword, - 'password2' => $sNewPassword, - 'api' => '1' - ), 'MailSo Http User Agent (v1)', $iCode, $this->oLogger); - - if (false !== $mResult && 200 === $iCode) - { - $aRes = null; - @\parse_str($mResult, $aRes); - if (is_array($aRes) && (!isset($aRes['error']) || (int) $aRes['error'] !== 1)) - { - $bResult = true; - } - else - { - if ($this->oLogger) - { - $this->oLogger->Write('DirectAdmin[Error]: Response: '.$mResult); - } - } - } - else - { - if ($this->oLogger) - { - $this->oLogger->Write('DirectAdmin[Error]: Empty Response: Code:'.$iCode); - } - } - } - - return $bResult; - } -} \ No newline at end of file diff --git a/plugins/directadmin-change-password/LICENSE b/plugins/directadmin-change-password/LICENSE deleted file mode 100644 index 271342337d..0000000000 --- a/plugins/directadmin-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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 -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/directadmin-change-password/README b/plugins/directadmin-change-password/README deleted file mode 100644 index 5249046b23..0000000000 --- a/plugins/directadmin-change-password/README +++ /dev/null @@ -1 +0,0 @@ -Plugin that adds functionality to change the email account password (DirectAdmin). \ No newline at end of file diff --git a/plugins/directadmin-change-password/VERSION b/plugins/directadmin-change-password/VERSION deleted file mode 100644 index b123147e2a..0000000000 --- a/plugins/directadmin-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 \ No newline at end of file diff --git a/plugins/directadmin-change-password/index.php b/plugins/directadmin-change-password/index.php deleted file mode 100644 index 79244735af..0000000000 --- a/plugins/directadmin-change-password/index.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php - -class DirectadminChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - $sHost = \trim($this->Config()->Get('plugin', 'direct_admin_host', '')); - $iPort = (int) $this->Config()->Get('plugin', 'direct_admin_port', 2222); - - if (!empty($sHost) && 0 < $iPort) - { - include_once __DIR__.'/DirectAdminChangePasswordDriver.php'; - - $oProvider = new DirectAdminChangePasswordDriver(); - $oProvider->SetLogger($this->Manager()->Actions()->Logger()); - $oProvider->SetConfig($sHost, $iPort); - $oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))); - } - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('direct_admin_host')->SetLabel('DirectAdmin Host') - ->SetDefaultValue('') - ->SetDescription('Allowed patterns: {user:host-imap}, {user:host-smtp}, {user:domain}'), - \RainLoop\Plugins\Property::NewInstance('direct_admin_port')->SetLabel('DirectAdmin Port') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) - ->SetDefaultValue(2222), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} \ No newline at end of file diff --git a/plugins/example/ContactSuggestions.php b/plugins/example/ContactSuggestions.php new file mode 100644 index 0000000000..1bd3e17aa9 --- /dev/null +++ b/plugins/example/ContactSuggestions.php @@ -0,0 +1,16 @@ +<?php + +namespace Plugins\Example; + +class ContactSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions +{ +// use \MailSo\Log\Inherit; + + public function Process(\RainLoop\Model\Account $oAccount, string $sQuery, int $iLimit = 20) : array + { + return array( + array($oAccount->Email(), ''), + array('email@domain.com', 'name') + ); + } +} diff --git a/plugins/example/LICENSE b/plugins/example/LICENSE new file mode 100644 index 0000000000..f709b02e27 --- /dev/null +++ b/plugins/example/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +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 +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/example/example.js b/plugins/example/example.js new file mode 100644 index 0000000000..7d66ec9d86 --- /dev/null +++ b/plugins/example/example.js @@ -0,0 +1,193 @@ +(rl => { + /** + * ViewModel class | class.viewModelTemplateID + * User & Admin: + * AskPopupView | PopupsAsk + * LanguagesPopupView | PopupsLanguages + * User: + * LoginUserView | Login + * SystemDropDownUserView | SystemDropDown + * MailFolderList | MailFolderList + * MailMessageList | MailMessageList + * MailMessageView | MailMessageView + * SettingsMenuUserView | SettingsMenu + * SettingsPaneUserView | SettingsPane + * UserSettingsAccounts | SettingsAccounts + * UserSettingsContacts | SettingsContacts + * UserSettingsFilters | SettingsFilters + * UserSettingsFolders | SettingsFolders + * UserSettingsGeneral | SettingsGeneral + * UserSettingsSecurity | SettingsSecurity + * UserSettingsThemes | SettingsThemes + * AccountPopupView | PopupsAccount + * AdvancedSearchPopupView | PopupsAdvancedSearch + * ComposePopupView | PopupsCompose + * ContactsPopupView | PopupsContacts + * FolderPopupView | PopupsFolder + * FolderClearPopupView | PopupsFolderClear + * FolderCreatePopupView | PopupsFolderCreate + * FolderSystemPopupView | PopupsFolderSystem + * IdentityPopupView | PopupsIdentity + * KeyboardShortcutsHelpPopupView | PopupsKeyboardShortcutsHelp + * OpenPgpGeneratePopupView | PopupsOpenPgpGenerate + * OpenPgpImportPopupView | PopupsOpenPgpImport + * OpenPgpKeyPopupView | PopupsOpenPgpKey + * SMimeImportPopupView | PopupsSMimeImport + * Admin: + * AdminLoginView | AdminLogin + * MenuSettingsAdminView | AdminMenu + * PaneSettingsAdminView | AdminPane + * AdminSettingsAbout | AdminSettingsAbout + * AdminSettingsBranding | AdminSettingsBranding + * AdminSettingsConfig | AdminSettingsConfig + * AdminSettingsContacts | AdminSettingsContacts + * AdminSettingsDomains | AdminSettingsDomains + * AdminSettingsGeneral | AdminSettingsGeneral + * AdminSettingsLogin | AdminSettingsLogin + * AdminSettingsPackages | AdminSettingsPackages + * AdminSettingsSecurity | AdminSettingsSecurity + * DomainPopupView | PopupsDomain + * DomainAliasPopupView | PopupsDomainAlias + * PluginPopupView | PopupsPlugin + */ + + /** + * Happens immediately after the ViewModel constructor + * event.detail contains the ViewModel class + */ + addEventListener('rl-view-model.create', event => { + console.dir({ + 'rl-view-model.create': event.detail + }); + }); + + /** + * Happens after the full build (vm.onBuild()) and contains viewModelDom + * event.detail contains the ViewModel class + */ + addEventListener('rl-view-model', event => { + console.dir({ + 'rl-view-model': event.detail + }); + }); + + /** + * event.detail value is one of: + * 0 = NoPreview + * 1 = SidePreview + * 2 = BottomPreview + */ + addEventListener('rl-layout', event => { + console.dir({ + 'rl-layout': event.detail + }); + }); + + /** + * event.detail contains the FormData + * cancelable using event.preventDefault() + */ + addEventListener('sm-admin-login', event => { + console.dir({ + 'sm-admin-login': event.detail + }); + }); + + /** + * event.detail contains { error: int, data: {JSON response} } + */ + addEventListener('sm-admin-login-response', event => { + console.dir({ + 'sm-admin-login-response': event.detail + }); + }); + + /** + * event.detail contains the FormData + * cancelable using event.preventDefault() + */ + addEventListener('sm-user-login', event => { + console.dir({ + 'sm-user-login': event.detail + }); + }); + + /** + * event.detail contains { error: int, data: {JSON response} } + */ + addEventListener('sm-user-login-response', event => { + console.dir({ + 'sm-user-login-response': event.detail + }); + }); + + /** + * event.detail contains the screenname + * cancelable using event.preventDefault() + * Options are: + * - login (user or admin login screen) + * - mailbox (user folders and messages, also like: mailbox/INBOX/test, mailbox/Sent) + * - settings (user settings like: settings/accounts, settings/general, settings/filters) + * - one of the admin sections (like: settings, domains, branding) + */ + addEventListener('sm-show-screen', event => { + console.dir({ + 'sm-show-screen': event.detail + }); + }); + + /** + * Use to show a specific message. + */ +/* + dispatchEvent( + new CustomEvent( + 'mailbox.message.show', + { + detail: { + folder: 'INBOX', + uid: 1 + }, + cancelable: false + } + ) + ); +*/ + + class ExamplePopupView extends rl.pluginPopupView { + constructor() { + super('Example'); + + this.addObservables({ + title: '' + }); + } + + // Happens before showModal() + beforeShow(...params) { + console.dir({beforeShow_params: params}); + } + + // Happens after showModal() + onShow(...params) { + console.dir({beforeShow_params: params}); + } + + // Happens after showModal() animation transitionend + afterShow() {} + + // Happens when user hits Escape or Close key + // return false to prevent closing, use close() manually + onClose() {} + + // Happens before animation transitionend + onHide() {} + + // Happens after animation transitionend + afterHide() {} + } + + /** Show the modal popup */ +// ExamplePopupView.showModal(['param1', 'param2']); + +})(window.rl); diff --git a/plugins/example/index.php b/plugins/example/index.php new file mode 100644 index 0000000000..761de22b48 --- /dev/null +++ b/plugins/example/index.php @@ -0,0 +1,461 @@ +<?php + +use RainLoop\Model\Account; +use MailSo\Imap\ImapClient; +use MailSo\Imap\Settings as ImapSettings; +use MailSo\Sieve\SieveClient; +use MailSo\Sieve\Settings as SieveSettings; +use MailSo\Smtp\SmtpClient; +use MailSo\Smtp\Settings as SmtpSettings; +use MailSo\Mime\Message as MimeMessage; + +class ExamplePlugin extends \RainLoop\Plugins\AbstractPlugin +{ +// use \MailSo\Log\Inherit; + + const + NAME = 'Example', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '0.0', + RELEASE = '2022-03-29', + REQUIRED = '2.14.0', + CATEGORY = 'General', + LICENSE = 'MIT', + DESCRIPTION = ''; + + public function Init() : void + { + $this->addHook('main.fabrica', 'MainFabrica'); +/* + $this->addCss(string $sFile, bool $bAdminScope = false); + $this->addJs(string $sFile, bool $bAdminScope = false); + $this->addHook(string $sHookName, string $sFunctionName); + + $this->addJsonHook(string $sActionName, string $sFunctionName); + $this->aAdditionalJson['DoPlugin'.$sActionName] = $mCallback; + To have your own callback on a json URI, like '/?/Json/&q[]=/0/Example' + + $this->addPartHook(string $sActionName, string $sFunctionName); + To use your own service URI, like '/example' + + $this->addTemplate(string $sFile, bool $bAdminScope = false); + $this->addTemplateHook(string $sName, string $sPlace, string $sLocalTemplateName, bool $bPrepend = false); +*/ + +/* + $this->addHook("json.before-{actionname}", "jsonBefore{actionname}"); + $this->addHook("json.after-{actionname}", "jsonAfter{actionname}"); + + $this->addHook('login.credentials.step-1', 'loginCredentialsStep1'); + $this->addHook('login.credentials.step-2', 'loginCredentialsStep2'); + $this->addHook('login.credentials', 'loginCredentials'); + $this->addHook('login.success', 'loginSuccess'); + $this->addHook('imap.before-connect', 'imapBeforeConnect'); + $this->addHook('imap.after-connect', 'imapAfterConnect'); + $this->addHook('imap.before-login', 'imapBeforeLogin'); + $this->addHook('imap.after-login', 'imapAfterLogin'); + $this->addHook('imap.message-headers', 'imapMessageHeaders'); + $this->addHook('sieve.before-connect', 'sieveBeforeConnect'); + $this->addHook('sieve.after-connect', 'sieveAfterConnect'); + $this->addHook('sieve.before-login', 'sieveBeforeLogin'); + $this->addHook('sieve.after-login', 'sieveAfterLogin'); + $this->addHook('smtp.before-connect', 'smtpBeforeConnect'); + $this->addHook('smtp.after-connect', 'smtpAfterConnect'); + $this->addHook('smtp.before-login', 'smtpBeforeLogin'); + $this->addHook('smtp.after-login', 'smtpAfterLogin'); + $this->addHook('filter.account', 'filterAccount'); + $this->addHook('filter.action-params', 'filterActionParams'); + $this->addHook('filter.app-data', 'filterAppData'); + $this->addHook('filter.application-config', 'filterApplicationConfig'); + $this->addHook('filter.build-message', 'filterBuildMessage'); + $this->addHook('filter.build-read-receipt-message', 'filterBuildReadReceiptMessage'); + $this->addHook('filter.domain', 'filterDomain'); + $this->addHook('filter.fabrica', 'filterFabrica'); + $this->addHook('filter.http-paths', 'filterHttpPaths'); + $this->addHook('filter.language', 'filterLanguage'); + $this->addHook('filter.message-html', 'filterMessageHtml'); + $this->addHook('filter.message-plain', 'filterMessagePlain'); + $this->addHook('filter.message-rcpt', 'filterMessageRcpt'); + $this->addHook('filter.read-receipt-message-plain', 'filterReadReceiptMessagePlain'); + $this->addHook('filter.result-message', 'filterResultMessage'); + $this->addHook('filter.save-message', 'filterSaveMessage'); + $this->addHook('filter.send-message', 'filterSendMessage'); + $this->addHook('filter.send-message-stream', 'filterSendMessageStream'); + $this->addHook('filter.send-read-receipt-message', 'filterSendReadReceiptMessage'); + $this->addHook('filter.smtp-from', 'filterSmtpFrom'); + $this->addHook('filter.smtp-hidden-rcpt', 'filterSmtpHiddenRcpt'); + $this->addHook('filter.smtp-message-stream', 'filterSmtpMessageStream'); + $this->addHook('filter.upload-response', 'filterUploadResponse'); + $this->addHook('json.attachments', 'jsonAttachments'); + $this->addHook('json.suggestions-input-parameters', 'jsonSuggestionsInputParameters'); + $this->addHook('main.content-security-policy', 'mainContentSecurityPolicy'); + $this->addHook('main.fabrica', 'mainFabrica'); + $this->addHook('service.app-delay-start-begin', 'serviceAppDelayStartBegin'); + $this->addHook('service.app-delay-start-end', 'serviceAppDelayStartEnd'); +*/ + + $this->UseLangs(true); // start use langs folder + + $this->addJs('example.js'); // add js file + $this->addJs('example.js', true); // add js file + + // User Settings tab + $this->addJs('js/ExampleUserSettings.js'); // add js file + $this->addJsonHook('JsonGetExampleUserData', 'JsonGetExampleUserData'); + $this->addJsonHook('JsonSaveExampleUserData', 'JsonSaveExampleUserData'); + $this->addTemplate('templates/ExampleUserSettingsTab.html'); + + // Admin Settings tab + $this->addJs('js/ExampleAdminSettings.js', true); // add js file + $this->addJsonHook('JsonAdminGetData', 'JsonAdminGetData', true); + $this->addTemplate('templates/ExampleAdminSettingsTab.html', true); + } + + /** + * @param mixed $mResult + */ + public function MainFabrica(string $sName, &$mResult) + { + switch ($sName) { + case 'files': + case 'storage': + case 'storage-local': + case 'settings': + case 'settings-local': + case 'login': + case 'domain': + case 'filters': + case 'address-book': + case 'identities': + break; + + case 'suggestions': + if (!\is_array($mResult)) { + $mResult = array(); + } + require_once __DIR__ . '/ContactsSuggestions.php'; + $mResult[] = new \Plugins\Example\ContactSuggestions(); + break; + } + } + + public function JsonAdminGetData() + { + if (!($this->Manager()->Actions() instanceof \RainLoop\ActionsAdmin) + || !$this->Manager()->Actions()->IsAdminLoggined() + ) { + return $this->jsonResponse(__FUNCTION__, false); + } + return $this->jsonResponse(__FUNCTION__, array( + 'PHP' => \phpversion() + )); + } + + /** + * @param string $sEmail + * @param string $sLogin + * @param string $sPassword + * + * @throws \RainLoop\Exceptions\ClientException + */ + public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword) + { + // Your custom php logic + // You may change login credentials + if ('demo@snappymail.eu' === $sEmail) + { + $sEmail = 'user@snappymail.eu'; + $sLogin = 'user@snappymail.eu'; + $sPassword = 'super-duper-password'; + } + else + { + // or throw auth exeption + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AuthError); + // or + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountNotAllowed); + // or + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DomainNotAllowed); + } + } + + /** + * @return array + */ + public function JsonGetExampleUserData() + { + $aSettings = $this->getUserSettings(); + + $sUserFacebook = isset($aSettings['UserFacebook']) ? $aSettings['UserFacebook'] : ''; + $sUserSkype = isset($aSettings['UserSkype']) ? $aSettings['UserSkype'] : ''; + + // or get user's data from your custom storage ( DB / LDAP / ... ). + + \sleep(1); + return $this->jsonResponse(__FUNCTION__, array( + 'UserFacebook' => $sUserFacebook, + 'UserSkype' => $sUserSkype + )); + } + + /** + * @return array + */ + public function JsonSaveExampleUserData() + { + $sUserFacebook = $this->jsonParam('UserFacebook'); + $sUserSkype = $this->jsonParam('UserSkype'); + + // or put user's data to your custom storage ( DB / LDAP / ... ). + + \sleep(1); + return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings(array( + 'UserFacebook' => $sUserFacebook, + 'UserSkype' => $sUserSkype + ))); + } + +/* + public function jsonBefore{actionname}(){}; + public function jsonAfter{actionname}(array &$aResponse){}; + + public function loginCredentialsStep1(string &$sEmail) + { + } + + public function loginCredentialsStep2(string &$sEmail, string &$sPassword) + { + } + + public function loginCredentials(string &$sEmail, string &$sLogin, string &$sPassword) + { + } + + public function loginSuccess(\RainLoop\Model\MainAccount $oAccount) + { + } + + public function imapBeforeConnect(Account $oAccount, ImapClient $oImapClient, ImapSettings $oSettings) + { + } + + public function imapAfterConnect(Account $oAccount, ImapClient $oImapClient, ImapSettings $oSettings) + { + } + + public function imapBeforeLogin(Account $oAccount, ImapClient $oImapClient, ImapSettings $oSettings) + { + } + + public function imapAfterLogin(Account $oAccount, ImapClient $oImapClient, bool $bSuccess, ImapSettings $oSettings) + { + } + + public function imapMessageHeaders(array &$aHeaders) + { + } + + public function sieveBeforeConnect(Account $oAccount, SieveClient $oSieveClient, SieveSettings $oSettings) + { + } + + public function sieveAfterConnect(Account $oAccount, SieveClient $oSieveClient, SieveSettings $oSettings) + { + } + + public function sieveBeforeLogin(Account $oAccount, SieveClient $oSieveClient, SieveSettings $oSettings) + { + } + + public function sieveAfterLogin(Account $oAccount, SieveClient $oSieveClient, bool $bSuccess, SieveSettings $oSettings) + { + } + + public function smtpBeforeConnect(Account $oAccount, SmtpClient $oSmtpClient, SmtpSettings $oSettings) + { + } + + public function smtpAfterConnect(Account $oAccount, SmtpClient $oSmtpClient, SmtpSettings $oSettings) + { + } + + public function smtpBeforeLogin(Account $oAccount, SmtpClient $oSmtpClient, SmtpSettings $oSettings) + { + } + + public function smtpAfterLogin(Account $oAccount, SmtpClient $oSmtpClient, bool $bSuccess, SmtpSettings $oSettings) + { + } + + public function filterAccount(Account $oAccount) + { + } + + public function filterActionParams(string $sMethodName, array &$aCurrentActionParams) + { + } + + public function filterAppData(bool $bAdmin, array &$aAppData) + { + } + + public function filterApplicationConfig(\RainLoop\Config\Application $oConfig) + { + } + + public function filterBuildMessage(MimeMessage $oMessage) + { + } + + public function filterBuildReadReceiptMessage(MimeMessage $oMessage, Account $oAccount) + { + } + + public function filterDomain(\RainLoop\Model\Domain $oDomain) + { + } + + public function filterFabrica(string $sName, mixed &$mResult, Account $oAccount) + { + } + + public function filterHttpPaths(array &$aPaths) + { + } + + public function filterLanguage(string &$sLanguage, bool $bAdmin) + { + } + + public function filterMessageHtml(Account $oAccount, MimeMessage $oMessage, string &$sTextConverted) + { + } + + public function filterMessagePlain(Account $oAccount, MimeMessage $oMessage, string &$sTextConverted) + { + } + + public function filterMessageRcpt(Account $oAccount, \MailSo\Mime\EmailCollection $oRcpt) + { + } + + public function filterReadReceiptMessagePlain(Account $oAccount, MimeMessage $oMessage, string &$sText) + { + } + + public function filterResultMessage(MimeMessage $oMessage) + { + } + + public function filterSaveMessage(MimeMessage $oMessage) + { + } + + public function filterSendMessage(MimeMessage $oMessage) + { + } + + public function filterSendMessageStream(Account $oAccount, resource &$rMessageStream, int &$iMessageStreamSize) + { + } + + public function filterSendReadReceiptMessage(MimeMessage $oMessage, Account $oAccount) + { + } + + public function filterSmtpFrom(Account $oAccount, MimeMessage $oMessage, string &$sFrom) + { + } + + public function filterSmtpHiddenRcpt(Account $oAccount, MimeMessage $oMessage, array &$aHiddenRcpt) + { + } + + public function filterSmtpMessageStream(Account $oAccount, resource &$rMessageStream, int &$iMessageStreamSize) + { + } + + public function filterUploadResponse(array &$aResponse) + { + } + + public function jsonAttachments(\SnappyMail\AttachmentsAction $oData) + { + } + + public function jsonSuggestionsInputParameters(string &$sQuery, int &$iLimit, Account $oAccount) + { + } + + public function mainContentSecurityPolicy(\SnappyMail\HTTP\CSP $oCSP) + { + } + + public function mainFabrica(string $sName, mixed &$mResult) + { + } + + public function serviceAppDelayStartBegin() + { + } + + public function serviceAppDelayStartEnd() + { + } + + public function Config() : \RainLoop\Config\Plugin + public function Description() : string + public function FilterAppDataPluginSection(bool $bAdmin, bool $bAuth, array &$aConfig) : void + public function Hash() : string + public function Manager() : \RainLoop\Plugins\Manager + public function Name() : string + public function Path() : string + public function SetName(string $sName) : self + public function SetPath(string $sPath) : self + public function SetPluginConfig(\RainLoop\Config\Plugin $oPluginConfig) : self + public function SetPluginManager(\RainLoop\Plugins\Manager $oPluginManager) : self + public function SetVersion(string $sVersion) : self + public function Supported() : string + public function UseLangs(?bool $bLangs = null) : bool + public function getUserSettings() : array + public function jsonParam(string $sKey, $mDefault = null) + public function saveUserSettings(array $aSettings) : bool + + protected function configMapping() : array + protected function jsonResponse(string $sFunctionName, $mData) + + $this->Manager() + $this->Manager()->Actions() + $this->Manager()->CreatePluginByName(string $sName) : ?\RainLoop\Plugins\AbstractPlugin + $this->Manager()->InstalledPlugins() : array + $this->Manager()->convertPluginFolderNameToClassName(string $sFolderName) : string + $this->Manager()->loadPluginByName(string $sName) : ?string + $this->Manager()->Actions() : \RainLoop\Actions + $this->Manager()->Hash() : string + $this->Manager()->HaveJs(bool $bAdminScope = false) : bool + $this->Manager()->CompileCss(bool $bAdminScope = false) : string + $this->Manager()->CompileJs(bool $bAdminScope = false) : string + $this->Manager()->CompileTemplate(array &$aList, bool $bAdminScope = false) : void + $this->Manager()->InitAppData(bool $bAdmin, array &$aAppData, ?Account $oAccount = null) : self + $this->Manager()->AddHook(string $sHookName, $mCallbak) : self + $this->Manager()->AddCss(string $sFile, bool $bAdminScope = false) : self + $this->Manager()->AddJs(string $sFile, bool $bAdminScope = false) : self + $this->Manager()->AddTemplate(string $sFile, bool $bAdminScope = false) : self + $this->Manager()->RunHook(string $sHookName, array $aArg = array(), bool $bLogHook = true) : self + $this->Manager()->AddAdditionalPartAction(string $sActionName, $mCallbak) : self + $this->Manager()->RunAdditionalPart(string $sActionName, array $aParts = array()) : bool + $this->Manager()->AddProcessTemplateAction(string $sName, string $sPlace, string $sHtml, bool $bPrepend = false) : self + $this->Manager()->AddAdditionalJsonAction(string $sActionName, $mCallback) : self + $this->Manager()->HasAdditionalJson(string $sActionName) : bool + $this->Manager()->RunAdditionalJson(string $sActionName) + $this->Manager()->JsonResponseHelper(string $sFunctionName, $mData) : array + $this->Manager()->GetUserPluginSettings(string $sPluginName) : array + $this->Manager()->SaveUserPluginSettings(string $sPluginName, array $aSettings) : bool + $this->Manager()->ReadLang(string $sLang, array &$aLang) : self + $this->Manager()->IsEnabled() : bool + $this->Manager()->Count() : int + $this->Manager()->WriteLog(string $sDesc, int $iType = \LOG_INFO) : void + $this->Manager()->WriteException(string $sDesc, int $iType = \LOG_INFO) : void +*/ +} diff --git a/plugins/example/js/ExampleAdminSettings.js b/plugins/example/js/ExampleAdminSettings.js new file mode 100644 index 0000000000..5a52ecfdaa --- /dev/null +++ b/plugins/example/js/ExampleAdminSettings.js @@ -0,0 +1,37 @@ + +(rl => { if (rl) { + + class ExampleAdminSettings + { + constructor() + { + this.php = ko.observable(''); + this.loading = ko.observable(false); + } + + onBuild() + { + this.loading(true); + + rl.pluginRemoteRequest((iError, oData) => { + + this.loading(false); + + if (iError) { + console.error({ + iError: iError, + oData: oData + }); + } else { + this.php(oData.Result.PHP || ''); + } + + }, 'JsonAdminGetData'); + + } + } + + rl.addSettingsViewModelForAdmin(ExampleAdminSettings, 'ExampleAdminSettingsTab', + 'SETTINGS_EXAMPLE_ADMIN_EXAMPLE_TAB_PLUGIN/TAB_NAME', 'Example'); + +}})(window.rl); diff --git a/plugins/example/js/ExampleUserSettings.js b/plugins/example/js/ExampleUserSettings.js new file mode 100644 index 0000000000..839e9e84af --- /dev/null +++ b/plugins/example/js/ExampleUserSettings.js @@ -0,0 +1,68 @@ + +(rl => { if (rl) { + + class ExampleUserSettings + { + constructor() + { + this.userSkype = ko.observable(''); + this.userFacebook = ko.observable(''); + + this.loading = ko.observable(false); + this.saving = ko.observable(false); + + this.savingOrLoading = ko.computed(() => { + return this.loading() || this.saving(); + }); + } + + exampleJsonSaveData() + { + if (!this.saving()) { + this.saving(true); + + rl.pluginRemoteRequest((iError, oData) => { + + this.saving(false); + + if (iError) { + console.error({ + iError: iError, + oData: oData + }); + } else { + console.dir({ + iError: iError, + oData: oData + }); + } + + }, 'JsonSaveExampleUserData', { + 'UserSkype': this.userSkype(), + 'UserFacebook': this.userFacebook() + }); + } + } + + onBuild() + { + this.loading(true); + + rl.pluginRemoteRequest((iError, oData) => { + + this.loading(false); + + if (!iError) { + self.userSkype(oData.Result.UserSkype || ''); + self.userFacebook(oData.Result.UserFacebook || ''); + } + + }, 'JsonGetExampleUserData'); + + } + } + + rl.addSettingsViewModel(ExampleUserSettings, 'ExampleUserSettingsTab', + 'SETTINGS_EXAMPLE_PLUGIN/TAB_NAME', 'Example'); + +}})(window.rl); diff --git a/plugins/example/langs/en.json b/plugins/example/langs/en.json new file mode 100644 index 0000000000..9c5f0c84cb --- /dev/null +++ b/plugins/example/langs/en.json @@ -0,0 +1,15 @@ +{ + "SETTINGS_EXAMPLE_ADMIN_EXAMPLE_TAB_PLUGIN": { + "TAB_NAME": "Example Extension Tab", + "LEGEND_EXAMPLE": "Example Extension Tab (Example)", + "LABEL_PHP_VERSION": "PHP version" + }, + "SETTINGS_EXAMPLE_PLUGIN": { + "TAB_NAME": "Example Extension", + "LEGEND_EXAMPLE": "Example Extension (Example)", + "LABEL_PHP_VERSION": "PHP version", + "LABEL_SKYPE": "Skype", + "LABEL_FACEBOOK": "Facebook", + "BUTTON_SAVE": "Save" + } +} diff --git a/plugins/example/templates/ExampleAdminSettingsTab.html b/plugins/example/templates/ExampleAdminSettingsTab.html new file mode 100644 index 0000000000..8fc98c5fc2 --- /dev/null +++ b/plugins/example/templates/ExampleAdminSettingsTab.html @@ -0,0 +1,17 @@ +<div> + <div class="form-horizontal"> + <div class="legend"> + <span class="i18n" data-i18n="SETTINGS_EXAMPLE_ADMIN_EXAMPLE_TAB_PLUGIN/LEGEND_EXAMPLE"></span> +     + <i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: loading"></i> + </div> + <div class="control-group"> + <label class="control-label"> + <span class="i18n" data-i18n="SETTINGS_EXAMPLE_ADMIN_EXAMPLE_TAB_PLUGIN/LABEL_PHP_VERSION"></span> + </label> + <div class="controls" style="padding-top: 5px"> + <b data-bind="text: php"></b> + </div> + </div> + </div> +</div> diff --git a/plugins/example/templates/ExampleUserSettingsTab.html b/plugins/example/templates/ExampleUserSettingsTab.html new file mode 100644 index 0000000000..3bbfad069a --- /dev/null +++ b/plugins/example/templates/ExampleUserSettingsTab.html @@ -0,0 +1,34 @@ +<div> + <div class="form-horizontal"> + <div class="legend"> + <span class="i18n" data-i18n="SETTINGS_EXAMPLE_PLUGIN/LEGEND_EXAMPLE"></span> +     + <i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: loading"></i> + </div> + <div class="control-group"> + <label class="control-label"> + <span class="i18n" data-i18n="SETTINGS_EXAMPLE_PLUGIN/LABEL_SKYPE"></span> + </label> + <div class="controls"> + <input type="text" data-bind="value: userSkype, enable: !savingOrLoading()" /> + </div> + </div> + <div class="control-group"> + <label class="control-label"> + <span class="i18n" data-i18n="SETTINGS_EXAMPLE_PLUGIN/LABEL_FACEBOOK"></span> + </label> + <div class="controls"> + <input type="text" data-bind="value: userFacebook, enable: !savingOrLoading()" /> + </div> + </div> + <div class="control-group"> + <div class="controls"> + <button class="btn" data-bind="click: exampleJsonSaveData, enable: !savingOrLoading()"> + <i data-bind="css: {'icon-floppy': !saving(), 'icon-spinner animated': saving()}" class="icon-floppy"></i> +    + <span class="i18n" data-i18n="SETTINGS_EXAMPLE_PLUGIN/BUTTON_SAVE"></span> + </button> + </div> + </div> + </div> +</div> diff --git a/plugins/excel-multirow-maillist-parser/LICENCE b/plugins/excel-multirow-maillist-parser/LICENCE deleted file mode 100644 index 634a69118d..0000000000 --- a/plugins/excel-multirow-maillist-parser/LICENCE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 https://github.com/sharq88 - -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. \ No newline at end of file diff --git a/plugins/excel-multirow-maillist-parser/README b/plugins/excel-multirow-maillist-parser/README deleted file mode 100644 index 9fa264c3c1..0000000000 --- a/plugins/excel-multirow-maillist-parser/README +++ /dev/null @@ -1 +0,0 @@ -Add ability to paste multi row email addresses from excel. \ No newline at end of file diff --git a/plugins/excel-multirow-maillist-parser/VERSION b/plugins/excel-multirow-maillist-parser/VERSION deleted file mode 100644 index ceab6e11ec..0000000000 --- a/plugins/excel-multirow-maillist-parser/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.1 \ No newline at end of file diff --git a/plugins/excel-multirow-maillist-parser/index.php b/plugins/excel-multirow-maillist-parser/index.php deleted file mode 100644 index 456a163373..0000000000 --- a/plugins/excel-multirow-maillist-parser/index.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php - -class ParseExcelListPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - /** - * @return void - */ - public function Init() - { - $this->addJs('js/parse_excel_list.js'); // add js file - } -} diff --git a/plugins/excel-multirow-maillist-parser/js/parse_excel_list.js b/plugins/excel-multirow-maillist-parser/js/parse_excel_list.js deleted file mode 100644 index 96204868dc..0000000000 --- a/plugins/excel-multirow-maillist-parser/js/parse_excel_list.js +++ /dev/null @@ -1,22 +0,0 @@ - -(function(window, $) { - - $(function() { - - $(window.document).on('keyup', '.b-compose .b-header .inputosaurus-input input[type="text"]', function() { - - var - $this = $(this), - value = $this.val() - ; - - if (value && value.match(/@/ig).length >= 2) - { - $this.val($this.val().replace(/\n| /ig, ',')); - } - - }); - - }); - -}(window, $)) diff --git a/plugins/froxlor-change-password/FroxlorChangePasswordDriver.php b/plugins/froxlor-change-password/FroxlorChangePasswordDriver.php deleted file mode 100644 index 6ee9cd5535..0000000000 --- a/plugins/froxlor-change-password/FroxlorChangePasswordDriver.php +++ /dev/null @@ -1,293 +0,0 @@ -<?php - -class FroxlorChangePasswordDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sDsn = ''; - - /** - * @var string - */ - private $sUser = ''; - - /** - * @var string - */ - private $sPassword = ''; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sDsn - * @param string $sUser - * @param string $sPassword - * - * @return \FroxlorChangePasswordDriver - */ - public function SetConfig($sDsn, $sUser, $sPassword) - { - $this->sDsn = $sDsn; - $this->sUser = $sUser; - $this->sPassword = $sPassword; - - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \FroxlorChangePasswordDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \FroxlorChangePasswordDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) { - $this->oLogger->Write('Froxlor: Try to change password for '.$oAccount->Email()); - } - - $bResult = false; - if (!empty($this->sDsn) && 0 < \strlen($this->sUser) && 0 < \strlen($this->sPassword) && $oAccount) - { - try - { - $oPdo = new \PDO($this->sDsn, $this->sUser, $this->sPassword); - $oPdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - - $oStmt = $oPdo->prepare('SELECT password_enc, id FROM mail_users WHERE username = ? LIMIT 1'); - if ($oStmt->execute(array($oAccount->IncLogin()))) - { - $aFetchResult = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetchResult) && isset($aFetchResult[0]['password_enc'], $aFetchResult[0]['id'])) - { - $sDbPassword = \stripslashes($aFetchResult[0]['password_enc']); - - if ( $this->validatePasswordLogin( $sDbPassword, $sPrevPassword ) ) { - $sEncNewPassword = $this->cryptPassword($sNewPassword, 3); - $oStmt = $oPdo->prepare('UPDATE mail_users SET password_enc = ?,password = ? WHERE id = ?'); - $bResult = (bool) $oStmt->execute( - array($sEncNewPassword, $sNewPassword, $aFetchResult[0]['id'])); - } - } - } - } - catch (\Exception $oException) - { - if ($this->oLogger) - { - $this->oLogger->WriteException($oException); - } - } - } - - return $bResult; - } - - /** - * @param string $sPassword - * @return string - */ - private function cryptPassword($sPassword, $type = 3) - { - return $this->makeCryptPassword($sPassword,$type); - } - - /** - * This file is part of the Froxlor project. - * Copyright (c) 2010 the Froxlor Team (see authors). - * - * For the full copyright and license information, please view the COPYING - * file that was distributed with this source code. You can also view the - * COPYING file online at http://files.froxlor.org/misc/COPYING.txt - * - * @copyright (c) the authors - * @author Michal Wojcik <m.wojcik@sonet3.pl> - * @author Michael Kaufmann <mkaufmann@nutime.de> - * @author Froxlor team <team@froxlor.org> (2010-) - * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt - * @package Functions - * - */ - - /** - * Make crypted password from clear text password - * - * @author Michal Wojcik <m.wojcik@sonet3.pl> - * @author Michael Kaufmann <mkaufmann@nutime.de> - * @author Froxlor team <team@froxlor.org> (2010-) - * - * 0 - default crypt (depenend on system configuration) - * 1 - MD5 $1$ - * 2 - BLOWFISH $2a$ | $2y$07$ (on php 5.3.7+) - * 3 - SHA-256 $5$ (default) - * 4 - SHA-512 $6$ - * - * @param string $password Password to be crypted - * - * @return string encrypted password - */ - private function makeCryptPassword ($password,$type = 3) { - switch ($type) { - case 0: - $cryptPassword = \crypt($password); - break; - case 1: - $cryptPassword = \crypt($password, '$1$' . $this->generatePassword(true). $this->generatePassword(true)); - break; - case 2: - if (\version_compare(\phpversion(), '5.3.7', '<')) { - $cryptPassword = \crypt($password, '$2a$' . $this->generatePassword(true). $this->generatePassword(true)); - } else { - // Blowfish hashing with a salt as follows: "$2a$", "$2x$" or "$2y$", - // a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z" - $cryptPassword = \crypt( - $password, - '$2y$07$' . \substr($this->generatePassword(true).$this->generatePassword(true).$this->generatePassword(true), 0, 22) - ); - } - break; - case 3: - $cryptPassword = \crypt($password, '$5$' . $this->generatePassword(true). $this->generatePassword(true)); - break; - case 4: - $cryptPassword = \crypt($password, '$6$' . $this->generatePassword(true). $this->generatePassword(true)); - break; - default: - $cryptPassword = \crypt($password); - break; - } - - return $cryptPassword; - } - - /** - * Generates a random password - * - * @param boolean $isSalt - * optional, create a hash for a salt used in makeCryptPassword because crypt() does not like some special characters in its salts, default is false - */ - private function generatePassword($isSalt = false) - { - $alpha_lower = 'abcdefghijklmnopqrstuvwxyz'; - $alpha_upper = \strtoupper($alpha_lower); - $numeric = '0123456789'; - $special = '!?<>§$%&+#='; - $length = 10; - - $pw = $this->special_shuffle($alpha_lower); - $n = \floor(($length) / 4); - $pw .= \mb_substr($this->special_shuffle($alpha_upper), 0, $n); - $pw .= \mb_substr($this->special_shuffle($numeric), 0, $n); - $pw = \mb_substr($pw, - $length); - return $this->special_shuffle($pw); - } - - /** - * multibyte-character safe shuffle function - * - * @param string $str - * - * @return string - */ - private function special_shuffle($str = null) - { - $len = \mb_strlen($str); - $sploded = array(); - while ($len -- > 0) { - $sploded[] = \mb_substr($str, $len, 1); - } - \shuffle($sploded); - return \join('', $sploded); - } - - /** - * Function validatePasswordLogin - * - * compare user password-hash with given user-password - * and check if they are the same - * additionally it updates the hash if the system settings changed - * or if the very old md5() sum is used - * - * @param array $userinfo user-data from table - * @param string $password the password to validate - * @param string $table either panel_customers or panel_admins - * @param string $uid user-id-field in $table - * - * @return boolean - */ - private function validatePasswordLogin($pwd_hash, $password = null) { - - $systype = 3; // SHA256 - $update_hash = false; - // check for good'ole md5 - if (\strlen($pwd_hash) == 32 && \ctype_xdigit($pwd_hash)) { - $pwd_check = \md5($password); - $update_hash = true; - } else { - // cut out the salt from the hash - $pwd_salt = \str_replace(\substr(\strrchr($pwd_hash, "$"), 1), "", $pwd_hash); - // create same hash to compare - $pwd_check = \crypt($password, $pwd_salt); - // check whether the hash needs to be updated - $hash_type_chk = \substr($pwd_hash, 0, 3); - if (($systype == 1 && $hash_type_chk != '$1$') || // MD5 - ($systype == 2 && $hash_type_chk != '$2$') || // BLOWFISH - ($systype == 3 && $hash_type_chk != '$5$') || // SHA256 - ($systype == 4 && $hash_type_chk != '$6$') // SHA512 - ) { - $update_hash = true; - } - } - - return $pwd_check; - } - - -} diff --git a/plugins/froxlor-change-password/LICENSE b/plugins/froxlor-change-password/LICENSE deleted file mode 100644 index 10a788cebd..0000000000 --- a/plugins/froxlor-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Bob Kromonos Achten - -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/froxlor-change-password/README b/plugins/froxlor-change-password/README deleted file mode 100644 index b3a26fa467..0000000000 --- a/plugins/froxlor-change-password/README +++ /dev/null @@ -1 +0,0 @@ -Plugin that adds functionality to change the email account password (Froxlor). diff --git a/plugins/froxlor-change-password/VERSION b/plugins/froxlor-change-password/VERSION deleted file mode 100644 index d3827e75a5..0000000000 --- a/plugins/froxlor-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 diff --git a/plugins/froxlor-change-password/index.php b/plugins/froxlor-change-password/index.php deleted file mode 100644 index ae63855d56..0000000000 --- a/plugins/froxlor-change-password/index.php +++ /dev/null @@ -1,76 +0,0 @@ -<?php - -class FroxlorchangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @return string - */ - public function Supported() - { - if (!extension_loaded('pdo') || !class_exists('PDO')) - { - return 'The PHP exention PDO (mysql) must be installed to use this plugin'; - } - - $aDrivers = \PDO::getAvailableDrivers(); - if (!is_array($aDrivers) || !in_array('mysql', $aDrivers)) - { - return 'The PHP exention PDO (mysql) must be installed to use this plugin'; - } - - return ''; - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - $sDsn = \trim($this->Config()->Get('plugin', 'pdo_dsn', '')); - $sUser = (string) $this->Config()->Get('plugin', 'user', ''); - $sPassword = (string) $this->Config()->Get('plugin', 'password', ''); - - if (!empty($sDsn) && 0 < \strlen($sUser) && 0 < \strlen($sPassword)) - { - include_once __DIR__.'/FroxlorChangePasswordDriver.php'; - - $oProvider = new FroxlorChangePasswordDriver(); - $oProvider->SetLogger($this->Manager()->Actions()->Logger()); - $oProvider->SetConfig($sDsn, $sUser, $sPassword); - $oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))); - } - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('pdo_dsn')->SetLabel('Froxlor PDO dsn') - ->SetDefaultValue('mysql:host=127.0.0.1;dbname=froxlor'), - \RainLoop\Plugins\Property::NewInstance('user')->SetLabel('DB User') - ->SetDefaultValue('root'), - \RainLoop\Plugins\Property::NewInstance('password')->SetLabel('DB Password') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} diff --git a/plugins/google-analytics/LICENSE b/plugins/google-analytics/LICENSE deleted file mode 100644 index 271342337d..0000000000 --- a/plugins/google-analytics/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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 -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/google-analytics/README b/plugins/google-analytics/README deleted file mode 100644 index c7ddf9b9ff..0000000000 --- a/plugins/google-analytics/README +++ /dev/null @@ -1 +0,0 @@ -Embed Google Analytics (Universal Analytics) code into your webmail installation pages. \ No newline at end of file diff --git a/plugins/google-analytics/VERSION b/plugins/google-analytics/VERSION deleted file mode 100644 index 400122e60f..0000000000 --- a/plugins/google-analytics/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.5 \ No newline at end of file diff --git a/plugins/google-analytics/index.php b/plugins/google-analytics/index.php deleted file mode 100644 index 3d714bbf9d..0000000000 --- a/plugins/google-analytics/index.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php - -class GoogleAnalyticsPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - /** - * @return void - */ - public function Init() - { - if ('' !== $this->Config()->Get('plugin', 'account', '')) - { - $this->addJs('js/include.js'); - } - } - - /** - * @return array - */ - public function configMapping() - { - $oAccount = \RainLoop\Plugins\Property::NewInstance('account')->SetLabel('Account') - ->SetAllowedInJs(true) - ->SetDefaultValue('') - ; - - if (\method_exists($oAccount, 'SetPlaceholder')) - { - $oAccount->SetPlaceholder('UA-XXXXXXXX-X'); - } - - return array($oAccount, - \RainLoop\Plugins\Property::NewInstance('domain_name')->SetLabel('Domain Name') - ->SetAllowedInJs(true) - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('universal_analytics')->SetLabel('Use Universal Analytics') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) - ->SetAllowedInJs(true) - ->SetDefaultValue(true), - \RainLoop\Plugins\Property::NewInstance('track_pageview')->SetLabel('Track Pageview') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) - ->SetAllowedInJs(true) - ->SetDefaultValue(true), - \RainLoop\Plugins\Property::NewInstance('send_events')->SetLabel('Send Events') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) - ->SetAllowedInJs(true) - ->SetDefaultValue(false) - ); - } -} diff --git a/plugins/google-analytics/js/include.js b/plugins/google-analytics/js/include.js deleted file mode 100644 index 6d73d35db0..0000000000 --- a/plugins/google-analytics/js/include.js +++ /dev/null @@ -1,100 +0,0 @@ - -$(function () { - - if (!window.rl) - { - return; - } - - var - sAccount = window.rl.pluginSettingsGet('google-analytics', 'account'), - sDomain = window.rl.pluginSettingsGet('google-analytics', 'domain_name'), - bUniversalAnalytics = !!window.rl.pluginSettingsGet('google-analytics', 'universal_analytics'), - bTrackPageview = !!window.rl.pluginSettingsGet('google-analytics', 'track_pageview'), - bSendEvent = !!window.rl.pluginSettingsGet('google-analytics', 'send_events'), - fSendEvent = null - ; - - if (sAccount && '' !== sAccount) - { - if (bUniversalAnalytics) - { - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); - - if (window.ga) - { - if (sDomain) - { - window.ga('create', sAccount, sDomain); - } - else - { - window.ga('create', sAccount); - } - - if (bTrackPageview) - { - window.ga('send', 'pageview'); - window.setInterval(function () { - window.ga('send', 'pageview'); - }, 1000 * 60 * 2); - } - - if (bSendEvent) - { - fSendEvent = function(sCategory, sAction, sLabel) { - window.ga('send', 'event', sCategory, sAction, sLabel); - }; - } - } - } - else - { - window._gaq = window._gaq || []; - window._gaq.push(['_setAccount', sAccount]); - - if (sDomain) - { - window._gaq.push(['_setDomainName', sDomain]); - } - - if (bTrackPageview) - { - window._gaq.push(['_trackPageview']); - window.setInterval(function () { - window._gaq.push(['_trackPageview']); - }, 1000 * 60 * 2); - } - - if (bSendEvent) - { - fSendEvent = function(sCategory, sAction, sLabel) { - window._gaq.push(['_trackEvent', sCategory, sAction, sLabel]); - }; - } - - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' === document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); - } - - if (fSendEvent) - { - window.rl.addHook('ajax-default-response', function (sAction, oData, sType) { - switch (sAction) - { - case 'Login': - case 'SendMessage': - case 'MessageMove': - case 'MessageDelete': - fSendEvent('RainLoop', sAction, - 'success' === sType && oData && oData['Result'] ? 'true' : 'false'); - break; - } - }); - } - } -}); \ No newline at end of file diff --git a/plugins/haveibeenpwned/hibp.js b/plugins/haveibeenpwned/hibp.js new file mode 100644 index 0000000000..98c8c5d065 --- /dev/null +++ b/plugins/haveibeenpwned/hibp.js @@ -0,0 +1,25 @@ +(doc => { + + addEventListener('rl-view-model.create', event => { + if ('SettingsSecurity' === event.detail.viewModelTemplateID) { + const template = doc.getElementById('SettingsSecurity'), + details = doc.createElement('details'), + summary = doc.createElement('summary'), + button = doc.createElement('button'); + summary.textContent = "Have i been pwned?" + button.dataset.bind = "click:HibpCheck"; + button.textContent = "Check"; + details.append(summary, button); + template.content.append(details); + + event.detail.HibpCheck = () => { + // JsonHibpCheck + rl.pluginRemoteRequest((iError, oData) => { + console.dir({iError, oData}); + }, 'HibpCheck'); + + }; + } + }); + +})(document); diff --git a/plugins/haveibeenpwned/index.php b/plugins/haveibeenpwned/index.php new file mode 100644 index 0000000000..7e22f707da --- /dev/null +++ b/plugins/haveibeenpwned/index.php @@ -0,0 +1,57 @@ +<?php +/** + * https://haveibeenpwned.com/API/v3 + */ + +use SnappyMail\Hibp; +use SnappyMail\SensitiveString; + +class HaveibeenpwnedPlugin extends \RainLoop\Plugins\AbstractPlugin +{ +// use \MailSo\Log\Inherit; + + const + NAME = 'Have i been pwned', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '0.1', + RELEASE = '2024-04-22', + REQUIRED = '2.36.1', + CATEGORY = 'General', + LICENSE = 'MIT', + DESCRIPTION = 'Check if your passphrase or email address is in a data breach'; + + public function Init() : void + { +// $this->UseLangs(true); + $this->addJs('hibp.js'); + $this->addJsonHook('HibpCheck'); + } + + public function HibpCheck() + { +// $oAccount = $this->Manager()->Actions()->GetAccount(); + $oAccount = $this->Manager()->Actions()->getAccountFromToken(); +// $oAccount = \RainLoop\Api::Actions()->getAccountFromToken(); + + $api_key = \trim($this->Config()->Get('plugin', 'hibp-api-key', '')); + $breaches = $api_key ? Hibp::account($api_key, $oAccount->Email()) : null; + + $pwned = Hibp::password(new SensitiveString($oAccount->ImapPass())); + + return $this->jsonResponse(__FUNCTION__, array( + 'pwned' => $pwned, + 'breaches' => $breaches + )); + } + + public function configMapping() : array + { + return [ + \RainLoop\Plugins\Property::NewInstance("hibp-api-key") + ->SetLabel('API key') + ->SetDescription('https://haveibeenpwned.com/API/Key') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) + ]; + } +} diff --git a/plugins/hmailserver-change-password/HmailserverChangePasswordDriver.php b/plugins/hmailserver-change-password/HmailserverChangePasswordDriver.php deleted file mode 100644 index d03c9c6159..0000000000 --- a/plugins/hmailserver-change-password/HmailserverChangePasswordDriver.php +++ /dev/null @@ -1,138 +0,0 @@ -<?php - -class HmailserverChangePasswordDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sLogin = ''; - - /** - * @var string - */ - private $sPassword = ''; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sLogin - * @param string $sPassword - * - * @return \HmailserverChangePasswordDriver - */ - public function SetConfig($sLogin, $sPassword) - { - $this->sLogin = $sLogin; - $this->sPassword = $sPassword; - - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \HmailserverChangePasswordDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \HmailserverChangePasswordDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Model\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Model\Account $oHmailAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oHmailAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('Try to change password for '.$oHmailAccount->Email()); - } - - $bResult = false; - - try - { - $oHmailApp = new COM("hMailServer.Application"); - $oHmailApp->Connect(); - - if ($oHmailApp->Authenticate($this->sLogin, $this->sPassword)) - { - $sEmail = $oHmailAccount->Email(); - $sDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail); - - $oHmailDomain = $oHmailApp->Domains->ItemByName($sDomain); - if ($oHmailDomain) - { - $oHmailAccount = $oHmailDomain->Accounts->ItemByAddress($sEmail); - if ($oHmailAccount) - { - $oHmailAccount->Password = $sNewPassword; - $oHmailAccount->Save(); - - $bResult = true; - } - else - { - $this->oLogger->Write('HMAILSERVER: Unknown account ('.$sEmail.')', \MailSo\Log\Enumerations\Type::ERROR); - } - } - else - { - $this->oLogger->Write('HMAILSERVER: Unknown domain ('.$sDomain.')', \MailSo\Log\Enumerations\Type::ERROR); - } - } - else - { - $this->oLogger->Write('HMAILSERVER: Auth error', \MailSo\Log\Enumerations\Type::ERROR); - } - } - catch (\Exception $oException) - { - if ($this->oLogger) - { - $this->oLogger->WriteException($oException); - } - } - - return $bResult; - } -} \ No newline at end of file diff --git a/plugins/hmailserver-change-password/VERSION b/plugins/hmailserver-change-password/VERSION deleted file mode 100644 index ea710abb95..0000000000 --- a/plugins/hmailserver-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.2 \ No newline at end of file diff --git a/plugins/hmailserver-change-password/index.php b/plugins/hmailserver-change-password/index.php deleted file mode 100644 index c499450e29..0000000000 --- a/plugins/hmailserver-change-password/index.php +++ /dev/null @@ -1,67 +0,0 @@ -<?php - -class HmailserverChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @return string - */ - public function Supported() - { - if (!class_exists('COM')) - { - return 'The PHP extension COM must be installed to use this plugin'; - } - - return ''; - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - $sLogin = (string) $this->Config()->Get('plugin', 'login', ''); - $sPassword = (string) $this->Config()->Get('plugin', 'password', ''); - - if (0 < \strlen($sLogin) && 0 < \strlen($sPassword)) - { - include_once __DIR__.'/HmailserverChangePasswordDriver.php'; - - $oProvider = new HmailserverChangePasswordDriver(); - $oProvider->SetLogger($this->Manager()->Actions()->Logger()); - $oProvider->SetConfig($sLogin, $sPassword); - $oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))); - } - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('login')->SetLabel('HmailServer Admin Login') - ->SetDefaultValue('Administrator'), - \RainLoop\Plugins\Property::NewInstance('password')->SetLabel('HmailServer Admin Password') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} \ No newline at end of file diff --git a/plugins/ics-viewer/LICENSE b/plugins/ics-viewer/LICENSE new file mode 100644 index 0000000000..9e5e56cdd9 --- /dev/null +++ b/plugins/ics-viewer/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2022 SnappyMail + +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/ics-viewer/ical.js b/plugins/ics-viewer/ical.js new file mode 100644 index 0000000000..97ec653051 --- /dev/null +++ b/plugins/ics-viewer/ical.js @@ -0,0 +1,9526 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +ICAL = (() => { + +/** + * Represents the BINARY value type, which contains extra methods for encoding and decoding. + * + * @memberof ICAL + */ +class Binary { + /** + * Creates a binary value from the given string. + * + * @param {String} aString The binary value string + * @return {Binary} The binary value instance + */ + static fromString(aString) { + return new Binary(aString); + } + + /** + * Creates a new ICAL.Binary instance + * + * @param {String} aValue The binary data for this value + */ + constructor(aValue) { + this.value = aValue; + } + + /** + * The type name, to be used in the jCal object. + * @default "binary" + * @constant + */ + icaltype = "binary"; + + /** + * Base64 decode the current value + * + * @return {String} The base64-decoded value + */ + decodeValue() { + return this._b64_decode(this.value); + } + + /** + * Encodes the passed parameter with base64 and sets the internal + * value to the result. + * + * @param {String} aValue The raw binary value to encode + */ + setEncodedValue(aValue) { + this.value = this._b64_encode(aValue); + } + + _b64_encode(data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Bayron Guevara + // + improved by: Thunder.m + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + improved by: Rafał Kukawski (http://kukawski.pl) + // * example 1: base64_encode('Kevin van Zonneveld'); + // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['atob'] == 'function') { + // return atob(data); + //} + let b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz0123456789+/="; + let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = "", + tmp_arr = []; + + if (!data) { + return data; + } + + do { // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + let r = data.length % 3; + + return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3); + + } + + _b64_decode(data) { + // http://kevin.vanzonneveld.net + // + original by: Tyler Akins (http://rumkin.com) + // + improved by: Thunder.m + // + input by: Aman Gupta + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Pellentesque Malesuada + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); + // * returns 1: 'Kevin van Zonneveld' + // mozilla has this native + // - but breaks in 2.0.0.12! + //if (typeof this.window['btoa'] == 'function') { + // return btoa(data); + //} + let b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz0123456789+/="; + let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ''; + + do { // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; + + o1 = bits >> 16 & 0xff; + o2 = bits >> 8 & 0xff; + o3 = bits & 0xff; + + if (h3 == 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 == 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(''); + + return dec; + } + + /** + * The string representation of this value + * @return {String} + */ + toString() { + return this.value; + } +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +const DURATION_LETTERS = /([PDWHMTS]{1,1})/; +const DATA_PROPS_TO_COPY = ["weeks", "days", "hours", "minutes", "seconds", "isNegative"]; + +/** + * This class represents the "duration" value type, with various calculation + * and manipulation methods. + * + * @memberof ICAL + */ +class Duration { + /** + * Returns a new ICAL.Duration instance from the passed seconds value. + * + * @param {Number} aSeconds The seconds to create the instance from + * @return {Duration} The newly created duration instance + */ + static fromSeconds(aSeconds) { + return (new Duration()).fromSeconds(aSeconds); + } + + /** + * Checks if the given string is an iCalendar duration value. + * + * @param {String} value The raw ical value + * @return {Boolean} True, if the given value is of the + * duration ical type + */ + static isValueString(string) { + return (string[0] === 'P' || string[1] === 'P'); + } + + /** + * Creates a new {@link ICAL.Duration} instance from the passed string. + * + * @param {String} aStr The string to parse + * @return {Duration} The created duration instance + */ + static fromString(aStr) { + let pos = 0; + let dict = Object.create(null); + let chunks = 0; + + while ((pos = aStr.search(DURATION_LETTERS)) !== -1) { + let type = aStr[pos]; + let numeric = aStr.slice(0, Math.max(0, pos)); + aStr = aStr.slice(pos + 1); + + chunks += parseDurationChunk(type, numeric, dict); + } + + if (chunks < 2) { + // There must be at least a chunk with "P" and some unit chunk + throw new Error( + 'invalid duration value: Not enough duration components in "' + aStr + '"' + ); + } + + return new Duration(dict); + } + + /** + * Creates a new ICAL.Duration instance from the given data object. + * + * @param {Object} aData An object with members of the duration + * @param {Number=} aData.weeks Duration in weeks + * @param {Number=} aData.days Duration in days + * @param {Number=} aData.hours Duration in hours + * @param {Number=} aData.minutes Duration in minutes + * @param {Number=} aData.seconds Duration in seconds + * @param {Boolean=} aData.isNegative If true, the duration is negative + * @return {Duration} The createad duration instance + */ + static fromData(aData) { + return new Duration(aData); + } + + /** + * Creates a new ICAL.Duration instance. + * + * @param {Object} data An object with members of the duration + * @param {Number=} data.weeks Duration in weeks + * @param {Number=} data.days Duration in days + * @param {Number=} data.hours Duration in hours + * @param {Number=} data.minutes Duration in minutes + * @param {Number=} data.seconds Duration in seconds + * @param {Boolean=} data.isNegative If true, the duration is negative + */ + constructor(data) { + this.wrappedJSObject = this; + this.fromData(data); + } + + /** + * The weeks in this duration + * @type {Number} + * @default 0 + */ + weeks = 0; + + /** + * The days in this duration + * @type {Number} + * @default 0 + */ + days = 0; + + /** + * The days in this duration + * @type {Number} + * @default 0 + */ + hours = 0; + + /** + * The minutes in this duration + * @type {Number} + * @default 0 + */ + minutes = 0; + + /** + * The seconds in this duration + * @type {Number} + * @default 0 + */ + seconds = 0; + + /** + * The seconds in this duration + * @type {Boolean} + * @default false + */ + isNegative = false; + + /** + * The class identifier. + * @constant + * @type {String} + * @default "icalduration" + */ + icalclass = "icalduration"; + + /** + * The type name, to be used in the jCal object. + * @constant + * @type {String} + * @default "duration" + */ + icaltype = "duration"; + + /** + * Returns a clone of the duration object. + * + * @return {Duration} The cloned object + */ + clone() { + return Duration.fromData(this); + } + + /** + * The duration value expressed as a number of seconds. + * + * @return {Number} The duration value in seconds + */ + toSeconds() { + let seconds = this.seconds + 60 * this.minutes + 3600 * this.hours + + 86400 * this.days + 7 * 86400 * this.weeks; + return (this.isNegative ? -seconds : seconds); + } + + /** + * Reads the passed seconds value into this duration object. Afterwards, + * members like {@link ICAL.Duration#days days} and {@link ICAL.Duration#weeks weeks} will be set up + * accordingly. + * + * @param {Number} aSeconds The duration value in seconds + * @return {Duration} Returns this instance + */ + fromSeconds(aSeconds) { + let secs = Math.abs(aSeconds); + + this.isNegative = (aSeconds < 0); + this.days = trunc(secs / 86400); + + // If we have a flat number of weeks, use them. + if (this.days % 7 == 0) { + this.weeks = this.days / 7; + this.days = 0; + } else { + this.weeks = 0; + } + + secs -= (this.days + 7 * this.weeks) * 86400; + + this.hours = trunc(secs / 3600); + secs -= this.hours * 3600; + + this.minutes = trunc(secs / 60); + secs -= this.minutes * 60; + + this.seconds = secs; + return this; + } + + /** + * Sets up the current instance using members from the passed data object. + * + * @param {Object} aData An object with members of the duration + * @param {Number=} aData.weeks Duration in weeks + * @param {Number=} aData.days Duration in days + * @param {Number=} aData.hours Duration in hours + * @param {Number=} aData.minutes Duration in minutes + * @param {Number=} aData.seconds Duration in seconds + * @param {Boolean=} aData.isNegative If true, the duration is negative + */ + fromData(aData) { + for (let prop of DATA_PROPS_TO_COPY) { + if (aData && prop in aData) { + this[prop] = aData[prop]; + } else { + this[prop] = 0; + } + } + } + + /** + * Resets the duration instance to the default values, i.e. PT0S + */ + reset() { + this.isNegative = false; + this.weeks = 0; + this.days = 0; + this.hours = 0; + this.minutes = 0; + this.seconds = 0; + } + + /** + * Compares the duration instance with another one. + * + * @param {Duration} aOther The instance to compare with + * @return {Number} -1, 0 or 1 for less/equal/greater + */ + compare(aOther) { + let thisSeconds = this.toSeconds(); + let otherSeconds = aOther.toSeconds(); + return (thisSeconds > otherSeconds) - (thisSeconds < otherSeconds); + } + + /** + * Normalizes the duration instance. For example, a duration with a value + * of 61 seconds will be normalized to 1 minute and 1 second. + */ + normalize() { + this.fromSeconds(this.toSeconds()); + } + + /** + * The string representation of this duration. + * @return {String} + */ + toString() { + if (this.toSeconds() == 0) { + return "PT0S"; + } else { + let str = ""; + if (this.isNegative) str += "-"; + str += "P"; + if (this.weeks) str += this.weeks + "W"; + if (this.days) str += this.days + "D"; + + if (this.hours || this.minutes || this.seconds) { + str += "T"; + if (this.hours) str += this.hours + "H"; + if (this.minutes) str += this.minutes + "M"; + if (this.seconds) str += this.seconds + "S"; + } + return str; + } + } + + /** + * The iCalendar string representation of this duration. + * @return {String} + */ + toICALString() { + return this.toString(); + } +} + +/** + * Internal helper function to handle a chunk of a duration. + * + * @private + * @param {String} letter type of duration chunk + * @param {String} number numeric value or -/+ + * @param {Object} dict target to assign values to + */ +function parseDurationChunk(letter, number, object) { + let type; + switch (letter) { + case 'P': + if (number && number === '-') { + object.isNegative = true; + } else { + object.isNegative = false; + } + // period + break; + case 'D': + type = 'days'; + break; + case 'W': + type = 'weeks'; + break; + case 'H': + type = 'hours'; + break; + case 'M': + type = 'minutes'; + break; + case 'S': + type = 'seconds'; + break; + default: + // Not a valid chunk + return 0; + } + + if (type) { + if (!number && number !== 0) { + throw new Error( + 'invalid duration value: Missing number before "' + letter + '"' + ); + } + let num = parseInt(number, 10); + if (isStrictlyNaN(num)) { + throw new Error( + 'invalid duration value: Invalid number "' + number + '" before "' + letter + '"' + ); + } + object[type] = num; + } + + return 1; +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * This lets typescript resolve our custom types in the + * generated d.ts files (jsdoc typedefs are converted to typescript types). + * Ignore prevents the typedefs from being documented more than once. + * + * @ignore + * @typedef {import("./types.js").weekDay} weekDay + * Imports the 'weekDay' type from the "types.js" module + */ + +/** + * @classdesc + * iCalendar Time representation (similar to JS Date object). Fully + * independent of system (OS) timezone / time. Unlike JS Date, the month + * January is 1, not zero. + * + * @example + * var time = new ICAL.Time({ + * year: 2012, + * month: 10, + * day: 11 + * minute: 0, + * second: 0, + * isDate: false + * }); + * + * + * @memberof ICAL +*/ +class Time { + static _dowCache = {}; + static _wnCache = {}; + + /** + * Returns the days in the given month + * + * @param {Number} month The month to check + * @param {Number} year The year to check + * @return {Number} The number of days in the month + */ + static daysInMonth(month, year) { + let _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + let days = 30; + + if (month < 1 || month > 12) return days; + + days = _daysInMonth[month]; + + if (month == 2) { + days += Time.isLeapYear(year); + } + + return days; + } + + /** + * Checks if the year is a leap year + * + * @param {Number} year The year to check + * @return {Boolean} True, if the year is a leap year + */ + static isLeapYear(year) { + if (year <= 1752) { + return ((year % 4) == 0); + } else { + return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)); + } + } + + /** + * Create a new ICAL.Time from the day of year and year. The date is returned + * in floating timezone. + * + * @param {Number} aDayOfYear The day of year + * @param {Number} aYear The year to create the instance in + * @return {Time} The created instance with the calculated date + */ + static fromDayOfYear(aDayOfYear, aYear) { + let year = aYear; + let doy = aDayOfYear; + let tt = new Time(); + tt.auto_normalize = false; + let is_leap = (Time.isLeapYear(year) ? 1 : 0); + + if (doy < 1) { + year--; + is_leap = (Time.isLeapYear(year) ? 1 : 0); + doy += Time.daysInYearPassedMonth[is_leap][12]; + return Time.fromDayOfYear(doy, year); + } else if (doy > Time.daysInYearPassedMonth[is_leap][12]) { + is_leap = (Time.isLeapYear(year) ? 1 : 0); + doy -= Time.daysInYearPassedMonth[is_leap][12]; + year++; + return Time.fromDayOfYear(doy, year); + } + + tt.year = year; + tt.isDate = true; + + for (let month = 11; month >= 0; month--) { + if (doy > Time.daysInYearPassedMonth[is_leap][month]) { + tt.month = month + 1; + tt.day = doy - Time.daysInYearPassedMonth[is_leap][month]; + break; + } + } + + tt.auto_normalize = true; + return tt; + } + + /** + * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02. + * + * @deprecated Use {@link ICAL.Time.fromDateString} instead + * @param {String} str The string to create from + * @return {Time} The date/time instance + */ + static fromStringv2(str) { + return new Time({ + year: parseInt(str.slice(0, 4), 10), + month: parseInt(str.slice(5, 7), 10), + day: parseInt(str.slice(8, 10), 10), + isDate: true + }); + } + + /** + * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02. + * + * @param {String} aValue The string to create from + * @return {Time} The date/time instance + */ + static fromDateString(aValue) { + // Dates should have no timezone. + // Google likes to sometimes specify Z on dates + // we specifically ignore that to avoid issues. + + // YYYY-MM-DD + // 2012-10-10 + return new Time({ + year: strictParseInt(aValue.slice(0, 4)), + month: strictParseInt(aValue.slice(5, 7)), + day: strictParseInt(aValue.slice(8, 10)), + isDate: true + }); + } + + /** + * Returns a new ICAL.Time instance from a date-time string, e.g + * 2015-01-02T03:04:05. If a property is specified, the timezone is set up + * from the property's TZID parameter. + * + * @param {String} aValue The string to create from + * @param {Property=} prop The property the date belongs to + * @return {Time} The date/time instance + */ + static fromDateTimeString(aValue, prop) { + if (aValue.length < 19) { + throw new Error( + 'invalid date-time value: "' + aValue + '"' + ); + } + + let zone; + let zoneId; + + if (aValue[19] && aValue[19] === 'Z') { + zone = Timezone.utcTimezone; + } else if (prop) { + zoneId = prop.getParameter('tzid'); + + if (prop.parent) { + if (prop.parent.name === 'standard' || prop.parent.name === 'daylight') { + // Per RFC 5545 3.8.2.4 and 3.8.2.2, start/end date-times within + // these components MUST be specified in local time. + zone = Timezone.localTimezone; + } else if (zoneId) { + // If the desired time zone is defined within the component tree, + // fetch its definition and prefer that. + zone = prop.parent.getTimeZoneByID(zoneId); + } + } + } + + const timeData = { + year: strictParseInt(aValue.slice(0, 4)), + month: strictParseInt(aValue.slice(5, 7)), + day: strictParseInt(aValue.slice(8, 10)), + hour: strictParseInt(aValue.slice(11, 13)), + minute: strictParseInt(aValue.slice(14, 16)), + second: strictParseInt(aValue.slice(17, 19)), + }; + + // Although RFC 5545 requires that all TZIDs used within a file have a + // corresponding time zone definition, we may not be parsing the full file + // or we may be dealing with a non-compliant file; in either case, we can + // check our own time zone service for the TZID in a last-ditch effort. + if (zoneId && !zone) { + timeData.timezone = zoneId; + } + + // 2012-10-10T10:10:10(Z)? + return new Time(timeData, zone); + } + + /** + * Returns a new ICAL.Time instance from a date or date-time string, + * + * @param {String} aValue The string to create from + * @param {Property=} prop The property the date belongs to + * @return {Time} The date/time instance + */ + static fromString(aValue, aProperty) { + if (aValue.length > 10) { + return Time.fromDateTimeString(aValue, aProperty); + } else { + return Time.fromDateString(aValue); + } + } + + /** + * Creates a new ICAL.Time instance from the given Javascript Date. + * + * @param {?Date} aDate The Javascript Date to read, or null to reset + * @param {Boolean} [useUTC=false] If true, the UTC values of the date will be used + */ + static fromJSDate(aDate, useUTC) { + let tt = new Time(); + return tt.fromJSDate(aDate, useUTC); + } + + /** + * Creates a new ICAL.Time instance from the the passed data object. + * + * @param {Object} aData Time initialization + * @param {Number=} aData.year The year for this date + * @param {Number=} aData.month The month for this date + * @param {Number=} aData.day The day for this date + * @param {Number=} aData.hour The hour for this date + * @param {Number=} aData.minute The minute for this date + * @param {Number=} aData.second The second for this date + * @param {Boolean=} aData.isDate If true, the instance represents a date + * (as opposed to a date-time) + * @param {Timezone=} aZone Timezone this position occurs in + */ + static fromData = function fromData(aData, aZone) { + let t = new Time(); + return t.fromData(aData, aZone); + }; + + /** + * Creates a new ICAL.Time instance from the current moment. + * The instance is “floating” - has no timezone relation. + * To create an instance considering the time zone, call + * ICAL.Time.fromJSDate(new Date(), true) + * @return {Time} + */ + static now() { + return Time.fromJSDate(new Date(), false); + } + + /** + * Returns the date on which ISO week number 1 starts. + * + * @see Time#weekNumber + * @param {Number} aYear The year to search in + * @param {weekDay=} aWeekStart The week start weekday, used for calculation. + * @return {Time} The date on which week number 1 starts + */ + static weekOneStarts(aYear, aWeekStart) { + let t = Time.fromData({ + year: aYear, + month: 1, + day: 1, + isDate: true + }); + + let dow = t.dayOfWeek(); + let wkst = aWeekStart || Time.DEFAULT_WEEK_START; + if (dow > Time.THURSDAY) { + t.day += 7; + } + if (wkst > Time.THURSDAY) { + t.day -= 7; + } + + t.day -= dow - wkst; + + return t; + } + + /** + * Get the dominical letter for the given year. Letters range from A - G for + * common years, and AG to GF for leap years. + * + * @param {Number} yr The year to retrieve the letter for + * @return {String} The dominical letter. + */ + static getDominicalLetter(yr) { + let LTRS = "GFEDCBA"; + let dom = (yr + (yr / 4 | 0) + (yr / 400 | 0) - (yr / 100 | 0) - 1) % 7; + let isLeap = Time.isLeapYear(yr); + if (isLeap) { + return LTRS[(dom + 6) % 7] + LTRS[dom]; + } else { + return LTRS[dom]; + } + } + + static #epochTime = null; + /** + * January 1st, 1970 as an ICAL.Time. + * @type {Time} + * @constant + * @instance + */ + static get epochTime() { + if (!this.#epochTime) { + this.#epochTime = Time.fromData({ + year: 1970, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + isDate: false, + timezone: "Z" + }); + } + return this.#epochTime; + } + + static _cmp_attr(a, b, attr) { + if (a[attr] > b[attr]) return 1; + if (a[attr] < b[attr]) return -1; + return 0; + } + + /** + * The days that have passed in the year after a given month. The array has + * two members, one being an array of passed days for non-leap years, the + * other analog for leap years. + * @example + * var isLeapYear = ICAL.Time.isLeapYear(year); + * var passedDays = ICAL.Time.daysInYearPassedMonth[isLeapYear][month]; + * @type {Array.<Array.<Number>>} + */ + static daysInYearPassedMonth = [ + [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365], + [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366] + ]; + + static SUNDAY = 1; + static MONDAY = 2; + static TUESDAY = 3; + static WEDNESDAY = 4; + static THURSDAY = 5; + static FRIDAY = 6; + static SATURDAY = 7; + + /** + * The default weekday for the WKST part. + * @constant + * @default ICAL.Time.MONDAY + */ + static DEFAULT_WEEK_START = 2; // MONDAY + + /** + * Creates a new ICAL.Time instance. + * + * @param {Object} data Time initialization + * @param {Number=} data.year The year for this date + * @param {Number=} data.month The month for this date + * @param {Number=} data.day The day for this date + * @param {Number=} data.hour The hour for this date + * @param {Number=} data.minute The minute for this date + * @param {Number=} data.second The second for this date + * @param {Boolean=} data.isDate If true, the instance represents a date (as + * opposed to a date-time) + * @param {Timezone} zone timezone this position occurs in + */ + constructor(data, zone) { + this.wrappedJSObject = this; + let time = this._time = Object.create(null); + + /* time defaults */ + time.year = 0; + time.month = 1; + time.day = 1; + time.hour = 0; + time.minute = 0; + time.second = 0; + time.isDate = false; + + this.fromData(data, zone); + } + + /** + * The class identifier. + * @constant + * @type {String} + * @default "icaltime" + */ + icalclass = "icaltime"; + _cachedUnixTime = null; + + /** + * The type name, to be used in the jCal object. This value may change and + * is strictly defined by the {@link ICAL.Time#isDate isDate} member. + * @type {String} + * @default "date-time" + */ + get icaltype() { + return this.isDate ? 'date' : 'date-time'; + } + + /** + * The timezone for this time. + * @type {Timezone} + */ + zone = null; + + /** + * Internal uses to indicate that a change has been made and the next read + * operation must attempt to normalize the value (for example changing the + * day to 33). + * + * @type {Boolean} + * @private + */ + _pendingNormalization = false; + + /** + * Returns a clone of the time object. + * + * @return {Time} The cloned object + */ + clone() { + return new Time(this._time, this.zone); + } + + /** + * Reset the time instance to epoch time + */ + reset() { + this.fromData(Time.epochTime); + this.zone = Timezone.utcTimezone; + } + + /** + * Reset the time instance to the given date/time values. + * + * @param {Number} year The year to set + * @param {Number} month The month to set + * @param {Number} day The day to set + * @param {Number} hour The hour to set + * @param {Number} minute The minute to set + * @param {Number} second The second to set + * @param {Timezone} timezone The timezone to set + */ + resetTo(year, month, day, hour, minute, second, timezone) { + this.fromData({ + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + zone: timezone + }); + } + + /** + * Set up the current instance from the Javascript date value. + * + * @param {?Date} aDate The Javascript Date to read, or null to reset + * @param {Boolean} [useUTC=false] If true, the UTC values of the date will be used + */ + fromJSDate(aDate, useUTC) { + if (!aDate) { + this.reset(); + } else { + if (useUTC) { + this.zone = Timezone.utcTimezone; + this.year = aDate.getUTCFullYear(); + this.month = aDate.getUTCMonth() + 1; + this.day = aDate.getUTCDate(); + this.hour = aDate.getUTCHours(); + this.minute = aDate.getUTCMinutes(); + this.second = aDate.getUTCSeconds(); + } else { + this.zone = Timezone.localTimezone; + this.year = aDate.getFullYear(); + this.month = aDate.getMonth() + 1; + this.day = aDate.getDate(); + this.hour = aDate.getHours(); + this.minute = aDate.getMinutes(); + this.second = aDate.getSeconds(); + } + } + this._cachedUnixTime = null; + return this; + } + + /** + * Sets up the current instance using members from the passed data object. + * + * @param {Object} aData Time initialization + * @param {Number=} aData.year The year for this date + * @param {Number=} aData.month The month for this date + * @param {Number=} aData.day The day for this date + * @param {Number=} aData.hour The hour for this date + * @param {Number=} aData.minute The minute for this date + * @param {Number=} aData.second The second for this date + * @param {Boolean=} aData.isDate If true, the instance represents a date + * (as opposed to a date-time) + * @param {Timezone=} aZone Timezone this position occurs in + */ + fromData(aData, aZone) { + if (aData) { + for (let [key, value] of Object.entries(aData)) { + // ical type cannot be set + if (key === 'icaltype') continue; + this[key] = value; + } + } + + if (aZone) { + this.zone = aZone; + } + + if (aData && !("isDate" in aData)) { + this.isDate = !("hour" in aData); + } else if (aData && ("isDate" in aData)) { + this.isDate = aData.isDate; + } + + if (aData && "timezone" in aData) { + let zone = TimezoneService.get( + aData.timezone + ); + + this.zone = zone || Timezone.localTimezone; + } + + if (aData && "zone" in aData) { + this.zone = aData.zone; + } + + if (!this.zone) { + this.zone = Timezone.localTimezone; + } + + this._cachedUnixTime = null; + return this; + } + + /** + * Calculate the day of week. + * @param {weekDay=} aWeekStart + * The week start weekday, defaults to SUNDAY + * @return {weekDay} + */ + dayOfWeek(aWeekStart) { + let firstDow = aWeekStart || Time.SUNDAY; + let dowCacheKey = (this.year << 12) + (this.month << 8) + (this.day << 3) + firstDow; + if (dowCacheKey in Time._dowCache) { + return Time._dowCache[dowCacheKey]; + } + + // Using Zeller's algorithm + let q = this.day; + let m = this.month + (this.month < 3 ? 12 : 0); + let Y = this.year - (this.month < 3 ? 1 : 0); + + let h = (q + Y + trunc(((m + 1) * 26) / 10) + trunc(Y / 4)); + { // eslint-disable-line no-constant-condition + h += trunc(Y / 100) * 6 + trunc(Y / 400); + } + + // Normalize to 1 = wkst + h = ((h + 7 - firstDow) % 7) + 1; + Time._dowCache[dowCacheKey] = h; + return h; + } + + /** + * Calculate the day of year. + * @return {Number} + */ + dayOfYear() { + let is_leap = (Time.isLeapYear(this.year) ? 1 : 0); + let diypm = Time.daysInYearPassedMonth; + return diypm[is_leap][this.month - 1] + this.day; + } + + /** + * Returns a copy of the current date/time, rewound to the start of the + * week. The resulting ICAL.Time instance is of icaltype date, even if this + * is a date-time. + * + * @param {weekDay=} aWeekStart + * The week start weekday, defaults to SUNDAY + * @return {Time} The start of the week (cloned) + */ + startOfWeek(aWeekStart) { + let firstDow = aWeekStart || Time.SUNDAY; + let result = this.clone(); + result.day -= ((this.dayOfWeek() + 7 - firstDow) % 7); + result.isDate = true; + result.hour = 0; + result.minute = 0; + result.second = 0; + return result; + } + + /** + * Returns a copy of the current date/time, shifted to the end of the week. + * The resulting ICAL.Time instance is of icaltype date, even if this is a + * date-time. + * + * @param {weekDay=} aWeekStart + * The week start weekday, defaults to SUNDAY + * @return {Time} The end of the week (cloned) + */ + endOfWeek(aWeekStart) { + let firstDow = aWeekStart || Time.SUNDAY; + let result = this.clone(); + result.day += (7 - this.dayOfWeek() + firstDow - Time.SUNDAY) % 7; + result.isDate = true; + result.hour = 0; + result.minute = 0; + result.second = 0; + return result; + } + + /** + * Returns a copy of the current date/time, rewound to the start of the + * month. The resulting ICAL.Time instance is of icaltype date, even if + * this is a date-time. + * + * @return {Time} The start of the month (cloned) + */ + startOfMonth() { + let result = this.clone(); + result.day = 1; + result.isDate = true; + result.hour = 0; + result.minute = 0; + result.second = 0; + return result; + } + + /** + * Returns a copy of the current date/time, shifted to the end of the + * month. The resulting ICAL.Time instance is of icaltype date, even if + * this is a date-time. + * + * @return {Time} The end of the month (cloned) + */ + endOfMonth() { + let result = this.clone(); + result.day = Time.daysInMonth(result.month, result.year); + result.isDate = true; + result.hour = 0; + result.minute = 0; + result.second = 0; + return result; + } + + /** + * Returns a copy of the current date/time, rewound to the start of the + * year. The resulting ICAL.Time instance is of icaltype date, even if + * this is a date-time. + * + * @return {Time} The start of the year (cloned) + */ + startOfYear() { + let result = this.clone(); + result.day = 1; + result.month = 1; + result.isDate = true; + result.hour = 0; + result.minute = 0; + result.second = 0; + return result; + } + + /** + * Returns a copy of the current date/time, shifted to the end of the + * year. The resulting ICAL.Time instance is of icaltype date, even if + * this is a date-time. + * + * @return {Time} The end of the year (cloned) + */ + endOfYear() { + let result = this.clone(); + result.day = 31; + result.month = 12; + result.isDate = true; + result.hour = 0; + result.minute = 0; + result.second = 0; + return result; + } + + /** + * First calculates the start of the week, then returns the day of year for + * this date. If the day falls into the previous year, the day is zero or negative. + * + * @param {weekDay=} aFirstDayOfWeek + * The week start weekday, defaults to SUNDAY + * @return {Number} The calculated day of year + */ + startDoyWeek(aFirstDayOfWeek) { + let firstDow = aFirstDayOfWeek || Time.SUNDAY; + let delta = this.dayOfWeek() - firstDow; + if (delta < 0) delta += 7; + return this.dayOfYear() - delta; + } + + /** + * Get the dominical letter for the current year. Letters range from A - G + * for common years, and AG to GF for leap years. + * + * @param {Number} yr The year to retrieve the letter for + * @return {String} The dominical letter. + */ + getDominicalLetter() { + return Time.getDominicalLetter(this.year); + } + + /** + * Finds the nthWeekDay relative to the current month (not day). The + * returned value is a day relative the month that this month belongs to so + * 1 would indicate the first of the month and 40 would indicate a day in + * the following month. + * + * @param {Number} aDayOfWeek Day of the week see the day name constants + * @param {Number} aPos Nth occurrence of a given week day values + * of 1 and 0 both indicate the first weekday of that type. aPos may + * be either positive or negative + * + * @return {Number} numeric value indicating a day relative + * to the current month of this time object + */ + nthWeekDay(aDayOfWeek, aPos) { + let daysInMonth = Time.daysInMonth(this.month, this.year); + let weekday; + let pos = aPos; + + let start = 0; + + let otherDay = this.clone(); + + if (pos >= 0) { + otherDay.day = 1; + + // because 0 means no position has been given + // 1 and 0 indicate the same day. + if (pos != 0) { + // remove the extra numeric value + pos--; + } + + // set current start offset to current day. + start = otherDay.day; + + // find the current day of week + let startDow = otherDay.dayOfWeek(); + + // calculate the difference between current + // day of the week and desired day of the week + let offset = aDayOfWeek - startDow; + + + // if the offset goes into the past + // week we add 7 so it goes into the next + // week. We only want to go forward in time here. + if (offset < 0) + // this is really important otherwise we would + // end up with dates from in the past. + offset += 7; + + // add offset to start so start is the same + // day of the week as the desired day of week. + start += offset; + + // because we are going to add (and multiply) + // the numeric value of the day we subtract it + // from the start position so not to add it twice. + start -= aDayOfWeek; + + // set week day + weekday = aDayOfWeek; + } else { + + // then we set it to the last day in the current month + otherDay.day = daysInMonth; + + // find the ends weekday + let endDow = otherDay.dayOfWeek(); + + pos++; + + weekday = (endDow - aDayOfWeek); + + if (weekday < 0) { + weekday += 7; + } + + weekday = daysInMonth - weekday; + } + + weekday += pos * 7; + + return start + weekday; + } + + /** + * Checks if current time is the nth weekday, relative to the current + * month. Will always return false when rule resolves outside of current + * month. + * + * @param {weekDay} aDayOfWeek Day of week to check + * @param {Number} aPos Relative position + * @return {Boolean} True, if it is the nth weekday + */ + isNthWeekDay(aDayOfWeek, aPos) { + let dow = this.dayOfWeek(); + + if (aPos === 0 && dow === aDayOfWeek) { + return true; + } + + // get pos + let day = this.nthWeekDay(aDayOfWeek, aPos); + + if (day === this.day) { + return true; + } + + return false; + } + + /** + * Calculates the ISO 8601 week number. The first week of a year is the + * week that contains the first Thursday. The year can have 53 weeks, if + * January 1st is a Friday. + * + * Note there are regions where the first week of the year is the one that + * starts on January 1st, which may offset the week number. Also, if a + * different week start is specified, this will also affect the week + * number. + * + * @see Time.weekOneStarts + * @param {weekDay} aWeekStart The weekday the week starts with + * @return {Number} The ISO week number + */ + weekNumber(aWeekStart) { + let wnCacheKey = (this.year << 12) + (this.month << 8) + (this.day << 3) + aWeekStart; + if (wnCacheKey in Time._wnCache) { + return Time._wnCache[wnCacheKey]; + } + // This function courtesty of Julian Bucknall, published under the MIT license + // http://www.boyet.com/articles/publishedarticles/calculatingtheisoweeknumb.html + // plus some fixes to be able to use different week starts. + let week1; + + let dt = this.clone(); + dt.isDate = true; + let isoyear = this.year; + + if (dt.month == 12 && dt.day > 25) { + week1 = Time.weekOneStarts(isoyear + 1, aWeekStart); + if (dt.compare(week1) < 0) { + week1 = Time.weekOneStarts(isoyear, aWeekStart); + } else { + isoyear++; + } + } else { + week1 = Time.weekOneStarts(isoyear, aWeekStart); + if (dt.compare(week1) < 0) { + week1 = Time.weekOneStarts(--isoyear, aWeekStart); + } + } + + let daysBetween = (dt.subtractDate(week1).toSeconds() / 86400); + let answer = trunc(daysBetween / 7) + 1; + Time._wnCache[wnCacheKey] = answer; + return answer; + } + + /** + * Adds the duration to the current time. The instance is modified in + * place. + * + * @param {Duration} aDuration The duration to add + */ + addDuration(aDuration) { + let mult = (aDuration.isNegative ? -1 : 1); + + // because of the duration optimizations it is much + // more efficient to grab all the values up front + // then set them directly (which will avoid a normalization call). + // So we don't actually normalize until we need it. + let second = this.second; + let minute = this.minute; + let hour = this.hour; + let day = this.day; + + second += mult * aDuration.seconds; + minute += mult * aDuration.minutes; + hour += mult * aDuration.hours; + day += mult * aDuration.days; + day += mult * 7 * aDuration.weeks; + + this.second = second; + this.minute = minute; + this.hour = hour; + this.day = day; + + this._cachedUnixTime = null; + } + + /** + * Subtract the date details (_excluding_ timezone). Useful for finding + * the relative difference between two time objects excluding their + * timezone differences. + * + * @param {Time} aDate The date to subtract + * @return {Duration} The difference as a duration + */ + subtractDate(aDate) { + let unixTime = this.toUnixTime() + this.utcOffset(); + let other = aDate.toUnixTime() + aDate.utcOffset(); + return Duration.fromSeconds(unixTime - other); + } + + /** + * Subtract the date details, taking timezones into account. + * + * @param {Time} aDate The date to subtract + * @return {Duration} The difference in duration + */ + subtractDateTz(aDate) { + let unixTime = this.toUnixTime(); + let other = aDate.toUnixTime(); + return Duration.fromSeconds(unixTime - other); + } + + /** + * Compares the ICAL.Time instance with another one. + * + * @param {Duration} aOther The instance to compare with + * @return {Number} -1, 0 or 1 for less/equal/greater + */ + compare(other) { + let a = this.toUnixTime(); + let b = other.toUnixTime(); + + if (a > b) return 1; + if (b > a) return -1; + return 0; + } + + /** + * Compares only the date part of this instance with another one. + * + * @param {Duration} other The instance to compare with + * @param {Timezone} tz The timezone to compare in + * @return {Number} -1, 0 or 1 for less/equal/greater + */ + compareDateOnlyTz(other, tz) { + let a = this.convertToZone(tz); + let b = other.convertToZone(tz); + let rc = 0; + + if ((rc = Time._cmp_attr(a, b, "year")) != 0) return rc; + if ((rc = Time._cmp_attr(a, b, "month")) != 0) return rc; + if ((rc = Time._cmp_attr(a, b, "day")) != 0) return rc; + + return rc; + } + + /** + * Convert the instance into another timezone. The returned ICAL.Time + * instance is always a copy. + * + * @param {Timezone} zone The zone to convert to + * @return {Time} The copy, converted to the zone + */ + convertToZone(zone) { + let copy = this.clone(); + let zone_equals = (this.zone.tzid == zone.tzid); + + if (!this.isDate && !zone_equals) { + Timezone.convert_time(copy, this.zone, zone); + } + + copy.zone = zone; + return copy; + } + + /** + * Calculates the UTC offset of the current date/time in the timezone it is + * in. + * + * @return {Number} UTC offset in seconds + */ + utcOffset() { + if (this.zone == Timezone.localTimezone || + this.zone == Timezone.utcTimezone) { + return 0; + } else { + return this.zone.utcOffset(this); + } + } + + /** + * Returns an RFC 5545 compliant ical representation of this object. + * + * @return {String} ical date/date-time + */ + toICALString() { + let string = this.toString(); + + if (string.length > 10) { + return design$1.icalendar.value['date-time'].toICAL(string); + } else { + return design$1.icalendar.value.date.toICAL(string); + } + } + + /** + * The string representation of this date/time, in jCal form + * (including : and - separators). + * @return {String} + */ + toString() { + let result = this.year + '-' + + pad2(this.month) + '-' + + pad2(this.day); + + if (!this.isDate) { + result += 'T' + pad2(this.hour) + ':' + + pad2(this.minute) + ':' + + pad2(this.second); + + if (this.zone === Timezone.utcTimezone) { + result += 'Z'; + } + } + + return result; + } + + /** + * Converts the current instance to a Javascript date + * @return {Date} + */ + toJSDate() { + if (this.zone == Timezone.localTimezone) { + if (this.isDate) { + return new Date(this.year, this.month - 1, this.day); + } else { + return new Date(this.year, this.month - 1, this.day, + this.hour, this.minute, this.second, 0); + } + } else { + return new Date(this.toUnixTime() * 1000); + } + } + + _normalize() { + if (this._time.isDate) { + this._time.hour = 0; + this._time.minute = 0; + this._time.second = 0; + } + this.adjust(0, 0, 0, 0); + + return this; + } + + /** + * Adjust the date/time by the given offset + * + * @param {Number} aExtraDays The extra amount of days + * @param {Number} aExtraHours The extra amount of hours + * @param {Number} aExtraMinutes The extra amount of minutes + * @param {Number} aExtraSeconds The extra amount of seconds + * @param {Number=} aTime The time to adjust, defaults to the + * current instance. + */ + adjust(aExtraDays, aExtraHours, aExtraMinutes, aExtraSeconds, aTime) { + + let minutesOverflow, hoursOverflow, + daysOverflow = 0, yearsOverflow = 0; + + let second, minute, hour, day; + let daysInMonth; + + let time = aTime || this._time; + + if (!time.isDate) { + second = time.second + aExtraSeconds; + time.second = second % 60; + minutesOverflow = trunc(second / 60); + if (time.second < 0) { + time.second += 60; + minutesOverflow--; + } + + minute = time.minute + aExtraMinutes + minutesOverflow; + time.minute = minute % 60; + hoursOverflow = trunc(minute / 60); + if (time.minute < 0) { + time.minute += 60; + hoursOverflow--; + } + + hour = time.hour + aExtraHours + hoursOverflow; + + time.hour = hour % 24; + daysOverflow = trunc(hour / 24); + if (time.hour < 0) { + time.hour += 24; + daysOverflow--; + } + } + + + // Adjust month and year first, because we need to know what month the day + // is in before adjusting it. + if (time.month > 12) { + yearsOverflow = trunc((time.month - 1) / 12); + } else if (time.month < 1) { + yearsOverflow = trunc(time.month / 12) - 1; + } + + time.year += yearsOverflow; + time.month -= 12 * yearsOverflow; + + // Now take care of the days (and adjust month if needed) + day = time.day + aExtraDays + daysOverflow; + + if (day > 0) { + for (;;) { + daysInMonth = Time.daysInMonth(time.month, time.year); + if (day <= daysInMonth) { + break; + } + + time.month++; + if (time.month > 12) { + time.year++; + time.month = 1; + } + + day -= daysInMonth; + } + } else { + while (day <= 0) { + if (time.month == 1) { + time.year--; + time.month = 12; + } else { + time.month--; + } + + day += Time.daysInMonth(time.month, time.year); + } + } + + time.day = day; + + this._cachedUnixTime = null; + return this; + } + + /** + * Sets up the current instance from unix time, the number of seconds since + * January 1st, 1970. + * + * @param {Number} seconds The seconds to set up with + */ + fromUnixTime(seconds) { + this.zone = Timezone.utcTimezone; + // We could use `fromJSDate` here, but this is about twice as fast. + // We could also clone `epochTime` and use `adjust` for a more + // ical.js-centric approach, but this is about 100 times as fast. + let date = new Date(seconds * 1000); + this.year = date.getUTCFullYear(); + this.month = date.getUTCMonth() + 1; + this.day = date.getUTCDate(); + if (this._time.isDate) { + this.hour = 0; + this.minute = 0; + this.second = 0; + } else { + this.hour = date.getUTCHours(); + this.minute = date.getUTCMinutes(); + this.second = date.getUTCSeconds(); + } + + this._cachedUnixTime = null; + } + + /** + * Converts the current instance to seconds since January 1st 1970. + * + * @return {Number} Seconds since 1970 + */ + toUnixTime() { + if (this._cachedUnixTime !== null) { + return this._cachedUnixTime; + } + let offset = this.utcOffset(); + + // we use the offset trick to ensure + // that we are getting the actual UTC time + let ms = Date.UTC( + this.year, + this.month - 1, + this.day, + this.hour, + this.minute, + this.second - offset + ); + + // seconds + this._cachedUnixTime = ms / 1000; + return this._cachedUnixTime; + } + + /** + * Converts time to into Object which can be serialized then re-created + * using the constructor. + * + * @example + * // toJSON will automatically be called + * var json = JSON.stringify(mytime); + * + * var deserialized = JSON.parse(json); + * + * var time = new ICAL.Time(deserialized); + * + * @return {Object} + */ + toJSON() { + let copy = [ + 'year', + 'month', + 'day', + 'hour', + 'minute', + 'second', + 'isDate' + ]; + + let result = Object.create(null); + + let i = 0; + let len = copy.length; + let prop; + + for (; i < len; i++) { + prop = copy[i]; + result[prop] = this[prop]; + } + + if (this.zone) { + result.timezone = this.zone.tzid; + } + + return result; + } +} + +(function setupNormalizeAttributes() { + // This needs to run before any instances are created! + function defineAttr(attr) { + Object.defineProperty(Time.prototype, attr, { + get: function getTimeAttr() { + if (this._pendingNormalization) { + this._normalize(); + this._pendingNormalization = false; + } + + return this._time[attr]; + }, + set: function setTimeAttr(val) { + // Check if isDate will be set and if was not set to normalize date. + // This avoids losing days when seconds, minutes and hours are zeroed + // what normalize will do when time is a date. + if (attr === "isDate" && val && !this._time.isDate) { + this.adjust(0, 0, 0, 0); + } + this._cachedUnixTime = null; + this._pendingNormalization = true; + this._time[attr] = val; + } + }); + + } + + defineAttr("year"); + defineAttr("month"); + defineAttr("day"); + defineAttr("hour"); + defineAttr("minute"); + defineAttr("second"); + defineAttr("isDate"); +})(); + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * This lets typescript resolve our custom types in the + * generated d.ts files (jsdoc typedefs are converted to typescript types). + * Ignore prevents the typedefs from being documented more than once. + * + * @ignore + * @typedef {import("./types.js").parserState} parserState + * Imports the 'parserState' type from the "types.js" module + * @typedef {import("./types.js").designSet} designSet + * Imports the 'designSet' type from the "types.js" module + */ + +const CHAR = /[^ \t]/; +const VALUE_DELIMITER = ':'; +const PARAM_DELIMITER = ';'; +const PARAM_NAME_DELIMITER = '='; +const DEFAULT_VALUE_TYPE$1 = 'unknown'; +const DEFAULT_PARAM_TYPE = 'text'; +const RFC6868_REPLACE_MAP$1 = { "^'": '"', "^n": "\n", "^^": "^" }; + +/** + * Parses iCalendar or vCard data into a raw jCal object. Consult + * documentation on the {@tutorial layers|layers of parsing} for more + * details. + * + * @function ICAL.parse + * @memberof ICAL + * @variation function + * @todo Fix the API to be more clear on the return type + * @param {String} input The string data to parse + * @return {Object|Object[]} A single jCal object, or an array thereof + */ +function parse(input) { + let state = {}; + let root = state.component = []; + + state.stack = [root]; + + parse._eachLine(input, function(err, line) { + parse._handleContentLine(line, state); + }); + + + // when there are still items on the stack + // throw a fatal error, a component was not closed + // correctly in that case. + if (state.stack.length > 1) { + throw new ParserError( + 'invalid ical body. component began but did not end' + ); + } + + state = null; + + return (root.length == 1 ? root[0] : root); +} + +/** + * Parse an iCalendar property value into the jCal for a single property + * + * @function ICAL.parse.property + * @param {String} str + * The iCalendar property string to parse + * @param {designSet=} designSet + * The design data to use for this property + * @return {Object} + * The jCal Object containing the property + */ +parse.property = function(str, designSet) { + let state = { + component: [[], []], + designSet: designSet || design$1.defaultSet + }; + parse._handleContentLine(str, state); + return state.component[1][0]; +}; + +/** + * Convenience method to parse a component. You can use ICAL.parse() directly + * instead. + * + * @function ICAL.parse.component + * @see ICAL.parse(function) + * @param {String} str The iCalendar component string to parse + * @return {Object} The jCal Object containing the component + */ +parse.component = function(str) { + return parse(str); +}; + + +/** + * An error that occurred during parsing. + * + * @param {String} message The error message + * @memberof ICAL.parse + * @extends {Error} + */ +class ParserError extends Error { + name = this.constructor.name; +} + +// classes & constants +parse.ParserError = ParserError; + + +/** + * Handles a single line of iCalendar/vCard, updating the state. + * + * @private + * @function ICAL.parse._handleContentLine + * @param {String} line The content line to process + * @param {parserState} state The current state of the line parsing + */ +parse._handleContentLine = function(line, state) { + // break up the parts of the line + let valuePos = line.indexOf(VALUE_DELIMITER); + let paramPos = line.indexOf(PARAM_DELIMITER); + + let lastParamIndex; + let lastValuePos; + + // name of property or begin/end + let name; + let value; + // params is only overridden if paramPos !== -1. + // we can't do params = params || {} later on + // because it sacrifices ops. + let params = {}; + + /** + * Different property cases + * + * + * 1. RRULE:FREQ=foo + * // FREQ= is not a param but the value + * + * 2. ATTENDEE;ROLE=REQ-PARTICIPANT; + * // ROLE= is a param because : has not happened yet + */ + // when the parameter delimiter is after the + // value delimiter then it is not a parameter. + + if ((paramPos !== -1 && valuePos !== -1)) { + // when the parameter delimiter is after the + // value delimiter then it is not a parameter. + if (paramPos > valuePos) { + paramPos = -1; + } + } + + let parsedParams; + if (paramPos !== -1) { + name = line.slice(0, Math.max(0, paramPos)).toLowerCase(); + parsedParams = parse._parseParameters(line.slice(Math.max(0, paramPos)), 0, state.designSet); + if (parsedParams[2] == -1) { + throw new ParserError("Invalid parameters in '" + line + "'"); + } + params = parsedParams[0]; + lastParamIndex = parsedParams[1].length + parsedParams[2] + paramPos; + if ((lastValuePos = + line.slice(Math.max(0, lastParamIndex)).indexOf(VALUE_DELIMITER)) !== -1) { + value = line.slice(Math.max(0, lastParamIndex + lastValuePos + 1)); + } else { + throw new ParserError("Missing parameter value in '" + line + "'"); + } + } else if (valuePos !== -1) { + // without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC) + name = line.slice(0, Math.max(0, valuePos)).toLowerCase(); + value = line.slice(Math.max(0, valuePos + 1)); + + if (name === 'begin') { + let newComponent = [value.toLowerCase(), [], []]; + if (state.stack.length === 1) { + state.component.push(newComponent); + } else { + state.component[2].push(newComponent); + } + state.stack.push(state.component); + state.component = newComponent; + if (!state.designSet) { + state.designSet = design$1.getDesignSet(state.component[0]); + } + return; + } else if (name === 'end') { + state.component = state.stack.pop(); + return; + } + // If it is not begin/end, then this is a property with an empty value, + // which should be considered valid. + } else { + /** + * Invalid line. + * The rational to throw an error is we will + * never be certain that the rest of the file + * is sane and it is unlikely that we can serialize + * the result correctly either. + */ + throw new ParserError( + 'invalid line (no token ";" or ":") "' + line + '"' + ); + } + + let valueType; + let multiValue = false; + let structuredValue = false; + let propertyDetails; + let splitName; + let ungroupedName; + + // fetch the ungrouped part of the name + if (state.designSet.propertyGroups && name.indexOf('.') !== -1) { + splitName = name.split('.'); + params.group = splitName[0]; + ungroupedName = splitName[1]; + } else { + ungroupedName = name; + } + + if (ungroupedName in state.designSet.property) { + propertyDetails = state.designSet.property[ungroupedName]; + + if ('multiValue' in propertyDetails) { + multiValue = propertyDetails.multiValue; + } + + if ('structuredValue' in propertyDetails) { + structuredValue = propertyDetails.structuredValue; + } + + if (value && 'detectType' in propertyDetails) { + valueType = propertyDetails.detectType(value); + } + } + + // attempt to determine value + if (!valueType) { + if (!('value' in params)) { + if (propertyDetails) { + valueType = propertyDetails.defaultType; + } else { + valueType = DEFAULT_VALUE_TYPE$1; + } + } else { + // possible to avoid this? + valueType = params.value.toLowerCase(); + } + } + + delete params.value; + + /** + * Note on `var result` juggling: + * + * I observed that building the array in pieces has adverse + * effects on performance, so where possible we inline the creation. + * It is a little ugly but resulted in ~2000 additional ops/sec. + */ + + let result; + if (multiValue && structuredValue) { + value = parse._parseMultiValue(value, structuredValue, valueType, [], multiValue, state.designSet, structuredValue); + result = [ungroupedName, params, valueType, value]; + } else if (multiValue) { + result = [ungroupedName, params, valueType]; + parse._parseMultiValue(value, multiValue, valueType, result, null, state.designSet, false); + } else if (structuredValue) { + value = parse._parseMultiValue(value, structuredValue, valueType, [], null, state.designSet, structuredValue); + result = [ungroupedName, params, valueType, value]; + } else { + value = parse._parseValue(value, valueType, state.designSet, false); + result = [ungroupedName, params, valueType, value]; + } + // rfc6350 requires that in vCard 4.0 the first component is the VERSION + // component with as value 4.0, note that 3.0 does not have this requirement. + if (state.component[0] === 'vcard' && state.component[1].length === 0 && + !(name === 'version' && value === '4.0')) { + state.designSet = design$1.getDesignSet("vcard3"); + } + state.component[1].push(result); +}; + +/** + * Parse a value from the raw value into the jCard/jCal value. + * + * @private + * @function ICAL.parse._parseValue + * @param {String} value Original value + * @param {String} type Type of value + * @param {Object} designSet The design data to use for this value + * @return {Object} varies on type + */ +parse._parseValue = function(value, type, designSet, structuredValue) { + if (type in designSet.value && 'fromICAL' in designSet.value[type]) { + return designSet.value[type].fromICAL(value, structuredValue); + } + return value; +}; + +/** + * Parse parameters from a string to object. + * + * @function ICAL.parse._parseParameters + * @private + * @param {String} line A single unfolded line + * @param {Number} start Position to start looking for properties + * @param {Object} designSet The design data to use for this property + * @return {Object} key/value pairs + */ +parse._parseParameters = function(line, start, designSet) { + let lastParam = start; + let pos = 0; + let delim = PARAM_NAME_DELIMITER; + let result = {}; + let name, lcname; + let value, valuePos = -1; + let type, multiValue, mvdelim; + + // find the next '=' sign + // use lastParam and pos to find name + // check if " is used if so get value from "->" + // then increment pos to find next ; + + while ((pos !== false) && + (pos = line.indexOf(delim, pos + 1)) !== -1) { + + name = line.slice(lastParam + 1, pos); + if (name.length == 0) { + throw new ParserError("Empty parameter name in '" + line + "'"); + } + lcname = name.toLowerCase(); + mvdelim = false; + multiValue = false; + + if (lcname in designSet.param && designSet.param[lcname].valueType) { + type = designSet.param[lcname].valueType; + } else { + type = DEFAULT_PARAM_TYPE; + } + + if (lcname in designSet.param) { + multiValue = designSet.param[lcname].multiValue; + if (designSet.param[lcname].multiValueSeparateDQuote) { + mvdelim = parse._rfc6868Escape('"' + multiValue + '"'); + } + } + + let nextChar = line[pos + 1]; + if (nextChar === '"') { + valuePos = pos + 2; + pos = line.indexOf('"', valuePos); + if (multiValue && pos != -1) { + let extendedValue = true; + while (extendedValue) { + if (line[pos + 1] == multiValue && line[pos + 2] == '"') { + pos = line.indexOf('"', pos + 3); + } else { + extendedValue = false; + } + } + } + if (pos === -1) { + throw new ParserError( + 'invalid line (no matching double quote) "' + line + '"' + ); + } + value = line.slice(valuePos, pos); + lastParam = line.indexOf(PARAM_DELIMITER, pos); + let propValuePos = line.indexOf(VALUE_DELIMITER, pos); + // if either no next parameter or delimeter in property value, let's stop here + if (lastParam === -1 || (propValuePos !== -1 && lastParam > propValuePos)) { + pos = false; + } + } else { + valuePos = pos + 1; + + // move to next ";" + let nextPos = line.indexOf(PARAM_DELIMITER, valuePos); + let propValuePos = line.indexOf(VALUE_DELIMITER, valuePos); + if (propValuePos !== -1 && nextPos > propValuePos) { + // this is a delimiter in the property value, let's stop here + nextPos = propValuePos; + pos = false; + } else if (nextPos === -1) { + // no ";" + if (propValuePos === -1) { + nextPos = line.length; + } else { + nextPos = propValuePos; + } + pos = false; + } else { + lastParam = nextPos; + pos = nextPos; + } + + value = line.slice(valuePos, nextPos); + } + + const length_before = value.length; + value = parse._rfc6868Escape(value); + valuePos += length_before - value.length; + if (multiValue) { + let delimiter = mvdelim || multiValue; + value = parse._parseMultiValue(value, delimiter, type, [], null, designSet); + } else { + value = parse._parseValue(value, type, designSet); + } + + if (multiValue && (lcname in result)) { + if (Array.isArray(result[lcname])) { + result[lcname].push(value); + } else { + result[lcname] = [ + result[lcname], + value + ]; + } + } else { + result[lcname] = value; + } + } + return [result, value, valuePos]; +}; + +/** + * Internal helper for rfc6868. Exposing this on ICAL.parse so that + * hackers can disable the rfc6868 parsing if the really need to. + * + * @function ICAL.parse._rfc6868Escape + * @param {String} val The value to escape + * @return {String} The escaped value + */ +parse._rfc6868Escape = function(val) { + return val.replace(/\^['n^]/g, function(x) { + return RFC6868_REPLACE_MAP$1[x]; + }); +}; + +/** + * Parse a multi value string. This function is used either for parsing + * actual multi-value property's values, or for handling parameter values. It + * can be used for both multi-value properties and structured value properties. + * + * @private + * @function ICAL.parse._parseMultiValue + * @param {String} buffer The buffer containing the full value + * @param {String} delim The multi-value delimiter + * @param {String} type The value type to be parsed + * @param {Array.<?>} result The array to append results to, varies on value type + * @param {String} innerMulti The inner delimiter to split each value with + * @param {designSet} designSet The design data for this value + * @return {?|Array.<?>} Either an array of results, or the first result + */ +parse._parseMultiValue = function(buffer, delim, type, result, innerMulti, designSet, structuredValue) { + let pos = 0; + let lastPos = 0; + let value; + if (delim.length === 0) { + return buffer; + } + + // split each piece + while ((pos = unescapedIndexOf(buffer, delim, lastPos)) !== -1) { + value = buffer.slice(lastPos, pos); + if (innerMulti) { + value = parse._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue); + } else { + value = parse._parseValue(value, type, designSet, structuredValue); + } + result.push(value); + lastPos = pos + delim.length; + } + + // on the last piece take the rest of string + value = buffer.slice(lastPos); + if (innerMulti) { + value = parse._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue); + } else { + value = parse._parseValue(value, type, designSet, structuredValue); + } + result.push(value); + + return result.length == 1 ? result[0] : result; +}; + +/** + * Process a complete buffer of iCalendar/vCard data line by line, correctly + * unfolding content. Each line will be processed with the given callback + * + * @private + * @function ICAL.parse._eachLine + * @param {String} buffer The buffer to process + * @param {function(?String, String)} callback The callback for each line + */ +parse._eachLine = function(buffer, callback) { + let len = buffer.length; + let lastPos = buffer.search(CHAR); + let pos = lastPos; + let line; + let firstChar; + + let newlineOffset; + + do { + pos = buffer.indexOf('\n', lastPos) + 1; + + if (pos > 1 && buffer[pos - 2] === '\r') { + newlineOffset = 2; + } else { + newlineOffset = 1; + } + + if (pos === 0) { + pos = len; + newlineOffset = 0; + } + + firstChar = buffer[lastPos]; + + if (firstChar === ' ' || firstChar === '\t') { + // add to line + line += buffer.slice(lastPos + 1, pos - newlineOffset); + } else { + if (line) + callback(null, line); + // push line + line = buffer.slice(lastPos, pos - newlineOffset); + } + + lastPos = pos; + } while (pos !== len); + + // extra ending line + line = line.trim(); + + if (line.length) + callback(null, line); +}; + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +const OPTIONS = ["tzid", "location", "tznames", "latitude", "longitude"]; + +/** + * Timezone representation. + * + * @example + * var vcalendar; + * var timezoneComp = vcalendar.getFirstSubcomponent('vtimezone'); + * var tzid = timezoneComp.getFirstPropertyValue('tzid'); + * + * var timezone = new ICAL.Timezone({ + * component: timezoneComp, + * tzid + * }); + * + * @memberof ICAL + */ +class Timezone { + static _compare_change_fn(a, b) { + if (a.year < b.year) return -1; + else if (a.year > b.year) return 1; + + if (a.month < b.month) return -1; + else if (a.month > b.month) return 1; + + if (a.day < b.day) return -1; + else if (a.day > b.day) return 1; + + if (a.hour < b.hour) return -1; + else if (a.hour > b.hour) return 1; + + if (a.minute < b.minute) return -1; + else if (a.minute > b.minute) return 1; + + if (a.second < b.second) return -1; + else if (a.second > b.second) return 1; + + return 0; + } + + /** + * Convert the date/time from one zone to the next. + * + * @param {Time} tt The time to convert + * @param {Timezone} from_zone The source zone to convert from + * @param {Timezone} to_zone The target zone to convert to + * @return {Time} The converted date/time object + */ + static convert_time(tt, from_zone, to_zone) { + if (tt.isDate || + from_zone.tzid == to_zone.tzid || + from_zone == Timezone.localTimezone || + to_zone == Timezone.localTimezone) { + tt.zone = to_zone; + return tt; + } + + let utcOffset = from_zone.utcOffset(tt); + tt.adjust(0, 0, 0, - utcOffset); + + utcOffset = to_zone.utcOffset(tt); + tt.adjust(0, 0, 0, utcOffset); + + return null; + } + + /** + * Creates a new ICAL.Timezone instance from the passed data object. + * + * @param {Component|Object} aData options for class + * @param {String|Component} aData.component + * If aData is a simple object, then this member can be set to either a + * string containing the component data, or an already parsed + * ICAL.Component + * @param {String} aData.tzid The timezone identifier + * @param {String} aData.location The timezone locationw + * @param {String} aData.tznames An alternative string representation of the + * timezone + * @param {Number} aData.latitude The latitude of the timezone + * @param {Number} aData.longitude The longitude of the timezone + */ + static fromData(aData) { + let tt = new Timezone(); + return tt.fromData(aData); + } + + /** + * The instance describing the UTC timezone + * @type {Timezone} + * @constant + * @instance + */ + static #utcTimezone = null; + static get utcTimezone() { + if (!this.#utcTimezone) { + this.#utcTimezone = Timezone.fromData({ + tzid: "UTC" + }); + } + return this.#utcTimezone; + } + + /** + * The instance describing the local timezone + * @type {Timezone} + * @constant + * @instance + */ + static #localTimezone = null; + static get localTimezone() { + if (!this.#localTimezone) { + this.#localTimezone = Timezone.fromData({ + tzid: "floating" + }); + } + return this.#localTimezone; + } + + /** + * Adjust a timezone change object. + * @private + * @param {Object} change The timezone change object + * @param {Number} days The extra amount of days + * @param {Number} hours The extra amount of hours + * @param {Number} minutes The extra amount of minutes + * @param {Number} seconds The extra amount of seconds + */ + static adjust_change(change, days, hours, minutes, seconds) { + return Time.prototype.adjust.call( + change, + days, + hours, + minutes, + seconds, + change + ); + } + + static _minimumExpansionYear = -1; + static EXTRA_COVERAGE = 5; + + /** + * Creates a new ICAL.Timezone instance, by passing in a tzid and component. + * + * @param {Component|Object} data options for class + * @param {String|Component} data.component + * If data is a simple object, then this member can be set to either a + * string containing the component data, or an already parsed + * ICAL.Component + * @param {String} data.tzid The timezone identifier + * @param {String} data.location The timezone locationw + * @param {String} data.tznames An alternative string representation of the + * timezone + * @param {Number} data.latitude The latitude of the timezone + * @param {Number} data.longitude The longitude of the timezone + */ + constructor(data) { + this.wrappedJSObject = this; + this.fromData(data); + } + + + /** + * Timezone identifier + * @type {String} + */ + tzid = ""; + + /** + * Timezone location + * @type {String} + */ + location = ""; + + /** + * Alternative timezone name, for the string representation + * @type {String} + */ + tznames = ""; + + /** + * The primary latitude for the timezone. + * @type {Number} + */ + latitude = 0.0; + + /** + * The primary longitude for the timezone. + * @type {Number} + */ + longitude = 0.0; + + /** + * The vtimezone component for this timezone. + * @type {Component} + */ + component = null; + + /** + * The year this timezone has been expanded to. All timezone transition + * dates until this year are known and can be used for calculation + * + * @private + * @type {Number} + */ + expandedUntilYear = 0; + + /** + * The class identifier. + * @constant + * @type {String} + * @default "icaltimezone" + */ + icalclass = "icaltimezone"; + + /** + * Sets up the current instance using members from the passed data object. + * + * @param {Component|Object} aData options for class + * @param {String|Component} aData.component + * If aData is a simple object, then this member can be set to either a + * string containing the component data, or an already parsed + * ICAL.Component + * @param {String} aData.tzid The timezone identifier + * @param {String} aData.location The timezone locationw + * @param {String} aData.tznames An alternative string representation of the + * timezone + * @param {Number} aData.latitude The latitude of the timezone + * @param {Number} aData.longitude The longitude of the timezone + */ + fromData(aData) { + this.expandedUntilYear = 0; + this.changes = []; + + if (aData instanceof Component) { + // Either a component is passed directly + this.component = aData; + } else { + // Otherwise the component may be in the data object + if (aData && "component" in aData) { + if (typeof aData.component == "string") { + // If a string was passed, parse it as a component + let jCal = parse(aData.component); + this.component = new Component(jCal); + } else if (aData.component instanceof Component) { + // If it was a component already, then just set it + this.component = aData.component; + } else { + // Otherwise just null out the component + this.component = null; + } + } + + // Copy remaining passed properties + for (let prop of OPTIONS) { + if (aData && prop in aData) { + this[prop] = aData[prop]; + } + } + } + + // If we have a component but no TZID, attempt to get it from the + // component's properties. + if (this.component instanceof Component && !this.tzid) { + this.tzid = this.component.getFirstPropertyValue('tzid'); + } + + return this; + } + + /** + * Finds the utcOffset the given time would occur in this timezone. + * + * @param {Time} tt The time to check for + * @return {Number} utc offset in seconds + */ + utcOffset(tt) { + if (this == Timezone.utcTimezone || this == Timezone.localTimezone) { + return 0; + } + + this._ensureCoverage(tt.year); + + if (!this.changes.length) { + return 0; + } + + let tt_change = { + year: tt.year, + month: tt.month, + day: tt.day, + hour: tt.hour, + minute: tt.minute, + second: tt.second + }; + + let change_num = this._findNearbyChange(tt_change); + let change_num_to_use = -1; + let step = 1; + + // TODO: replace with bin search? + for (;;) { + let change = clone(this.changes[change_num], true); + if (change.utcOffset < change.prevUtcOffset) { + Timezone.adjust_change(change, 0, 0, 0, change.utcOffset); + } else { + Timezone.adjust_change(change, 0, 0, 0, + change.prevUtcOffset); + } + + let cmp = Timezone._compare_change_fn(tt_change, change); + + if (cmp >= 0) { + change_num_to_use = change_num; + } else { + step = -1; + } + + if (step == -1 && change_num_to_use != -1) { + break; + } + + change_num += step; + + if (change_num < 0) { + return 0; + } + + if (change_num >= this.changes.length) { + break; + } + } + + let zone_change = this.changes[change_num_to_use]; + let utcOffset_change = zone_change.utcOffset - zone_change.prevUtcOffset; + + if (utcOffset_change < 0 && change_num_to_use > 0) { + let tmp_change = clone(zone_change, true); + Timezone.adjust_change(tmp_change, 0, 0, 0, tmp_change.prevUtcOffset); + + if (Timezone._compare_change_fn(tt_change, tmp_change) < 0) { + let prev_zone_change = this.changes[change_num_to_use - 1]; + + let want_daylight = false; // TODO + + if (zone_change.is_daylight != want_daylight && + prev_zone_change.is_daylight == want_daylight) { + zone_change = prev_zone_change; + } + } + } + + // TODO return is_daylight? + return zone_change.utcOffset; + } + + _findNearbyChange(change) { + // find the closest match + let idx = binsearchInsert( + this.changes, + change, + Timezone._compare_change_fn + ); + + if (idx >= this.changes.length) { + return this.changes.length - 1; + } + + return idx; + } + + _ensureCoverage(aYear) { + if (Timezone._minimumExpansionYear == -1) { + let today = Time.now(); + Timezone._minimumExpansionYear = today.year; + } + + let changesEndYear = aYear; + if (changesEndYear < Timezone._minimumExpansionYear) { + changesEndYear = Timezone._minimumExpansionYear; + } + + changesEndYear += Timezone.EXTRA_COVERAGE; + + if (!this.changes.length || this.expandedUntilYear < aYear) { + let subcomps = this.component.getAllSubcomponents(); + let compLen = subcomps.length; + let compIdx = 0; + + for (; compIdx < compLen; compIdx++) { + this._expandComponent( + subcomps[compIdx], changesEndYear, this.changes + ); + } + + this.changes.sort(Timezone._compare_change_fn); + this.expandedUntilYear = changesEndYear; + } + } + + _expandComponent(aComponent, aYear, changes) { + if (!aComponent.hasProperty("dtstart") || + !aComponent.hasProperty("tzoffsetto") || + !aComponent.hasProperty("tzoffsetfrom")) { + return null; + } + + let dtstart = aComponent.getFirstProperty("dtstart").getFirstValue(); + let change; + + function convert_tzoffset(offset) { + return offset.factor * (offset.hours * 3600 + offset.minutes * 60); + } + + function init_changes() { + let changebase = {}; + changebase.is_daylight = (aComponent.name == "daylight"); + changebase.utcOffset = convert_tzoffset( + aComponent.getFirstProperty("tzoffsetto").getFirstValue() + ); + + changebase.prevUtcOffset = convert_tzoffset( + aComponent.getFirstProperty("tzoffsetfrom").getFirstValue() + ); + + return changebase; + } + + if (!aComponent.hasProperty("rrule") && !aComponent.hasProperty("rdate")) { + change = init_changes(); + change.year = dtstart.year; + change.month = dtstart.month; + change.day = dtstart.day; + change.hour = dtstart.hour; + change.minute = dtstart.minute; + change.second = dtstart.second; + + Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset); + changes.push(change); + } else { + let props = aComponent.getAllProperties("rdate"); + for (let rdate of props) { + let time = rdate.getFirstValue(); + change = init_changes(); + + change.year = time.year; + change.month = time.month; + change.day = time.day; + + if (time.isDate) { + change.hour = dtstart.hour; + change.minute = dtstart.minute; + change.second = dtstart.second; + + if (dtstart.zone != Timezone.utcTimezone) { + Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset); + } + } else { + change.hour = time.hour; + change.minute = time.minute; + change.second = time.second; + + if (time.zone != Timezone.utcTimezone) { + Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset); + } + } + + changes.push(change); + } + + let rrule = aComponent.getFirstProperty("rrule"); + + if (rrule) { + rrule = rrule.getFirstValue(); + change = init_changes(); + + if (rrule.until && rrule.until.zone == Timezone.utcTimezone) { + rrule.until.adjust(0, 0, 0, change.prevUtcOffset); + rrule.until.zone = Timezone.localTimezone; + } + + let iterator = rrule.iterator(dtstart); + + let occ; + while ((occ = iterator.next())) { + change = init_changes(); + if (occ.year > aYear || !occ) { + break; + } + + change.year = occ.year; + change.month = occ.month; + change.day = occ.day; + change.hour = occ.hour; + change.minute = occ.minute; + change.second = occ.second; + change.isDate = occ.isDate; + + Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset); + changes.push(change); + } + } + } + + return changes; + } + + /** + * The string representation of this timezone. + * @return {String} + */ + toString() { + return (this.tznames ? this.tznames : this.tzid); + } +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +let zones = null; + +/** + * @classdesc + * Singleton class to contain timezones. Right now it is all manual registry in + * the future we may use this class to download timezone information or handle + * loading pre-expanded timezones. + * + * @exports module:ICAL.TimezoneService + * @memberof ICAL + */ +const TimezoneService = { + get count() { + if (zones === null) { + return 0; + } + + return Object.keys(zones).length; + }, + + reset: function() { + zones = Object.create(null); + let utc = Timezone.utcTimezone; + + zones.Z = utc; + zones.UTC = utc; + zones.GMT = utc; + }, + _hard_reset: function() { + zones = null; + }, + + /** + * Checks if timezone id has been registered. + * + * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) + * @return {Boolean} False, when not present + */ + has: function(tzid) { + if (zones === null) { + return false; + } + + return !!zones[tzid]; + }, + + /** + * Returns a timezone by its tzid if present. + * + * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) + * @return {Timezone | undefined} The timezone, or undefined if not found + */ + get: function(tzid) { + if (zones === null) { + this.reset(); + } + + return zones[tzid]; + }, + + /** + * Registers a timezone object or component. + * + * @param {Component|Timezone} timezone + * The initialized zone or vtimezone. + * + * @param {String=} name + * The name of the timezone. Defaults to the component's TZID if not + * passed. + */ + register: function(timezone, name) { + if (zones === null) { + this.reset(); + } + + // This avoids a breaking change by the change of argument order + // TODO remove in v3 + if (typeof timezone === "string" && name instanceof Timezone) { + [timezone, name] = [name, timezone]; + } + + if (!name) { + if (timezone instanceof Timezone) { + name = timezone.tzid; + } else { + if (timezone.name === 'vtimezone') { + timezone = new Timezone(timezone); + name = timezone.tzid; + } + } + } + + if (!name) { + throw new TypeError("Neither a timezone nor a name was passed"); + } + + if (timezone instanceof Timezone) { + zones[name] = timezone; + } else { + throw new TypeError('timezone must be ICAL.Timezone or ICAL.Component'); + } + }, + + /** + * Removes a timezone by its tzid from the list. + * + * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) + * @return {?Timezone} The removed timezone, or null if not registered + */ + remove: function(tzid) { + if (zones === null) { + return null; + } + + return (delete zones[tzid]); + } +}; + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * Helper functions used in various places within ical.js + * @module ICAL.helpers + */ + +/** + * Compiles a list of all referenced TZIDs in all subcomponents and + * removes any extra VTIMEZONE subcomponents. In addition, if any TZIDs + * are referenced by a component, but a VTIMEZONE does not exist, + * an attempt will be made to generate a VTIMEZONE using ICAL.TimezoneService. + * + * @param {Component} vcal The top-level VCALENDAR component. + * @return {Component} The ICAL.Component that was passed in. + */ +function updateTimezones(vcal) { + let allsubs, properties, vtimezones, reqTzid, i; + + if (!vcal || vcal.name !== "vcalendar") { + //not a top-level vcalendar component + return vcal; + } + + //Store vtimezone subcomponents in an object reference by tzid. + //Store properties from everything else in another array + allsubs = vcal.getAllSubcomponents(); + properties = []; + vtimezones = {}; + for (i = 0; i < allsubs.length; i++) { + if (allsubs[i].name === "vtimezone") { + let tzid = allsubs[i].getFirstProperty("tzid").getFirstValue(); + vtimezones[tzid] = allsubs[i]; + } else { + properties = properties.concat(allsubs[i].getAllProperties()); + } + } + + //create an object with one entry for each required tz + reqTzid = {}; + for (i = 0; i < properties.length; i++) { + let tzid = properties[i].getParameter("tzid"); + if (tzid) { + reqTzid[tzid] = true; + } + } + + //delete any vtimezones that are not on the reqTzid list. + for (let [tzid, comp] of Object.entries(vtimezones)) { + if (!reqTzid[tzid]) { + vcal.removeSubcomponent(comp); + } + } + + //create any missing, but registered timezones + for (let tzid of Object.keys(reqTzid)) { + if (!vtimezones[tzid] && TimezoneService.has(tzid)) { + vcal.addSubcomponent(TimezoneService.get(tzid).component); + } + } + + return vcal; +} + +/** + * Checks if the given type is of the number type and also NaN. + * + * @param {Number} number The number to check + * @return {Boolean} True, if the number is strictly NaN + */ +function isStrictlyNaN(number) { + return typeof(number) === 'number' && isNaN(number); +} + +/** + * Parses a string value that is expected to be an integer, when the valid is + * not an integer throws a decoration error. + * + * @param {String} string Raw string input + * @return {Number} Parsed integer + */ +function strictParseInt(string) { + let result = parseInt(string, 10); + + if (isStrictlyNaN(result)) { + throw new Error( + 'Could not extract integer from "' + string + '"' + ); + } + + return result; +} + +/** + * Creates or returns a class instance of a given type with the initialization + * data if the data is not already an instance of the given type. + * + * @example + * var time = new ICAL.Time(...); + * var result = ICAL.helpers.formatClassType(time, ICAL.Time); + * + * (result instanceof ICAL.Time) + * // => true + * + * result = ICAL.helpers.formatClassType({}, ICAL.Time); + * (result isntanceof ICAL.Time) + * // => true + * + * + * @param {Object} data object initialization data + * @param {Object} type object type (like ICAL.Time) + * @return {?} An instance of the found type. + */ +function formatClassType(data, type) { + if (typeof(data) === 'undefined') { + return undefined; + } + + if (data instanceof type) { + return data; + } + return new type(data); +} + +/** + * Identical to indexOf but will only match values when they are not preceded + * by a backslash character. + * + * @param {String} buffer String to search + * @param {String} search Value to look for + * @param {Number} pos Start position + * @return {Number} The position, or -1 if not found + */ +function unescapedIndexOf(buffer, search, pos) { + while ((pos = buffer.indexOf(search, pos)) !== -1) { + if (pos > 0 && buffer[pos - 1] === '\\') { + pos += 1; + } else { + return pos; + } + } + return -1; +} + +/** + * Find the index for insertion using binary search. + * + * @param {Array} list The list to search + * @param {?} seekVal The value to insert + * @param {function(?,?)} cmpfunc The comparison func, that can + * compare two seekVals + * @return {Number} The insert position + */ +function binsearchInsert(list, seekVal, cmpfunc) { + if (!list.length) + return 0; + + let low = 0, high = list.length - 1, + mid, cmpval; + + while (low <= high) { + mid = low + Math.floor((high - low) / 2); + cmpval = cmpfunc(seekVal, list[mid]); + + if (cmpval < 0) + high = mid - 1; + else if (cmpval > 0) + low = mid + 1; + else + break; + } + + if (cmpval < 0) + return mid; // insertion is displacing, so use mid outright. + else if (cmpval > 0) + return mid + 1; + else + return mid; +} + +/** + * Clone the passed object or primitive. By default a shallow clone will be + * executed. + * + * @param {*} aSrc The thing to clone + * @param {Boolean=} aDeep If true, a deep clone will be performed + * @return {*} The copy of the thing + */ +function clone(aSrc, aDeep) { + if (!aSrc || typeof aSrc != "object") { + return aSrc; + } else if (aSrc instanceof Date) { + return new Date(aSrc.getTime()); + } else if ("clone" in aSrc) { + return aSrc.clone(); + } else if (Array.isArray(aSrc)) { + let arr = []; + for (let i = 0; i < aSrc.length; i++) { + arr.push(aDeep ? clone(aSrc[i], true) : aSrc[i]); + } + return arr; + } else { + let obj = {}; + for (let [name, value] of Object.entries(aSrc)) { + if (aDeep) { + obj[name] = clone(value, true); + } else { + obj[name] = value; + } + } + return obj; + } +} + +/** + * Performs iCalendar line folding. A line ending character is inserted and + * the next line begins with a whitespace. + * + * @example + * SUMMARY:This line will be fold + * ed right in the middle of a word. + * + * @param {String} aLine The line to fold + * @return {String} The folded line + */ +function foldline(aLine) { + let result = ""; + let line = aLine || "", pos = 0, line_length = 0; + //pos counts position in line for the UTF-16 presentation + //line_length counts the bytes for the UTF-8 presentation + while (line.length) { + let cp = line.codePointAt(pos); + if (cp < 128) ++line_length; + else if (cp < 2048) line_length += 2;//needs 2 UTF-8 bytes + else if (cp < 65536) line_length += 3; + else line_length += 4; //cp is less than 1114112 + if (line_length < ICALmodule.foldLength + 1) + pos += cp > 65535 ? 2 : 1; + else { + result += ICALmodule.newLineChar + " " + line.slice(0, Math.max(0, pos)); + line = line.slice(Math.max(0, pos)); + pos = line_length = 0; + } + } + return result.slice(ICALmodule.newLineChar.length + 1); +} + +/** + * Pads the given string or number with zeros so it will have at least two + * characters. + * + * @param {String|Number} data The string or number to pad + * @return {String} The number padded as a string + */ +function pad2(data) { + if (typeof(data) !== 'string') { + // handle fractions. + if (typeof(data) === 'number') { + data = parseInt(data); + } + data = String(data); + } + + let len = data.length; + + switch (len) { + case 0: + return '00'; + case 1: + return '0' + data; + default: + return data; + } +} + +/** + * Truncates the given number, correctly handling negative numbers. + * + * @param {Number} number The number to truncate + * @return {Number} The truncated number + */ +function trunc(number) { + return (number < 0 ? Math.ceil(number) : Math.floor(number)); +} + +/** + * Poor-man's cross-browser object extension. Doesn't support all the + * features, but enough for our usage. Note that the target's properties are + * not overwritten with the source properties. + * + * @example + * var child = ICAL.helpers.extend(parent, { + * "bar": 123 + * }); + * + * @param {Object} source The object to extend + * @param {Object} target The object to extend with + * @return {Object} Returns the target. + */ +function extend(source, target) { + for (let key in source) { + let descr = Object.getOwnPropertyDescriptor(source, key); + if (descr && !Object.getOwnPropertyDescriptor(target, key)) { + Object.defineProperty(target, key, descr); + } + } + return target; +} + +var helpers = /*#__PURE__*/Object.freeze({ + __proto__: null, + updateTimezones: updateTimezones, + isStrictlyNaN: isStrictlyNaN, + strictParseInt: strictParseInt, + formatClassType: formatClassType, + unescapedIndexOf: unescapedIndexOf, + binsearchInsert: binsearchInsert, + clone: clone, + foldline: foldline, + pad2: pad2, + trunc: trunc, + extend: extend +}); + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * This class represents the "utc-offset" value type, with various calculation and manipulation + * methods. + * + * @memberof ICAL + */ +class UtcOffset { + /** + * Creates a new {@link ICAL.UtcOffset} instance from the passed string. + * + * @param {String} aString The string to parse + * @return {Duration} The created utc-offset instance + */ + static fromString(aString) { + // -05:00 + let options = {}; + //TODO: support seconds per rfc5545 ? + options.factor = (aString[0] === '+') ? 1 : -1; + options.hours = strictParseInt(aString.slice(1, 3)); + options.minutes = strictParseInt(aString.slice(4, 6)); + + return new UtcOffset(options); + } + + /** + * Creates a new {@link ICAL.UtcOffset} instance from the passed seconds + * value. + * + * @param {Number} aSeconds The number of seconds to convert + */ + static fromSeconds(aSeconds) { + let instance = new UtcOffset(); + instance.fromSeconds(aSeconds); + return instance; + } + + /** + * Creates a new ICAL.UtcOffset instance. + * + * @param {Object} aData An object with members of the utc offset + * @param {Number=} aData.hours The hours for the utc offset + * @param {Number=} aData.minutes The minutes in the utc offset + * @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1 + */ + constructor(aData) { + this.fromData(aData); + } + + /** + * The hours in the utc-offset + * @type {Number} + */ + hours = 0; + + /** + * The minutes in the utc-offset + * @type {Number} + */ + minutes = 0; + + /** + * The sign of the utc offset, 1 for positive offset, -1 for negative + * offsets. + * @type {Number} + */ + factor = 1; + + /** + * The type name, to be used in the jCal object. + * @constant + * @type {String} + * @default "utc-offset" + */ + icaltype = "utc-offset"; + + /** + * Returns a clone of the utc offset object. + * + * @return {UtcOffset} The cloned object + */ + clone() { + return UtcOffset.fromSeconds(this.toSeconds()); + } + + /** + * Sets up the current instance using members from the passed data object. + * + * @param {Object} aData An object with members of the utc offset + * @param {Number=} aData.hours The hours for the utc offset + * @param {Number=} aData.minutes The minutes in the utc offset + * @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1 + */ + fromData(aData) { + if (aData) { + for (let [key, value] of Object.entries(aData)) { + this[key] = value; + } + } + this._normalize(); + } + + /** + * Sets up the current instance from the given seconds value. The seconds + * value is truncated to the minute. Offsets are wrapped when the world + * ends, the hour after UTC+14:00 is UTC-12:00. + * + * @param {Number} aSeconds The seconds to convert into an offset + */ + fromSeconds(aSeconds) { + let secs = Math.abs(aSeconds); + + this.factor = aSeconds < 0 ? -1 : 1; + this.hours = trunc(secs / 3600); + + secs -= (this.hours * 3600); + this.minutes = trunc(secs / 60); + return this; + } + + /** + * Convert the current offset to a value in seconds + * + * @return {Number} The offset in seconds + */ + toSeconds() { + return this.factor * (60 * this.minutes + 3600 * this.hours); + } + + /** + * Compare this utc offset with another one. + * + * @param {UtcOffset} other The other offset to compare with + * @return {Number} -1, 0 or 1 for less/equal/greater + */ + compare(other) { + let a = this.toSeconds(); + let b = other.toSeconds(); + return (a > b) - (b > a); + } + + _normalize() { + // Range: 97200 seconds (with 1 hour inbetween) + let secs = this.toSeconds(); + let factor = this.factor; + while (secs < -43200) { // = UTC-12:00 + secs += 97200; + } + while (secs > 50400) { // = UTC+14:00 + secs -= 97200; + } + + this.fromSeconds(secs); + + // Avoid changing the factor when on zero seconds + if (secs == 0) { + this.factor = factor; + } + } + + /** + * The iCalendar string representation of this utc-offset. + * @return {String} + */ + toICALString() { + return design$1.icalendar.value['utc-offset'].toICAL(this.toString()); + } + + /** + * The string representation of this utc-offset. + * @return {String} + */ + toString() { + return (this.factor == 1 ? "+" : "-") + pad2(this.hours) + ':' + pad2(this.minutes); + } +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * Describes a vCard time, which has slight differences to the ICAL.Time. + * Properties can be null if not specified, for example for dates with + * reduced accuracy or truncation. + * + * Note that currently not all methods are correctly re-implemented for + * VCardTime. For example, comparison will have undefined results when some + * members are null. + * + * Also, normalization is not yet implemented for this class! + * + * @memberof ICAL + * @extends {ICAL.Time} + */ +class VCardTime extends Time { + /** + * Returns a new ICAL.VCardTime instance from a date and/or time string. + * + * @param {String} aValue The string to create from + * @param {String} aIcalType The type for this instance, e.g. date-and-or-time + * @return {VCardTime} The date/time instance + */ + static fromDateAndOrTimeString(aValue, aIcalType) { + function part(v, s, e) { + return v ? strictParseInt(v.slice(s, s + e)) : null; + } + let parts = aValue.split('T'); + let dt = parts[0], tmz = parts[1]; + let splitzone = tmz ? design$1.vcard.value.time._splitZone(tmz) : []; + let zone = splitzone[0], tm = splitzone[1]; + + let dtlen = dt ? dt.length : 0; + let tmlen = tm ? tm.length : 0; + + let hasDashDate = dt && dt[0] == '-' && dt[1] == '-'; + let hasDashTime = tm && tm[0] == '-'; + + let o = { + year: hasDashDate ? null : part(dt, 0, 4), + month: hasDashDate && (dtlen == 4 || dtlen == 7) ? part(dt, 2, 2) : dtlen == 7 ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 5, 2) : null, + day: dtlen == 5 ? part(dt, 3, 2) : dtlen == 7 && hasDashDate ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 8, 2) : null, + + hour: hasDashTime ? null : part(tm, 0, 2), + minute: hasDashTime && tmlen == 3 ? part(tm, 1, 2) : tmlen > 4 ? hasDashTime ? part(tm, 1, 2) : part(tm, 3, 2) : null, + second: tmlen == 4 ? part(tm, 2, 2) : tmlen == 6 ? part(tm, 4, 2) : tmlen == 8 ? part(tm, 6, 2) : null + }; + + if (zone == 'Z') { + zone = Timezone.utcTimezone; + } else if (zone && zone[3] == ':') { + zone = UtcOffset.fromString(zone); + } else { + zone = null; + } + + return new VCardTime(o, zone, aIcalType); + } + + + /** + * Creates a new ICAL.VCardTime instance. + * + * @param {Object} data The data for the time instance + * @param {Number=} data.year The year for this date + * @param {Number=} data.month The month for this date + * @param {Number=} data.day The day for this date + * @param {Number=} data.hour The hour for this date + * @param {Number=} data.minute The minute for this date + * @param {Number=} data.second The second for this date + * @param {Timezone|UtcOffset} zone The timezone to use + * @param {String} icaltype The type for this date/time object + */ + constructor(data, zone, icaltype) { + super(data, zone); + this.icaltype = icaltype || "date-and-or-time"; + } + + /** + * The class identifier. + * @constant + * @type {String} + * @default "vcardtime" + */ + icalclass = "vcardtime"; + + /** + * The type name, to be used in the jCal object. + * @type {String} + * @default "date-and-or-time" + */ + icaltype = "date-and-or-time"; + + /** + * Returns a clone of the vcard date/time object. + * + * @return {VCardTime} The cloned object + */ + clone() { + return new VCardTime(this._time, this.zone, this.icaltype); + } + + _normalize() { + return this; + } + + /** + * @inheritdoc + */ + utcOffset() { + if (this.zone instanceof UtcOffset) { + return this.zone.toSeconds(); + } else { + return Time.prototype.utcOffset.apply(this, arguments); + } + } + + /** + * Returns an RFC 6350 compliant representation of this object. + * + * @return {String} vcard date/time string + */ + toICALString() { + return design$1.vcard.value[this.icaltype].toICAL(this.toString()); + } + + /** + * The string representation of this date/time, in jCard form + * (including : and - separators). + * @return {String} + */ + toString() { + let y = this.year, m = this.month, d = this.day; + let h = this.hour, mm = this.minute, s = this.second; + + let hasYear = y !== null, hasMonth = m !== null, hasDay = d !== null; + let hasHour = h !== null, hasMinute = mm !== null, hasSecond = s !== null; + + let datepart = (hasYear ? pad2(y) + (hasMonth || hasDay ? '-' : '') : (hasMonth || hasDay ? '--' : '')) + + (hasMonth ? pad2(m) : '') + + (hasDay ? '-' + pad2(d) : ''); + let timepart = (hasHour ? pad2(h) : '-') + (hasHour && hasMinute ? ':' : '') + + (hasMinute ? pad2(mm) : '') + (!hasHour && !hasMinute ? '-' : '') + + (hasMinute && hasSecond ? ':' : '') + + (hasSecond ? pad2(s) : ''); + + let zone; + if (this.zone === Timezone.utcTimezone) { + zone = 'Z'; + } else if (this.zone instanceof UtcOffset) { + zone = this.zone.toString(); + } else if (this.zone === Timezone.localTimezone) { + zone = ''; + } else if (this.zone instanceof Timezone) { + let offset = UtcOffset.fromSeconds(this.zone.utcOffset(this)); + zone = offset.toString(); + } else { + zone = ''; + } + + switch (this.icaltype) { + case "time": + return timepart + zone; + case "date-and-or-time": + case "date-time": + return datepart + (timepart == '--' ? '' : 'T' + timepart + zone); + case "date": + return datepart; + } + return null; + } +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * This lets typescript resolve our custom types in the + * generated d.ts files (jsdoc typedefs are converted to typescript types). + * Ignore prevents the typedefs from being documented more than once. + * + * @ignore + * @typedef {import("./types.js").weekDay} weekDay + * Imports the 'weekDay' type from the "types.js" module + */ + +/** + * An iterator for a single recurrence rule. This class usually doesn't have to be instanciated + * directly, the convenience method {@link ICAL.Recur#iterator} can be used. + * + * @memberof ICAL + */ +class RecurIterator { + static _indexMap = { + "BYSECOND": 0, + "BYMINUTE": 1, + "BYHOUR": 2, + "BYDAY": 3, + "BYMONTHDAY": 4, + "BYYEARDAY": 5, + "BYWEEKNO": 6, + "BYMONTH": 7, + "BYSETPOS": 8 + }; + + static _expandMap = { + "SECONDLY": [1, 1, 1, 1, 1, 1, 1, 1], + "MINUTELY": [2, 1, 1, 1, 1, 1, 1, 1], + "HOURLY": [2, 2, 1, 1, 1, 1, 1, 1], + "DAILY": [2, 2, 2, 1, 1, 1, 1, 1], + "WEEKLY": [2, 2, 2, 2, 3, 3, 1, 1], + "MONTHLY": [2, 2, 2, 2, 2, 3, 3, 1], + "YEARLY": [2, 2, 2, 2, 2, 2, 2, 2] + }; + + static UNKNOWN = 0; + static CONTRACT = 1; + static EXPAND = 2; + static ILLEGAL = 3; + + /** + * Creates a new ICAL.RecurIterator instance. The options object may contain additional members + * when resuming iteration from a previous run. + * + * @param {Object} options The iterator options + * @param {Recur} options.rule The rule to iterate. + * @param {Time} options.dtstart The start date of the event. + * @param {Boolean=} options.initialized When true, assume that options are + * from a previously constructed iterator. Initialization will not be + * repeated. + */ + constructor(options) { + this.fromData(options); + } + + /** + * True when iteration is finished. + * @type {Boolean} + */ + completed = false; + + /** + * The rule that is being iterated + * @type {Recur} + */ + rule = null; + + /** + * The start date of the event being iterated. + * @type {Time} + */ + dtstart = null; + + /** + * The last occurrence that was returned from the + * {@link RecurIterator#next} method. + * @type {Time} + */ + last = null; + + /** + * The sequence number from the occurrence + * @type {Number} + */ + occurrence_number = 0; + + /** + * The indices used for the {@link ICAL.RecurIterator#by_data} object. + * @type {Object} + * @private + */ + by_indices = null; + + /** + * If true, the iterator has already been initialized + * @type {Boolean} + * @private + */ + initialized = false; + + /** + * The initializd by-data. + * @type {Object} + * @private + */ + by_data = null; + + /** + * The expanded yeardays + * @type {Array} + * @private + */ + days = null; + + /** + * The index in the {@link ICAL.RecurIterator#days} array. + * @type {Number} + * @private + */ + days_index = 0; + + /** + * Initialize the recurrence iterator from the passed data object. This + * method is usually not called directly, you can initialize the iterator + * through the constructor. + * + * @param {Object} options The iterator options + * @param {Recur} options.rule The rule to iterate. + * @param {Time} options.dtstart The start date of the event. + * @param {Boolean=} options.initialized When true, assume that options are + * from a previously constructed iterator. Initialization will not be + * repeated. + */ + fromData(options) { + this.rule = formatClassType(options.rule, Recur); + + if (!this.rule) { + throw new Error('iterator requires a (ICAL.Recur) rule'); + } + + this.dtstart = formatClassType(options.dtstart, Time); + + if (!this.dtstart) { + throw new Error('iterator requires a (ICAL.Time) dtstart'); + } + + if (options.by_data) { + this.by_data = options.by_data; + } else { + this.by_data = clone(this.rule.parts, true); + } + + if (options.occurrence_number) + this.occurrence_number = options.occurrence_number; + + this.days = options.days || []; + if (options.last) { + this.last = formatClassType(options.last, Time); + } + + this.by_indices = options.by_indices; + + if (!this.by_indices) { + this.by_indices = { + "BYSECOND": 0, + "BYMINUTE": 0, + "BYHOUR": 0, + "BYDAY": 0, + "BYMONTH": 0, + "BYWEEKNO": 0, + "BYMONTHDAY": 0 + }; + } + + this.initialized = options.initialized || false; + + if (!this.initialized) { + try { + this.init(); + } catch (e) { + if (e instanceof InvalidRecurrenceRuleError) { + // Init may error if there are no possible recurrence instances from + // the rule, but we don't want to bubble this error up. Instead, we + // create an empty iterator. + this.completed = true; + } else { + // Propagate other errors to consumers. + throw e; + } + } + } + } + + /** + * Initialize the iterator + * @private + */ + init() { + this.initialized = true; + this.last = this.dtstart.clone(); + let parts = this.by_data; + + if ("BYDAY" in parts) { + // libical does this earlier when the rule is loaded, but we postpone to + // now so we can preserve the original order. + this.sort_byday_rules(parts.BYDAY); + } + + // If the BYYEARDAY appares, no other date rule part may appear + if ("BYYEARDAY" in parts) { + if ("BYMONTH" in parts || "BYWEEKNO" in parts || + "BYMONTHDAY" in parts || "BYDAY" in parts) { + throw new Error("Invalid BYYEARDAY rule"); + } + } + + // BYWEEKNO and BYMONTHDAY rule parts may not both appear + if ("BYWEEKNO" in parts && "BYMONTHDAY" in parts) { + throw new Error("BYWEEKNO does not fit to BYMONTHDAY"); + } + + // For MONTHLY recurrences (FREQ=MONTHLY) neither BYYEARDAY nor + // BYWEEKNO may appear. + if (this.rule.freq == "MONTHLY" && + ("BYYEARDAY" in parts || "BYWEEKNO" in parts)) { + throw new Error("For MONTHLY recurrences neither BYYEARDAY nor BYWEEKNO may appear"); + } + + // For WEEKLY recurrences (FREQ=WEEKLY) neither BYMONTHDAY nor + // BYYEARDAY may appear. + if (this.rule.freq == "WEEKLY" && + ("BYYEARDAY" in parts || "BYMONTHDAY" in parts)) { + throw new Error("For WEEKLY recurrences neither BYMONTHDAY nor BYYEARDAY may appear"); + } + + // BYYEARDAY may only appear in YEARLY rules + if (this.rule.freq != "YEARLY" && "BYYEARDAY" in parts) { + throw new Error("BYYEARDAY may only appear in YEARLY rules"); + } + + this.last.second = this.setup_defaults("BYSECOND", "SECONDLY", this.dtstart.second); + this.last.minute = this.setup_defaults("BYMINUTE", "MINUTELY", this.dtstart.minute); + this.last.hour = this.setup_defaults("BYHOUR", "HOURLY", this.dtstart.hour); + this.last.day = this.setup_defaults("BYMONTHDAY", "DAILY", this.dtstart.day); + this.last.month = this.setup_defaults("BYMONTH", "MONTHLY", this.dtstart.month); + + if (this.rule.freq == "WEEKLY") { + if ("BYDAY" in parts) { + let [, dow] = this.ruleDayOfWeek(parts.BYDAY[0], this.rule.wkst); + let wkdy = dow - this.last.dayOfWeek(this.rule.wkst); + if ((this.last.dayOfWeek(this.rule.wkst) < dow && wkdy >= 0) || wkdy < 0) { + // Initial time is after first day of BYDAY data + this.last.day += wkdy; + } + } else { + let dayName = Recur.numericDayToIcalDay(this.dtstart.dayOfWeek()); + parts.BYDAY = [dayName]; + } + } + + if (this.rule.freq == "YEARLY") { + // Some yearly recurrence rules may be specific enough to not actually + // occur on a yearly basis, e.g. the 29th day of February or the fifth + // Monday of a given month. The standard isn't clear on the intended + // behavior in these cases, but `libical` at least will iterate until it + // finds a matching year. + // CAREFUL: Some rules may specify an occurrence that can never happen, + // e.g. the first Monday of April so long as it falls on the 15th + // through the 21st. Detecting these is non-trivial, so ensure that we + // stop iterating at some point. + const untilYear = this.rule.until ? this.rule.until.year : 20000; + while (this.last.year <= untilYear) { + this.expand_year_days(this.last.year); + if (this.days.length > 0) { + break; + } + this.increment_year(this.rule.interval); + } + + if (this.days.length == 0) { + throw new InvalidRecurrenceRuleError(); + } + + this._nextByYearDay(); + } + + if (this.rule.freq == "MONTHLY") { + if (this.has_by_data("BYDAY")) { + let tempLast = null; + let initLast = this.last.clone(); + let daysInMonth = Time.daysInMonth(this.last.month, this.last.year); + + // Check every weekday in BYDAY with relative dow and pos. + for (let bydow of this.by_data.BYDAY) { + this.last = initLast.clone(); + let [pos, dow] = this.ruleDayOfWeek(bydow); + let dayOfMonth = this.last.nthWeekDay(dow, pos); + + // If |pos| >= 6, the byday is invalid for a monthly rule. + if (pos >= 6 || pos <= -6) { + throw new Error("Malformed values in BYDAY part"); + } + + // If a Byday with pos=+/-5 is not in the current month it + // must be searched in the next months. + if (dayOfMonth > daysInMonth || dayOfMonth <= 0) { + // Skip if we have already found a "last" in this month. + if (tempLast && tempLast.month == initLast.month) { + continue; + } + while (dayOfMonth > daysInMonth || dayOfMonth <= 0) { + this.increment_month(); + daysInMonth = Time.daysInMonth(this.last.month, this.last.year); + dayOfMonth = this.last.nthWeekDay(dow, pos); + } + } + + this.last.day = dayOfMonth; + if (!tempLast || this.last.compare(tempLast) < 0) { + tempLast = this.last.clone(); + } + } + this.last = tempLast.clone(); + + //XXX: This feels like a hack, but we need to initialize + // the BYMONTHDAY case correctly and byDayAndMonthDay handles + // this case. It accepts a special flag which will avoid incrementing + // the initial value without the flag days that match the start time + // would be missed. + if (this.has_by_data('BYMONTHDAY')) { + this._byDayAndMonthDay(true); + } + + if (this.last.day > daysInMonth || this.last.day == 0) { + throw new Error("Malformed values in BYDAY part"); + } + } else if (this.has_by_data("BYMONTHDAY")) { + // Change the day value so that normalisation won't change the month. + this.last.day = 1; + + // Get a sorted list of days in the starting month that match the rule. + let normalized = this.normalizeByMonthDayRules( + this.last.year, + this.last.month, + this.rule.parts.BYMONTHDAY + ).filter(d => d >= this.last.day); + + if (normalized.length) { + // There's at least one valid day, use it. + this.last.day = normalized[0]; + this.by_data.BYMONTHDAY = normalized; + } else { + // There's no occurrence in this month, find the next valid month. + // The longest possible sequence of skipped months is February-April-June, + // so we might need to call next_month up to three times. + if (!this.next_month() && !this.next_month() && !this.next_month()) { + throw new Error("No possible occurrences"); + } + } + } + } + } + + /** + * Retrieve the next occurrence from the iterator. + * @return {Time} + */ + next(again = false) { + let before = (this.last ? this.last.clone() : null); + + if ((this.rule.count && this.occurrence_number >= this.rule.count) || + (this.rule.until && this.last.compare(this.rule.until) > 0)) { + this.completed = true; + } + + if (this.completed) { + return null; + } + + if (this.occurrence_number == 0 && this.last.compare(this.dtstart) >= 0) { + // First of all, give the instance that was initialized + this.occurrence_number++; + return this.last; + } + + let valid; + do { + valid = 1; + + switch (this.rule.freq) { + case "SECONDLY": + this.next_second(); + break; + case "MINUTELY": + this.next_minute(); + break; + case "HOURLY": + this.next_hour(); + break; + case "DAILY": + this.next_day(); + break; + case "WEEKLY": + this.next_week(); + break; + case "MONTHLY": + valid = this.next_month(); + break; + case "YEARLY": + this.next_year(); + break; + + default: + return null; + } + } while (!this.check_contracting_rules() || + this.last.compare(this.dtstart) < 0 || + !valid); + + if (this.last.compare(before) == 0) { + if (again) { + throw new Error("Same occurrence found twice, protecting you from death by recursion"); + } + this.next(true); + } + + if (this.rule.until && this.last.compare(this.rule.until) > 0) { + this.completed = true; + return null; + } else { + this.occurrence_number++; + return this.last; + } + } + + next_second() { + return this.next_generic("BYSECOND", "SECONDLY", "second", "minute"); + } + + increment_second(inc) { + return this.increment_generic(inc, "second", 60, "minute"); + } + + next_minute() { + return this.next_generic("BYMINUTE", "MINUTELY", + "minute", "hour", "next_second"); + } + + increment_minute(inc) { + return this.increment_generic(inc, "minute", 60, "hour"); + } + + next_hour() { + return this.next_generic("BYHOUR", "HOURLY", "hour", + "monthday", "next_minute"); + } + + increment_hour(inc) { + this.increment_generic(inc, "hour", 24, "monthday"); + } + + next_day() { + let this_freq = (this.rule.freq == "DAILY"); + + if (this.next_hour() == 0) { + return 0; + } + + if (this_freq) { + this.increment_monthday(this.rule.interval); + } else { + this.increment_monthday(1); + } + + return 0; + } + + next_week() { + let end_of_data = 0; + + if (this.next_weekday_by_week() == 0) { + return end_of_data; + } + + if (this.has_by_data("BYWEEKNO")) { + this.by_indices.BYWEEKNO++; + + if (this.by_indices.BYWEEKNO == this.by_data.BYWEEKNO.length) { + this.by_indices.BYWEEKNO = 0; + end_of_data = 1; + } + + // HACK should be first month of the year + this.last.month = 1; + this.last.day = 1; + + let week_no = this.by_data.BYWEEKNO[this.by_indices.BYWEEKNO]; + + this.last.day += 7 * week_no; + + if (end_of_data) { + this.increment_year(1); + } + } else { + // Jump to the next week + this.increment_monthday(7 * this.rule.interval); + } + + return end_of_data; + } + + /** + * Normalize each by day rule for a given year/month. + * Takes into account ordering and negative rules + * + * @private + * @param {Number} year Current year. + * @param {Number} month Current month. + * @param {Array} rules Array of rules. + * + * @return {Array} sorted and normalized rules. + * Negative rules will be expanded to their + * correct positive values for easier processing. + */ + normalizeByMonthDayRules(year, month, rules) { + let daysInMonth = Time.daysInMonth(month, year); + + // XXX: This is probably bad for performance to allocate + // a new array for each month we scan, if possible + // we should try to optimize this... + let newRules = []; + + let ruleIdx = 0; + let len = rules.length; + let rule; + + for (; ruleIdx < len; ruleIdx++) { + rule = parseInt(rules[ruleIdx], 10); + if (isNaN(rule)) { + throw new Error('Invalid BYMONTHDAY value'); + } + + // if this rule falls outside of given + // month discard it. + if (Math.abs(rule) > daysInMonth) { + continue; + } + + // negative case + if (rule < 0) { + // we add (not subtract it is a negative number) + // one from the rule because 1 === last day of month + rule = daysInMonth + (rule + 1); + } else if (rule === 0) { + // skip zero: it is invalid. + continue; + } + + // only add unique items... + if (newRules.indexOf(rule) === -1) { + newRules.push(rule); + } + + } + + // unique and sort + return newRules.sort(function(a, b) { return a - b; }); + } + + /** + * NOTES: + * We are given a list of dates in the month (BYMONTHDAY) (23, etc..) + * Also we are given a list of days (BYDAY) (MO, 2SU, etc..) when + * both conditions match a given date (this.last.day) iteration stops. + * + * @private + * @param {Boolean=} isInit When given true will not increment the + * current day (this.last). + */ + _byDayAndMonthDay(isInit) { + let byMonthDay; // setup in initMonth + let byDay = this.by_data.BYDAY; + + let date; + let dateIdx = 0; + let dateLen; // setup in initMonth + let dayLen = byDay.length; + + // we are not valid by default + let dataIsValid = 0; + + let daysInMonth; + let self = this; + // we need a copy of this, because a DateTime gets normalized + // automatically if the day is out of range. At some points we + // set the last day to 0 to start counting. + let lastDay = this.last.day; + + function initMonth() { + daysInMonth = Time.daysInMonth( + self.last.month, self.last.year + ); + + byMonthDay = self.normalizeByMonthDayRules( + self.last.year, + self.last.month, + self.by_data.BYMONTHDAY + ); + + dateLen = byMonthDay.length; + + // For the case of more than one occurrence in one month + // we have to be sure to start searching after the last + // found date or at the last BYMONTHDAY, unless we are + // initializing the iterator because in this case we have + // to consider the last found date too. + while (byMonthDay[dateIdx] <= lastDay && + !(isInit && byMonthDay[dateIdx] == lastDay) && + dateIdx < dateLen - 1) { + dateIdx++; + } + } + + function nextMonth() { + // since the day is incremented at the start + // of the loop below, we need to start at 0 + lastDay = 0; + self.increment_month(); + dateIdx = 0; + initMonth(); + } + + initMonth(); + + // should come after initMonth + if (isInit) { + lastDay -= 1; + } + + // Use a counter to avoid an infinite loop with malformed rules. + // Stop checking after 4 years so we consider also a leap year. + let monthsCounter = 48; + + while (!dataIsValid && monthsCounter) { + monthsCounter--; + // increment the current date. This is really + // important otherwise we may fall into the infinite + // loop trap. The initial date takes care of the case + // where the current date is the date we are looking + // for. + date = lastDay + 1; + + if (date > daysInMonth) { + nextMonth(); + continue; + } + + // find next date + let next = byMonthDay[dateIdx++]; + + // this logic is dependent on the BYMONTHDAYS + // being in order (which is done by #normalizeByMonthDayRules) + if (next >= date) { + // if the next month day is in the future jump to it. + lastDay = next; + } else { + // in this case the 'next' monthday has past + // we must move to the month. + nextMonth(); + continue; + } + + // Now we can loop through the day rules to see + // if one matches the current month date. + for (let dayIdx = 0; dayIdx < dayLen; dayIdx++) { + let parts = this.ruleDayOfWeek(byDay[dayIdx]); + let pos = parts[0]; + let dow = parts[1]; + + this.last.day = lastDay; + if (this.last.isNthWeekDay(dow, pos)) { + // when we find the valid one we can mark + // the conditions as met and break the loop. + // (Because we have this condition above + // it will also break the parent loop). + dataIsValid = 1; + break; + } + } + + // It is completely possible that the combination + // cannot be matched in the current month. + // When we reach the end of possible combinations + // in the current month we iterate to the next one. + // since dateIdx is incremented right after getting + // "next", we don't need dateLen -1 here. + if (!dataIsValid && dateIdx === dateLen) { + nextMonth(); + continue; + } + } + + if (monthsCounter <= 0) { + // Checked 4 years without finding a Byday that matches + // a Bymonthday. Maybe the rule is not correct. + throw new Error("Malformed values in BYDAY combined with BYMONTHDAY parts"); + } + + + return dataIsValid; + } + + next_month() { + let data_valid = 1; + + if (this.next_hour() == 0) { + return data_valid; + } + + if (this.has_by_data("BYDAY") && this.has_by_data("BYMONTHDAY")) { + data_valid = this._byDayAndMonthDay(); + } else if (this.has_by_data("BYDAY")) { + let daysInMonth = Time.daysInMonth(this.last.month, this.last.year); + let setpos = 0; + let setpos_total = 0; + + if (this.has_by_data("BYSETPOS")) { + let last_day = this.last.day; + for (let day = 1; day <= daysInMonth; day++) { + this.last.day = day; + if (this.is_day_in_byday(this.last)) { + setpos_total++; + if (day <= last_day) { + setpos++; + } + } + } + this.last.day = last_day; + } + + data_valid = 0; + let day; + for (day = this.last.day + 1; day <= daysInMonth; day++) { + this.last.day = day; + + if (this.is_day_in_byday(this.last)) { + if (!this.has_by_data("BYSETPOS") || + this.check_set_position(++setpos) || + this.check_set_position(setpos - setpos_total - 1)) { + + data_valid = 1; + break; + } + } + } + + if (day > daysInMonth) { + this.last.day = 1; + this.increment_month(); + + if (this.is_day_in_byday(this.last)) { + if (!this.has_by_data("BYSETPOS") || this.check_set_position(1)) { + data_valid = 1; + } + } else { + data_valid = 0; + } + } + } else if (this.has_by_data("BYMONTHDAY")) { + this.by_indices.BYMONTHDAY++; + + if (this.by_indices.BYMONTHDAY >= this.by_data.BYMONTHDAY.length) { + this.by_indices.BYMONTHDAY = 0; + this.increment_month(); + if (this.by_indices.BYMONTHDAY >= this.by_data.BYMONTHDAY.length) { + return 0; + } + } + + let daysInMonth = Time.daysInMonth(this.last.month, this.last.year); + let day = this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY]; + + if (day < 0) { + day = daysInMonth + day + 1; + } + + if (day > daysInMonth) { + this.last.day = 1; + data_valid = this.is_day_in_byday(this.last); + } else { + this.last.day = day; + } + } else { + this.increment_month(); + let daysInMonth = Time.daysInMonth(this.last.month, this.last.year); + if (this.by_data.BYMONTHDAY[0] > daysInMonth) { + data_valid = 0; + } else { + this.last.day = this.by_data.BYMONTHDAY[0]; + } + } + + return data_valid; + } + + next_weekday_by_week() { + let end_of_data = 0; + + if (this.next_hour() == 0) { + return end_of_data; + } + + if (!this.has_by_data("BYDAY")) { + return 1; + } + + for (;;) { + let tt = new Time(); + this.by_indices.BYDAY++; + + if (this.by_indices.BYDAY == Object.keys(this.by_data.BYDAY).length) { + this.by_indices.BYDAY = 0; + end_of_data = 1; + } + + let coded_day = this.by_data.BYDAY[this.by_indices.BYDAY]; + let parts = this.ruleDayOfWeek(coded_day); + let dow = parts[1]; + + dow -= this.rule.wkst; + + if (dow < 0) { + dow += 7; + } + + tt.year = this.last.year; + tt.month = this.last.month; + tt.day = this.last.day; + + let startOfWeek = tt.startDoyWeek(this.rule.wkst); + + if (dow + startOfWeek < 1) { + // The selected date is in the previous year + if (!end_of_data) { + continue; + } + } + + let next = Time.fromDayOfYear(startOfWeek + dow, this.last.year); + + /** + * The normalization horrors below are due to + * the fact that when the year/month/day changes + * it can effect the other operations that come after. + */ + this.last.year = next.year; + this.last.month = next.month; + this.last.day = next.day; + + return end_of_data; + } + } + + next_year() { + if (this.next_hour() == 0) { + return 0; + } + + if (++this.days_index == this.days.length) { + this.days_index = 0; + do { + this.increment_year(this.rule.interval); + if (this.has_by_data("BYMONTHDAY")) { + this.by_data.BYMONTHDAY = this.normalizeByMonthDayRules( + this.last.year, + this.last.month, + this.rule.parts.BYMONTHDAY + ); + } + this.expand_year_days(this.last.year); + } while (this.days.length == 0); + } + + this._nextByYearDay(); + + return 1; + } + + _nextByYearDay() { + let doy = this.days[this.days_index]; + let year = this.last.year; + if (doy < 1) { + // Time.fromDayOfYear(doy, year) indexes relative to the + // start of the given year. That is different from the + // semantics of BYYEARDAY where negative indexes are an + // offset from the end of the given year. + doy += 1; + year += 1; + } + let next = Time.fromDayOfYear(doy, year); + this.last.day = next.day; + this.last.month = next.month; + } + + /** + * @param dow (eg: '1TU', '-1MO') + * @param {weekDay=} aWeekStart The week start weekday + * @return [pos, numericDow] (eg: [1, 3]) numericDow is relative to aWeekStart + */ + ruleDayOfWeek(dow, aWeekStart) { + let matches = dow.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/); + if (matches) { + let pos = parseInt(matches[1] || 0, 10); + dow = Recur.icalDayToNumericDay(matches[2], aWeekStart); + return [pos, dow]; + } else { + return [0, 0]; + } + } + + next_generic(aRuleType, aInterval, aDateAttr, aFollowingAttr, aPreviousIncr) { + let has_by_rule = (aRuleType in this.by_data); + let this_freq = (this.rule.freq == aInterval); + let end_of_data = 0; + + if (aPreviousIncr && this[aPreviousIncr]() == 0) { + return end_of_data; + } + + if (has_by_rule) { + this.by_indices[aRuleType]++; + let dta = this.by_data[aRuleType]; + + if (this.by_indices[aRuleType] == dta.length) { + this.by_indices[aRuleType] = 0; + end_of_data = 1; + } + this.last[aDateAttr] = dta[this.by_indices[aRuleType]]; + } else if (this_freq) { + this["increment_" + aDateAttr](this.rule.interval); + } + + if (has_by_rule && end_of_data && this_freq) { + this["increment_" + aFollowingAttr](1); + } + + return end_of_data; + } + + increment_monthday(inc) { + for (let i = 0; i < inc; i++) { + let daysInMonth = Time.daysInMonth(this.last.month, this.last.year); + this.last.day++; + + if (this.last.day > daysInMonth) { + this.last.day -= daysInMonth; + this.increment_month(); + } + } + } + + increment_month() { + this.last.day = 1; + if (this.has_by_data("BYMONTH")) { + this.by_indices.BYMONTH++; + + if (this.by_indices.BYMONTH == this.by_data.BYMONTH.length) { + this.by_indices.BYMONTH = 0; + this.increment_year(1); + } + + this.last.month = this.by_data.BYMONTH[this.by_indices.BYMONTH]; + } else { + if (this.rule.freq == "MONTHLY") { + this.last.month += this.rule.interval; + } else { + this.last.month++; + } + + this.last.month--; + let years = trunc(this.last.month / 12); + this.last.month %= 12; + this.last.month++; + + if (years != 0) { + this.increment_year(years); + } + } + + if (this.has_by_data("BYMONTHDAY")) { + this.by_data.BYMONTHDAY = this.normalizeByMonthDayRules( + this.last.year, + this.last.month, + this.rule.parts.BYMONTHDAY + ); + } + } + + increment_year(inc) { + // Don't jump into the next month if this.last is Feb 29. + this.last.day = 1; + this.last.year += inc; + } + + increment_generic(inc, aDateAttr, aFactor, aNextIncrement) { + this.last[aDateAttr] += inc; + let nextunit = trunc(this.last[aDateAttr] / aFactor); + this.last[aDateAttr] %= aFactor; + if (nextunit != 0) { + this["increment_" + aNextIncrement](nextunit); + } + } + + has_by_data(aRuleType) { + return (aRuleType in this.rule.parts); + } + + expand_year_days(aYear) { + let t = new Time(); + this.days = []; + + // We need our own copy with a few keys set + let parts = {}; + let rules = ["BYDAY", "BYWEEKNO", "BYMONTHDAY", "BYMONTH", "BYYEARDAY"]; + for (let part of rules) { + if (part in this.rule.parts) { + parts[part] = this.rule.parts[part]; + } + } + + if ("BYMONTH" in parts && "BYWEEKNO" in parts) { + let valid = 1; + let validWeeks = {}; + t.year = aYear; + t.isDate = true; + + for (let monthIdx = 0; monthIdx < this.by_data.BYMONTH.length; monthIdx++) { + let month = this.by_data.BYMONTH[monthIdx]; + t.month = month; + t.day = 1; + let first_week = t.weekNumber(this.rule.wkst); + t.day = Time.daysInMonth(month, aYear); + let last_week = t.weekNumber(this.rule.wkst); + for (monthIdx = first_week; monthIdx < last_week; monthIdx++) { + validWeeks[monthIdx] = 1; + } + } + + for (let weekIdx = 0; weekIdx < this.by_data.BYWEEKNO.length && valid; weekIdx++) { + let weekno = this.by_data.BYWEEKNO[weekIdx]; + if (weekno < 52) { + valid &= validWeeks[weekIdx]; + } else { + valid = 0; + } + } + + if (valid) { + delete parts.BYMONTH; + } else { + delete parts.BYWEEKNO; + } + } + + let partCount = Object.keys(parts).length; + + if (partCount == 0) { + let t1 = this.dtstart.clone(); + t1.year = this.last.year; + this.days.push(t1.dayOfYear()); + } else if (partCount == 1 && "BYMONTH" in parts) { + for (let month of this.by_data.BYMONTH) { + let t2 = this.dtstart.clone(); + t2.year = aYear; + t2.month = month; + t2.isDate = true; + this.days.push(t2.dayOfYear()); + } + } else if (partCount == 1 && "BYMONTHDAY" in parts) { + for (let monthday of this.by_data.BYMONTHDAY) { + let t3 = this.dtstart.clone(); + if (monthday < 0) { + let daysInMonth = Time.daysInMonth(t3.month, aYear); + monthday = monthday + daysInMonth + 1; + } + t3.day = monthday; + t3.year = aYear; + t3.isDate = true; + this.days.push(t3.dayOfYear()); + } + } else if (partCount == 2 && + "BYMONTHDAY" in parts && + "BYMONTH" in parts) { + for (let month of this.by_data.BYMONTH) { + let daysInMonth = Time.daysInMonth(month, aYear); + for (let monthday of this.by_data.BYMONTHDAY) { + if (monthday < 0) { + monthday = monthday + daysInMonth + 1; + } + t.day = monthday; + t.month = month; + t.year = aYear; + t.isDate = true; + + this.days.push(t.dayOfYear()); + } + } + } else if (partCount == 1 && "BYWEEKNO" in parts) ; else if (partCount == 2 && + "BYWEEKNO" in parts && + "BYMONTHDAY" in parts) ; else if (partCount == 1 && "BYDAY" in parts) { + this.days = this.days.concat(this.expand_by_day(aYear)); + } else if (partCount == 2 && "BYDAY" in parts && "BYMONTH" in parts) { + for (let month of this.by_data.BYMONTH) { + let daysInMonth = Time.daysInMonth(month, aYear); + + t.year = aYear; + t.month = month; + t.day = 1; + t.isDate = true; + + let first_dow = t.dayOfWeek(); + let doy_offset = t.dayOfYear() - 1; + + t.day = daysInMonth; + let last_dow = t.dayOfWeek(); + + if (this.has_by_data("BYSETPOS")) { + let by_month_day = []; + for (let day = 1; day <= daysInMonth; day++) { + t.day = day; + if (this.is_day_in_byday(t)) { + by_month_day.push(day); + } + } + + for (let spIndex = 0; spIndex < by_month_day.length; spIndex++) { + if (this.check_set_position(spIndex + 1) || + this.check_set_position(spIndex - by_month_day.length)) { + this.days.push(doy_offset + by_month_day[spIndex]); + } + } + } else { + for (let coded_day of this.by_data.BYDAY) { + let bydayParts = this.ruleDayOfWeek(coded_day); + let pos = bydayParts[0]; + let dow = bydayParts[1]; + let month_day; + + let first_matching_day = ((dow + 7 - first_dow) % 7) + 1; + let last_matching_day = daysInMonth - ((last_dow + 7 - dow) % 7); + + if (pos == 0) { + for (let day = first_matching_day; day <= daysInMonth; day += 7) { + this.days.push(doy_offset + day); + } + } else if (pos > 0) { + month_day = first_matching_day + (pos - 1) * 7; + + if (month_day <= daysInMonth) { + this.days.push(doy_offset + month_day); + } + } else { + month_day = last_matching_day + (pos + 1) * 7; + + if (month_day > 0) { + this.days.push(doy_offset + month_day); + } + } + } + } + } + // Return dates in order of occurrence (1,2,3,...) instead + // of by groups of weekdays (1,8,15,...,2,9,16,...). + this.days.sort(function(a, b) { return a - b; }); // Comparator function allows to sort numbers. + } else if (partCount == 2 && "BYDAY" in parts && "BYMONTHDAY" in parts) { + let expandedDays = this.expand_by_day(aYear); + + for (let day of expandedDays) { + let tt = Time.fromDayOfYear(day, aYear); + if (this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) { + this.days.push(day); + } + } + } else if (partCount == 3 && + "BYDAY" in parts && + "BYMONTHDAY" in parts && + "BYMONTH" in parts) { + let expandedDays = this.expand_by_day(aYear); + + for (let day of expandedDays) { + let tt = Time.fromDayOfYear(day, aYear); + + if (this.by_data.BYMONTH.indexOf(tt.month) >= 0 && + this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) { + this.days.push(day); + } + } + } else if (partCount == 2 && "BYDAY" in parts && "BYWEEKNO" in parts) { + let expandedDays = this.expand_by_day(aYear); + + for (let day of expandedDays) { + let tt = Time.fromDayOfYear(day, aYear); + let weekno = tt.weekNumber(this.rule.wkst); + + if (this.by_data.BYWEEKNO.indexOf(weekno)) { + this.days.push(day); + } + } + } else if (partCount == 3 && + "BYDAY" in parts && + "BYWEEKNO" in parts && + "BYMONTHDAY" in parts) ; else if (partCount == 1 && "BYYEARDAY" in parts) { + this.days = this.days.concat(this.by_data.BYYEARDAY); + } else { + this.days = []; + } + + let daysInYear = Time.isLeapYear(aYear) ? 366 : 365; + this.days.sort((a, b) => { + if (a < 0) a += daysInYear + 1; + if (b < 0) b += daysInYear + 1; + return a - b; + }); + + return 0; + } + + expand_by_day(aYear) { + + let days_list = []; + let tmp = this.last.clone(); + + tmp.year = aYear; + tmp.month = 1; + tmp.day = 1; + tmp.isDate = true; + + let start_dow = tmp.dayOfWeek(); + + tmp.month = 12; + tmp.day = 31; + tmp.isDate = true; + + let end_dow = tmp.dayOfWeek(); + let end_year_day = tmp.dayOfYear(); + + for (let day of this.by_data.BYDAY) { + let parts = this.ruleDayOfWeek(day); + let pos = parts[0]; + let dow = parts[1]; + + if (pos == 0) { + let tmp_start_doy = ((dow + 7 - start_dow) % 7) + 1; + + for (let doy = tmp_start_doy; doy <= end_year_day; doy += 7) { + days_list.push(doy); + } + + } else if (pos > 0) { + let first; + if (dow >= start_dow) { + first = dow - start_dow + 1; + } else { + first = dow - start_dow + 8; + } + + days_list.push(first + (pos - 1) * 7); + } else { + let last; + pos = -pos; + + if (dow <= end_dow) { + last = end_year_day - end_dow + dow; + } else { + last = end_year_day - end_dow + dow - 7; + } + + days_list.push(last - (pos - 1) * 7); + } + } + return days_list; + } + + is_day_in_byday(tt) { + if (this.by_data.BYDAY) { + for (let day of this.by_data.BYDAY) { + let parts = this.ruleDayOfWeek(day); + let pos = parts[0]; + let dow = parts[1]; + let this_dow = tt.dayOfWeek(); + + if ((pos == 0 && dow == this_dow) || + (tt.nthWeekDay(dow, pos) == tt.day)) { + return 1; + } + } + } + + return 0; + } + + /** + * Checks if given value is in BYSETPOS. + * + * @private + * @param {Numeric} aPos position to check for. + * @return {Boolean} false unless BYSETPOS rules exist + * and the given value is present in rules. + */ + check_set_position(aPos) { + if (this.has_by_data('BYSETPOS')) { + let idx = this.by_data.BYSETPOS.indexOf(aPos); + // negative numbers are not false-y + return idx !== -1; + } + return false; + } + + sort_byday_rules(aRules) { + for (let i = 0; i < aRules.length; i++) { + for (let j = 0; j < i; j++) { + let one = this.ruleDayOfWeek(aRules[j], this.rule.wkst)[1]; + let two = this.ruleDayOfWeek(aRules[i], this.rule.wkst)[1]; + + if (one > two) { + let tmp = aRules[i]; + aRules[i] = aRules[j]; + aRules[j] = tmp; + } + } + } + } + + check_contract_restriction(aRuleType, v) { + let indexMapValue = RecurIterator._indexMap[aRuleType]; + let ruleMapValue = RecurIterator._expandMap[this.rule.freq][indexMapValue]; + let pass = false; + + if (aRuleType in this.by_data && + ruleMapValue == RecurIterator.CONTRACT) { + + let ruleType = this.by_data[aRuleType]; + + for (let bydata of ruleType) { + if (bydata == v) { + pass = true; + break; + } + } + } else { + // Not a contracting byrule or has no data, test passes + pass = true; + } + return pass; + } + + check_contracting_rules() { + let dow = this.last.dayOfWeek(); + let weekNo = this.last.weekNumber(this.rule.wkst); + let doy = this.last.dayOfYear(); + + return (this.check_contract_restriction("BYSECOND", this.last.second) && + this.check_contract_restriction("BYMINUTE", this.last.minute) && + this.check_contract_restriction("BYHOUR", this.last.hour) && + this.check_contract_restriction("BYDAY", Recur.numericDayToIcalDay(dow)) && + this.check_contract_restriction("BYWEEKNO", weekNo) && + this.check_contract_restriction("BYMONTHDAY", this.last.day) && + this.check_contract_restriction("BYMONTH", this.last.month) && + this.check_contract_restriction("BYYEARDAY", doy)); + } + + setup_defaults(aRuleType, req, deftime) { + let indexMapValue = RecurIterator._indexMap[aRuleType]; + let ruleMapValue = RecurIterator._expandMap[this.rule.freq][indexMapValue]; + + if (ruleMapValue != RecurIterator.CONTRACT) { + if (!(aRuleType in this.by_data)) { + this.by_data[aRuleType] = [deftime]; + } + if (this.rule.freq != req) { + return this.by_data[aRuleType][0]; + } + } + return deftime; + } + + /** + * Convert iterator into a serialize-able object. Will preserve current + * iteration sequence to ensure the seamless continuation of the recurrence + * rule. + * @return {Object} + */ + toJSON() { + let result = Object.create(null); + + result.initialized = this.initialized; + result.rule = this.rule.toJSON(); + result.dtstart = this.dtstart.toJSON(); + result.by_data = this.by_data; + result.days = this.days; + result.last = this.last.toJSON(); + result.by_indices = this.by_indices; + result.occurrence_number = this.occurrence_number; + + return result; + } +} + +/** + * An error indicating that a recurrence rule is invalid and produces no + * occurrences. + * + * @extends {Error} + * @class + */ +class InvalidRecurrenceRuleError extends Error { + constructor() { + super("Recurrence rule has no valid occurrences"); + } +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * This lets typescript resolve our custom types in the + * generated d.ts files (jsdoc typedefs are converted to typescript types). + * Ignore prevents the typedefs from being documented more than once. + * + * @ignore + * @typedef {import("./types.js").weekDay} weekDay + * Imports the 'weekDay' type from the "types.js" module + * @typedef {import("./types.js").frequencyValues} frequencyValues + * Imports the 'frequencyValues' type from the "types.js" module + */ + +const VALID_DAY_NAMES = /^(SU|MO|TU|WE|TH|FR|SA)$/; +const VALID_BYDAY_PART = /^([+-])?(5[0-3]|[1-4][0-9]|[1-9])?(SU|MO|TU|WE|TH|FR|SA)$/; +const DOW_MAP = { + SU: Time.SUNDAY, + MO: Time.MONDAY, + TU: Time.TUESDAY, + WE: Time.WEDNESDAY, + TH: Time.THURSDAY, + FR: Time.FRIDAY, + SA: Time.SATURDAY +}; + +const REVERSE_DOW_MAP = Object.fromEntries(Object.entries(DOW_MAP).map(entry => entry.reverse())); + +const ALLOWED_FREQ = ['SECONDLY', 'MINUTELY', 'HOURLY', + 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY']; + +/** + * This class represents the "recur" value type, used for example by RRULE. It provides methods to + * calculate occurrences among others. + * + * @memberof ICAL + */ +class Recur { + /** + * Creates a new {@link ICAL.Recur} instance from the passed string. + * + * @param {String} string The string to parse + * @return {Recur} The created recurrence instance + */ + static fromString(string) { + let data = this._stringToData(string, false); + return new Recur(data); + } + + /** + * Creates a new {@link ICAL.Recur} instance using members from the passed + * data object. + * + * @param {Object} aData An object with members of the recurrence + * @param {frequencyValues=} aData.freq The frequency value + * @param {Number=} aData.interval The INTERVAL value + * @param {weekDay=} aData.wkst The week start value + * @param {Time=} aData.until The end of the recurrence set + * @param {Number=} aData.count The number of occurrences + * @param {Array.<Number>=} aData.bysecond The seconds for the BYSECOND part + * @param {Array.<Number>=} aData.byminute The minutes for the BYMINUTE part + * @param {Array.<Number>=} aData.byhour The hours for the BYHOUR part + * @param {Array.<String>=} aData.byday The BYDAY values + * @param {Array.<Number>=} aData.bymonthday The days for the BYMONTHDAY part + * @param {Array.<Number>=} aData.byyearday The days for the BYYEARDAY part + * @param {Array.<Number>=} aData.byweekno The weeks for the BYWEEKNO part + * @param {Array.<Number>=} aData.bymonth The month for the BYMONTH part + * @param {Array.<Number>=} aData.bysetpos The positionals for the BYSETPOS part + */ + static fromData(aData) { + return new Recur(aData); + } + + /** + * Converts a recurrence string to a data object, suitable for the fromData + * method. + * + * @private + * @param {String} string The string to parse + * @param {Boolean} fmtIcal If true, the string is considered to be an + * iCalendar string + * @return {Recur} The recurrence instance + */ + static _stringToData(string, fmtIcal) { + let dict = Object.create(null); + + // split is slower in FF but fast enough. + // v8 however this is faster then manual split? + let values = string.split(';'); + let len = values.length; + + for (let i = 0; i < len; i++) { + let parts = values[i].split('='); + let ucname = parts[0].toUpperCase(); + let lcname = parts[0].toLowerCase(); + let name = (fmtIcal ? lcname : ucname); + let value = parts[1]; + + if (ucname in partDesign) { + let partArr = value.split(','); + let partSet = new Set(); + + for (let part of partArr) { + partSet.add(partDesign[ucname](part)); + } + partArr = [...partSet]; + + dict[name] = (partArr.length == 1 ? partArr[0] : partArr); + } else if (ucname in optionDesign) { + optionDesign[ucname](value, dict, fmtIcal); + } else { + // Don't swallow unknown values. Just set them as they are. + dict[lcname] = value; + } + } + + return dict; + } + + /** + * Convert an ical representation of a day (SU, MO, etc..) + * into a numeric value of that day. + * + * @param {String} string The iCalendar day name + * @param {weekDay=} aWeekStart + * The week start weekday, defaults to SUNDAY + * @return {Number} Numeric value of given day + */ + static icalDayToNumericDay(string, aWeekStart) { + //XXX: this is here so we can deal + // with possibly invalid string values. + let firstDow = aWeekStart || Time.SUNDAY; + return ((DOW_MAP[string] - firstDow + 7) % 7) + 1; + } + + /** + * Convert a numeric day value into its ical representation (SU, MO, etc..) + * + * @param {Number} num Numeric value of given day + * @param {weekDay=} aWeekStart + * The week start weekday, defaults to SUNDAY + * @return {String} The ICAL day value, e.g SU,MO,... + */ + static numericDayToIcalDay(num, aWeekStart) { + //XXX: this is here so we can deal with possibly invalid number values. + // Also, this allows consistent mapping between day numbers and day + // names for external users. + let firstDow = aWeekStart || Time.SUNDAY; + let dow = (num + firstDow - Time.SUNDAY); + if (dow > 7) { + dow -= 7; + } + return REVERSE_DOW_MAP[dow]; + } + + /** + * Create a new instance of the Recur class. + * + * @param {Object} data An object with members of the recurrence + * @param {frequencyValues=} data.freq The frequency value + * @param {Number=} data.interval The INTERVAL value + * @param {weekDay=} data.wkst The week start value + * @param {Time=} data.until The end of the recurrence set + * @param {Number=} data.count The number of occurrences + * @param {Array.<Number>=} data.bysecond The seconds for the BYSECOND part + * @param {Array.<Number>=} data.byminute The minutes for the BYMINUTE part + * @param {Array.<Number>=} data.byhour The hours for the BYHOUR part + * @param {Array.<String>=} data.byday The BYDAY values + * @param {Array.<Number>=} data.bymonthday The days for the BYMONTHDAY part + * @param {Array.<Number>=} data.byyearday The days for the BYYEARDAY part + * @param {Array.<Number>=} data.byweekno The weeks for the BYWEEKNO part + * @param {Array.<Number>=} data.bymonth The month for the BYMONTH part + * @param {Array.<Number>=} data.bysetpos The positionals for the BYSETPOS part + */ + constructor(data) { + this.wrappedJSObject = this; + this.parts = {}; + + if (data && typeof(data) === 'object') { + this.fromData(data); + } + } + + /** + * An object holding the BY-parts of the recurrence rule + * @memberof ICAL.Recur + * @typedef {Object} byParts + * @property {Array.<Number>=} BYSECOND The seconds for the BYSECOND part + * @property {Array.<Number>=} BYMINUTE The minutes for the BYMINUTE part + * @property {Array.<Number>=} BYHOUR The hours for the BYHOUR part + * @property {Array.<String>=} BYDAY The BYDAY values + * @property {Array.<Number>=} BYMONTHDAY The days for the BYMONTHDAY part + * @property {Array.<Number>=} BYYEARDAY The days for the BYYEARDAY part + * @property {Array.<Number>=} BYWEEKNO The weeks for the BYWEEKNO part + * @property {Array.<Number>=} BYMONTH The month for the BYMONTH part + * @property {Array.<Number>=} BYSETPOS The positionals for the BYSETPOS part + */ + + /** + * An object holding the BY-parts of the recurrence rule + * @type {byParts} + */ + parts = null; + + /** + * The interval value for the recurrence rule. + * @type {Number} + */ + interval = 1; + + /** + * The week start day + * + * @type {weekDay} + * @default ICAL.Time.MONDAY + */ + wkst = Time.MONDAY; + + /** + * The end of the recurrence + * @type {?Time} + */ + until = null; + + /** + * The maximum number of occurrences + * @type {?Number} + */ + count = null; + + /** + * The frequency value. + * @type {frequencyValues} + */ + freq = null; + + /** + * The class identifier. + * @constant + * @type {String} + * @default "icalrecur" + */ + icalclass = "icalrecur"; + + /** + * The type name, to be used in the jCal object. + * @constant + * @type {String} + * @default "recur" + */ + icaltype = "recur"; + + /** + * Create a new iterator for this recurrence rule. The passed start date + * must be the start date of the event, not the start of the range to + * search in. + * + * @example + * let recur = comp.getFirstPropertyValue('rrule'); + * let dtstart = comp.getFirstPropertyValue('dtstart'); + * let iter = recur.iterator(dtstart); + * for (let next = iter.next(); next; next = iter.next()) { + * if (next.compare(rangeStart) < 0) { + * continue; + * } + * console.log(next.toString()); + * } + * + * @param {Time} aStart The item's start date + * @return {RecurIterator} The recurrence iterator + */ + iterator(aStart) { + return new RecurIterator({ + rule: this, + dtstart: aStart + }); + } + + /** + * Returns a clone of the recurrence object. + * + * @return {Recur} The cloned object + */ + clone() { + return new Recur(this.toJSON()); + } + + /** + * Checks if the current rule is finite, i.e. has a count or until part. + * + * @return {Boolean} True, if the rule is finite + */ + isFinite() { + return !!(this.count || this.until); + } + + /** + * Checks if the current rule has a count part, and not limited by an until + * part. + * + * @return {Boolean} True, if the rule is by count + */ + isByCount() { + return !!(this.count && !this.until); + } + + /** + * Adds a component (part) to the recurrence rule. This is not a component + * in the sense of {@link ICAL.Component}, but a part of the recurrence + * rule, i.e. BYMONTH. + * + * @param {String} aType The name of the component part + * @param {Array|String} aValue The component value + */ + addComponent(aType, aValue) { + let ucname = aType.toUpperCase(); + if (ucname in this.parts) { + this.parts[ucname].push(aValue); + } else { + this.parts[ucname] = [aValue]; + } + } + + /** + * Sets the component value for the given by-part. + * + * @param {String} aType The component part name + * @param {Array} aValues The component values + */ + setComponent(aType, aValues) { + this.parts[aType.toUpperCase()] = aValues.slice(); + } + + /** + * Gets (a copy) of the requested component value. + * + * @param {String} aType The component part name + * @return {Array} The component part value + */ + getComponent(aType) { + let ucname = aType.toUpperCase(); + return (ucname in this.parts ? this.parts[ucname].slice() : []); + } + + /** + * Retrieves the next occurrence after the given recurrence id. See the + * guide on {@tutorial terminology} for more details. + * + * NOTE: Currently, this method iterates all occurrences from the start + * date. It should not be called in a loop for performance reasons. If you + * would like to get more than one occurrence, you can iterate the + * occurrences manually, see the example on the + * {@link ICAL.Recur#iterator iterator} method. + * + * @param {Time} aStartTime The start of the event series + * @param {Time} aRecurrenceId The date of the last occurrence + * @return {Time} The next occurrence after + */ + getNextOccurrence(aStartTime, aRecurrenceId) { + let iter = this.iterator(aStartTime); + let next; + + do { + next = iter.next(); + } while (next && next.compare(aRecurrenceId) <= 0); + + if (next && aRecurrenceId.zone) { + next.zone = aRecurrenceId.zone; + } + + return next; + } + + /** + * Sets up the current instance using members from the passed data object. + * + * @param {Object} data An object with members of the recurrence + * @param {frequencyValues=} data.freq The frequency value + * @param {Number=} data.interval The INTERVAL value + * @param {weekDay=} data.wkst The week start value + * @param {Time=} data.until The end of the recurrence set + * @param {Number=} data.count The number of occurrences + * @param {Array.<Number>=} data.bysecond The seconds for the BYSECOND part + * @param {Array.<Number>=} data.byminute The minutes for the BYMINUTE part + * @param {Array.<Number>=} data.byhour The hours for the BYHOUR part + * @param {Array.<String>=} data.byday The BYDAY values + * @param {Array.<Number>=} data.bymonthday The days for the BYMONTHDAY part + * @param {Array.<Number>=} data.byyearday The days for the BYYEARDAY part + * @param {Array.<Number>=} data.byweekno The weeks for the BYWEEKNO part + * @param {Array.<Number>=} data.bymonth The month for the BYMONTH part + * @param {Array.<Number>=} data.bysetpos The positionals for the BYSETPOS part + */ + fromData(data) { + for (let key in data) { + let uckey = key.toUpperCase(); + + if (uckey in partDesign) { + if (Array.isArray(data[key])) { + this.parts[uckey] = data[key]; + } else { + this.parts[uckey] = [data[key]]; + } + } else { + this[key] = data[key]; + } + } + + if (this.interval && typeof this.interval != "number") { + optionDesign.INTERVAL(this.interval, this); + } + + if (this.wkst && typeof this.wkst != "number") { + this.wkst = Recur.icalDayToNumericDay(this.wkst); + } + + if (this.until && !(this.until instanceof Time)) { + this.until = Time.fromString(this.until); + } + } + + /** + * The jCal representation of this recurrence type. + * @return {Object} + */ + toJSON() { + let res = Object.create(null); + res.freq = this.freq; + + if (this.count) { + res.count = this.count; + } + + if (this.interval > 1) { + res.interval = this.interval; + } + + for (let [k, kparts] of Object.entries(this.parts)) { + if (Array.isArray(kparts) && kparts.length == 1) { + res[k.toLowerCase()] = kparts[0]; + } else { + res[k.toLowerCase()] = clone(kparts); + } + } + + if (this.until) { + res.until = this.until.toString(); + } + if ('wkst' in this && this.wkst !== Time.DEFAULT_WEEK_START) { + res.wkst = Recur.numericDayToIcalDay(this.wkst); + } + return res; + } + + /** + * The string representation of this recurrence rule. + * @return {String} + */ + toString() { + // TODO retain order + let str = "FREQ=" + this.freq; + if (this.count) { + str += ";COUNT=" + this.count; + } + if (this.interval > 1) { + str += ";INTERVAL=" + this.interval; + } + for (let [k, v] of Object.entries(this.parts)) { + str += ";" + k + "=" + v; + } + if (this.until) { + str += ';UNTIL=' + this.until.toICALString(); + } + if ('wkst' in this && this.wkst !== Time.DEFAULT_WEEK_START) { + str += ';WKST=' + Recur.numericDayToIcalDay(this.wkst); + } + return str; + } +} + +function parseNumericValue(type, min, max, value) { + let result = value; + + if (value[0] === '+') { + result = value.slice(1); + } + + result = strictParseInt(result); + + if (min !== undefined && value < min) { + throw new Error( + type + ': invalid value "' + value + '" must be > ' + min + ); + } + + if (max !== undefined && value > max) { + throw new Error( + type + ': invalid value "' + value + '" must be < ' + min + ); + } + + return result; +} + +const optionDesign = { + FREQ: function(value, dict, fmtIcal) { + // yes this is actually equal or faster then regex. + // upside here is we can enumerate the valid values. + if (ALLOWED_FREQ.indexOf(value) !== -1) { + dict.freq = value; + } else { + throw new Error( + 'invalid frequency "' + value + '" expected: "' + + ALLOWED_FREQ.join(', ') + '"' + ); + } + }, + + COUNT: function(value, dict, fmtIcal) { + dict.count = strictParseInt(value); + }, + + INTERVAL: function(value, dict, fmtIcal) { + dict.interval = strictParseInt(value); + if (dict.interval < 1) { + // 0 or negative values are not allowed, some engines seem to generate + // it though. Assume 1 instead. + dict.interval = 1; + } + }, + + UNTIL: function(value, dict, fmtIcal) { + if (value.length > 10) { + dict.until = design$1.icalendar.value['date-time'].fromICAL(value); + } else { + dict.until = design$1.icalendar.value.date.fromICAL(value); + } + if (!fmtIcal) { + dict.until = Time.fromString(dict.until); + } + }, + + WKST: function(value, dict, fmtIcal) { + if (VALID_DAY_NAMES.test(value)) { + dict.wkst = Recur.icalDayToNumericDay(value); + } else { + throw new Error('invalid WKST value "' + value + '"'); + } + } +}; + +const partDesign = { + BYSECOND: parseNumericValue.bind(undefined, 'BYSECOND', 0, 60), + BYMINUTE: parseNumericValue.bind(undefined, 'BYMINUTE', 0, 59), + BYHOUR: parseNumericValue.bind(undefined, 'BYHOUR', 0, 23), + BYDAY: function(value) { + if (VALID_BYDAY_PART.test(value)) { + return value; + } else { + throw new Error('invalid BYDAY value "' + value + '"'); + } + }, + BYMONTHDAY: parseNumericValue.bind(undefined, 'BYMONTHDAY', -31, 31), + BYYEARDAY: parseNumericValue.bind(undefined, 'BYYEARDAY', -366, 366), + BYWEEKNO: parseNumericValue.bind(undefined, 'BYWEEKNO', -53, 53), + BYMONTH: parseNumericValue.bind(undefined, 'BYMONTH', 1, 12), + BYSETPOS: parseNumericValue.bind(undefined, 'BYSETPOS', -366, 366) +}; + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * This lets typescript resolve our custom types in the + * generated d.ts files (jsdoc typedefs are converted to typescript types). + * Ignore prevents the typedefs from being documented more than once. + * @ignore + * @typedef {import("./types.js").jCalComponent} jCalComponent + * Imports the 'occurrenceDetails' type from the "types.js" module + */ + +/** + * This class represents the "period" value type, with various calculation and manipulation methods. + * + * @memberof ICAL + */ +class Period { + /** + * Creates a new {@link ICAL.Period} instance from the passed string. + * + * @param {String} str The string to parse + * @param {Property} prop The property this period will be on + * @return {Period} The created period instance + */ + static fromString(str, prop) { + let parts = str.split('/'); + + if (parts.length !== 2) { + throw new Error( + 'Invalid string value: "' + str + '" must contain a "/" char.' + ); + } + + let options = { + start: Time.fromDateTimeString(parts[0], prop) + }; + + let end = parts[1]; + + if (Duration.isValueString(end)) { + options.duration = Duration.fromString(end); + } else { + options.end = Time.fromDateTimeString(end, prop); + } + + return new Period(options); + } + + /** + * Creates a new {@link ICAL.Period} instance from the given data object. + * The passed data object cannot contain both and end date and a duration. + * + * @param {Object} aData An object with members of the period + * @param {Time=} aData.start The start of the period + * @param {Time=} aData.end The end of the period + * @param {Duration=} aData.duration The duration of the period + * @return {Period} The period instance + */ + static fromData(aData) { + return new Period(aData); + } + + /** + * Returns a new period instance from the given jCal data array. The first + * member is always the start date string, the second member is either a + * duration or end date string. + * + * @param {jCalComponent} aData The jCal data array + * @param {Property} aProp The property this jCal data is on + * @param {Boolean} aLenient If true, data value can be both date and date-time + * @return {Period} The period instance + */ + static fromJSON(aData, aProp, aLenient) { + function fromDateOrDateTimeString(aValue, dateProp) { + if (aLenient) { + return Time.fromString(aValue, dateProp); + } else { + return Time.fromDateTimeString(aValue, dateProp); + } + } + + if (Duration.isValueString(aData[1])) { + return Period.fromData({ + start: fromDateOrDateTimeString(aData[0], aProp), + duration: Duration.fromString(aData[1]) + }); + } else { + return Period.fromData({ + start: fromDateOrDateTimeString(aData[0], aProp), + end: fromDateOrDateTimeString(aData[1], aProp) + }); + } + } + + /** + * Creates a new ICAL.Period instance. The passed data object cannot contain both and end date and + * a duration. + * + * @param {Object} aData An object with members of the period + * @param {Time=} aData.start The start of the period + * @param {Time=} aData.end The end of the period + * @param {Duration=} aData.duration The duration of the period + */ + constructor(aData) { + this.wrappedJSObject = this; + + if (aData && 'start' in aData) { + if (aData.start && !(aData.start instanceof Time)) { + throw new TypeError('.start must be an instance of ICAL.Time'); + } + this.start = aData.start; + } + + if (aData && aData.end && aData.duration) { + throw new Error('cannot accept both end and duration'); + } + + if (aData && 'end' in aData) { + if (aData.end && !(aData.end instanceof Time)) { + throw new TypeError('.end must be an instance of ICAL.Time'); + } + this.end = aData.end; + } + + if (aData && 'duration' in aData) { + if (aData.duration && !(aData.duration instanceof Duration)) { + throw new TypeError('.duration must be an instance of ICAL.Duration'); + } + this.duration = aData.duration; + } + } + + + /** + * The start of the period + * @type {Time} + */ + start = null; + + /** + * The end of the period + * @type {Time} + */ + end = null; + + /** + * The duration of the period + * @type {Duration} + */ + duration = null; + + /** + * The class identifier. + * @constant + * @type {String} + * @default "icalperiod" + */ + icalclass = "icalperiod"; + + /** + * The type name, to be used in the jCal object. + * @constant + * @type {String} + * @default "period" + */ + icaltype = "period"; + + /** + * Returns a clone of the duration object. + * + * @return {Period} The cloned object + */ + clone() { + return Period.fromData({ + start: this.start ? this.start.clone() : null, + end: this.end ? this.end.clone() : null, + duration: this.duration ? this.duration.clone() : null + }); + } + + /** + * Calculates the duration of the period, either directly or by subtracting + * start from end date. + * + * @return {Duration} The calculated duration + */ + getDuration() { + if (this.duration) { + return this.duration; + } else { + return this.end.subtractDate(this.start); + } + } + + /** + * Calculates the end date of the period, either directly or by adding + * duration to start date. + * + * @return {Time} The calculated end date + */ + getEnd() { + if (this.end) { + return this.end; + } else { + let end = this.start.clone(); + end.addDuration(this.duration); + return end; + } + } + + /** + * The string representation of this period. + * @return {String} + */ + toString() { + return this.start + "/" + (this.end || this.duration); + } + + /** + * The jCal representation of this period type. + * @return {Object} + */ + toJSON() { + return [this.start.toString(), (this.end || this.duration).toString()]; + } + + /** + * The iCalendar string representation of this period. + * @return {String} + */ + toICALString() { + return this.start.toICALString() + "/" + + (this.end || this.duration).toICALString(); + } +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * This lets typescript resolve our custom types in the + * generated d.ts files (jsdoc typedefs are converted to typescript types). + * Ignore prevents the typedefs from being documented more than once. + * @ignore + * @typedef {import("./types.js").designSet} designSet + * Imports the 'designSet' type from the "types.js" module + */ + +/** @module ICAL.design */ + +const FROM_ICAL_NEWLINE = /\\\\|\\;|\\,|\\[Nn]/g; +const TO_ICAL_NEWLINE = /\\|;|,|\n/g; +const FROM_VCARD_NEWLINE = /\\\\|\\,|\\[Nn]/g; +const TO_VCARD_NEWLINE = /\\|,|\n/g; + +function createTextType(fromNewline, toNewline) { + let result = { + matches: /.*/, + + fromICAL: function(aValue, structuredEscape) { + return replaceNewline(aValue, fromNewline, structuredEscape); + }, + + toICAL: function(aValue, structuredEscape) { + let regEx = toNewline; + if (structuredEscape) + regEx = new RegExp(regEx.source + '|' + structuredEscape, regEx.flags); + return aValue.replace(regEx, function(str) { + switch (str) { + case "\\": + return "\\\\"; + case ";": + return "\\;"; + case ",": + return "\\,"; + case "\n": + return "\\n"; + /* c8 ignore next 2 */ + default: + return str; + } + }); + } + }; + return result; +} + +// default types used multiple times +const DEFAULT_TYPE_TEXT = { defaultType: "text" }; +const DEFAULT_TYPE_TEXT_MULTI = { defaultType: "text", multiValue: "," }; +const DEFAULT_TYPE_TEXT_STRUCTURED = { defaultType: "text", structuredValue: ";" }; +const DEFAULT_TYPE_INTEGER = { defaultType: "integer" }; +const DEFAULT_TYPE_DATETIME_DATE = { defaultType: "date-time", allowedTypes: ["date-time", "date"] }; +const DEFAULT_TYPE_DATETIME = { defaultType: "date-time" }; +const DEFAULT_TYPE_URI = { defaultType: "uri" }; +const DEFAULT_TYPE_UTCOFFSET = { defaultType: "utc-offset" }; +const DEFAULT_TYPE_RECUR = { defaultType: "recur" }; +const DEFAULT_TYPE_DATE_ANDOR_TIME = { defaultType: "date-and-or-time", allowedTypes: ["date-time", "date", "text"] }; + +function replaceNewlineReplace(string) { + switch (string) { + case "\\\\": + return "\\"; + case "\\;": + return ";"; + case "\\,": + return ","; + case "\\n": + case "\\N": + return "\n"; + /* c8 ignore next 2 */ + default: + return string; + } +} + +function replaceNewline(value, newline, structuredEscape) { + // avoid regex when possible. + if (value.indexOf('\\') === -1) { + return value; + } + if (structuredEscape) + newline = new RegExp(newline.source + '|\\\\' + structuredEscape, newline.flags); + return value.replace(newline, replaceNewlineReplace); +} + +let commonProperties = { + "categories": DEFAULT_TYPE_TEXT_MULTI, + "url": DEFAULT_TYPE_URI, + "version": DEFAULT_TYPE_TEXT, + "uid": DEFAULT_TYPE_TEXT +}; + +let commonValues = { + "boolean": { + values: ["TRUE", "FALSE"], + + fromICAL: function(aValue) { + switch (aValue) { + case 'TRUE': + return true; + case 'FALSE': + return false; + default: + //TODO: parser warning + return false; + } + }, + + toICAL: function(aValue) { + if (aValue) { + return 'TRUE'; + } + return 'FALSE'; + } + + }, + float: { + matches: /^[+-]?\d+\.\d+$/, + + fromICAL: function(aValue) { + let parsed = parseFloat(aValue); + if (isStrictlyNaN(parsed)) { + // TODO: parser warning + return 0.0; + } + return parsed; + }, + + toICAL: function(aValue) { + return String(aValue); + } + }, + integer: { + fromICAL: function(aValue) { + let parsed = parseInt(aValue); + if (isStrictlyNaN(parsed)) { + return 0; + } + return parsed; + }, + + toICAL: function(aValue) { + return String(aValue); + } + }, + "utc-offset": { + toICAL: function(aValue) { + if (aValue.length < 7) { + // no seconds + // -0500 + return aValue.slice(0, 3) + + aValue.slice(4, 6); + } else { + // seconds + // -050000 + return aValue.slice(0, 3) + + aValue.slice(4, 6) + + aValue.slice(7, 9); + } + }, + + fromICAL: function(aValue) { + if (aValue.length < 6) { + // no seconds + // -05:00 + return aValue.slice(0, 3) + ':' + + aValue.slice(3, 5); + } else { + // seconds + // -05:00:00 + return aValue.slice(0, 3) + ':' + + aValue.slice(3, 5) + ':' + + aValue.slice(5, 7); + } + }, + + decorate: function(aValue) { + return UtcOffset.fromString(aValue); + }, + + undecorate: function(aValue) { + return aValue.toString(); + } + } +}; + +let icalParams = { + // Although the syntax is DQUOTE uri DQUOTE, I don't think we should + // enforce anything aside from it being a valid content line. + // + // At least some params require - if multi values are used - DQUOTEs + // for each of its values - e.g. delegated-from="uri1","uri2" + // To indicate this, I introduced the new k/v pair + // multiValueSeparateDQuote: true + // + // "ALTREP": { ... }, + + // CN just wants a param-value + // "CN": { ... } + + "cutype": { + values: ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", "UNKNOWN"], + allowXName: true, + allowIanaToken: true + }, + + "delegated-from": { + valueType: "cal-address", + multiValue: ",", + multiValueSeparateDQuote: true + }, + "delegated-to": { + valueType: "cal-address", + multiValue: ",", + multiValueSeparateDQuote: true + }, + // "DIR": { ... }, // See ALTREP + "encoding": { + values: ["8BIT", "BASE64"] + }, + // "FMTTYPE": { ... }, // See ALTREP + "fbtype": { + values: ["FREE", "BUSY", "BUSY-UNAVAILABLE", "BUSY-TENTATIVE"], + allowXName: true, + allowIanaToken: true + }, + // "LANGUAGE": { ... }, // See ALTREP + "member": { + valueType: "cal-address", + multiValue: ",", + multiValueSeparateDQuote: true + }, + "partstat": { + // TODO These values are actually different per-component + values: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE", + "DELEGATED", "COMPLETED", "IN-PROCESS"], + allowXName: true, + allowIanaToken: true + }, + "range": { + values: ["THISANDFUTURE"] + }, + "related": { + values: ["START", "END"] + }, + "reltype": { + values: ["PARENT", "CHILD", "SIBLING"], + allowXName: true, + allowIanaToken: true + }, + "role": { + values: ["REQ-PARTICIPANT", "CHAIR", + "OPT-PARTICIPANT", "NON-PARTICIPANT"], + allowXName: true, + allowIanaToken: true + }, + "rsvp": { + values: ["TRUE", "FALSE"] + }, + "sent-by": { + valueType: "cal-address" + }, + "tzid": { + matches: /^\// + }, + "value": { + // since the value here is a 'type' lowercase is used. + values: ["binary", "boolean", "cal-address", "date", "date-time", + "duration", "float", "integer", "period", "recur", "text", + "time", "uri", "utc-offset"], + allowXName: true, + allowIanaToken: true + } +}; + +// When adding a value here, be sure to add it to the parameter types! +const icalValues = extend(commonValues, { + text: createTextType(FROM_ICAL_NEWLINE, TO_ICAL_NEWLINE), + + uri: { + // TODO + /* ... */ + }, + + "binary": { + decorate: function(aString) { + return Binary.fromString(aString); + }, + + undecorate: function(aBinary) { + return aBinary.toString(); + } + }, + "cal-address": { + // needs to be an uri + }, + "date": { + decorate: function(aValue, aProp) { + if (design.strict) { + return Time.fromDateString(aValue, aProp); + } else { + return Time.fromString(aValue, aProp); + } + }, + + /** + * undecorates a time object. + */ + undecorate: function(aValue) { + return aValue.toString(); + }, + + fromICAL: function(aValue) { + // from: 20120901 + // to: 2012-09-01 + if (!design.strict && aValue.length >= 15) { + // This is probably a date-time, e.g. 20120901T130000Z + return icalValues["date-time"].fromICAL(aValue); + } else { + return aValue.slice(0, 4) + '-' + + aValue.slice(4, 6) + '-' + + aValue.slice(6, 8); + } + }, + + toICAL: function(aValue) { + // from: 2012-09-01 + // to: 20120901 + let len = aValue.length; + + if (len == 10) { + return aValue.slice(0, 4) + + aValue.slice(5, 7) + + aValue.slice(8, 10); + } else if (len >= 19) { + return icalValues["date-time"].toICAL(aValue); + } else { + //TODO: serialize warning? + return aValue; + } + + } + }, + "date-time": { + fromICAL: function(aValue) { + // from: 20120901T130000 + // to: 2012-09-01T13:00:00 + if (!design.strict && aValue.length == 8) { + // This is probably a date, e.g. 20120901 + return icalValues.date.fromICAL(aValue); + } else { + let result = aValue.slice(0, 4) + '-' + + aValue.slice(4, 6) + '-' + + aValue.slice(6, 8) + 'T' + + aValue.slice(9, 11) + ':' + + aValue.slice(11, 13) + ':' + + aValue.slice(13, 15); + + if (aValue[15] && aValue[15] === 'Z') { + result += 'Z'; + } + + return result; + } + }, + + toICAL: function(aValue) { + // from: 2012-09-01T13:00:00 + // to: 20120901T130000 + let len = aValue.length; + + if (len == 10 && !design.strict) { + return icalValues.date.toICAL(aValue); + } else if (len >= 19) { + let result = aValue.slice(0, 4) + + aValue.slice(5, 7) + + // grab the (DDTHH) segment + aValue.slice(8, 13) + + // MM + aValue.slice(14, 16) + + // SS + aValue.slice(17, 19); + + if (aValue[19] && aValue[19] === 'Z') { + result += 'Z'; + } + return result; + } else { + // TODO: error + return aValue; + } + }, + + decorate: function(aValue, aProp) { + if (design.strict) { + return Time.fromDateTimeString(aValue, aProp); + } else { + return Time.fromString(aValue, aProp); + } + }, + + undecorate: function(aValue) { + return aValue.toString(); + } + }, + duration: { + decorate: function(aValue) { + return Duration.fromString(aValue); + }, + undecorate: function(aValue) { + return aValue.toString(); + } + }, + period: { + fromICAL: function(string) { + let parts = string.split('/'); + parts[0] = icalValues['date-time'].fromICAL(parts[0]); + + if (!Duration.isValueString(parts[1])) { + parts[1] = icalValues['date-time'].fromICAL(parts[1]); + } + + return parts; + }, + + toICAL: function(parts) { + parts = parts.slice(); + if (!design.strict && parts[0].length == 10) { + parts[0] = icalValues.date.toICAL(parts[0]); + } else { + parts[0] = icalValues['date-time'].toICAL(parts[0]); + } + + if (!Duration.isValueString(parts[1])) { + if (!design.strict && parts[1].length == 10) { + parts[1] = icalValues.date.toICAL(parts[1]); + } else { + parts[1] = icalValues['date-time'].toICAL(parts[1]); + } + } + + return parts.join("/"); + }, + + decorate: function(aValue, aProp) { + return Period.fromJSON(aValue, aProp, !design.strict); + }, + + undecorate: function(aValue) { + return aValue.toJSON(); + } + }, + recur: { + fromICAL: function(string) { + return Recur._stringToData(string, true); + }, + + toICAL: function(data) { + let str = ""; + for (let [k, val] of Object.entries(data)) { + if (k == "until") { + if (val.length > 10) { + val = icalValues['date-time'].toICAL(val); + } else { + val = icalValues.date.toICAL(val); + } + } else if (k == "wkst") { + if (typeof val === 'number') { + val = Recur.numericDayToIcalDay(val); + } + } else if (Array.isArray(val)) { + val = val.join(","); + } + str += k.toUpperCase() + "=" + val + ";"; + } + return str.slice(0, Math.max(0, str.length - 1)); + }, + + decorate: function decorate(aValue) { + return Recur.fromData(aValue); + }, + + undecorate: function(aRecur) { + return aRecur.toJSON(); + } + }, + + time: { + fromICAL: function(aValue) { + // from: MMHHSS(Z)? + // to: HH:MM:SS(Z)? + if (aValue.length < 6) { + // TODO: parser exception? + return aValue; + } + + // HH::MM::SSZ? + let result = aValue.slice(0, 2) + ':' + + aValue.slice(2, 4) + ':' + + aValue.slice(4, 6); + + if (aValue[6] === 'Z') { + result += 'Z'; + } + + return result; + }, + + toICAL: function(aValue) { + // from: HH:MM:SS(Z)? + // to: MMHHSS(Z)? + if (aValue.length < 8) { + //TODO: error + return aValue; + } + + let result = aValue.slice(0, 2) + + aValue.slice(3, 5) + + aValue.slice(6, 8); + + if (aValue[8] === 'Z') { + result += 'Z'; + } + + return result; + } + } +}); + +let icalProperties = extend(commonProperties, { + + "action": DEFAULT_TYPE_TEXT, + "attach": { defaultType: "uri" }, + "attendee": { defaultType: "cal-address" }, + "calscale": DEFAULT_TYPE_TEXT, + "class": DEFAULT_TYPE_TEXT, + "comment": DEFAULT_TYPE_TEXT, + "completed": DEFAULT_TYPE_DATETIME, + "contact": DEFAULT_TYPE_TEXT, + "created": DEFAULT_TYPE_DATETIME, + "description": DEFAULT_TYPE_TEXT, + "dtend": DEFAULT_TYPE_DATETIME_DATE, + "dtstamp": DEFAULT_TYPE_DATETIME, + "dtstart": DEFAULT_TYPE_DATETIME_DATE, + "due": DEFAULT_TYPE_DATETIME_DATE, + "duration": { defaultType: "duration" }, + "exdate": { + defaultType: "date-time", + allowedTypes: ["date-time", "date"], + multiValue: ',' + }, + "exrule": DEFAULT_TYPE_RECUR, + "freebusy": { defaultType: "period", multiValue: "," }, + "geo": { defaultType: "float", structuredValue: ";" }, + "last-modified": DEFAULT_TYPE_DATETIME, + "location": DEFAULT_TYPE_TEXT, + "method": DEFAULT_TYPE_TEXT, + "organizer": { defaultType: "cal-address" }, + "percent-complete": DEFAULT_TYPE_INTEGER, + "priority": DEFAULT_TYPE_INTEGER, + "prodid": DEFAULT_TYPE_TEXT, + "related-to": DEFAULT_TYPE_TEXT, + "repeat": DEFAULT_TYPE_INTEGER, + "rdate": { + defaultType: "date-time", + allowedTypes: ["date-time", "date", "period"], + multiValue: ',', + detectType: function(string) { + if (string.indexOf('/') !== -1) { + return 'period'; + } + return (string.indexOf('T') === -1) ? 'date' : 'date-time'; + } + }, + "recurrence-id": DEFAULT_TYPE_DATETIME_DATE, + "resources": DEFAULT_TYPE_TEXT_MULTI, + "request-status": DEFAULT_TYPE_TEXT_STRUCTURED, + "rrule": DEFAULT_TYPE_RECUR, + "sequence": DEFAULT_TYPE_INTEGER, + "status": DEFAULT_TYPE_TEXT, + "summary": DEFAULT_TYPE_TEXT, + "transp": DEFAULT_TYPE_TEXT, + "trigger": { defaultType: "duration", allowedTypes: ["duration", "date-time"] }, + "tzoffsetfrom": DEFAULT_TYPE_UTCOFFSET, + "tzoffsetto": DEFAULT_TYPE_UTCOFFSET, + "tzurl": DEFAULT_TYPE_URI, + "tzid": DEFAULT_TYPE_TEXT, + "tzname": DEFAULT_TYPE_TEXT +}); + +// When adding a value here, be sure to add it to the parameter types! +const vcardValues = extend(commonValues, { + text: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE), + uri: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE), + + date: { + decorate: function(aValue) { + return VCardTime.fromDateAndOrTimeString(aValue, "date"); + }, + undecorate: function(aValue) { + return aValue.toString(); + }, + fromICAL: function(aValue) { + if (aValue.length == 8) { + return icalValues.date.fromICAL(aValue); + } else if (aValue[0] == '-' && aValue.length == 6) { + return aValue.slice(0, 4) + '-' + aValue.slice(4); + } else { + return aValue; + } + }, + toICAL: function(aValue) { + if (aValue.length == 10) { + return icalValues.date.toICAL(aValue); + } else if (aValue[0] == '-' && aValue.length == 7) { + return aValue.slice(0, 4) + aValue.slice(5); + } else { + return aValue; + } + } + }, + + time: { + decorate: function(aValue) { + return VCardTime.fromDateAndOrTimeString("T" + aValue, "time"); + }, + undecorate: function(aValue) { + return aValue.toString(); + }, + fromICAL: function(aValue) { + let splitzone = vcardValues.time._splitZone(aValue, true); + let zone = splitzone[0], value = splitzone[1]; + + //console.log("SPLIT: ",splitzone); + + if (value.length == 6) { + value = value.slice(0, 2) + ':' + + value.slice(2, 4) + ':' + + value.slice(4, 6); + } else if (value.length == 4 && value[0] != '-') { + value = value.slice(0, 2) + ':' + value.slice(2, 4); + } else if (value.length == 5) { + value = value.slice(0, 3) + ':' + value.slice(3, 5); + } + + if (zone.length == 5 && (zone[0] == '-' || zone[0] == '+')) { + zone = zone.slice(0, 3) + ':' + zone.slice(3); + } + + return value + zone; + }, + + toICAL: function(aValue) { + let splitzone = vcardValues.time._splitZone(aValue); + let zone = splitzone[0], value = splitzone[1]; + + if (value.length == 8) { + value = value.slice(0, 2) + + value.slice(3, 5) + + value.slice(6, 8); + } else if (value.length == 5 && value[0] != '-') { + value = value.slice(0, 2) + value.slice(3, 5); + } else if (value.length == 6) { + value = value.slice(0, 3) + value.slice(4, 6); + } + + if (zone.length == 6 && (zone[0] == '-' || zone[0] == '+')) { + zone = zone.slice(0, 3) + zone.slice(4); + } + + return value + zone; + }, + + _splitZone: function(aValue, isFromIcal) { + let lastChar = aValue.length - 1; + let signChar = aValue.length - (isFromIcal ? 5 : 6); + let sign = aValue[signChar]; + let zone, value; + + if (aValue[lastChar] == 'Z') { + zone = aValue[lastChar]; + value = aValue.slice(0, Math.max(0, lastChar)); + } else if (aValue.length > 6 && (sign == '-' || sign == '+')) { + zone = aValue.slice(signChar); + value = aValue.slice(0, Math.max(0, signChar)); + } else { + zone = ""; + value = aValue; + } + + return [zone, value]; + } + }, + + "date-time": { + decorate: function(aValue) { + return VCardTime.fromDateAndOrTimeString(aValue, "date-time"); + }, + + undecorate: function(aValue) { + return aValue.toString(); + }, + + fromICAL: function(aValue) { + return vcardValues['date-and-or-time'].fromICAL(aValue); + }, + + toICAL: function(aValue) { + return vcardValues['date-and-or-time'].toICAL(aValue); + } + }, + + "date-and-or-time": { + decorate: function(aValue) { + return VCardTime.fromDateAndOrTimeString(aValue, "date-and-or-time"); + }, + + undecorate: function(aValue) { + return aValue.toString(); + }, + + fromICAL: function(aValue) { + let parts = aValue.split('T'); + return (parts[0] ? vcardValues.date.fromICAL(parts[0]) : '') + + (parts[1] ? 'T' + vcardValues.time.fromICAL(parts[1]) : ''); + }, + + toICAL: function(aValue) { + let parts = aValue.split('T'); + return vcardValues.date.toICAL(parts[0]) + + (parts[1] ? 'T' + vcardValues.time.toICAL(parts[1]) : ''); + + } + }, + timestamp: icalValues['date-time'], + "language-tag": { + matches: /^[a-zA-Z0-9-]+$/ // Could go with a more strict regex here + }, + "phone-number": { + fromICAL: function(aValue) { + return Array.from(aValue).filter(function(c) { + return c === '\\' ? undefined : c; + }).join(''); + }, + toICAL: function(aValue) { + return Array.from(aValue).map(function(c) { + return c === ',' || c === ";" ? '\\' + c : c; + }).join(''); + } + } +}); + +let vcardParams = { + "type": { + valueType: "text", + multiValue: "," + }, + "value": { + // since the value here is a 'type' lowercase is used. + values: ["text", "uri", "date", "time", "date-time", "date-and-or-time", + "timestamp", "boolean", "integer", "float", "utc-offset", + "language-tag"], + allowXName: true, + allowIanaToken: true + } +}; + +let vcardProperties = extend(commonProperties, { + "adr": { defaultType: "text", structuredValue: ";", multiValue: "," }, + "anniversary": DEFAULT_TYPE_DATE_ANDOR_TIME, + "bday": DEFAULT_TYPE_DATE_ANDOR_TIME, + "caladruri": DEFAULT_TYPE_URI, + "caluri": DEFAULT_TYPE_URI, + "clientpidmap": DEFAULT_TYPE_TEXT_STRUCTURED, + "email": DEFAULT_TYPE_TEXT, + "fburl": DEFAULT_TYPE_URI, + "fn": DEFAULT_TYPE_TEXT, + "gender": DEFAULT_TYPE_TEXT_STRUCTURED, + "geo": DEFAULT_TYPE_URI, + "impp": DEFAULT_TYPE_URI, + "key": DEFAULT_TYPE_URI, + "kind": DEFAULT_TYPE_TEXT, + "lang": { defaultType: "language-tag" }, + "logo": DEFAULT_TYPE_URI, + "member": DEFAULT_TYPE_URI, + "n": { defaultType: "text", structuredValue: ";", multiValue: "," }, + "nickname": DEFAULT_TYPE_TEXT_MULTI, + "note": DEFAULT_TYPE_TEXT, + "org": { defaultType: "text", structuredValue: ";" }, + "photo": DEFAULT_TYPE_URI, + "related": DEFAULT_TYPE_URI, + "rev": { defaultType: "timestamp" }, + "role": DEFAULT_TYPE_TEXT, + "sound": DEFAULT_TYPE_URI, + "source": DEFAULT_TYPE_URI, + "tel": { defaultType: "uri", allowedTypes: ["uri", "text"] }, + "title": DEFAULT_TYPE_TEXT, + "tz": { defaultType: "text", allowedTypes: ["text", "utc-offset", "uri"] }, + "xml": DEFAULT_TYPE_TEXT +}); + +let vcard3Values = extend(commonValues, { + binary: icalValues.binary, + date: vcardValues.date, + "date-time": vcardValues["date-time"], + "phone-number": vcardValues["phone-number"], + uri: icalValues.uri, + text: icalValues.text, + time: icalValues.time, + vcard: icalValues.text, + "utc-offset": { + toICAL: function(aValue) { + return aValue.slice(0, 7); + }, + + fromICAL: function(aValue) { + return aValue.slice(0, 7); + }, + + decorate: function(aValue) { + return UtcOffset.fromString(aValue); + }, + + undecorate: function(aValue) { + return aValue.toString(); + } + } +}); + +let vcard3Params = { + "type": { + valueType: "text", + multiValue: "," + }, + "value": { + // since the value here is a 'type' lowercase is used. + values: ["text", "uri", "date", "date-time", "phone-number", "time", + "boolean", "integer", "float", "utc-offset", "vcard", "binary"], + allowXName: true, + allowIanaToken: true + } +}; + +let vcard3Properties = extend(commonProperties, { + fn: DEFAULT_TYPE_TEXT, + n: { defaultType: "text", structuredValue: ";", multiValue: "," }, + nickname: DEFAULT_TYPE_TEXT_MULTI, + photo: { defaultType: "binary", allowedTypes: ["binary", "uri"] }, + bday: { + defaultType: "date-time", + allowedTypes: ["date-time", "date"], + detectType: function(string) { + return (string.indexOf('T') === -1) ? 'date' : 'date-time'; + } + }, + + adr: { defaultType: "text", structuredValue: ";", multiValue: "," }, + label: DEFAULT_TYPE_TEXT, + + tel: { defaultType: "phone-number" }, + email: DEFAULT_TYPE_TEXT, + mailer: DEFAULT_TYPE_TEXT, + + tz: { defaultType: "utc-offset", allowedTypes: ["utc-offset", "text"] }, + geo: { defaultType: "float", structuredValue: ";" }, + + title: DEFAULT_TYPE_TEXT, + role: DEFAULT_TYPE_TEXT, + logo: { defaultType: "binary", allowedTypes: ["binary", "uri"] }, + agent: { defaultType: "vcard", allowedTypes: ["vcard", "text", "uri"] }, + org: DEFAULT_TYPE_TEXT_STRUCTURED, + + note: DEFAULT_TYPE_TEXT_MULTI, + prodid: DEFAULT_TYPE_TEXT, + rev: { + defaultType: "date-time", + allowedTypes: ["date-time", "date"], + detectType: function(string) { + return (string.indexOf('T') === -1) ? 'date' : 'date-time'; + } + }, + "sort-string": DEFAULT_TYPE_TEXT, + sound: { defaultType: "binary", allowedTypes: ["binary", "uri"] }, + + class: DEFAULT_TYPE_TEXT, + key: { defaultType: "binary", allowedTypes: ["binary", "text"] } +}); + +/** + * iCalendar design set + * @type {designSet} + */ +let icalSet = { + value: icalValues, + param: icalParams, + property: icalProperties, + propertyGroups: false +}; + +/** + * vCard 4.0 design set + * @type {designSet} + */ +let vcardSet = { + value: vcardValues, + param: vcardParams, + property: vcardProperties, + propertyGroups: true +}; + +/** + * vCard 3.0 design set + * @type {designSet} + */ +let vcard3Set = { + value: vcard3Values, + param: vcard3Params, + property: vcard3Properties, + propertyGroups: true +}; + +/** + * The design data, used by the parser to determine types for properties and + * other metadata needed to produce correct jCard/jCal data. + * + * @alias ICAL.design + * @exports module:ICAL.design + */ +const design = { + /** + * Can be set to false to make the parser more lenient. + */ + strict: true, + + /** + * The default set for new properties and components if none is specified. + * @type {designSet} + */ + defaultSet: icalSet, + + /** + * The default type for unknown properties + * @type {String} + */ + defaultType: 'unknown', + + /** + * Holds the design set for known top-level components + * + * @type {Object} + * @property {designSet} vcard vCard VCARD + * @property {designSet} vevent iCalendar VEVENT + * @property {designSet} vtodo iCalendar VTODO + * @property {designSet} vjournal iCalendar VJOURNAL + * @property {designSet} valarm iCalendar VALARM + * @property {designSet} vtimezone iCalendar VTIMEZONE + * @property {designSet} daylight iCalendar DAYLIGHT + * @property {designSet} standard iCalendar STANDARD + * + * @example + * let propertyName = 'fn'; + * let componentDesign = ICAL.design.components.vcard; + * let propertyDetails = componentDesign.property[propertyName]; + * if (propertyDetails.defaultType == 'text') { + * // Yep, sure is... + * } + */ + components: { + vcard: vcardSet, + vcard3: vcard3Set, + vevent: icalSet, + vtodo: icalSet, + vjournal: icalSet, + valarm: icalSet, + vtimezone: icalSet, + daylight: icalSet, + standard: icalSet + }, + + + /** + * The design set for iCalendar (rfc5545/rfc7265) components. + * @type {designSet} + */ + icalendar: icalSet, + + /** + * The design set for vCard (rfc6350/rfc7095) components. + * @type {designSet} + */ + vcard: vcardSet, + + /** + * The design set for vCard (rfc2425/rfc2426/rfc7095) components. + * @type {designSet} + */ + vcard3: vcard3Set, + + /** + * Gets the design set for the given component name. + * + * @param {String} componentName The name of the component + * @return {designSet} The design set for the component + */ + getDesignSet: function(componentName) { + let isInDesign = componentName && componentName in design.components; + return isInDesign ? design.components[componentName] : design.defaultSet; + } +}; +var design$1 = design; + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * This lets typescript resolve our custom types in the + * generated d.ts files (jsdoc typedefs are converted to typescript types). + * Ignore prevents the typedefs from being documented more than once. + * + * @ignore + * @typedef {import("./types.js").designSet} designSet + * Imports the 'designSet' type from the "types.js" module + */ + +const LINE_ENDING = '\r\n'; +const DEFAULT_VALUE_TYPE = 'unknown'; +const RFC6868_REPLACE_MAP = { '"': "^'", "\n": "^n", "^": "^^" }; + +/** + * Convert a full jCal/jCard array into a iCalendar/vCard string. + * + * @function ICAL.stringify + * @variation function + * @param {Array} jCal The jCal/jCard document + * @return {String} The stringified iCalendar/vCard document + */ +function stringify(jCal) { + if (typeof jCal[0] == "string") { + // This is a single component + jCal = [jCal]; + } + + let i = 0; + let len = jCal.length; + let result = ''; + + for (; i < len; i++) { + result += stringify.component(jCal[i]) + LINE_ENDING; + } + + return result; +} + +/** + * Converts an jCal component array into a ICAL string. + * Recursive will resolve sub-components. + * + * Exact component/property order is not saved all + * properties will come before subcomponents. + * + * @function ICAL.stringify.component + * @param {Array} component + * jCal/jCard fragment of a component + * @param {designSet} designSet + * The design data to use for this component + * @return {String} The iCalendar/vCard string + */ +stringify.component = function(component, designSet) { + let name = component[0].toUpperCase(); + let result = 'BEGIN:' + name + LINE_ENDING; + + let props = component[1]; + let propIdx = 0; + let propLen = props.length; + + let designSetName = component[0]; + // rfc6350 requires that in vCard 4.0 the first component is the VERSION + // component with as value 4.0, note that 3.0 does not have this requirement. + if (designSetName === 'vcard' && component[1].length > 0 && + !(component[1][0][0] === "version" && component[1][0][3] === "4.0")) { + designSetName = "vcard3"; + } + designSet = designSet || design$1.getDesignSet(designSetName); + + for (; propIdx < propLen; propIdx++) { + result += stringify.property(props[propIdx], designSet) + LINE_ENDING; + } + + // Ignore subcomponents if none exist, e.g. in vCard. + let comps = component[2] || []; + let compIdx = 0; + let compLen = comps.length; + + for (; compIdx < compLen; compIdx++) { + result += stringify.component(comps[compIdx], designSet) + LINE_ENDING; + } + + result += 'END:' + name; + return result; +}; + +/** + * Converts a single jCal/jCard property to a iCalendar/vCard string. + * + * @function ICAL.stringify.property + * @param {Array} property + * jCal/jCard property array + * @param {designSet} designSet + * The design data to use for this property + * @param {Boolean} noFold + * If true, the line is not folded + * @return {String} The iCalendar/vCard string + */ +stringify.property = function(property, designSet, noFold) { + let name = property[0].toUpperCase(); + let jsName = property[0]; + let params = property[1]; + + if (!designSet) { + designSet = design$1.defaultSet; + } + + let groupName = params.group; + let line; + if (designSet.propertyGroups && groupName) { + line = groupName.toUpperCase() + "." + name; + } else { + line = name; + } + + for (let [paramName, value] of Object.entries(params)) { + if (designSet.propertyGroups && paramName == 'group') { + continue; + } + + let paramDesign = designSet.param[paramName]; + let multiValue = paramDesign && paramDesign.multiValue; + if (multiValue && Array.isArray(value)) { + value = value.map(function(val) { + val = stringify._rfc6868Unescape(val); + val = stringify.paramPropertyValue(val, paramDesign.multiValueSeparateDQuote); + return val; + }); + value = stringify.multiValue(value, multiValue, "unknown", null, designSet); + } else { + value = stringify._rfc6868Unescape(value); + value = stringify.paramPropertyValue(value); + } + + line += ';' + paramName.toUpperCase() + '=' + value; + } + + if (property.length === 3) { + // If there are no values, we must assume a blank value + return line + ':'; + } + + let valueType = property[2]; + + let propDetails; + let multiValue = false; + let structuredValue = false; + let isDefault = false; + + if (jsName in designSet.property) { + propDetails = designSet.property[jsName]; + + if ('multiValue' in propDetails) { + multiValue = propDetails.multiValue; + } + + if (('structuredValue' in propDetails) && Array.isArray(property[3])) { + structuredValue = propDetails.structuredValue; + } + + if ('defaultType' in propDetails) { + if (valueType === propDetails.defaultType) { + isDefault = true; + } + } else { + if (valueType === DEFAULT_VALUE_TYPE) { + isDefault = true; + } + } + } else { + if (valueType === DEFAULT_VALUE_TYPE) { + isDefault = true; + } + } + + // push the VALUE property if type is not the default + // for the current property. + if (!isDefault) { + // value will never contain ;/:/, so we don't escape it here. + line += ';VALUE=' + valueType.toUpperCase(); + } + + line += ':'; + + if (multiValue && structuredValue) { + line += stringify.multiValue( + property[3], structuredValue, valueType, multiValue, designSet, structuredValue + ); + } else if (multiValue) { + line += stringify.multiValue( + property.slice(3), multiValue, valueType, null, designSet, false + ); + } else if (structuredValue) { + line += stringify.multiValue( + property[3], structuredValue, valueType, null, designSet, structuredValue + ); + } else { + line += stringify.value(property[3], valueType, designSet, false); + } + + return noFold ? line : foldline(line); +}; + +/** + * Handles escaping of property values that may contain: + * + * COLON (:), SEMICOLON (;), or COMMA (,) + * + * If any of the above are present the result is wrapped + * in double quotes. + * + * @function ICAL.stringify.paramPropertyValue + * @param {String} value Raw property value + * @param {boolean} force If value should be escaped even when unnecessary + * @return {String} Given or escaped value when needed + */ +stringify.paramPropertyValue = function(value, force) { + if (!force && + (value.indexOf(',') === -1) && + (value.indexOf(':') === -1) && + (value.indexOf(';') === -1)) { + + return value; + } + + return '"' + value + '"'; +}; + +/** + * Converts an array of ical values into a single + * string based on a type and a delimiter value (like ","). + * + * @function ICAL.stringify.multiValue + * @param {Array} values List of values to convert + * @param {String} delim Used to join the values (",", ";", ":") + * @param {String} type Lowecase ical value type + * (like boolean, date-time, etc..) + * @param {?String} innerMulti If set, each value will again be processed + * Used for structured values + * @param {designSet} designSet + * The design data to use for this property + * + * @return {String} iCalendar/vCard string for value + */ +stringify.multiValue = function(values, delim, type, innerMulti, designSet, structuredValue) { + let result = ''; + let len = values.length; + let i = 0; + + for (; i < len; i++) { + if (innerMulti && Array.isArray(values[i])) { + result += stringify.multiValue(values[i], innerMulti, type, null, designSet, structuredValue); + } else { + result += stringify.value(values[i], type, designSet, structuredValue); + } + + if (i !== (len - 1)) { + result += delim; + } + } + + return result; +}; + +/** + * Processes a single ical value runs the associated "toICAL" method from the + * design value type if available to convert the value. + * + * @function ICAL.stringify.value + * @param {String|Number} value A formatted value + * @param {String} type Lowercase iCalendar/vCard value type + * (like boolean, date-time, etc..) + * @return {String} iCalendar/vCard value for single value + */ +stringify.value = function(value, type, designSet, structuredValue) { + if (type in designSet.value && 'toICAL' in designSet.value[type]) { + return designSet.value[type].toICAL(value, structuredValue); + } + return value; +}; + +/** + * Internal helper for rfc6868. Exposing this on ICAL.stringify so that + * hackers can disable the rfc6868 parsing if the really need to. + * + * @param {String} val The value to unescape + * @return {String} The escaped value + */ +stringify._rfc6868Unescape = function(val) { + return val.replace(/[\n^"]/g, function(x) { + return RFC6868_REPLACE_MAP[x]; + }); +}; + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +const NAME_INDEX$1 = 0; +const PROP_INDEX = 1; +const TYPE_INDEX = 2; +const VALUE_INDEX = 3; + +/** + * This lets typescript resolve our custom types in the + * generated d.ts files (jsdoc typedefs are converted to typescript types). + * Ignore prevents the typedefs from being documented more than once. + * @ignore + * @typedef {import("./types.js").designSet} designSet + * Imports the 'designSet' type from the "types.js" module + * @typedef {import("./types.js").Geo} Geo + * Imports the 'Geo' type from the "types.js" module + */ + +/** + * Provides a layer on top of the raw jCal object for manipulating a single property, with its + * parameters and value. + * + * @memberof ICAL + */ +class Property { + /** + * Create an {@link ICAL.Property} by parsing the passed iCalendar string. + * + * @param {String} str The iCalendar string to parse + * @param {designSet=} designSet The design data to use for this property + * @return {Property} The created iCalendar property + */ + static fromString(str, designSet) { + return new Property(parse.property(str, designSet)); + } + + /** + * Creates a new ICAL.Property instance. + * + * It is important to note that mutations done in the wrapper directly mutate the jCal object used + * to initialize. + * + * Can also be used to create new properties by passing the name of the property (as a String). + * + * @param {Array|String} jCal Raw jCal representation OR the new name of the property + * @param {Component=} parent Parent component + */ + constructor(jCal, parent) { + this._parent = parent || null; + + if (typeof(jCal) === 'string') { + // We are creating the property by name and need to detect the type + this.jCal = [jCal, {}, design$1.defaultType]; + this.jCal[TYPE_INDEX] = this.getDefaultType(); + } else { + this.jCal = jCal; + } + this._updateType(); + } + + /** + * The value type for this property + * @type {String} + */ + get type() { + return this.jCal[TYPE_INDEX]; + } + + /** + * The name of this property, in lowercase. + * @type {String} + */ + get name() { + return this.jCal[NAME_INDEX$1]; + } + + /** + * The parent component for this property. + * @type {Component} + */ + get parent() { + return this._parent; + } + + set parent(p) { + // Before setting the parent, check if the design set has changed. If it + // has, we later need to update the type if it was unknown before. + let designSetChanged = !this._parent || (p && p._designSet != this._parent._designSet); + + this._parent = p; + + if (this.type == design$1.defaultType && designSetChanged) { + this.jCal[TYPE_INDEX] = this.getDefaultType(); + this._updateType(); + } + } + + /** + * The design set for this property, e.g. icalendar vs vcard + * + * @type {designSet} + * @private + */ + get _designSet() { + return this.parent ? this.parent._designSet : design$1.defaultSet; + } + + /** + * Updates the type metadata from the current jCal type and design set. + * + * @private + */ + _updateType() { + let designSet = this._designSet; + + if (this.type in designSet.value) { + if ('decorate' in designSet.value[this.type]) { + this.isDecorated = true; + } else { + this.isDecorated = false; + } + + if (this.name in designSet.property) { + this.isMultiValue = ('multiValue' in designSet.property[this.name]); + this.isStructuredValue = ('structuredValue' in designSet.property[this.name]); + } + } + } + + /** + * Hydrate a single value. The act of hydrating means turning the raw jCal + * value into a potentially wrapped object, for example {@link ICAL.Time}. + * + * @private + * @param {Number} index The index of the value to hydrate + * @return {?Object} The decorated value. + */ + _hydrateValue(index) { + if (this._values && this._values[index]) { + return this._values[index]; + } + + // for the case where there is no value. + if (this.jCal.length <= (VALUE_INDEX + index)) { + return null; + } + + if (this.isDecorated) { + if (!this._values) { + this._values = []; + } + return (this._values[index] = this._decorate( + this.jCal[VALUE_INDEX + index] + )); + } else { + return this.jCal[VALUE_INDEX + index]; + } + } + + /** + * Decorate a single value, returning its wrapped object. This is used by + * the hydrate function to actually wrap the value. + * + * @private + * @param {?} value The value to decorate + * @return {Object} The decorated value + */ + _decorate(value) { + return this._designSet.value[this.type].decorate(value, this); + } + + /** + * Undecorate a single value, returning its raw jCal data. + * + * @private + * @param {Object} value The value to undecorate + * @return {?} The undecorated value + */ + _undecorate(value) { + return this._designSet.value[this.type].undecorate(value, this); + } + + /** + * Sets the value at the given index while also hydrating it. The passed + * value can either be a decorated or undecorated value. + * + * @private + * @param {?} value The value to set + * @param {Number} index The index to set it at + */ + _setDecoratedValue(value, index) { + if (!this._values) { + this._values = []; + } + + if (typeof(value) === 'object' && 'icaltype' in value) { + // decorated value + this.jCal[VALUE_INDEX + index] = this._undecorate(value); + this._values[index] = value; + } else { + // undecorated value + this.jCal[VALUE_INDEX + index] = value; + this._values[index] = this._decorate(value); + } + } + + /** + * Gets a parameter on the property. + * + * @param {String} name Parameter name (lowercase) + * @return {Array|String} Parameter value + */ + getParameter(name) { + if (name in this.jCal[PROP_INDEX]) { + return this.jCal[PROP_INDEX][name]; + } else { + return undefined; + } + } + + /** + * Gets first parameter on the property. + * + * @param {String} name Parameter name (lowercase) + * @return {String} Parameter value + */ + getFirstParameter(name) { + let parameters = this.getParameter(name); + + if (Array.isArray(parameters)) { + return parameters[0]; + } + + return parameters; + } + + /** + * Sets a parameter on the property. + * + * @param {String} name The parameter name + * @param {Array|String} value The parameter value + */ + setParameter(name, value) { + let lcname = name.toLowerCase(); + if (typeof value === "string" && + lcname in this._designSet.param && + 'multiValue' in this._designSet.param[lcname]) { + value = [value]; + } + this.jCal[PROP_INDEX][name] = value; + } + + /** + * Removes a parameter + * + * @param {String} name The parameter name + */ + removeParameter(name) { + delete this.jCal[PROP_INDEX][name]; + } + + /** + * Get the default type based on this property's name. + * + * @return {String} The default type for this property + */ + getDefaultType() { + let name = this.jCal[NAME_INDEX$1]; + let designSet = this._designSet; + + if (name in designSet.property) { + let details = designSet.property[name]; + if ('defaultType' in details) { + return details.defaultType; + } + } + return design$1.defaultType; + } + + /** + * Sets type of property and clears out any existing values of the current + * type. + * + * @param {String} type New iCAL type (see design.*.values) + */ + resetType(type) { + this.removeAllValues(); + this.jCal[TYPE_INDEX] = type; + this._updateType(); + } + + /** + * Finds the first property value. + * + * @return {Binary | Duration | Period | + * Recur | Time | UtcOffset | Geo | string | null} First property value + */ + getFirstValue() { + return this._hydrateValue(0); + } + + /** + * Gets all values on the property. + * + * NOTE: this creates an array during each call. + * + * @return {Array} List of values + */ + getValues() { + let len = this.jCal.length - VALUE_INDEX; + + if (len < 1) { + // it is possible for a property to have no value. + return []; + } + + let i = 0; + let result = []; + + for (; i < len; i++) { + result[i] = this._hydrateValue(i); + } + + return result; + } + + /** + * Removes all values from this property + */ + removeAllValues() { + if (this._values) { + this._values.length = 0; + } + this.jCal.length = 3; + } + + /** + * Sets the values of the property. Will overwrite the existing values. + * This can only be used for multi-value properties. + * + * @param {Array} values An array of values + */ + setValues(values) { + if (!this.isMultiValue) { + throw new Error( + this.name + ': does not not support mulitValue.\n' + + 'override isMultiValue' + ); + } + + let len = values.length; + let i = 0; + this.removeAllValues(); + + if (len > 0 && + typeof(values[0]) === 'object' && + 'icaltype' in values[0]) { + this.resetType(values[0].icaltype); + } + + if (this.isDecorated) { + for (; i < len; i++) { + this._setDecoratedValue(values[i], i); + } + } else { + for (; i < len; i++) { + this.jCal[VALUE_INDEX + i] = values[i]; + } + } + } + + /** + * Sets the current value of the property. If this is a multi-value + * property, all other values will be removed. + * + * @param {String|Object} value New property value. + */ + setValue(value) { + this.removeAllValues(); + if (typeof(value) === 'object' && 'icaltype' in value) { + this.resetType(value.icaltype); + } + + if (this.isDecorated) { + this._setDecoratedValue(value, 0); + } else { + this.jCal[VALUE_INDEX] = value; + } + } + + /** + * Returns the Object representation of this component. The returned object + * is a live jCal object and should be cloned if modified. + * @return {Object} + */ + toJSON() { + return this.jCal; + } + + /** + * The string representation of this component. + * @return {String} + */ + toICALString() { + return stringify.property( + this.jCal, this._designSet, true + ); + } +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * This lets typescript resolve our custom types in the + * generated d.ts files (jsdoc typedefs are converted to typescript types). + * Ignore prevents the typedefs from being documented more than once. + * @ignore + * @typedef {import("./types.js").designSet} designSet + * Imports the 'designSet' type from the "types.js" module + * @typedef {import("./types.js").Geo} Geo + * Imports the 'Geo' type from the "types.js" module + */ + +const NAME_INDEX = 0; +const PROPERTY_INDEX = 1; +const COMPONENT_INDEX = 2; + +/** + * Wraps a jCal component, adding convenience methods to add, remove and update subcomponents and + * properties. + * + * @memberof ICAL + */ +class Component { + /** + * Create an {@link ICAL.Component} by parsing the passed iCalendar string. + * + * @param {String} str The iCalendar string to parse + */ + static fromString(str) { + return new Component(parse.component(str)); + } + + /** + * Creates a new Component instance. + * + * @param {Array|String} jCal Raw jCal component data OR name of new + * component + * @param {Component=} parent Parent component to associate + */ + constructor(jCal, parent) { + if (typeof(jCal) === 'string') { + // jCal spec (name, properties, components) + jCal = [jCal, [], []]; + } + + // mostly for legacy reasons. + this.jCal = jCal; + + this.parent = parent || null; + + if (!this.parent && this.name === 'vcalendar') { + this._timezoneCache = new Map(); + } + } + + /** + * Hydrated properties are inserted into the _properties array at the same + * position as in the jCal array, so it is possible that the array contains + * undefined values for unhydrdated properties. To avoid iterating the + * array when checking if all properties have been hydrated, we save the + * count here. + * + * @type {Number} + * @private + */ + _hydratedPropertyCount = 0; + + /** + * The same count as for _hydratedPropertyCount, but for subcomponents + * + * @type {Number} + * @private + */ + _hydratedComponentCount = 0; + + /** + * A cache of hydrated time zone objects which may be used by consumers, keyed + * by time zone ID. + * + * @type {Map} + * @private + */ + _timezoneCache = null; + + /** + * @private + */ + _components = null; + + /** + * @private + */ + _properties = null; + + /** + * The name of this component + * + * @type {String} + */ + get name() { + return this.jCal[NAME_INDEX]; + } + + /** + * The design set for this component, e.g. icalendar vs vcard + * + * @type {designSet} + * @private + */ + get _designSet() { + let parentDesign = this.parent && this.parent._designSet; + return parentDesign || design$1.getDesignSet(this.name); + } + + /** + * @private + */ + _hydrateComponent(index) { + if (!this._components) { + this._components = []; + this._hydratedComponentCount = 0; + } + + if (this._components[index]) { + return this._components[index]; + } + + let comp = new Component( + this.jCal[COMPONENT_INDEX][index], + this + ); + + this._hydratedComponentCount++; + return (this._components[index] = comp); + } + + /** + * @private + */ + _hydrateProperty(index) { + if (!this._properties) { + this._properties = []; + this._hydratedPropertyCount = 0; + } + + if (this._properties[index]) { + return this._properties[index]; + } + + let prop = new Property( + this.jCal[PROPERTY_INDEX][index], + this + ); + + this._hydratedPropertyCount++; + return (this._properties[index] = prop); + } + + /** + * Finds first sub component, optionally filtered by name. + * + * @param {String=} name Optional name to filter by + * @return {?Component} The found subcomponent + */ + getFirstSubcomponent(name) { + if (name) { + let i = 0; + let comps = this.jCal[COMPONENT_INDEX]; + let len = comps.length; + + for (; i < len; i++) { + if (comps[i][NAME_INDEX] === name) { + let result = this._hydrateComponent(i); + return result; + } + } + } else { + if (this.jCal[COMPONENT_INDEX].length) { + return this._hydrateComponent(0); + } + } + + // ensure we return a value (strict mode) + return null; + } + + /** + * Finds all sub components, optionally filtering by name. + * + * @param {String=} name Optional name to filter by + * @return {Component[]} The found sub components + */ + getAllSubcomponents(name) { + let jCalLen = this.jCal[COMPONENT_INDEX].length; + let i = 0; + + if (name) { + let comps = this.jCal[COMPONENT_INDEX]; + let result = []; + + for (; i < jCalLen; i++) { + if (name === comps[i][NAME_INDEX]) { + result.push( + this._hydrateComponent(i) + ); + } + } + return result; + } else { + if (!this._components || + (this._hydratedComponentCount !== jCalLen)) { + for (; i < jCalLen; i++) { + this._hydrateComponent(i); + } + } + + return this._components || []; + } + } + + /** + * Returns true when a named property exists. + * + * @param {String} name The property name + * @return {Boolean} True, when property is found + */ + hasProperty(name) { + let props = this.jCal[PROPERTY_INDEX]; + let len = props.length; + + let i = 0; + for (; i < len; i++) { + // 0 is property name + if (props[i][NAME_INDEX] === name) { + return true; + } + } + + return false; + } + + /** + * Finds the first property, optionally with the given name. + * + * @param {String=} name Lowercase property name + * @return {?Property} The found property + */ + getFirstProperty(name) { + if (name) { + let i = 0; + let props = this.jCal[PROPERTY_INDEX]; + let len = props.length; + + for (; i < len; i++) { + if (props[i][NAME_INDEX] === name) { + let result = this._hydrateProperty(i); + return result; + } + } + } else { + if (this.jCal[PROPERTY_INDEX].length) { + return this._hydrateProperty(0); + } + } + + return null; + } + + /** + * Returns first property's value, if available. + * + * @param {String=} name Lowercase property name + * @return {Binary | Duration | Period | + * Recur | Time | UtcOffset | Geo | string | null} The found property value. + */ + getFirstPropertyValue(name) { + let prop = this.getFirstProperty(name); + if (prop) { + return prop.getFirstValue(); + } + + return null; + } + + /** + * Get all properties in the component, optionally filtered by name. + * + * @param {String=} name Lowercase property name + * @return {Property[]} List of properties + */ + getAllProperties(name) { + let jCalLen = this.jCal[PROPERTY_INDEX].length; + let i = 0; + + if (name) { + let props = this.jCal[PROPERTY_INDEX]; + let result = []; + + for (; i < jCalLen; i++) { + if (name === props[i][NAME_INDEX]) { + result.push( + this._hydrateProperty(i) + ); + } + } + return result; + } else { + if (!this._properties || + (this._hydratedPropertyCount !== jCalLen)) { + for (; i < jCalLen; i++) { + this._hydrateProperty(i); + } + } + + return this._properties || []; + } + } + + /** + * @private + */ + _removeObjectByIndex(jCalIndex, cache, index) { + cache = cache || []; + // remove cached version + if (cache[index]) { + let obj = cache[index]; + if ("parent" in obj) { + obj.parent = null; + } + } + + cache.splice(index, 1); + + // remove it from the jCal + this.jCal[jCalIndex].splice(index, 1); + } + + /** + * @private + */ + _removeObject(jCalIndex, cache, nameOrObject) { + let i = 0; + let objects = this.jCal[jCalIndex]; + let len = objects.length; + let cached = this[cache]; + + if (typeof(nameOrObject) === 'string') { + for (; i < len; i++) { + if (objects[i][NAME_INDEX] === nameOrObject) { + this._removeObjectByIndex(jCalIndex, cached, i); + return true; + } + } + } else if (cached) { + for (; i < len; i++) { + if (cached[i] && cached[i] === nameOrObject) { + this._removeObjectByIndex(jCalIndex, cached, i); + return true; + } + } + } + + return false; + } + + /** + * @private + */ + _removeAllObjects(jCalIndex, cache, name) { + let cached = this[cache]; + + // Unfortunately we have to run through all children to reset their + // parent property. + let objects = this.jCal[jCalIndex]; + let i = objects.length - 1; + + // descending search required because splice + // is used and will effect the indices. + for (; i >= 0; i--) { + if (!name || objects[i][NAME_INDEX] === name) { + this._removeObjectByIndex(jCalIndex, cached, i); + } + } + } + + /** + * Adds a single sub component. + * + * @param {Component} component The component to add + * @return {Component} The passed in component + */ + addSubcomponent(component) { + if (!this._components) { + this._components = []; + this._hydratedComponentCount = 0; + } + + if (component.parent) { + component.parent.removeSubcomponent(component); + } + + let idx = this.jCal[COMPONENT_INDEX].push(component.jCal); + this._components[idx - 1] = component; + this._hydratedComponentCount++; + component.parent = this; + return component; + } + + /** + * Removes a single component by name or the instance of a specific + * component. + * + * @param {Component|String} nameOrComp Name of component, or component + * @return {Boolean} True when comp is removed + */ + removeSubcomponent(nameOrComp) { + let removed = this._removeObject(COMPONENT_INDEX, '_components', nameOrComp); + if (removed) { + this._hydratedComponentCount--; + } + return removed; + } + + /** + * Removes all components or (if given) all components by a particular + * name. + * + * @param {String=} name Lowercase component name + */ + removeAllSubcomponents(name) { + let removed = this._removeAllObjects(COMPONENT_INDEX, '_components', name); + this._hydratedComponentCount = 0; + return removed; + } + + /** + * Adds an {@link ICAL.Property} to the component. + * + * @param {Property} property The property to add + * @return {Property} The passed in property + */ + addProperty(property) { + if (!(property instanceof Property)) { + throw new TypeError('must be instance of ICAL.Property'); + } + + if (!this._properties) { + this._properties = []; + this._hydratedPropertyCount = 0; + } + + if (property.parent) { + property.parent.removeProperty(property); + } + + let idx = this.jCal[PROPERTY_INDEX].push(property.jCal); + this._properties[idx - 1] = property; + this._hydratedPropertyCount++; + property.parent = this; + return property; + } + + /** + * Helper method to add a property with a value to the component. + * + * @param {String} name Property name to add + * @param {String|Number|Object} value Property value + * @return {Property} The created property + */ + addPropertyWithValue(name, value) { + let prop = new Property(name); + prop.setValue(value); + + this.addProperty(prop); + + return prop; + } + + /** + * Helper method that will update or create a property of the given name + * and sets its value. If multiple properties with the given name exist, + * only the first is updated. + * + * @param {String} name Property name to update + * @param {String|Number|Object} value Property value + * @return {Property} The created property + */ + updatePropertyWithValue(name, value) { + let prop = this.getFirstProperty(name); + + if (prop) { + prop.setValue(value); + } else { + prop = this.addPropertyWithValue(name, value); + } + + return prop; + } + + /** + * Removes a single property by name or the instance of the specific + * property. + * + * @param {String|Property} nameOrProp Property name or instance to remove + * @return {Boolean} True, when deleted + */ + removeProperty(nameOrProp) { + let removed = this._removeObject(PROPERTY_INDEX, '_properties', nameOrProp); + if (removed) { + this._hydratedPropertyCount--; + } + return removed; + } + + /** + * Removes all properties associated with this component, optionally + * filtered by name. + * + * @param {String=} name Lowercase property name + * @return {Boolean} True, when deleted + */ + removeAllProperties(name) { + let removed = this._removeAllObjects(PROPERTY_INDEX, '_properties', name); + this._hydratedPropertyCount = 0; + return removed; + } + + /** + * Returns the Object representation of this component. The returned object + * is a live jCal object and should be cloned if modified. + * @return {Object} + */ + toJSON() { + return this.jCal; + } + + /** + * The string representation of this component. + * @return {String} + */ + toString() { + return stringify.component( + this.jCal, this._designSet + ); + } + + /** + * Retrieve a time zone definition from the component tree, if any is present. + * If the tree contains no time zone definitions or the TZID cannot be + * matched, returns null. + * + * @param {String} tzid The ID of the time zone to retrieve + * @return {Timezone} The time zone corresponding to the ID, or null + */ + getTimeZoneByID(tzid) { + // VTIMEZONE components can only appear as a child of the VCALENDAR + // component; walk the tree if we're not the root. + if (this.parent) { + return this.parent.getTimeZoneByID(tzid); + } + + // If there is no time zone cache, we are probably parsing an incomplete + // file and will have no time zone definitions. + if (!this._timezoneCache) { + return null; + } + + if (this._timezoneCache.has(tzid)) { + return this._timezoneCache.get(tzid); + } + + // If the time zone is not already cached, hydrate it from the + // subcomponents. + const zones = this.getAllSubcomponents('vtimezone'); + for (const zone of zones) { + if (zone.getFirstProperty('tzid').getFirstValue() === tzid) { + const hydratedZone = new Timezone({ + component: zone, + tzid: tzid, + }); + + this._timezoneCache.set(tzid, hydratedZone); + + return hydratedZone; + } + } + + // Per the standard, we should always have a time zone defined in a file + // for any referenced TZID, but don't blow up if the file is invalid. + return null; + } +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * Primary class for expanding recurring rules. Can take multiple rrules, rdates, exdate(s) and + * iterate (in order) over each next occurrence. + * + * Once initialized this class can also be serialized saved and continue iteration from the last + * point. + * + * NOTE: it is intended that this class is to be used with {@link ICAL.Event} which handles recurrence + * exceptions. + * + * @example + * // assuming event is a parsed ical component + * var event; + * + * var expand = new ICAL.RecurExpansion({ + * component: event, + * dtstart: event.getFirstPropertyValue('dtstart') + * }); + * + * // remember there are infinite rules so it is a good idea to limit the scope of the iterations + * // then resume later on. + * + * // next is always an ICAL.Time or null + * var next; + * + * while (someCondition && (next = expand.next())) { + * // do something with next + * } + * + * // save instance for later + * var json = JSON.stringify(expand); + * + * //... + * + * // NOTE: if the component's properties have changed you will need to rebuild the class and start + * // over. This only works when the component's recurrence info is the same. + * var expand = new ICAL.RecurExpansion(JSON.parse(json)); + * + * @memberof ICAL + */ +class RecurExpansion { + /** + * Creates a new ICAL.RecurExpansion instance. + * + * The options object can be filled with the specified initial values. It can also contain + * additional members, as a result of serializing a previous expansion state, as shown in the + * example. + * + * @param {Object} options + * Recurrence expansion options + * @param {Time} options.dtstart + * Start time of the event + * @param {Component=} options.component + * Component for expansion, required if not resuming. + */ + constructor(options) { + this.ruleDates = []; + this.exDates = []; + this.fromData(options); + } + + /** + * True when iteration is fully completed. + * @type {Boolean} + */ + complete = false; + + /** + * Array of rrule iterators. + * + * @type {RecurIterator[]} + * @private + */ + ruleIterators = null; + + /** + * Array of rdate instances. + * + * @type {Time[]} + * @private + */ + ruleDates = null; + + /** + * Array of exdate instances. + * + * @type {Time[]} + * @private + */ + exDates = null; + + /** + * Current position in ruleDates array. + * @type {Number} + * @private + */ + ruleDateInc = 0; + + /** + * Current position in exDates array + * @type {Number} + * @private + */ + exDateInc = 0; + + /** + * Current negative date. + * + * @type {Time} + * @private + */ + exDate = null; + + /** + * Current additional date. + * + * @type {Time} + * @private + */ + ruleDate = null; + + /** + * Start date of recurring rules. + * + * @type {Time} + */ + dtstart = null; + + /** + * Last expanded time + * + * @type {Time} + */ + last = null; + + /** + * Initialize the recurrence expansion from the data object. The options + * object may also contain additional members, see the + * {@link ICAL.RecurExpansion constructor} for more details. + * + * @param {Object} options + * Recurrence expansion options + * @param {Time} options.dtstart + * Start time of the event + * @param {Component=} options.component + * Component for expansion, required if not resuming. + */ + fromData(options) { + let start = formatClassType(options.dtstart, Time); + + if (!start) { + throw new Error('.dtstart (ICAL.Time) must be given'); + } else { + this.dtstart = start; + } + + if (options.component) { + this._init(options.component); + } else { + this.last = formatClassType(options.last, Time) || start.clone(); + + if (!options.ruleIterators) { + throw new Error('.ruleIterators or .component must be given'); + } + + this.ruleIterators = options.ruleIterators.map(function(item) { + return formatClassType(item, RecurIterator); + }); + + this.ruleDateInc = options.ruleDateInc; + this.exDateInc = options.exDateInc; + + if (options.ruleDates) { + this.ruleDates = options.ruleDates.map(item => formatClassType(item, Time)); + this.ruleDate = this.ruleDates[this.ruleDateInc]; + } + + if (options.exDates) { + this.exDates = options.exDates.map(item => formatClassType(item, Time)); + this.exDate = this.exDates[this.exDateInc]; + } + + if (typeof(options.complete) !== 'undefined') { + this.complete = options.complete; + } + } + } + + /** + * Retrieve the next occurrence in the series. + * @return {Time} + */ + next() { + let iter; + let next; + let compare; + + let maxTries = 500; + let currentTry = 0; + + while (true) { + if (currentTry++ > maxTries) { + throw new Error( + 'max tries have occurred, rule may be impossible to fulfill.' + ); + } + + next = this.ruleDate; + iter = this._nextRecurrenceIter(this.last); + + // no more matches + // because we increment the rule day or rule + // _after_ we choose a value this should be + // the only spot where we need to worry about the + // end of events. + if (!next && !iter) { + // there are no more iterators or rdates + this.complete = true; + break; + } + + // no next rule day or recurrence rule is first. + if (!next || (iter && next.compare(iter.last) > 0)) { + // must be cloned, recur will reuse the time element. + next = iter.last.clone(); + // move to next so we can continue + iter.next(); + } + + // if the ruleDate is still next increment it. + if (this.ruleDate === next) { + this._nextRuleDay(); + } + + this.last = next; + + // check the negative rules + if (this.exDate) { + compare = this.exDate.compare(this.last); + + if (compare < 0) { + this._nextExDay(); + } + + // if the current rule is excluded skip it. + if (compare === 0) { + this._nextExDay(); + continue; + } + } + + //XXX: The spec states that after we resolve the final + // list of dates we execute exdate this seems somewhat counter + // intuitive to what I have seen most servers do so for now + // I exclude based on the original date not the one that may + // have been modified by the exception. + return this.last; + } + } + + /** + * Converts object into a serialize-able format. This format can be passed + * back into the expansion to resume iteration. + * @return {Object} + */ + toJSON() { + function toJSON(item) { + return item.toJSON(); + } + + let result = Object.create(null); + result.ruleIterators = this.ruleIterators.map(toJSON); + + if (this.ruleDates) { + result.ruleDates = this.ruleDates.map(toJSON); + } + + if (this.exDates) { + result.exDates = this.exDates.map(toJSON); + } + + result.ruleDateInc = this.ruleDateInc; + result.exDateInc = this.exDateInc; + result.last = this.last.toJSON(); + result.dtstart = this.dtstart.toJSON(); + result.complete = this.complete; + + return result; + } + + /** + * Extract all dates from the properties in the given component. The + * properties will be filtered by the property name. + * + * @private + * @param {Component} component The component to search in + * @param {String} propertyName The property name to search for + * @return {Time[]} The extracted dates. + */ + _extractDates(component, propertyName) { + let result = []; + let props = component.getAllProperties(propertyName); + + for (let i = 0, len = props.length; i < len; i++) { + for (let prop of props[i].getValues()) { + let idx = binsearchInsert( + result, + prop, + (a, b) => a.compare(b) + ); + + // ordered insert + result.splice(idx, 0, prop); + } + } + + return result; + } + + /** + * Initialize the recurrence expansion. + * + * @private + * @param {Component} component The component to initialize from. + */ + _init(component) { + this.ruleIterators = []; + + this.last = this.dtstart.clone(); + + // to provide api consistency non-recurring + // events can also use the iterator though it will + // only return a single time. + if (!component.hasProperty('rdate') && + !component.hasProperty('rrule') && + !component.hasProperty('recurrence-id')) { + this.ruleDate = this.last.clone(); + this.complete = true; + return; + } + + if (component.hasProperty('rdate')) { + this.ruleDates = this._extractDates(component, 'rdate'); + + // special hack for cases where first rdate is prior + // to the start date. We only check for the first rdate. + // This is mostly for google's crazy recurring date logic + // (contacts birthdays). + if ((this.ruleDates[0]) && + (this.ruleDates[0].compare(this.dtstart) < 0)) { + + this.ruleDateInc = 0; + this.last = this.ruleDates[0].clone(); + } else { + this.ruleDateInc = binsearchInsert( + this.ruleDates, + this.last, + (a, b) => a.compare(b) + ); + } + + this.ruleDate = this.ruleDates[this.ruleDateInc]; + } + + if (component.hasProperty('rrule')) { + let rules = component.getAllProperties('rrule'); + let i = 0; + let len = rules.length; + + let rule; + let iter; + + for (; i < len; i++) { + rule = rules[i].getFirstValue(); + iter = rule.iterator(this.dtstart); + this.ruleIterators.push(iter); + + // increment to the next occurrence so future + // calls to next return times beyond the initial iteration. + // XXX: I find this suspicious might be a bug? + iter.next(); + } + } + + if (component.hasProperty('exdate')) { + this.exDates = this._extractDates(component, 'exdate'); + // if we have a .last day we increment the index to beyond it. + this.exDateInc = binsearchInsert( + this.exDates, + this.last, + (a, b) => a.compare(b) + ); + + this.exDate = this.exDates[this.exDateInc]; + } + } + + /** + * Advance to the next exdate + * @private + */ + _nextExDay() { + this.exDate = this.exDates[++this.exDateInc]; + } + + /** + * Advance to the next rule date + * @private + */ + _nextRuleDay() { + this.ruleDate = this.ruleDates[++this.ruleDateInc]; + } + + /** + * Find and return the recurrence rule with the most recent event and + * return it. + * + * @private + * @return {?RecurIterator} Found iterator. + */ + _nextRecurrenceIter() { + let iters = this.ruleIterators; + + if (iters.length === 0) { + return null; + } + + let len = iters.length; + let iter; + let iterTime; + let iterIdx = 0; + let chosenIter; + + // loop through each iterator + for (; iterIdx < len; iterIdx++) { + iter = iters[iterIdx]; + iterTime = iter.last; + + // if iteration is complete + // then we must exclude it from + // the search and remove it. + if (iter.completed) { + len--; + if (iterIdx !== 0) { + iterIdx--; + } + iters.splice(iterIdx, 1); + continue; + } + + // find the most recent possible choice + if (!chosenIter || chosenIter.last.compare(iterTime) > 0) { + // that iterator is saved + chosenIter = iter; + } + } + + // the chosen iterator is returned but not mutated + // this iterator contains the most recent event. + return chosenIter; + } +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * This lets typescript resolve our custom types in the + * generated d.ts files (jsdoc typedefs are converted to typescript types). + * Ignore prevents the typedefs from being documented more than once. + * @ignore + * @typedef {import("./types.js").frequencyValues} frequencyValues + * Imports the 'frequencyValues' type from the "types.js" module + * @typedef {import("./types.js").occurrenceDetails} occurrenceDetails + * Imports the 'occurrenceDetails' type from the "types.js" module + */ + +/** + * ICAL.js is organized into multiple layers. The bottom layer is a raw jCal + * object, followed by the component/property layer. The highest level is the + * event representation, which this class is part of. See the + * {@tutorial layers} guide for more details. + * + * @memberof ICAL + */ +class Event { + /** + * Creates a new ICAL.Event instance. + * + * @param {Component=} component The ICAL.Component to base this event on + * @param {Object} [options] Options for this event + * @param {Boolean=} options.strictExceptions When true, will verify exceptions are related by + * their UUID + * @param {Array<Component|Event>=} options.exceptions + * Exceptions to this event, either as components or events. If not + * specified exceptions will automatically be set in relation of + * component's parent + */ + constructor(component, options) { + if (!(component instanceof Component)) { + options = component; + component = null; + } + + if (component) { + this.component = component; + } else { + this.component = new Component('vevent'); + } + + this._rangeExceptionCache = Object.create(null); + this.exceptions = Object.create(null); + this.rangeExceptions = []; + + if (options && options.strictExceptions) { + this.strictExceptions = options.strictExceptions; + } + + if (options && options.exceptions) { + options.exceptions.forEach(this.relateException, this); + } else if (this.component.parent && !this.isRecurrenceException()) { + this.component.parent.getAllSubcomponents('vevent').forEach(function(event) { + if (event.hasProperty('recurrence-id')) { + this.relateException(event); + } + }, this); + } + } + + + static THISANDFUTURE = 'THISANDFUTURE'; + + /** + * List of related event exceptions. + * + * @type {Event[]} + */ + exceptions = null; + + /** + * When true, will verify exceptions are related by their UUID. + * + * @type {Boolean} + */ + strictExceptions = false; + + /** + * Relates a given event exception to this object. If the given component + * does not share the UID of this event it cannot be related and will throw + * an exception. + * + * If this component is an exception it cannot have other exceptions + * related to it. + * + * @param {Component|Event} obj Component or event + */ + relateException(obj) { + if (this.isRecurrenceException()) { + throw new Error('cannot relate exception to exceptions'); + } + + if (obj instanceof Component) { + obj = new Event(obj); + } + + if (this.strictExceptions && obj.uid !== this.uid) { + throw new Error('attempted to relate unrelated exception'); + } + + let id = obj.recurrenceId.toString(); + + // we don't sort or manage exceptions directly + // here the recurrence expander handles that. + this.exceptions[id] = obj; + + // index RANGE=THISANDFUTURE exceptions so we can + // look them up later in getOccurrenceDetails. + if (obj.modifiesFuture()) { + let item = [ + obj.recurrenceId.toUnixTime(), id + ]; + + // we keep them sorted so we can find the nearest + // value later on... + let idx = binsearchInsert( + this.rangeExceptions, + item, + compareRangeException + ); + + this.rangeExceptions.splice(idx, 0, item); + } + } + + /** + * Checks if this record is an exception and has the RANGE=THISANDFUTURE + * value. + * + * @return {Boolean} True, when exception is within range + */ + modifiesFuture() { + if (!this.component.hasProperty('recurrence-id')) { + return false; + } + + let range = this.component.getFirstProperty('recurrence-id').getParameter('range'); + return range === Event.THISANDFUTURE; + } + + /** + * Finds the range exception nearest to the given date. + * + * @param {Time} time usually an occurrence time of an event + * @return {?Event} the related event/exception or null + */ + findRangeException(time) { + if (!this.rangeExceptions.length) { + return null; + } + + let utc = time.toUnixTime(); + let idx = binsearchInsert( + this.rangeExceptions, + [utc], + compareRangeException + ); + + idx -= 1; + + // occurs before + if (idx < 0) { + return null; + } + + let rangeItem = this.rangeExceptions[idx]; + + /* c8 ignore next 4 */ + if (utc < rangeItem[0]) { + // sanity check only + return null; + } + + return rangeItem[1]; + } + + /** + * Returns the occurrence details based on its start time. If the + * occurrence has an exception will return the details for that exception. + * + * NOTE: this method is intend to be used in conjunction + * with the {@link ICAL.Event#iterator iterator} method. + * + * @param {Time} occurrence time occurrence + * @return {occurrenceDetails} Information about the occurrence + */ + getOccurrenceDetails(occurrence) { + let id = occurrence.toString(); + let utcId = occurrence.convertToZone(Timezone.utcTimezone).toString(); + let item; + let result = { + //XXX: Clone? + recurrenceId: occurrence + }; + + if (id in this.exceptions) { + item = result.item = this.exceptions[id]; + result.startDate = item.startDate; + result.endDate = item.endDate; + result.item = item; + } else if (utcId in this.exceptions) { + item = this.exceptions[utcId]; + result.startDate = item.startDate; + result.endDate = item.endDate; + result.item = item; + } else { + // range exceptions (RANGE=THISANDFUTURE) have a + // lower priority then direct exceptions but + // must be accounted for first. Their item is + // always the first exception with the range prop. + let rangeExceptionId = this.findRangeException( + occurrence + ); + let end; + + if (rangeExceptionId) { + let exception = this.exceptions[rangeExceptionId]; + + // range exception must modify standard time + // by the difference (if any) in start/end times. + result.item = exception; + + let startDiff = this._rangeExceptionCache[rangeExceptionId]; + + if (!startDiff) { + let original = exception.recurrenceId.clone(); + let newStart = exception.startDate.clone(); + + // zones must be same otherwise subtract may be incorrect. + original.zone = newStart.zone; + startDiff = newStart.subtractDate(original); + + this._rangeExceptionCache[rangeExceptionId] = startDiff; + } + + let start = occurrence.clone(); + start.zone = exception.startDate.zone; + start.addDuration(startDiff); + + end = start.clone(); + end.addDuration(exception.duration); + + result.startDate = start; + result.endDate = end; + } else { + // no range exception standard expansion + end = occurrence.clone(); + end.addDuration(this.duration); + + result.endDate = end; + result.startDate = occurrence; + result.item = this; + } + } + + return result; + } + + /** + * Builds a recur expansion instance for a specific point in time (defaults + * to startDate). + * + * @param {Time=} startTime Starting point for expansion + * @return {RecurExpansion} Expansion object + */ + iterator(startTime) { + return new RecurExpansion({ + component: this.component, + dtstart: startTime || this.startDate + }); + } + + /** + * Checks if the event is recurring + * + * @return {Boolean} True, if event is recurring + */ + isRecurring() { + let comp = this.component; + return comp.hasProperty('rrule') || comp.hasProperty('rdate'); + } + + /** + * Checks if the event describes a recurrence exception. See + * {@tutorial terminology} for details. + * + * @return {Boolean} True, if the event describes a recurrence exception + */ + isRecurrenceException() { + return this.component.hasProperty('recurrence-id'); + } + + /** + * Returns the types of recurrences this event may have. + * + * Returned as an object with the following possible keys: + * + * - YEARLY + * - MONTHLY + * - WEEKLY + * - DAILY + * - MINUTELY + * - SECONDLY + * + * @return {Object.<frequencyValues, Boolean>} + * Object of recurrence flags + */ + getRecurrenceTypes() { + let rules = this.component.getAllProperties('rrule'); + let i = 0; + let len = rules.length; + let result = Object.create(null); + + for (; i < len; i++) { + let value = rules[i].getFirstValue(); + result[value.freq] = true; + } + + return result; + } + + /** + * The uid of this event + * @type {String} + */ + get uid() { + return this._firstProp('uid'); + } + + set uid(value) { + this._setProp('uid', value); + } + + /** + * The start date + * @type {Time} + */ + get startDate() { + return this._firstProp('dtstart'); + } + + set startDate(value) { + this._setTime('dtstart', value); + } + + /** + * The end date. This can be the result directly from the property, or the + * end date calculated from start date and duration. Setting the property + * will remove any duration properties. + * @type {Time} + */ + get endDate() { + let endDate = this._firstProp('dtend'); + if (!endDate) { + let duration = this._firstProp('duration'); + endDate = this.startDate.clone(); + if (duration) { + endDate.addDuration(duration); + } else if (endDate.isDate) { + endDate.day += 1; + } + } + return endDate; + } + + set endDate(value) { + if (this.component.hasProperty('duration')) { + this.component.removeProperty('duration'); + } + this._setTime('dtend', value); + } + + /** + * The duration. This can be the result directly from the property, or the + * duration calculated from start date and end date. Setting the property + * will remove any `dtend` properties. + * @type {Duration} + */ + get duration() { + let duration = this._firstProp('duration'); + if (!duration) { + return this.endDate.subtractDateTz(this.startDate); + } + return duration; + } + + set duration(value) { + if (this.component.hasProperty('dtend')) { + this.component.removeProperty('dtend'); + } + + this._setProp('duration', value); + } + + /** + * The location of the event. + * @type {String} + */ + get location() { + return this._firstProp('location'); + } + + set location(value) { + this._setProp('location', value); + } + + /** + * The attendees in the event + * @type {Property[]} + */ + get attendees() { + //XXX: This is way lame we should have a better + // data structure for this later. + return this.component.getAllProperties('attendee'); + } + + /** + * The event summary + * @type {String} + */ + get summary() { + return this._firstProp('summary'); + } + + set summary(value) { + this._setProp('summary', value); + } + + /** + * The event description. + * @type {String} + */ + get description() { + return this._firstProp('description'); + } + + set description(value) { + this._setProp('description', value); + } + + /** + * The event color from [rfc7986](https://datatracker.ietf.org/doc/html/rfc7986) + * @type {String} + */ + get color() { + return this._firstProp('color'); + } + + set color(value) { + this._setProp('color', value); + } + + /** + * The organizer value as an uri. In most cases this is a mailto: uri, but + * it can also be something else, like urn:uuid:... + * @type {String} + */ + get organizer() { + return this._firstProp('organizer'); + } + + set organizer(value) { + this._setProp('organizer', value); + } + + /** + * The sequence value for this event. Used for scheduling + * see {@tutorial terminology}. + * @type {Number} + */ + get sequence() { + return this._firstProp('sequence'); + } + + set sequence(value) { + this._setProp('sequence', value); + } + + /** + * The recurrence id for this event. See {@tutorial terminology} for details. + * @type {Time} + */ + get recurrenceId() { + return this._firstProp('recurrence-id'); + } + + set recurrenceId(value) { + this._setTime('recurrence-id', value); + } + + /** + * Set/update a time property's value. + * This will also update the TZID of the property. + * + * TODO: this method handles the case where we are switching + * from a known timezone to an implied timezone (one without TZID). + * This does _not_ handle the case of moving between a known + * (by TimezoneService) timezone to an unknown timezone... + * + * We will not add/remove/update the VTIMEZONE subcomponents + * leading to invalid ICAL data... + * @private + * @param {String} propName The property name + * @param {Time} time The time to set + */ + _setTime(propName, time) { + let prop = this.component.getFirstProperty(propName); + + if (!prop) { + prop = new Property(propName); + this.component.addProperty(prop); + } + + // utc and local don't get a tzid + if ( + time.zone === Timezone.localTimezone || + time.zone === Timezone.utcTimezone + ) { + // remove the tzid + prop.removeParameter('tzid'); + } else { + prop.setParameter('tzid', time.zone.tzid); + } + + prop.setValue(time); + } + + _setProp(name, value) { + this.component.updatePropertyWithValue(name, value); + } + + _firstProp(name) { + return this.component.getFirstPropertyValue(name); + } + + /** + * The string representation of this event. + * @return {String} + */ + toString() { + return this.component.toString(); + } +} + +function compareRangeException(a, b) { + if (a[0] > b[0]) return 1; + if (b[0] > a[0]) return -1; + return 0; +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * The ComponentParser is used to process a String or jCal Object, + * firing callbacks for various found components, as well as completion. + * + * @example + * var options = { + * // when false no events will be emitted for type + * parseEvent: true, + * parseTimezone: true + * }; + * + * var parser = new ICAL.ComponentParser(options); + * + * parser.onevent(eventComponent) { + * //... + * } + * + * // ontimezone, etc... + * + * parser.oncomplete = function() { + * + * }; + * + * parser.process(stringOrComponent); + * + * @memberof ICAL + */ +class ComponentParser { + /** + * Creates a new ICAL.ComponentParser instance. + * + * @param {Object=} options Component parser options + * @param {Boolean} options.parseEvent Whether events should be parsed + * @param {Boolean} options.parseTimezeone Whether timezones should be parsed + */ + constructor(options) { + if (typeof(options) === 'undefined') { + options = {}; + } + + for (let [key, value] of Object.entries(options)) { + this[key] = value; + } + } + + /** + * When true, parse events + * + * @type {Boolean} + */ + parseEvent = true; + + /** + * When true, parse timezones + * + * @type {Boolean} + */ + parseTimezone = true; + + + /* SAX like events here for reference */ + + /** + * Fired when parsing is complete + * @callback + */ + oncomplete = /* c8 ignore next */ function() {}; + + /** + * Fired if an error occurs during parsing. + * + * @callback + * @param {Error} err details of error + */ + onerror = /* c8 ignore next */ function(err) {}; + + /** + * Fired when a top level component (VTIMEZONE) is found + * + * @callback + * @param {Timezone} component Timezone object + */ + ontimezone = /* c8 ignore next */ function(component) {}; + + /** + * Fired when a top level component (VEVENT) is found. + * + * @callback + * @param {Event} component Top level component + */ + onevent = /* c8 ignore next */ function(component) {}; + + /** + * Process a string or parse ical object. This function itself will return + * nothing but will start the parsing process. + * + * Events must be registered prior to calling this method. + * + * @param {Component|String|Object} ical The component to process, + * either in its final form, as a jCal Object, or string representation + */ + process(ical) { + //TODO: this is sync now in the future we will have a incremental parser. + if (typeof(ical) === 'string') { + ical = parse(ical); + } + + if (!(ical instanceof Component)) { + ical = new Component(ical); + } + + let components = ical.getAllSubcomponents(); + let i = 0; + let len = components.length; + let component; + + for (; i < len; i++) { + component = components[i]; + + switch (component.name) { + case 'vtimezone': + if (this.parseTimezone) { + let tzid = component.getFirstPropertyValue('tzid'); + if (tzid) { + this.ontimezone(new Timezone({ + tzid: tzid, + component: component + })); + } + } + break; + case 'vevent': + if (this.parseEvent) { + this.onevent(new Event(component)); + } + break; + default: + continue; + } + } + + //XXX: ideally we should do a "nextTick" here + // so in all cases this is actually async. + this.oncomplete(); + } +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Portions Copyright (C) Philipp Kewisch */ + +/** + * The main ICAL module. Provides access to everything else. + * + * @alias ICAL + * @namespace ICAL + * @property {ICAL.design} design + * @property {ICAL.helpers} helpers + */ +var ICALmodule = { + /** + * The number of characters before iCalendar line folding should occur + * @type {Number} + * @default 75 + */ + foldLength: 75, + + debug: false, + + /** + * The character(s) to be used for a newline. The default value is provided by + * rfc5545. + * @type {String} + * @default "\r\n" + */ + newLineChar: '\r\n', + + Binary, + Component, + ComponentParser, + Duration, + Event, + Period, + Property, + Recur, + RecurExpansion, + RecurIterator, + Time, + Timezone, + TimezoneService, + UtcOffset, + VCardTime, + + parse, + stringify, + + design: design$1, + helpers +}; + +return ICALmodule; + +})(); diff --git a/plugins/ics-viewer/index.php b/plugins/ics-viewer/index.php new file mode 100644 index 0000000000..591288392e --- /dev/null +++ b/plugins/ics-viewer/index.php @@ -0,0 +1,22 @@ +<?php + +class ICSViewerPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'ICS Viewer', + AUTHOR = 'PhieF', + VERSION = '1.0', + RELEASE = '2024-09-17', + CATEGORY = 'Messages', + DESCRIPTION = 'Display ICS attachment using ical lib, or JSON-LD details, based on viewICS', + REQUIRED = '2.34.0'; + + public function Init() : void + { +// $this->UseLangs(true); + $this->addJs('message.js'); + $this->addJs('windowsZones.js'); + // Load https://github.com/kewisch/ical.js/releases + $this->addJs('ical.js'); + } +} diff --git a/plugins/ics-viewer/message.js b/plugins/ics-viewer/message.js new file mode 100644 index 0000000000..380b732bb4 --- /dev/null +++ b/plugins/ics-viewer/message.js @@ -0,0 +1,136 @@ +(rl => { + const templateId = 'MailMessageView'; + + addEventListener('rl-view-model.create', e => { + if (templateId === e.detail.viewModelTemplateID) { + + const + template = document.getElementById(templateId), + view = e.detail, + attachmentsPlace = template.content.querySelector('.attachmentsPlace'), + dateRegEx = /(TZID=(?<tz>[^:]+):)?(?<year>[0-9]{4})(?<month>[0-9]{2})(?<day>[0-9]{2})T(?<hour>[0-9]{2})(?<minute>[0-9]{2})(?<second>[0-9]{2})(?<utc>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(` + <details data-bind="if: ICSViewer, visible: ICSViewer"> + <summary data-icon="📅" data-bind="text: ICSViewer().SUMMARY"></summary> + <table><tbody style="white-space:pre"> + <tr data-bind="visible: ICSViewer().ORGANIZER_TXT"><td>Organizer: </td><td><a data-bind="text: ICSViewer().ORGANIZER_TXT, attr: { href: ICSViewer().ORGANIZER_MAIL }"></a></td></tr> + <tr><td>Start: </td><td data-bind="text: ICSViewer().DTSTART"></td></tr> + <tr><td>End: </td><td data-bind="text: ICSViewer().DTEND"></td></tr> + <tr data-bind="visible: ICSViewer().LOCATION"><td>Location: </td><td data-bind="text: ICSViewer().LOCATION"></td></tr> +<!-- <tr><td>Transparency</td><td data-bind="text: ICSViewer().TRANSP"></td></tr>--> + <tr><td>Attendees: </td><td data-bind="foreach: ICSViewer().ATTENDEE"><span data-bind="text: $data.replace(/;/g,';\\n')"></span> </td> + + </tbody></table> + </details>`)); + + view.ICSViewer = 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.ICSViewer(null); + if (msg) { + // JSON-LD after parsing HTML + // See http://schema.org/ + msg.linkedData.subscribe(data => { + if (!view.ICSViewer()) { + 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.ICSViewer(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) { + + // fetch it and parse the VEVENT + rl.fetch(ics.linkDownload()) + .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) + .then(text => { + let jcalData = ICAL.parse(text) + var comp = new ICAL.Component(jcalData); + var vevent = comp.getFirstSubcomponent("vevent"); + var event = new ICAL.Event(vevent); + let VEVENT = {}; + if (event.organizer && event.organizer.startsWith("mailto:")) { + VEVENT.ORGANIZER_TXT = event.organizer.substr(7) + VEVENT.ORGANIZER_MAIL = event.organizer + } else + VEVENT.ORGANIZER_TXT = event.organizer + VEVENT.SUMMARY = event.summary; + VEVENT.DTSTART = parseDate(vevent.getFirstPropertyValue("dtstart")); + VEVENT.DTEND = parseDate(vevent.getFirstPropertyValue("dtend")); + VEVENT.LOCATION = event.location; + VEVENT.ATTENDEE = [] + for (let attendee of event.attendees) { + VEVENT.ATTENDEE.push(attendee.getFirstParameter("cn")); + } + + if (VEVENT) { + VEVENT.rawText = text; + VEVENT.isCancelled = () => VEVENT.STATUS?.includes('CANCELLED'); + VEVENT.isConfirmed = () => VEVENT.STATUS?.includes('CONFIRMED'); + VEVENT.shouldReply = () => VEVENT.METHOD?.includes('REPLY'); + console.dir({ + isCancelled: VEVENT.isCancelled(), + shouldReply: VEVENT.shouldReply() + }); + view.ICSViewer(VEVENT); + } + }); + } + } + }); + } + }); + +})(window.rl); diff --git a/plugins/ics-viewer/style.css b/plugins/ics-viewer/style.css new file mode 100644 index 0000000000..b3a4e5c8ce --- /dev/null +++ b/plugins/ics-viewer/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/ics-viewer/windowsZones.js b/plugins/ics-viewer/windowsZones.js new file mode 100644 index 0000000000..1c2c915c38 --- /dev/null +++ b/plugins/ics-viewer/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/imap-contacts-suggestions/ImapContactsSuggestions.php b/plugins/imap-contacts-suggestions/ImapContactsSuggestions.php new file mode 100644 index 0000000000..9a379ff261 --- /dev/null +++ b/plugins/imap-contacts-suggestions/ImapContactsSuggestions.php @@ -0,0 +1,44 @@ +<?php + +class ImapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions +{ + // TODO: make setting + public $sFolderName = 'INBOX'; + + public function Process(\RainLoop\Model\Account $oAccount, string $sQuery, int $iLimit = 20): array + { + $sQuery = \trim($sQuery); + if (2 > \strlen($sQuery)) { + return []; + } + + $oActions = \RainLoop\Api::Actions(); + $oMailClient = $oActions->MailClient(); + if (!$oMailClient->IsLoggined()) { + $oAccount = $oActions->getAccountFromToken(); + $oAccount->ImapConnectAndLogin($oActions->Plugins(), $oMailClient->ImapClient(), $oActions->Config()); + } + $oImapClient = $oMailClient->ImapClient(); + + $oImapClient->FolderSelect($this->sFolderName); + + $sQuery = \MailSo\Imap\SearchCriterias::escapeSearchString($oImapClient, $sQuery); + $aUids = \array_slice( + $oImapClient->MessageSearch("FROM {$sQuery}"), + 0, $iLimit + ); + + $aResult = []; + if ($aUids) { + foreach ($oImapClient->Fetch(['BODY.PEEK[HEADER.FIELDS (FROM)]'], \implode(',', $aUids), true) as $oFetchResponse) { + $oHeaders = new \MailSo\Mime\HeaderCollection($oFetchResponse->GetHeaderFieldsValue()); + $oFrom = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::FROM_, true); + foreach ($oFrom as $oMail) { + $aResult[] = [$oMail->GetEmail(), $oMail->GetDisplayName()]; + } + } + } + + return $aResult; + } +} diff --git a/plugins/recaptcha/LICENSE b/plugins/imap-contacts-suggestions/LICENSE similarity index 100% rename from plugins/recaptcha/LICENSE rename to plugins/imap-contacts-suggestions/LICENSE diff --git a/plugins/imap-contacts-suggestions/index.php b/plugins/imap-contacts-suggestions/index.php new file mode 100644 index 0000000000..559e777707 --- /dev/null +++ b/plugins/imap-contacts-suggestions/index.php @@ -0,0 +1,39 @@ +<?php + +class ImapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Contacts suggestions (IMAP folder)', + VERSION = '2.36', + RELEASE = '2024-03-19', + CATEGORY = 'Contacts', + DESCRIPTION = 'Get contacts suggestions from IMAP INBOX folder.', + REQUIRED = '2.36.0'; + + public function Init() : void + { + $this->addHook('main.fabrica', 'MainFabrica'); + } + + public function Supported() : string + { + return ''; + } + + /** + * @param mixed $mResult + */ + public function MainFabrica(string $sName, &$mResult) + { + if ('suggestions' === $sName) { + if (!\is_array($mResult)) { + $mResult = array(); + } +// $sFolder = \trim($this->Config()->Get('plugin', 'mailbox', 'INBOX')); +// if ($sFolder) { + include_once __DIR__ . '/ImapContactsSuggestions.php'; + $mResult[] = new ImapContactsSuggestions(); +// } + } + } +} diff --git a/plugins/ispconfig-change-password/IspConfigChangePasswordDriver.php b/plugins/ispconfig-change-password/IspConfigChangePasswordDriver.php deleted file mode 100644 index 40059247a6..0000000000 --- a/plugins/ispconfig-change-password/IspConfigChangePasswordDriver.php +++ /dev/null @@ -1,151 +0,0 @@ -<?php - -class IspConfigChangePasswordDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sDsn = ''; - - /** - * @var string - */ - private $sUser = ''; - - /** - * @var string - */ - private $sPassword = ''; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sDsn - * @param string $sUser - * @param string $sPassword - * - * @return \IspConfigChangePasswordDriver - */ - public function SetConfig($sDsn, $sUser, $sPassword) - { - $this->sDsn = $sDsn; - $this->sUser = $sUser; - $this->sPassword = $sPassword; - - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \IspConfigChangePasswordDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \IspConfigChangePasswordDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Model\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('ISP: Try to change password for '.$oAccount->Email()); - } - - $bResult = false; - if (!empty($this->sDsn) && 0 < \strlen($this->sUser) && 0 < \strlen($this->sPassword) && $oAccount) - { - try - { - $oPdo = new \PDO($this->sDsn, $this->sUser, $this->sPassword); - $oPdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - - $oStmt = $oPdo->prepare('SELECT password, mailuser_id FROM mail_user WHERE login = ? LIMIT 1'); - if ($oStmt->execute(array($oAccount->IncLogin()))) - { - $aFetchResult = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetchResult) && isset($aFetchResult[0]['password'], $aFetchResult[0]['mailuser_id'])) - { - $sDbPassword = \stripslashes($aFetchResult[0]['password']); - $sDbSalt = '$1$'.\substr($sDbPassword, 3, 8).'$'; - - if (\crypt(\stripslashes($sPrevPassword), $sDbSalt) === $sDbPassword) - { - $oStmt = $oPdo->prepare('UPDATE mail_user SET password = ? WHERE mailuser_id = ?'); - $bResult = (bool) $oStmt->execute( - array($this->cryptPassword($sNewPassword), $aFetchResult[0]['mailuser_id'])); - } - } - } - } - catch (\Exception $oException) - { - if ($this->oLogger) - { - $this->oLogger->WriteException($oException); - } - } - } - - return $bResult; - } - - /** - * @param string $sPassword - * @return string - */ - private function cryptPassword($sPassword) - { - $sSalt = ''; - $sBase64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - - for ($iIndex = 0; $iIndex < 8; $iIndex++) - { - $sSalt .= $sBase64[\rand(0, 63)]; - } - - return \crypt($sPassword, '$1$'.$sSalt.'$'); - } -} \ No newline at end of file diff --git a/plugins/ispconfig-change-password/LICENSE b/plugins/ispconfig-change-password/LICENSE deleted file mode 100644 index 271342337d..0000000000 --- a/plugins/ispconfig-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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 -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/ispconfig-change-password/README b/plugins/ispconfig-change-password/README deleted file mode 100644 index 529de7bfb5..0000000000 --- a/plugins/ispconfig-change-password/README +++ /dev/null @@ -1 +0,0 @@ -Plugin that adds functionality to change the email account password (ISPConfig). \ No newline at end of file diff --git a/plugins/ispconfig-change-password/VERSION b/plugins/ispconfig-change-password/VERSION deleted file mode 100644 index b123147e2a..0000000000 --- a/plugins/ispconfig-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 \ No newline at end of file diff --git a/plugins/ispconfig-change-password/index.php b/plugins/ispconfig-change-password/index.php deleted file mode 100644 index 34906074f7..0000000000 --- a/plugins/ispconfig-change-password/index.php +++ /dev/null @@ -1,76 +0,0 @@ -<?php - -class IspconfigChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @return string - */ - public function Supported() - { - if (!extension_loaded('pdo') || !class_exists('PDO')) - { - return 'The PHP extension PDO (mysql) must be installed to use this plugin'; - } - - $aDrivers = \PDO::getAvailableDrivers(); - if (!is_array($aDrivers) || !in_array('mysql', $aDrivers)) - { - return 'The PHP extension PDO (mysql) must be installed to use this plugin'; - } - - return ''; - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - $sDsn = \trim($this->Config()->Get('plugin', 'pdo_dsn', '')); - $sUser = (string) $this->Config()->Get('plugin', 'user', ''); - $sPassword = (string) $this->Config()->Get('plugin', 'password', ''); - - if (!empty($sDsn) && 0 < \strlen($sUser) && 0 < \strlen($sPassword)) - { - include_once __DIR__.'/IspConfigChangePasswordDriver.php'; - - $oProvider = new IspConfigChangePasswordDriver(); - $oProvider->SetLogger($this->Manager()->Actions()->Logger()); - $oProvider->SetConfig($sDsn, $sUser, $sPassword); - $oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))); - } - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('pdo_dsn')->SetLabel('ISPConfig PDO dsn') - ->SetDefaultValue('mysql:host=127.0.0.1;dbname=dbispconfig'), - \RainLoop\Plugins\Property::NewInstance('user')->SetLabel('DB User') - ->SetDefaultValue('root'), - \RainLoop\Plugins\Property::NewInstance('password')->SetLabel('DB Password') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} \ No newline at end of file diff --git a/plugins/ispmail-change-password/ChangePasswordISPmailDriver.php b/plugins/ispmail-change-password/ChangePasswordISPmailDriver.php deleted file mode 100755 index 41857bc131..0000000000 --- a/plugins/ispmail-change-password/ChangePasswordISPmailDriver.php +++ /dev/null @@ -1,276 +0,0 @@ -<?php - -class ChangePasswordISPmailDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sHost = '127.0.0.1'; - - /** - * @var int - */ - private $iPort = 3306; - - /** - * @var string - */ - private $sDatabase = 'mailserver'; - - /** - * @var string - */ - private $sTable = 'virtual_users'; - - /** - * @var string - */ - private $sUsercol = 'email'; - - /** - * @var string - */ - private $sPasscol = 'password'; - - /** - * @var string - */ - private $sUser = 'mailuser'; - - /** - * @var string - */ - private $sPassword = ''; - - /** - * @var string - */ - private $sEncrypt = ''; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sHost - * - * @return \ChangePasswordISPmailDriver - */ - public function SetHost($sHost) - { - $this->sHost = $sHost; - return $this; - } - - /** - * @param int $iPort - * - * @return \ChangePasswordISPmailDriver - */ - public function SetPort($iPort) - { - $this->iPort = (int) $iPort; - return $this; - } - - /** - * @param string $sDatabase - * - * @return \ChangePasswordISPmailDriver - */ - public function SetDatabase($sDatabase) - { - $this->sDatabase = $sDatabase; - return $this; - } - - /** - * @param string $sTable - * - * @return \ChangePasswordISPmailDriver - */ - public function SetTable($sTable) - { - $this->sTable = $sTable; - return $this; - } - - /** - * @param string $sUsercol - * - * @return \ChangePasswordISPmailDriver - */ - public function SetUserColumn($sUsercol) - { - $this->sUsercol = $sUsercol; - return $this; - } - - /** - * @param string $sPasscol - * - * @return \ChangePasswordISPmailDriver - */ - public function SetPasswordColumn($sPasscol) - { - $this->sPasscol = $sPasscol; - return $this; - } - - /** - * @param string $sUser - * - * @return \ChangePasswordISPmailDriver - */ - public function SetUser($sUser) - { - $this->sUser = $sUser; - return $this; - } - - /** - * @param string $sPassword - * - * @return \ChangePasswordISPmailDriver - */ - public function SetPassword($sPassword) - { - $this->sPassword = $sPassword; - return $this; - } - - /** - * @param string $sEncrypt - * - * @return \ChangePasswordISPmailDriver - */ - public function SetEncrypt($sEncrypt) - { - $this->sEncrypt = $sEncrypt; - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \ChangePasswordISPmailDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \ChangePasswordISPmailDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Model\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('ISPmail: Try to change password for '.$oAccount->Email()); - } - - unset($sPrevPassword); - - $bResult = false; - - if (0 < \strlen($sNewPassword)) - { - try - { - $sDsn = 'mysql:host='.$this->sHost.';port='.$this->iPort.';dbname='.$this->sDatabase; - - $oPdo = new \PDO($sDsn, $this->sUser, $this->sPassword); - $oPdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - - $sUpdatePassword = $this->cryptPassword($sNewPassword, $oPdo); - if (0 < \strlen($sUpdatePassword)) - { - $oStmt = $oPdo->prepare("UPDATE {$this->sTable} SET {$this->sPasscol} = ? WHERE {$this->sUsercol} = ?"); - $bResult = (bool) $oStmt->execute(array($sUpdatePassword, $oAccount->Email())); - } - else - { - if ($this->oLogger) - { - $this->oLogger->Write('ISPmail: Encrypted password is empty', - \MailSo\Log\Enumerations\Type::ERROR); - } - } - - $oPdo = null; - } - catch (\Exception $oException) - { - if ($this->oLogger) - { - $this->oLogger->WriteException($oException); - } - } - } - - return $bResult; - } - - /** - * @param string $sPassword - * @param \PDO $oPdo - * - * @return string - */ - private function cryptPassword($sPassword, $oPdo) - { - $sResult = ''; - $sSalt = substr(str_shuffle('./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'), 0, 16); - switch (strtolower($this->sEncrypt)) - { - default: - case 'plain-md5': - $sResult = '{PLAIN-MD5}' . md5($sPassword); - break; - - case 'sha256-crypt': - $sResult = '{SHA256-CRYPT}' . crypt($sPassword,'$5$'.$sSalt); - break; - } - - return $sResult; - } -} diff --git a/plugins/ispmail-change-password/LICENSE b/plugins/ispmail-change-password/LICENSE deleted file mode 100644 index 2730b26221..0000000000 --- a/plugins/ispmail-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Julien Lutran (https://github.com/jlutran) - -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/ispmail-change-password/README b/plugins/ispmail-change-password/README deleted file mode 100644 index 1fc44b8a30..0000000000 --- a/plugins/ispmail-change-password/README +++ /dev/null @@ -1 +0,0 @@ -Plugin that adds functionality to change the email account password (ISPmail). diff --git a/plugins/ispmail-change-password/VERSION b/plugins/ispmail-change-password/VERSION deleted file mode 100644 index d3827e75a5..0000000000 --- a/plugins/ispmail-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 diff --git a/plugins/ispmail-change-password/index.php b/plugins/ispmail-change-password/index.php deleted file mode 100755 index 5dceae56e9..0000000000 --- a/plugins/ispmail-change-password/index.php +++ /dev/null @@ -1,95 +0,0 @@ -<?php - -class IspmailChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @return string - */ - public function Supported() - { - if (!extension_loaded('pdo') || !class_exists('PDO')) - { - return 'The PHP extension PDO (mysql) must be installed to use this plugin'; - } - - $aDrivers = \PDO::getAvailableDrivers(); - if (!is_array($aDrivers) || !in_array('mysql', $aDrivers)) - { - return 'The PHP extension PDO (mysql) must be installed to use this plugin'; - } - - return ''; - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - include_once __DIR__.'/ChangePasswordISPmailDriver.php'; - - $oProvider = new ChangePasswordISPmailDriver(); - - $oProvider - ->SetHost($this->Config()->Get('plugin', 'host', '')) - ->SetPort((int) $this->Config()->Get('plugin', 'port', 3306)) - ->SetDatabase($this->Config()->Get('plugin', 'database', '')) - ->SetTable($this->Config()->Get('plugin', 'table', '')) - ->SetUserColumn($this->Config()->Get('plugin', 'usercol', '')) - ->SetPasswordColumn($this->Config()->Get('plugin', 'passcol', '')) - ->SetUser($this->Config()->Get('plugin', 'user', '')) - ->SetPassword($this->Config()->Get('plugin', 'password', '')) - ->SetEncrypt($this->Config()->Get('plugin', 'encrypt', '')) - ->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))) - ->SetLogger($this->Manager()->Actions()->Logger()) - ; - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('host')->SetLabel('MySQL Host') - ->SetDefaultValue('127.0.0.1'), - \RainLoop\Plugins\Property::NewInstance('port')->SetLabel('MySQL Port') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) - ->SetDefaultValue(3306), - \RainLoop\Plugins\Property::NewInstance('database')->SetLabel('MySQL Database') - ->SetDefaultValue('mailserver'), - \RainLoop\Plugins\Property::NewInstance('table')->SetLabel('MySQL table') - ->SetDefaultValue('virtual_users'), - \RainLoop\Plugins\Property::NewInstance('usercol')->SetLabel('MySQL username column') - ->SetDefaultValue('email'), - \RainLoop\Plugins\Property::NewInstance('passcol')->SetLabel('MySQL password column') - ->SetDefaultValue('password'), - \RainLoop\Plugins\Property::NewInstance('user')->SetLabel('MySQL User') - ->SetDefaultValue('mailuser'), - \RainLoop\Plugins\Property::NewInstance('password')->SetLabel('MySQL Password') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('encrypt')->SetLabel('Encrypt') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) - ->SetDefaultValue(array('PLAIN-MD5', 'SHA256-CRYPT')) - ->SetDescription('In what way do you want the passwords to be crypted ?'), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} diff --git a/plugins/kolab/KolabAddressBook.php b/plugins/kolab/KolabAddressBook.php new file mode 100644 index 0000000000..c934af1d7a --- /dev/null +++ b/plugins/kolab/KolabAddressBook.php @@ -0,0 +1,396 @@ +<?php + +use RainLoop\Providers\AddressBook\Classes\Contact; +use Sabre\VObject\Component\VCard; + +class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInterface +{ + use \RainLoop\Providers\AddressBook\CardDAV; + + protected + $oImapClient, + $sFolderName; + + function __construct(string $sFolderName) + { + $metadata = $this->ImapClient()->FolderGetMetadata($sFolderName, [\MailSo\Imap\Enumerations\MetadataKeys::KOLAB_CTYPE]); + if (!$metadata || 'contact' !== \array_shift($metadata)) { + $sFolderName = ''; +// throw new \Exception("Invalid kolab contact folder: {$sFolderName}"); + } + + $this->sFolderName = $sFolderName; + } + + protected function MailClient() : \MailSo\Mail\MailClient + { + $oActions = \RainLoop\Api::Actions(); + $oMailClient = $oActions->MailClient(); + if (!$oMailClient->IsLoggined()) { + $oActions->getAccountFromToken()->ImapConnectAndLogin($oActions->Plugins(), $oMailClient->ImapClient(), $oActions->Config()); + } + return $oMailClient; + } + + protected function ImapClient() : \MailSo\Imap\ImapClient + { + if (!$this->oImapClient) { + $this->oImapClient = $this->MailClient()->ImapClient(); + } + return $this->oImapClient; + } + + protected function SelectFolder() : bool + { + $sFolderName = $this->sFolderName; + if ($sFolderName) { + try { + $this->ImapClient()->FolderSelect($sFolderName); + return true; + } catch (\Throwable $e) { + \trigger_error("KolabAddressBook {$sFolderName} error: {$e->getMessage()}"); + } + } + return false; + } + + protected function fetchXCardFromMessage(\MailSo\Mail\Message $oMessage) : ?VCard + { + $xCard = null; + try { + foreach ($oMessage->Attachments() ?: [] as $oAttachment) { + if ('application/vcard+xml' === $oAttachment->ContentType()) { + $result = $this->MailClient()->MessageMimeStream(function ($rResource) use (&$xCard) { + if (\is_resource($rResource)) { + $xCard = \Sabre\VObject\Reader::readXML($rResource); + } + }, $this->sFolderName, $oMessage->Uid(), $oAttachment->PartID()); + break; + } + } + } catch (\Throwable $e) { + \error_log("KolabAddressBook message {$oMessage->Uid()} error: {$e->getMessage()}"); + } + return $xCard; + } + + protected function MessageAsContact(\MailSo\Mail\Message $oMessage) : Contact + { + $oContact = new Contact; + $oContact->id = $oMessage->Uid(); + $oContact->Changed = $oMessage->HeaderTimeStampInUTC(); + + // Fetch xCard attachment and populate $oContact with it + $xCard = $this->fetchXCardFromMessage($oMessage); + if ($xCard) { + $oContact->setVCard($xCard); + } + + // Reset, else it is 'urn:uuid:01234567-89AB-CDEF-0123-456789ABCDEF' + $oContact->IdContactStr = $oMessage->Subject(); + $oContact->IdContactStr = \str_replace('urn:uuid:', '', $oContact->IdContactStr); + + return $oContact; + } + + public function IsSupported() : bool + { + // Check $this->ImapClient()->hasCapability('METADATA') + return true; + } + + public function SetEmail(string $sEmail) : bool + { + return true; + } + + /** + * Sync with davClient + */ + public function Sync() : bool + { + // TODO + return false; + } + + public function Export(string $sType = 'vcf') : bool + { + $rCsv = 'csv' === $sType ? \fopen('php://output', 'w') : null; + $bCsvHeader = true; + + if (!\strlen($this->sFolderName)) { +// return false; + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::CantGetMessageList); + } + + $this->ImapClient(); + + try + { + $oParams = new \MailSo\Mail\MessageListParams; + $oParams->sFolderName = $this->sFolderName; +// $oParams->iOffset = 0; + $oParams->iLimit = 999; // Is the max + $oMessageList = $this->MailClient()->MessageList($oParams); + foreach ($oMessageList as $oMessage) { + try { + if ($rCsv) { + $oContact = $this->MessageAsContact($oMessage); + \RainLoop\Providers\AddressBook\Utils::VCardToCsv($rCsv, $oContact->vCard, $bCsvHeader); + $bCsvHeader = false; + } else if ($xCard = $this->fetchXCardFromMessage($oMessage)) { + echo $xCard->serialize(); + } + } catch (\Throwable $oExc) { + $this->oLogger && $this->oLogger->WriteException($oExc); + } + } + } + catch (\Throwable $oException) + { + throw $oException; + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::CantGetMessageList, $oException); + } + return true; + } + + public function ContactSave(Contact $oContact) : bool + { + if (!$this->SelectFolder()) { + return false; + } + + $id = \intval($oContact->id); + $sUID = ''; + + $oVCard = $oContact->vCard; + + $oPrevMessage = $this->MailClient()->Message($this->sFolderName, $id); + if ($oPrevMessage) { + $sUID = $oPrevMessage->Subject(); + if (!$sUID) { + $oVCard = $this->fetchXCardFromMessage($oPrevMessage); + $sUID = \str_replace('urn:uuid:', '', $oVCard->UID); + } + } else { + $id = 0; + } + + if (!$sUID || !\SnappyMail\UUID::isValid($sUID)) { + $sUID = \SnappyMail\UUID::generate(); + } + $oVCard->UID = new \Sabre\VObject\Property\Uri($oVCard, 'uid', 'urn:uuid:' . $sUID); + $oContact->IdContactStr = $sUID; + + if (!\count($oVCard->select('x-kolab-version'))) { + $oVCard->add(new \Sabre\VObject\Property\Text($oVCard, 'x-kolab-version', '3.1.0')); + } + + $oVCard->VERSION = '3.0'; +// $oVCard->PRODID = 'SnappyMail-'.APP_VERSION; + $oVCard->KIND = 'individual'; + + $oMessage = new \MailSo\Mime\Message(); + $oMessage->DoesNotAddDefaultXMailer(); + $oMessage->messageIdRequired = false; + + $sEmail = ''; + if ($oVCard && isset($oVCard->EMAIL)) { + foreach ($oVCard->EMAIL as $oProp) { + $oTypes = $oProp ? $oProp['TYPE'] : null; + $sValue = $oProp ? \trim($oProp->getValue()) : ''; + if ($sValue && (!$sEmail || ($oTypes && $oTypes->has('PREF')))) { + $sEmail = $sValue; + } + } + if ($sEmail) { + $oMessage->SetFrom(new \MailSo\Mime\Email($sEmail, (string) $oVCard->FN)); + } + } + + $oMessage->SetSubject($sUID); +// $oMessage->SetDate(\time()); + $oMessage->SetCustomHeader('X-Kolab-Type', 'application/x-vnd.kolab.contact'); + $oMessage->SetCustomHeader('X-Kolab-Mime-Version', '3.0'); + $oMessage->SetCustomHeader('User-Agent', 'SnappyMail'); + + $oPart = new \MailSo\Mime\Part; + $oPart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'text/plain; charset="us-ascii"'); + $oPart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oPart->Body = "This is a Kolab Groupware object.\r\n" + . "To view this object you will need an email client that can understand the Kolab Groupware format.\r\n" + . "For a list of such email clients please visit\r\n" + . "https://en.wikipedia.org/wiki/Kolab\r\n"; + $oMessage->SubParts->append($oPart); + + // Now the vCard + $oPart = new \MailSo\Mime\Part; + $oPart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'application/vcard+xml; name="kolab.xml"'); + $oPart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, 'quoted-printable'); + $oPart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_DISPOSITION, 'attachment; filename="kolab.xml"'); + $oPart->Body = \quoted_printable_encode(\preg_replace('/\r?\n/s', "\r\n", + \str_replace('encoding="UTF-8"', 'encoding="UTF-8" standalone="no" ', \Sabre\VObject\Writer::writeXml($oVCard)) + )); + $oMessage->SubParts->append($oPart); + + // Store Message + $rMessageStream = \MailSo\Base\ResourceRegistry::CreateMemoryResource(); + $iMessageStreamSize = \MailSo\Base\Utils::MultipleStreamWriter( + $oMessage->ToStream(false), array($rMessageStream), 8192, true, true); + if (false !== $iMessageStreamSize) { + \rewind($rMessageStream); + $this->ImapClient()->MessageReplaceStream($this->sFolderName, $id, $rMessageStream, $iMessageStreamSize); + } + + return true; + } + + public function DeleteContacts(array $aContactIds) : bool + { + try { + $this->MailClient()->MessageDelete( + $this->sFolderName, + new \MailSo\Imap\SequenceSet($aContactIds) + ); +/* + // Delete remote when Mode = read + write + if (1 === $oConfig['Mode']) { + $oClient = $this->getDavClient(); + if ($oClient) { + $sPath = $oClient->__UrlPath__; + $aRemoteSyncData = $this->prepareDavSyncData($oClient, $sPath); + if ($aRemoteSyncData && isset($aRemoteSyncData[$sKey], $aRemoteSyncData[$sKey]['vcf'])) { + $this->davClientRequest($oClient, 'DELETE', $sPath.$aRemoteSyncData[$sKey]['vcf']); + } + } + } +*/ + return true; + } catch (\Throwable $e) { + } + return false; + } + + public function DeleteAllContacts(string $sEmail) : bool + { + // Called by \RainLoop\Api::ClearUserData() + // Not needed as the contacts are inside IMAP mailbox +// $this->MailClient()->FolderClear($this->sFolderName); + return false; + } + + public function GetContacts(int $iOffset = 0, int $iLimit = 20, string $sSearch = '', int &$iResultCount = 0) : array + { + if (!\strlen($this->sFolderName)) { +// return []; + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::CantGetMessageList); + } + + $this->ImapClient(); + + $aResult = []; + + try + { + $oParams = new \MailSo\Mail\MessageListParams; + $oParams->sFolderName = $this->sFolderName; + $oParams->iOffset = $iOffset; + $oParams->iLimit = $iLimit; + if ($sSearch) { + $oParams->sSearch = 'from='.$sSearch; + } + $oParams->sSort = 'FROM'; + + $oMessageList = $this->MailClient()->MessageList($oParams); + foreach ($oMessageList as $oMessage) { + $aResult[] = $this->MessageAsContact($oMessage); + } + } + catch (\Throwable $oException) + { + throw $oException; + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::CantGetMessageList, $oException); + } + + return $aResult; + } + + public function GetContactByEmail(string $sEmail) : ?Contact + { + // TODO + return null; + } + + public function GetContactByID($mID, bool $bIsStrID = false) : ?Contact + { + if ($bIsStrID) { + $oMessage = null; + } else { + $oMessage = $this->MailClient()->Message($this->sFolderName, $mID); + } + return $oMessage ? $this->MessageAsContact($oMessage) : null; + } + + public function GetSuggestions(string $sSearch, int $iLimit = 20) : array + { + $sSearch = \trim($sSearch); + if (2 > \strlen($sSearch) || !$this->SelectFolder()) { + return []; + } + + $sSearch = \MailSo\Imap\SearchCriterias::escapeSearchString($this->ImapClient(), $sSearch); + $aUids = \array_slice( + $this->ImapClient()->MessageSearch("FROM {$sSearch}"), + 0, $iLimit + ); + + $aResult = []; + foreach ($this->ImapClient()->Fetch(['BODY.PEEK[HEADER.FIELDS (FROM)]'], \implode(',', $aUids), true) as $oFetchResponse) { + $oHeaders = new \MailSo\Mime\HeaderCollection($oFetchResponse->GetHeaderFieldsValue()); + $oFrom = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::FROM_, true); + foreach ($oFrom as $oMail) { + $aResult[] = [$oMail->GetEmail(), $oMail->GetDisplayName()]; + } + } + + return $aResult; + } + + public function IncFrec(array $aEmails, bool $bCreateAuto = true) : bool + { + if ($bCreateAuto) { + foreach ($aEmails as $sEmail => $sAddress) { + $sSearch = \MailSo\Imap\SearchCriterias::escapeSearchString($this->ImapClient(), $sEmail); + if (!$this->ImapClient()->MessageSearch("FROM {$sSearch}")) { + $oVCard = new VCard; + $oVCard->add('EMAIL', $sEmail); + $sFullName = \trim(\MailSo\Mime\Email::Parse(\trim($sAddress))->GetDisplayName()); + if ('' !== $sFullName) { + $sFirst = $sLast = ''; + if (false !== \strpos($sFullName, ' ')) { + $aNames = \explode(' ', $sFullName, 2); + $sFirst = isset($aNames[0]) ? $aNames[0] : ''; + $sLast = isset($aNames[1]) ? $aNames[1] : ''; + } else { + $sFirst = $sFullName; + } + if (\strlen($sFirst) || \strlen($sLast)) { + $oVCard->N = array($sLast, $sFirst, '', '', ''); + } + } + $oContact = new Contact(); + $oContact->setVCard($oVCard); + $this->ContactSave($oContact); + } + } + return true; + } + return false; + } + + public function Test() : string + { + // Nothing to test + return ''; + } +} diff --git a/plugins/kolab/LICENSE b/plugins/kolab/LICENSE new file mode 100644 index 0000000000..4aed64b3af --- /dev/null +++ b/plugins/kolab/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 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 +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/kolab/index.php b/plugins/kolab/index.php new file mode 100644 index 0000000000..e0a9063aa7 --- /dev/null +++ b/plugins/kolab/index.php @@ -0,0 +1,94 @@ +<?php + +class KolabPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Kolab', + VERSION = '2.37', + RELEASE = '2024-03-29', + CATEGORY = 'Contacts', + DESCRIPTION = 'Use an Address Book of Kolab.', + REQUIRED = '2.36.0'; + + public function Init() : void + { +// \RainLoop\Api::Config()->Set('contacts', 'enable', true); + if (\RainLoop\Api::Config()->Get('contacts', 'enable', false)) { + $this->UseLangs(true); + + $this->addHook('filter.app-data', 'FilterAppData'); + $this->addHook('main.fabrica', 'MainFabrica'); + + $this->addJs('js/settings.js'); + $this->addTemplate('templates/KolabSettings.html'); + $this->addJsonHook('KolabFolder', 'DoKolabFolder'); + } + } + + public function Supported() : string + { + return ''; + } + + private function Account() : \RainLoop\Model\Account + { + return \RainLoop\Api::Actions()->getAccountFromToken(); + } + + private function SettingsProvider() : \RainLoop\Providers\Settings + { + return \RainLoop\Api::Actions()->SettingsProvider(true); + } + + private function Settings() : \RainLoop\Settings + { + return $this->SettingsProvider()->Load($this->Account()); + } + + public function DoKolabFolder() : array + { +// \error_log(\print_r($this->Manager()->Actions()->GetActionParams(), 1)); + $sValue = $this->jsonParam('contact'); + $oSettings = $this->Settings(); + if (\is_string($sValue)) { + $oSettings->SetConf('KolabContactFolder', $sValue); + $this->SettingsProvider()->Save($this->Account(), $oSettings); + } + return $this->jsonResponse(__FUNCTION__, true); + } + + public function FilterAppData($bAdmin, &$aResult) : void + { +// if ImapClient->hasCapability('METADATA') + if (!$bAdmin && \is_array($aResult) && !empty($aResult['Auth'])) { + $aResult['Capa']['Kolab'] = true; + $aResult['KolabContactFolder'] = (string) $this->Settings()->GetConf('KolabContactFolder', ''); + } + } + + /** + * @param mixed $mResult + */ + public function MainFabrica(string $sName, &$mResult) + { +/* + if ('suggestions' === $sName) { + if (!\is_array($mResult)) { + $mResult = array(); + } +// $sFolder = \trim($this->Config()->Get('plugin', 'mailbox', '')); +// if ($sFolder) { + require_once __DIR__ . '/KolabContactsSuggestions.php'; + $mResult[] = new KolabContactsSuggestions(); +// } + } +*/ + if ('address-book' === $sName) { + $sFolderName = $this->Settings()->GetConf('KolabContactFolder', ''); + if ($sFolderName) { + require_once __DIR__ . '/KolabAddressBook.php'; + $mResult = new KolabAddressBook($sFolderName); + } + } + } +} diff --git a/plugins/kolab/js/settings.js b/plugins/kolab/js/settings.js new file mode 100644 index 0000000000..1f83fb0bde --- /dev/null +++ b/plugins/kolab/js/settings.js @@ -0,0 +1,54 @@ +(rl => { + +const getFolders = type => { + const + aResult = [{ + id: '', + name: '', + }], + foldersWalk = folders => { + folders.forEach(oItem => { + if (type === oItem.kolabType()) { + aResult.push({ + id: oItem.fullName, + name: oItem.fullName + }); + } + if (oItem.subFolders.length) { + foldersWalk(oItem.subFolders()); + } + }); + }; + foldersWalk(rl.app.folderList()); + return aResult; +}; + + +class KolabSettings /* extends AbstractViewSettings */ +{ + constructor() { + this.contactFolder = ko.observable(rl.settings.get('KolabContactFolder')); +// rl.app.FolderUserStore.hasCapability('METADATA'); + this.contactFolder.subscribe(value => { + rl.pluginRemoteRequest(()=>{}, 'KolabFolder', { + contact: value + }); + }); + this.contactFoldersList = ko.computed(() => getFolders('contact'), {'pure':true}); +// this.eventFoldersList = ko.computed(() => getFolders('event'), {'pure':true}); +// this.taskFoldersList = ko.computed(() => getFolders('task'), {'pure':true}); +// this.noteFoldersList = ko.computed(() => getFolders('note'), {'pure':true}); +// this.fileFoldersList = ko.computed(() => getFolders('file'), {'pure':true}); +// this.journalFoldersList = ko.computed(() => getFolders('journal'), {'pure':true}); +// this.configFoldersList = ko.computed(() => getFolders('configuration'), {'pure':true}); + } +} + +rl.addSettingsViewModel( + KolabSettings, + 'KolabSettings', + 'Kolab', + 'kolab' +); + +})(window.rl); diff --git a/plugins/kolab/langs/ar.json b/plugins/kolab/langs/ar.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/ar.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/bg.json b/plugins/kolab/langs/bg.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/bg.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/cs.json b/plugins/kolab/langs/cs.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/cs.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/da.json b/plugins/kolab/langs/da.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/da.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/de.json b/plugins/kolab/langs/de.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/de.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/el.json b/plugins/kolab/langs/el.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/el.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/en.json b/plugins/kolab/langs/en.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/en.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/es.json b/plugins/kolab/langs/es.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/es.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/et.json b/plugins/kolab/langs/et.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/et.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/fa.json b/plugins/kolab/langs/fa.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/fa.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/fi.json b/plugins/kolab/langs/fi.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/fi.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/fr.json b/plugins/kolab/langs/fr.json new file mode 100644 index 0000000000..da740ca950 --- /dev/null +++ b/plugins/kolab/langs/fr.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Dossier Contacts" + } +} diff --git a/plugins/kolab/langs/hu.json b/plugins/kolab/langs/hu.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/hu.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/id.json b/plugins/kolab/langs/id.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/id.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/is.json b/plugins/kolab/langs/is.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/is.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/it.json b/plugins/kolab/langs/it.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/it.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/ja.json b/plugins/kolab/langs/ja.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/ja.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/ko.json b/plugins/kolab/langs/ko.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/ko.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/lt.json b/plugins/kolab/langs/lt.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/lt.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/lv.json b/plugins/kolab/langs/lv.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/lv.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/nb.json b/plugins/kolab/langs/nb.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/nb.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/nl.json b/plugins/kolab/langs/nl.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/nl.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/pl.json b/plugins/kolab/langs/pl.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/pl.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/pt.json b/plugins/kolab/langs/pt.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/pt.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/ro.json b/plugins/kolab/langs/ro.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/ro.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/ru.json b/plugins/kolab/langs/ru.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/ru.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/sk.json b/plugins/kolab/langs/sk.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/sk.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/sl.json b/plugins/kolab/langs/sl.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/sl.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/sv.json b/plugins/kolab/langs/sv.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/sv.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/tr.json b/plugins/kolab/langs/tr.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/tr.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/uk.json b/plugins/kolab/langs/uk.json new file mode 100644 index 0000000000..39fe71e652 --- /dev/null +++ b/plugins/kolab/langs/uk.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "Contacts Folder" + } +} diff --git a/plugins/kolab/langs/zh-TW.json b/plugins/kolab/langs/zh-TW.json new file mode 100644 index 0000000000..a635a4ef5e --- /dev/null +++ b/plugins/kolab/langs/zh-TW.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "聯絡人名刺夾" + } +} diff --git a/plugins/kolab/langs/zh.json b/plugins/kolab/langs/zh.json new file mode 100644 index 0000000000..25cbc5656e --- /dev/null +++ b/plugins/kolab/langs/zh.json @@ -0,0 +1,5 @@ +{ + "KOLAB": { + "CONTACTS_FOLDER": "联系人文件夹" + } +} diff --git a/plugins/kolab/templates/KolabSettings.html b/plugins/kolab/templates/KolabSettings.html new file mode 100644 index 0000000000..71cd3cc93d --- /dev/null +++ b/plugins/kolab/templates/KolabSettings.html @@ -0,0 +1,12 @@ +<div class="b-settings-kolab form-horizontal"> + <div class="legend">Kolab</div> + <div class="form-horizontal"> + + <div class="control-group"> + <label data-i18n="KOLAB/CONTACTS_FOLDER"></label> + <select data-bind="options: contactFoldersList, value: contactFolder, + optionsText: 'name', optionsValue: 'id'"></select> + </div> + + </div> +</div> diff --git a/plugins/ldap-change-password/ChangePasswordLdapDriver.php b/plugins/ldap-change-password/ChangePasswordLdapDriver.php deleted file mode 100644 index 27d735268e..0000000000 --- a/plugins/ldap-change-password/ChangePasswordLdapDriver.php +++ /dev/null @@ -1,242 +0,0 @@ -<?php - -class ChangePasswordLdapDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sHostName = '127.0.0.1'; - - /** - * @var int - */ - private $iHostPort = 389; - - /** - * @var string - */ - private $sUserDnFormat = ''; - - /** - * @var string - */ - private $sPasswordField = 'userPassword'; - - /** - * @var string - */ - private $sPasswordEncType = 'SHA'; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @param string $sHostName - * @param int $iHostPort - * @param string $sUserDnFormat - * @param string $sPasswordField - * @param string $sPasswordEncType - * - * @return \ChangePasswordLdapDriver - */ - public function SetConfig($sHostName, $iHostPort, $sUserDnFormat, $sPasswordField, $sPasswordEncType) - { - $this->sHostName = $sHostName; - $this->iHostPort = $iHostPort; - $this->sUserDnFormat = $sUserDnFormat; - $this->sPasswordField = $sPasswordField; - $this->sPasswordEncType = $sPasswordEncType; - - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \ChangePasswordLdapDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \ChangePasswordLdapDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - $bResult = false; - - try - { - $sDomain = \MailSo\Base\Utils::GetDomainFromEmail($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:domain}' => $sDomain, - '{login}' => $oAccount->Login(), - '{imap:login}' => $oAccount->Login(), - '{imap:host}' => $oAccount->DomainIncHost(), - '{imap:port}' => $oAccount->DomainIncPort(), - '{gecos}' => function_exists('posix_getpwnam') ? posix_getpwnam($oAccount->Login()) : '' - )); - - $oCon = @\ldap_connect($this->sHostName, $this->iHostPort); - if ($oCon) - { - if (!@\ldap_set_option($oCon, LDAP_OPT_PROTOCOL_VERSION, 3)) - { - $this->oLogger->Write( - 'Failed to set LDAP Protocol version to 3, TLS not supported.', - \MailSo\Log\Enumerations\Type::WARNING, - 'LDAP' - ); - } - else if (@!ldap_start_tls($oCon)) - { - $this->oLogger->Write("ldap_start_tls failed: ".$oCon, \MailSo\Log\Enumerations\Type::WARNING, 'LDAP'); - } - - if (!@\ldap_bind($oCon, $sUserDn, $sPrevPassword)) - { - if ($this->oLogger) - { - $sError = $oCon ? @\ldap_error($oCon) : ''; - $iErrno = $oCon ? @\ldap_errno($oCon) : 0; - - $this->oLogger->Write('ldap_bind error: '.$sError.' ('.$iErrno.')', - \MailSo\Log\Enumerations\Type::WARNING, 'LDAP'); - } - - return false; - } - } - else - { - return false; - } - - $sSshaSalt = ''; - $sShaPrefix = '{SHA}'; - $sEncodedNewPassword = $sNewPassword; - switch (\strtolower($this->sPasswordEncType)) - { - case 'ssha': - $sSshaSalt = $this->getSalt(4); - $sShaPrefix = '{SSHA}'; - case 'sha': - switch (true) - { - default: - case \function_exists('sha1'): - $sEncodedNewPassword = $sShaPrefix.\base64_encode(\sha1($sNewPassword.$sSshaSalt, true).$sSshaSalt); - break; - case \function_exists('hash'): - $sEncodedNewPassword = $sShaPrefix.\base64_encode(\hash('sha1', $sNewPassword, true).$sSshaSalt); - break; - case \function_exists('mhash') && defined('MHASH_SHA1'): - $sEncodedNewPassword = $sShaPrefix.\base64_encode(\mhash(MHASH_SHA1, $sNewPassword).$sSshaSalt); - break; - } - break; - case 'md5': - $sEncodedNewPassword = '{MD5}'.\base64_encode(\pack('H*', \md5($sNewPassword))); - break; - case 'crypt': - $sEncodedNewPassword = '{CRYPT}'.\crypt($sNewPassword, $this->getSalt(2)); - break; - } - - $aEntry = array(); - $aEntry[$this->sPasswordField] = (string) $sEncodedNewPassword; - - if (!!@\ldap_modify($oCon, $sUserDn, $aEntry)) - { - $bResult = true; - } - else - { - if ($this->oLogger) - { - $sError = $oCon ? @\ldap_error($oCon) : ''; - $iErrno = $oCon ? @\ldap_errno($oCon) : 0; - - $this->oLogger->Write('ldap_modify error: '.$sError.' ('.$iErrno.')', - \MailSo\Log\Enumerations\Type::WARNING, 'LDAP'); - } - } - } - catch (\Exception $oException) - { - if ($this->oLogger) - { - $this->oLogger->WriteException($oException, - \MailSo\Log\Enumerations\Type::WARNING, 'LDAP'); - } - - $bResult = false; - } - - return $bResult; - } - - /** - * @param int $iLength - * - * @return string - */ - private function getSalt($iLength) - { - $sChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - $iCharsLength = \strlen($sChars); - - $sResult = ''; - while (\strlen($sResult) < $iLength) - { - $sResult .= \substr($sChars, \rand() % $iCharsLength, 1); - } - - return $sResult; - } -} diff --git a/plugins/ldap-change-password/README b/plugins/ldap-change-password/README deleted file mode 100644 index 53c1abf7f6..0000000000 --- a/plugins/ldap-change-password/README +++ /dev/null @@ -1 +0,0 @@ -Plugin that adds functionality to change the email account password (LDAP Password). diff --git a/plugins/ldap-change-password/VERSION b/plugins/ldap-change-password/VERSION deleted file mode 100644 index b123147e2a..0000000000 --- a/plugins/ldap-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 \ No newline at end of file diff --git a/plugins/ldap-change-password/index.php b/plugins/ldap-change-password/index.php deleted file mode 100644 index 128557937f..0000000000 --- a/plugins/ldap-change-password/index.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php - -class LdapChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @return string - */ - public function Supported() - { - if (!\function_exists('ldap_connect')) - { - return 'The LDAP PHP extension must be installed to use this plugin'; - } - - return ''; - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - $sHostName = \trim($this->Config()->Get('plugin', 'hostname', '')); - $iHostPort = (int) $this->Config()->Get('plugin', 'port', 389); - $sUserDnFormat = \trim($this->Config()->Get('plugin', 'user_dn_format', '')); - $sPasswordField = \trim($this->Config()->Get('plugin', 'password_field', '')); - $sPasswordEncType = \trim($this->Config()->Get('plugin', 'password_enc_type', '')); - - if (!empty($sHostName) && 0 < $iHostPort && !empty($sUserDnFormat) && !empty($sPasswordField) && !empty($sPasswordEncType)) - { - include_once __DIR__.'/ChangePasswordLdapDriver.php'; - - $oProvider = new \ChangePasswordLdapDriver(); - - $oProvider - ->SetConfig($sHostName, $iHostPort, $sUserDnFormat, $sPasswordField, $sPasswordEncType) - ->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))) - ->SetLogger($this->Manager()->Actions()->Logger()) - ; - } - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('hostname')->SetLabel('LDAP hostname') - ->SetDefaultValue('127.0.0.1'), - \RainLoop\Plugins\Property::NewInstance('port')->SetLabel('LDAP port') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) - ->SetDefaultValue(389), - \RainLoop\Plugins\Property::NewInstance('user_dn_format')->SetLabel('User DN format') - ->SetDescription('LDAP user dn format. Supported tokens: {email}, {email:user}, {email:domain}, {login}, {domain}, {domain:dc}, {imap:login}, {imap:host}, {imap:port}, {gecos}') - ->SetDefaultValue('uid={imap:login},ou=Users,{domain:dc}'), - \RainLoop\Plugins\Property::NewInstance('password_field')->SetLabel('Password field') - ->SetDefaultValue('userPassword'), - \RainLoop\Plugins\Property::NewInstance('password_enc_type')->SetLabel('Encryption type') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) - ->SetDefaultValue(array('SHA', 'SSHA', 'MD5', 'Crypt', 'Clear')), - \RainLoop\Plugins\Property::NewInstance('allowed_emails')->SetLabel('Allowed emails') - ->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net') - ->SetDefaultValue('*') - ); - } -} diff --git a/plugins/ldap-contacts-suggestions/LdapContactsSuggestions.php b/plugins/ldap-contacts-suggestions/LdapContactsSuggestions.php index f8c985709c..d7eb3eb9a0 100644 --- a/plugins/ldap-contacts-suggestions/LdapContactsSuggestions.php +++ b/plugins/ldap-contacts-suggestions/LdapContactsSuggestions.php @@ -2,297 +2,216 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions { - /** - * @var string - */ - private $sHostName = '127.0.0.1'; + use \MailSo\Log\Inherit; - /** - * @var int - */ - private $iHostPort = 389; + private string $sLdapUri = 'ldap://localhost:389'; - /** - * @var string - */ - private $sAccessDn = null; + private bool $bUseStartTLS = true; - /** - * @var string - */ - private $sAccessPassword = null; + private string $sBindDn = ''; - /** - * @var string - */ - private $sUsersDn = ''; + private string $sBindPassword = ''; - /** - * @var string - */ - private $sObjectClass = 'inetOrgPerson'; + private string $sBaseDn = 'ou=People,dc=example,dc=com'; - /** - * @var string - */ - private $sUidField = 'uid'; + private string $sObjectClasses = 'inetOrgPerson'; - /** - * @var string - */ - private $sNameField = 'givenname'; + private string $sUidAttributes = 'uid'; - /** - * @var string - */ - private $sEmailField = 'mail'; + private string $sNameAttributes = 'displayName,cn,givenName,sn'; - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; + private string $sEmailAttributes = 'mailAddress,mail,mailAlternateAddress,mailAlias'; - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @param string $sHostName - * @param int $iHostPort - * @param string $sAccessDn - * @param string $sAccessPassword - * @param string $sUsersDn - * @param string $sObjectClass - * @param string $sNameField - * @param string $sEmailField - * - * @return \LdapContactsSuggestions - */ - public function SetConfig($sHostName, $iHostPort, $sAccessDn, $sAccessPassword, $sUsersDn, $sObjectClass, $sUidField, $sNameField, $sEmailField) - { - $this->sHostName = $sHostName; - $this->iHostPort = $iHostPort; - if (0 < \strlen($sAccessDn)) - { - $this->sAccessDn = $sAccessDn; - $this->sAccessPassword = $sAccessPassword; - } - $this->sUsersDn = $sUsersDn; - $this->sObjectClass = $sObjectClass; - $this->sUidField = $sUidField; - $this->sNameField = $sNameField; - $this->sEmailField = $sEmailField; - - return $this; - } + private string $sAllowedEmails = '*'; /** + * @param string $sLdapUri + * @param bool $bUseStartTLS + * @param string $sBindDn + * @param string $sBindPassword + * @param string $sBaseDn + * @param string $sObjectClasses + * @param string $sNameAttributes + * @param string $sEmailAttributes + * @param string $sUidAttributes * @param string $sAllowedEmails * * @return \LdapContactsSuggestions */ - public function SetAllowedEmails($sAllowedEmails) + public function SetConfig($sLdapUri, $bUseStartTLS, $sBindDn, $sBindPassword, $sBaseDn, $sObjectClasses, $sUidAttributes, $sNameAttributes, $sEmailAttributes, $sAllowedEmails) { + $this->sLdapUri = $sLdapUri; + $this->bUseStartTLS = $bUseStartTLS; + if (\strlen($sBindDn)) { + $this->sBindDn = $sBindDn; + $this->sBindPassword = $sBindPassword; + } + $this->sBaseDn = $sBaseDn; + $this->sObjectClasses = $sObjectClasses; + $this->sUidAttributes = $sUidAttributes; + $this->sNameAttributes = $sNameAttributes; + $this->sEmailAttributes = $sEmailAttributes; $this->sAllowedEmails = $sAllowedEmails; return $this; } - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sQuery - * @param int $iLimit = 20 - * - * @return array - */ - public function Process($oAccount, $sQuery, $iLimit = 20) + public function Process(\RainLoop\Model\Account $oAccount, string $sQuery, int $iLimit = 20): array { $sQuery = \trim($sQuery); - if (2 > \strlen($sQuery)) + if (2 > \strlen($sQuery) + || !$oAccount + || !\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails)) { return array(); } - else if (!$oAccount || !\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails)) - { - return array(); - } - - $aResult = $this->ldapSearch($oAccount, $sQuery); - - $aResult = \RainLoop\Utils::RemoveSuggestionDuplicates($aResult); - if ($iLimit < \count($aResult)) - { - $aResult = \array_slice($aResult, 0, $iLimit); - } - - return $aResult; - } - - /** - * @param array $aLdapItem - * @param array $aEmailFields - * @param array $aNameFields - * - * @return array - */ - private function findNameAndEmail($aLdapItem, $aEmailFields, $aNameFields, $aUidFields) - { - $sEmail = $sName = $sUid = ''; - if ($aLdapItem) - { - foreach ($aEmailFields as $sField) - { - if (!empty($aLdapItem[$sField][0])) - { - $sEmail = \trim($aLdapItem[$sField][0]); - if (!empty($sEmail)) - { - break; - } - } - } - foreach ($aNameFields as $sField) - { - if (!empty($aLdapItem[$sField][0])) - { - $sName = \trim($aLdapItem[$sField][0]); - if (!empty($sName)) - { - break; - } - } - } - - foreach ($aUidFields as $sField) - { - if (!empty($aLdapItem[$sField][0])) - { - $sUid = \trim($aLdapItem[$sField][0]); - if (!empty($sUid)) - { - break; - } - } - } - } - - return array($sEmail, $sName, $sUid); - } - - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sQuery - * - * @return array - */ - private function ldapSearch($oAccount, $sQuery) - { $sSearchEscaped = $this->escape($sQuery); $aResult = array(); - $oCon = @\ldap_connect($this->sHostName, $this->iHostPort); - if ($oCon) - { - $this->oLogger->Write('ldap_connect: connected', \MailSo\Log\Enumerations\Type::INFO, 'LDAP'); + $oCon = @\ldap_connect($this->sLdapUri); + if ($oCon) { + $this->logWrite('ldap_connect: connected', \LOG_INFO, 'LDAP'); @\ldap_set_option($oCon, LDAP_OPT_PROTOCOL_VERSION, 3); - if (!@\ldap_bind($oCon, $this->sAccessDn, $this->sAccessPassword)) - { - if (is_null($this->sAccessDn)) - { + if ($this->bUseStartTLS && !@\ldap_start_tls($oCon)) { + $this->logLdapError($oCon, 'ldap_start_tls'); + return $aResult; + } + + if (!@\ldap_bind($oCon, $this->sBindDn, $this->sBindPassword)) { + if (\is_null($this->sBindDn)) { $this->logLdapError($oCon, 'ldap_bind (anonymous)'); - } - else - { + } else { $this->logLdapError($oCon, 'ldap_bind'); } - return $aResult; } - $sDomain = \MailSo\Base\Utils::GetDomainFromEmail($oAccount->Email()); - $sSearchDn = \strtr($this->sUsersDn, array( + $sDomain = \MailSo\Base\Utils::getEmailAddressDomain($oAccount->Email()); + $sBaseDn = \strtr($this->sBaseDn, 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->Login(), - '{imap:login}' => $oAccount->Login(), - '{imap:host}' => $oAccount->DomainIncHost(), - '{imap:port}' => $oAccount->DomainIncPort() + '{login}' => $oAccount->IncLogin(), + '{imap:login}' => $oAccount->IncLogin(), + '{imap:host}' => $oAccount->Domain()->ImapSettings()->host, + '{imap:port}' => $oAccount->Domain()->ImapSettings()->port )); - $aEmails = empty($this->sEmailField) ? array() : \explode(',', $this->sEmailField); - $aNames = empty($this->sNameField) ? array() : \explode(',', $this->sNameField); - $aUIDs = empty($this->sUidField) ? array() : \explode(',', $this->sUidField); + $aObjectClasses = empty($this->sObjectClasses) ? array() : \explode(',', $this->sObjectClasses); + $aEmails = empty($this->sEmailAttributes) ? array() : \explode(',', $this->sEmailAttributes); + $aNames = empty($this->sNameAttributes) ? array() : \explode(',', $this->sNameAttributes); + $aUIDs = empty($this->sUidAttributes) ? array() : \explode(',', $this->sUidAttributes); + $aObjectClasses = \array_map('trim', $aObjectClasses); $aEmails = \array_map('trim', $aEmails); $aNames = \array_map('trim', $aNames); $aUIDs = \array_map('trim', $aUIDs); $aFields = \array_merge($aEmails, $aNames, $aUIDs); + $iObjCount = 0; + $sObjFilter = ''; + foreach ($aObjectClasses as $sItem) { + if (!empty($sItem)) { + ++$iObjCount; + $sObjFilter .= '(objectClass='.$sItem.')'; + } + } + + $aItems = array(); $sSubFilter = ''; - foreach ($aFields as $sItem) - { - if (!empty($sItem)) - { + foreach ($aFields as $sItem) { + if (!empty($sItem)) { $aItems[] = $sItem; $sSubFilter .= '('.$sItem.'=*'.$sSearchEscaped.'*)'; } } - $sFilter = '(&(objectclass='.$this->sObjectClass.')'; + $sFilter = '(&'; + $sFilter .= (1 < $iObjCount ? '(|' : '').$sObjFilter.(1 < $iObjCount ? ')' : ''); $sFilter .= (1 < count($aItems) ? '(|' : '').$sSubFilter.(1 < count($aItems) ? ')' : ''); $sFilter .= ')'; - $this->oLogger->Write('ldap_search: start: '.$sSearchDn.' / '.$sFilter, \MailSo\Log\Enumerations\Type::INFO, 'LDAP'); - $oS = @\ldap_search($oCon, $sSearchDn, $sFilter, $aItems, 0, 30, 30); - if ($oS) - { + $this->logWrite('ldap_search: start: '.$sBaseDn.' / '.$sFilter, \LOG_INFO, 'LDAP'); + $oS = @\ldap_search($oCon, $sBaseDn, $sFilter, $aItems, 0, 30, 30); + if ($oS) { $aEntries = @\ldap_get_entries($oCon, $oS); - if (is_array($aEntries)) - { - if (isset($aEntries['count'])) - { + if (is_array($aEntries)) { + if (isset($aEntries['count'])) { unset($aEntries['count']); } - foreach ($aEntries as $aItem) - { - if ($aItem) - { + foreach ($aEntries as $aItem) { + if ($aItem) { $sName = $sEmail = ''; list ($sEmail, $sName) = $this->findNameAndEmail($aItem, $aEmails, $aNames, $aUIDs); - if (!empty($sEmail)) - { + if (!empty($sEmail)) { $aResult[] = array($sEmail, $sName); } } } - } - else - { + } else { $this->logLdapError($oCon, 'ldap_get_entries'); } - } - else - { + } else { $this->logLdapError($oCon, 'ldap_search'); } } - else - { - return $aResult; + + return \array_slice($aResult, 0, $iLimit); + } + + /** + * @param array $aLdapItem + * @param array $aEmailAttributes + * @param array $aNameAttributes + * @param array $aUidAttributes + * + * @return array + */ + private function findNameAndEmail($aLdapItem, $aEmailAttributes, $aNameAttributes, $aUidAttributes) + { + $sEmail = $sName = $sUid = ''; + if ($aLdapItem) { + foreach ($aEmailAttributes as $sField) { + $sField = \strtolower($sField); + if (!empty($aLdapItem[$sField][0])) { + $sEmail = \trim($aLdapItem[$sField][0]); + if (!empty($sEmail)) { + break; + } + } + } + + foreach ($aNameAttributes as $sField) { + $sField = \strtolower($sField); + if (!empty($aLdapItem[$sField][0])) { + $sName = \trim($aLdapItem[$sField][0]); + if (!empty($sName)) { + break; + } + } + } + + foreach ($aUidAttributes as $sField) { + $sField = \strtolower($sField); + if (!empty($aLdapItem[$sField][0])) { + $sUid = \trim($aLdapItem[$sField][0]); + if (!empty($sUid)) { + break; + } + } + } } - return $aResult; + return array($sEmail, $sName, $sUid); } /** @@ -305,8 +224,7 @@ public function escape($sStr) $aNewChars = array(); $aChars = array('\\', '*', '(', ')', \chr(0)); - foreach ($aChars as $iIndex => $sValue) - { + foreach ($aChars as $iIndex => $sValue) { $aNewChars[$iIndex] = '\\'.\str_pad(\dechex(\ord($sValue)), 2, '0'); } @@ -321,28 +239,10 @@ public function escape($sStr) */ public function logLdapError($oCon, $sCmd) { - if ($this->oLogger) - { + if ($this->oLogger) { $sError = $oCon ? @\ldap_error($oCon) : ''; $iErrno = $oCon ? @\ldap_errno($oCon) : 0; - - $this->oLogger->Write($sCmd.' error: '.$sError.' ('.$iErrno.')', - \MailSo\Log\Enumerations\Type::WARNING, 'LDAP'); + $this->logWrite($sCmd.' error: '.$sError.' ('.$iErrno.')', \LOG_WARNING, 'LDAP'); } } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \LdapContactsSuggestions - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } } diff --git a/plugins/ldap-contacts-suggestions/VERSION b/plugins/ldap-contacts-suggestions/VERSION deleted file mode 100644 index 9459d4ba2a..0000000000 --- a/plugins/ldap-contacts-suggestions/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 diff --git a/plugins/ldap-contacts-suggestions/index.php b/plugins/ldap-contacts-suggestions/index.php index a75363d971..dbbf2d515e 100644 --- a/plugins/ldap-contacts-suggestions/index.php +++ b/plugins/ldap-contacts-suggestions/index.php @@ -2,19 +2,24 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin { - public function Init() + const + NAME = 'Contacts suggestions (LDAP)', + VERSION = '2.14', + RELEASE = '2024-03-12', + REQUIRED = '2.35.3', + CATEGORY = 'Contacts', + DESCRIPTION = 'Get contacts suggestions from LDAP.'; + + public function Init() : void { $this->addHook('main.fabrica', 'MainFabrica'); } - /** - * @return string - */ - public function Supported() + public function Supported() : string { if (!\function_exists('ldap_connect')) { - return 'The LDAP PHP extension must be installed to use this plugin'; + return 'The LDAP PHP extension must be installed to use this extension'; } return ''; @@ -26,69 +31,70 @@ public function Supported() */ public function MainFabrica($sName, &$mResult) { - switch ($sName) - { - case 'suggestions': + if ('suggestions' == $sName) { + if (!\is_array($mResult)) { + $mResult = array(); + } - if (!\is_array($mResult)) - { - $mResult = array(); - } + $sLdapUri = \trim($this->Config()->Get('plugin', 'ldap_uri', '')); + $sBaseDn = \trim($this->Config()->Get('plugin', 'base_dn', '')); + $sObjectClasses = \trim($this->Config()->Get('plugin', 'object_classes', '')); + $sEmailAttributes = \trim($this->Config()->Get('plugin', 'mail_attributes', '')); - $sHostName = \trim($this->Config()->Get('plugin', 'hostname', '')); - $iHostPort = (int) $this->Config()->Get('plugin', 'port', 389); - $sAccessDn = \trim($this->Config()->Get('plugin', 'access_dn', '')); - $sAccessPassword = \trim($this->Config()->Get('plugin', 'access_password', '')); - $sUsersDn = \trim($this->Config()->Get('plugin', 'users_dn_format', '')); - $sObjectClass = \trim($this->Config()->Get('plugin', 'object_class', '')); - $sSearchField = \trim($this->Config()->Get('plugin', 'search_field', '')); - $sNameField = \trim($this->Config()->Get('plugin', 'name_field', '')); - $sEmailField = \trim($this->Config()->Get('plugin', 'mail_field', '')); + if (\strlen($sLdapUri) && \strlen($sBaseDn) && \strlen($sObjectClasses) && \strlen($sEmailAttributes)) { + require_once __DIR__.'/LdapContactsSuggestions.php'; - if (0 < \strlen($sUsersDn) && 0 < \strlen($sObjectClass) && 0 < \strlen($sEmailField)) - { - include_once __DIR__.'/LdapContactsSuggestions.php'; + $oProvider = new LdapContactsSuggestions(); + $oProvider->SetConfig( + $sLdapUri, + (bool) $this->Config()->Get('plugin', 'use_start_tls', true), + \trim($this->Config()->Get('plugin', 'bind_dn', '')), + \trim($this->Config()->Get('plugin', 'bind_password', '')), + $sBaseDn, + $sObjectClasses, + \trim($this->Config()->Get('plugin', 'uid_attributes', '')), + \trim($this->Config()->Get('plugin', 'name_attributes', '')), + $sEmailAttributes, + \trim($this->Config()->Get('plugin', 'allowed_emails', '')) + ); - $oProvider = new LdapContactsSuggestions(); - $oProvider->SetConfig($sHostName, $iHostPort, $sAccessDn, $sAccessPassword, $sUsersDn, $sObjectClass, $sSearchField, $sNameField, $sEmailField); - - $mResult[] = $oProvider; - } - - break; + $mResult[] = $oProvider; + } } } - /** - * @return array - */ - public function configMapping() + protected function configMapping() : array { return array( - \RainLoop\Plugins\Property::NewInstance('hostname')->SetLabel('LDAP hostname') - ->SetDefaultValue('127.0.0.1'), - \RainLoop\Plugins\Property::NewInstance('port')->SetLabel('LDAP port') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) - ->SetDefaultValue(389), - \RainLoop\Plugins\Property::NewInstance('access_dn')->SetLabel('Access dn (login)') - ->SetDescription('LDAP bind DN to authentifcate with. If left blank, anonymous bind will be tried and Access password will be ignored') + \RainLoop\Plugins\Property::NewInstance('ldap_uri')->SetLabel('LDAP URI') + ->SetDescription('LDAP server URI(s), space separated') + ->SetDefaultValue('ldap://localhost:389'), + \RainLoop\Plugins\Property::NewInstance('use_start_tls')->SetLabel('Use StartTLS') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(True), + \RainLoop\Plugins\Property::NewInstance('bind_dn')->SetLabel('Bind DN') + ->SetDescription('DN to bind (login) with. If left blank, anonymous bind will be tried and the password will be ignored') ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('access_password')->SetLabel('Access password') + \RainLoop\Plugins\Property::NewInstance('bind_password')->SetLabel('Bind password') ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('users_dn_format')->SetLabel('Users DN format') - ->SetDescription('LDAP users dn format. Supported tokens: {email}, {login}, {domain}, {domain:dc}, {imap:login}, {imap:host}, {imap:port}') - ->SetDefaultValue('ou=People,dc=domain,dc=com'), - \RainLoop\Plugins\Property::NewInstance('object_class')->SetLabel('objectClass value') + \RainLoop\Plugins\Property::NewInstance('base_dn')->SetLabel('Search base DN') + ->SetDescription('DN to use as the search base. Supported tokens: {domain}, {domain:dc}, {email}, {email:user}, {email:domain}, {login}, {imap:login}, {imap:host}, {imap:port}') + ->SetDefaultValue('ou=People,dc=example,dc=com'), + \RainLoop\Plugins\Property::NewInstance('object_classes')->SetLabel('objectClasses') + ->SetDescription('LDAP objectClasses to search for, comma separated list') ->SetDefaultValue('inetOrgPerson'), - \RainLoop\Plugins\Property::NewInstance('search_field')->SetLabel('Search field') + \RainLoop\Plugins\Property::NewInstance('uid_attributes')->SetLabel('uid attributes') + ->SetDescription('LDAP attributes for userids, comma separated list in order of preference') ->SetDefaultValue('uid'), - \RainLoop\Plugins\Property::NewInstance('name_field')->SetLabel('Name field') - ->SetDefaultValue('givenname'), - \RainLoop\Plugins\Property::NewInstance('mail_field')->SetLabel('Mail field') - ->SetDefaultValue('mail'), + \RainLoop\Plugins\Property::NewInstance('name_attributes')->SetLabel('Name attributes') + ->SetDescription('LDAP attributes for user names, comma separated list in order of preference') + ->SetDefaultValue('displayName,cn,givenName,sn'), + \RainLoop\Plugins\Property::NewInstance('mail_attributes')->SetLabel('Mail attributes') + ->SetDescription('LDAP attributes for user email addresses, comma separated list in order of preference') + ->SetDefaultValue('mailAddress,mail,mailAlternateAddress,mailAlias'), \RainLoop\Plugins\Property::NewInstance('allowed_emails')->SetLabel('Allowed emails') - ->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net') + ->SetDescription('Email addresses of users which should be allowed to do LDAP lookups, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net') ->SetDefaultValue('*') ); } diff --git a/plugins/ldap-identities/LdapConfig.php b/plugins/ldap-identities/LdapConfig.php new file mode 100644 index 0000000000..c55f88bcb6 --- /dev/null +++ b/plugins/ldap-identities/LdapConfig.php @@ -0,0 +1,74 @@ +<?php + + +use RainLoop\Config\Plugin; + +class LdapConfig +{ + public const CONFIG_SERVER = "server"; + public const CONFIG_PROTOCOL_VERSION = "server_version"; + public const CONFIG_STARTTLS = "starttls"; + public const CONFIG_MAIL_PREFIX = "mail_prefix"; + + public const CONFIG_BIND_USER = "bind_user"; + public const CONFIG_BIND_PASSWORD = "bind_password"; + + public const CONFIG_USER_BASE = "user_base"; + public const CONFIG_USER_OBJECTCLASS = "user_objectclass"; + public const CONFIG_USER_FIELD_NAME = "user_field_name"; + public const CONFIG_USER_FIELD_SEARCH = "user_field_search"; + public const CONFIG_USER_FIELD_MAIL = "user_field_mail"; + + public const CONFIG_GROUP_GET = "group_get"; + public const CONFIG_GROUP_BASE = "group_base"; + public const CONFIG_GROUP_OBJECTCLASS = "group_objectclass"; + public const CONFIG_GROUP_FIELD_NAME = "group_field_name"; + public const CONFIG_GROUP_FIELD_MEMBER = "group_field_member"; + public const CONFIG_GROUP_FIELD_MAIL = "group_field_mail"; + public const CONFIG_GROUP_SENDER_FORMAT = "group_sender_format"; + + + public $server; + public $protocol; + public $starttls; + public $mail_prefix; + public $bind_user; + public $bind_password; + public $user_base; + public $user_objectclass; + public $user_field_name; + public $user_field_search; + public $user_field_mail; + public $group_get; + public $group_base; + public $group_objectclass; + public $group_field_name; + public $group_field_member; + public $group_field_mail; + public $group_sender_format; + + public static function MakeConfig(Plugin $config): LdapConfig + { + $ldap = new self(); + $ldap->server = trim($config->Get("plugin", self::CONFIG_SERVER)); + $ldap->protocol = (int)trim($config->Get("plugin", self::CONFIG_PROTOCOL_VERSION, 3)); + $ldap->starttls = (bool)trim($config->Get("plugin", self::CONFIG_STARTTLS)); + $ldap->mail_prefix = trim($config->Get("plugin", self::CONFIG_MAIL_PREFIX)); + $ldap->bind_user = trim($config->Get("plugin", self::CONFIG_BIND_USER)); + $ldap->bind_password = trim($config->Get("plugin", self::CONFIG_BIND_PASSWORD)); + $ldap->user_base = trim($config->Get("plugin", self::CONFIG_USER_BASE)); + $ldap->user_objectclass = trim($config->Get("plugin", self::CONFIG_USER_OBJECTCLASS)); + $ldap->user_field_name = trim($config->Get("plugin", self::CONFIG_USER_FIELD_NAME)); + $ldap->user_field_search = trim($config->Get("plugin", self::CONFIG_USER_FIELD_SEARCH)); + $ldap->user_field_mail = trim($config->Get("plugin", self::CONFIG_USER_FIELD_MAIL)); + $ldap->group_get = (bool)trim($config->Get("plugin", self::CONFIG_GROUP_GET)); + $ldap->group_base = trim($config->Get("plugin", self::CONFIG_GROUP_BASE)); + $ldap->group_objectclass = trim($config->Get("plugin", self::CONFIG_GROUP_OBJECTCLASS)); + $ldap->group_field_name = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_NAME)); + $ldap->group_field_member = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_MEMBER)); + $ldap->group_field_mail = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_MAIL)); + $ldap->group_sender_format = trim($config->Get("plugin", self::CONFIG_GROUP_SENDER_FORMAT)); + + return $ldap; + } +} \ No newline at end of file diff --git a/plugins/ldap-identities/LdapException.php b/plugins/ldap-identities/LdapException.php new file mode 100644 index 0000000000..df489e5778 --- /dev/null +++ b/plugins/ldap-identities/LdapException.php @@ -0,0 +1,5 @@ +<?php + +class LdapException extends \RainLoop\Exceptions\ClientException +{ +} \ No newline at end of file diff --git a/plugins/ldap-identities/LdapIdentities.php b/plugins/ldap-identities/LdapIdentities.php new file mode 100644 index 0000000000..4cc3e69d0d --- /dev/null +++ b/plugins/ldap-identities/LdapIdentities.php @@ -0,0 +1,365 @@ +<?php + +use MailSo\Log\Logger; +use RainLoop\Model\Account; +use RainLoop\Model\Identity; +use RainLoop\Providers\Identities\IIdentities; + +class LdapIdentities implements IIdentities +{ + /** @var resource */ + private $ldap; + + /** @var bool */ + private $ldapAvailable = true; + /** @var bool */ + private $ldapConnected = false; + /** @var bool */ + private $ldapBound = false; + + /** @var LdapConfig */ + private $config; + + /** @var Logger */ + private $logger; + + private const LOG_KEY = "Ldap"; + + /** + * LdapIdentities constructor. + * + * @param LdapConfig $config + * @param Logger $logger + */ + public function __construct(LdapConfig $config, Logger $logger) + { + $this->config = $config; + $this->logger = $logger; + + // Check if LDAP is available + if (!extension_loaded('ldap') || !function_exists('ldap_connect')) { + $this->ldapAvailable = false; + $logger->Write("The LDAP extension is not available!", \LOG_WARNING, self::LOG_KEY); + return; + } + + $this->Connect(); + } + + /** + * @inheritDoc + */ + public function GetIdentities(Account $account): array + { + try { + $this->EnsureBound(); + } catch (LdapException $e) { + return []; // exceptions are only thrown from the handleerror function that does logging already + } + + $identities = []; + + // Try and get identity information + $username = @ldap_escape($account->Email(), "", LDAP_ESCAPE_FILTER); + + try { + $userResults = $this->FindLdapResults( + $this->config->user_field_search, + $username, + $this->config->user_base, + $this->config->user_objectclass, + $this->config->user_field_name, + $this->config->user_field_mail, + $this->config->mail_prefix + ); + } catch (LdapException $e) { + return []; // exceptions are only thrown from the handleerror function that does logging already + } + + if (count($userResults) < 1) { + $this->logger->Write("Could not find user $username", \LOG_NOTICE, self::LOG_KEY); + return []; + } else if (count($userResults) > 1) { + $this->logger->Write("Found multiple matches for user $username", \LOG_WARNING, self::LOG_KEY); + } + + $userResult = $userResults[0]; + + foreach ($userResult->emails as $email) { + $identity = new Identity($email, $email); + $identity->SetName($userResult->name); + + if ($email === $account->Email()) + $identity->SetId(""); // primary identity + + $identities[] = $identity; + } + + if (!$this->config->group_get) + return $identities; + + try { + $groupResults = $this->FindLdapResults( + $this->config->group_field_member, + $userResult->dn, + $this->config->group_base, + $this->config->group_objectclass, + $this->config->group_field_name, + $this->config->group_field_mail, + $this->config->mail_prefix + ); + } catch (LdapException $e) { + return []; // exceptions are only thrown from the handleerror function that does logging already + } + + foreach ($groupResults as $group) { + foreach ($group->emails as $email) { + $name = $this->config->group_sender_format; + $name = str_replace("#USER#", $userResult->name, $name); + $name = str_replace("#GROUP#", $group->name, $name); + + $identity = new Identity($email, $email); + $identity->SetName($name); + $identity->SetBcc($email); + + $identities[] = $identity; + } + } + + return $identities; + } + + /** + * @inheritDoc + * @throws \RainLoop\Exceptions\ClientException + */ + public function SetIdentities(Account $account, array $identities): void + { + throw new \RainLoop\Exceptions\ClientException("Ldap identities provider does not support storage"); + } + + /** + * @inheritDoc + */ + public function SupportsStore(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function Name(): string + { + return "Ldap"; + } + + /** @throws LdapException */ + private function EnsureConnected(): void + { + if ($this->ldapConnected) return; + + $res = $this->Connect(); + if (!$res) + $this->HandleLdapError("Connect"); + } + + private function Connect(): bool + { + // Set up connection + $ldap = @ldap_connect($this->config->server); + if ($ldap === false) { + $this->ldapAvailable = false; + return false; + } + + // Set protocol version + $option = @ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, $this->config->protocol); + if (!$option) { + $this->ldapAvailable = false; + return false; + } + + // Activate StartTLS + if ($this->config->starttls) { + $starttlsResult = ldap_start_tls($ldap); + if (!$starttlsResult) { + $this->ldapAvailable = false; + return false; + } + } + + $this->ldap = $ldap; + $this->ldapConnected = true; + return true; + } + + /** @throws LdapException */ + private function EnsureBound(): void + { + if ($this->ldapBound) return; + $this->EnsureConnected(); + + $res = $this->Bind(); + if (!$res) + $this->HandleLdapError("Bind"); + } + + private function Bind(): bool + { + // Bind to LDAP here + $bindResult = @ldap_bind($this->ldap, $this->config->bind_user, $this->config->bind_password); + if (!$bindResult) { + $this->ldapAvailable = false; + return false; + } + + $this->ldapBound = true; + return true; + } + + /** + * @param string $op + * @throws LdapException + */ + private function HandleLdapError(string $op = ""): void + { + // Obtain LDAP error and write logs + $errorNo = @ldap_errno($this->ldap); + $errorMsg = @ldap_error($this->ldap); + + $message = empty($op) ? "LDAP Error: {$errorMsg} ({$errorNo})" : "LDAP Error during {$op}: {$errorMsg} ({$errorNo})"; + $this->logger->Write($message, \LOG_ERR, self::LOG_KEY); + throw new LdapException($message, $errorNo); + } + + /** + * @param string $searchField + * @param string $searchValue + * @param string $searchBase + * @param string $objectClass + * @param string $nameField + * @param string $mailField + * @return LdapResult[] + * @throws LdapException + */ + private function FindLdapResults(string $searchField, string $searchValue, string $searchBase, string $objectClass, string $nameField, string $mailField, string $mailPrefix): array + { + $this->EnsureBound(); + + $nameField = strtolower($nameField); + $mailField = strtolower($mailField); + + $filter = "(&(objectclass=$objectClass)($searchField=$searchValue))"; + $ldapResult = @ldap_search($this->ldap, $searchBase, $filter, ['dn', $mailField, $nameField]); + if (!$ldapResult) { + $this->HandleLdapError("Fetch $objectClass"); + return []; + } + + $entries = @ldap_get_entries($this->ldap, $ldapResult); + if (!$entries) { + $this->HandleLdapError("Fetch $objectClass"); + return []; + } + + $entries = $this->CleanupMailAddresses($entries, $mailField, $mailPrefix); + + $results = []; + for ($i = 0; $i < $entries["count"]; $i++) { + $entry = $entries[$i]; + + $result = new LdapResult(); + $result->dn = $entry["dn"]; + $result->name = $this->LdapGetAttribute($entry, $nameField, true, true); + $result->emails = $this->LdapGetAttribute($entry, $mailField, false, false); + + $results[] = $result; + } + + return $results; + } + + // Function CleanupMailAddresses(): If a prefix is given this function removes addresses without / with the wrong prefix and then the prefix itself from all remaining values. + // This is usefull for example for importing Active Directory LDAP entry "proxyAddresses" which can hold different address types with prefixes like "X400:", "smtp:" "sip:" and others. + + /** + @param array $entries + @param string $mailField + @paraam string $mailPrefix + @return array + */ + private function CleanupMailAddresses(array $entries, string $mailField, string $mailPrefix) + { + if (!empty($mailPrefix)) { + for ($i = 0; $i < $entries["count"]; $i++) { + // Remove addresses without the given prefix + $entries[$i]["$mailField"] = array_filter($entries[$i]["$mailField"], + function($prefixMail) + { + // $mailPrefix can't be used here, because it's nailed to the CleanupMailAddresses function and can't be passed to the array_filter function afaik. + // Ideas to avoid this are welcome. + if (stripos($prefixMail, $this->config->mail_prefix) === 0) { + return TRUE; + } + return FALSE; + } + ); + // Set "count" to new value + $newcount = count($entries[$i]["$mailField"]); + if (array_key_exists("count", $entries[$i]["$mailField"])) { + $newcount = $newcount - 1; + } + $entries[$i]["$mailField"]["count"] = $newcount; + + // Remove the prefix + for ($j = 0; $j < $entries[$i]["$mailField"]["count"]; $j++) { + $mailPrefixLen = mb_strlen($mailPrefix); + $entries[$i]["$mailField"][$j] = substr($entries[$i]["$mailField"][$j], $mailPrefixLen); + } + } + } + + return $entries; + } + + /** + * @param array $entry + * @param string $attribute + * @param bool $single + * @param bool $required + * @return string|string[] + */ + private function LdapGetAttribute(array $entry, string $attribute, bool $single = true, bool $required = false) + { + if (!isset($entry[$attribute])) { + if ($required) + $this->logger->Write("Attribute $attribute not found on object {$entry['dn']} while required", \LOG_NOTICE, self::LOG_KEY); + + return $single ? "" : []; + } + + if ($single) { + if ($entry[$attribute]["count"] > 1) + $this->logger->Write("Attribute $attribute is multivalues while only a single value is expected", \LOG_NOTICE, self::LOG_KEY); + + return $entry[$attribute][0]; + } + + $result = $entry[$attribute]; + unset($result["count"]); + return array_values($result); + } +} + +class LdapResult +{ + /** @var string */ + public $dn; + + /** @var string */ + public $name; + + /** @var string[] */ + public $emails; +} diff --git a/plugins/ldap-identities/index.php b/plugins/ldap-identities/index.php new file mode 100644 index 0000000000..4ac3f2faf5 --- /dev/null +++ b/plugins/ldap-identities/index.php @@ -0,0 +1,152 @@ +<?php + +use RainLoop\Enumerations\PluginPropertyType; +use RainLoop\Plugins\AbstractPlugin; +use RainLoop\Plugins\Property; + +class LdapIdentitiesPlugin extends AbstractPlugin +{ + const + NAME = 'LDAP Identities', + VERSION = '2.3', + AUTHOR = 'FWest98', + URL = 'https://github.com/FWest98', + RELEASE = '2024-02-27', + REQUIRED = '2.20.0', + CATEGORY = 'Accounts', + DESCRIPTION = 'Adds functionality to import account identities from LDAP.'; + + public function __construct() + { + include_once __DIR__ . '/LdapIdentities.php'; + include_once __DIR__ . '/LdapConfig.php'; + include_once __DIR__ . '/LdapException.php'; + + parent::__construct(); + } + + public function Init(): void + { + $this->addHook("main.fabrica", 'MainFabrica'); + } + + public function MainFabrica(string $name, &$result) + { + if ($name !== 'identities') return; + + if (!is_array($result)) + $result = []; + + // Set up config + $config = LdapConfig::MakeConfig($this->Config()); + + $ldap = new LdapIdentities($config, $this->Manager()->Actions()->Logger()); + + $result[] = $ldap; + } + + protected function configMapping(): array + { + return [ + Property::NewInstance(LdapConfig::CONFIG_SERVER) + ->SetLabel("LDAP Server URL") + ->SetPlaceholder("ldap://server:port") + ->SetType(PluginPropertyType::STRING), + + Property::NewInstance(LdapConfig::CONFIG_PROTOCOL_VERSION) + ->SetLabel("LDAP Protocol Version") + ->SetType(PluginPropertyType::SELECTION) + ->SetDefaultValue([2, 3]), + + Property::NewInstance(LdapConfig::CONFIG_STARTTLS) + ->SetLabel("Use StartTLS") + ->SetType(PluginPropertyType::BOOL) + ->SetDescription("Whether or not to use TLS encrypted connection") + ->SetDefaultValue(true), + + Property::NewInstance(LdapConfig::CONFIG_MAIL_PREFIX) + ->SetLabel("Email prefix") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("Only addresses with this prefix will be used as identity. The prefix is removed from the identity list.\nThis is useful for example to import identities from Exchange, which stores mail addresses in the ProxyAddresses attribut of Active Directory with \"smtp:\" as prefix. (e.g. \"smtp:john.doe@topsecret.info\")\n-> To use addresses set by Exchange use \"smtp:\" as prefix.") + ->SetDefaultValue(""), + + Property::NewInstance(LdapConfig::CONFIG_BIND_USER) + ->SetLabel("Bind User DN") + ->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind") + ->SetType(PluginPropertyType::STRING), + + Property::NewInstance(LdapConfig::CONFIG_BIND_PASSWORD) + ->SetLabel("Bind User Password") + ->SetDescription("Leave empty for anonymous bind") + ->SetType(PluginPropertyType::PASSWORD), + + Property::NewInstance(LdapConfig::CONFIG_USER_OBJECTCLASS) + ->SetLabel("User object class") + ->SetType(PluginPropertyType::STRING) + ->SetDefaultValue("user"), + + Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_SEARCH) + ->SetLabel("User search field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the user object to search using the email the user logged in with") + ->SetDefaultValue("mail"), + + Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_MAIL) + ->SetLabel("User mail field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the user object listing all identities (email addresses) of the user") + ->SetDefaultValue("mail"), + + Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_NAME) + ->SetLabel("User name field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the user object with their default sender name") + ->SetDefaultValue("cn"), + + Property::NewInstance(LdapConfig::CONFIG_USER_BASE) + ->SetLabel("User base DN") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The base DN to search in for users"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_GET) + ->SetLabel("Find groups?") + ->SetType(PluginPropertyType::BOOL) + ->SetDescription("Whether or not to search for groups") + ->SetDefaultValue(true), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_OBJECTCLASS) + ->SetLabel("Group object class") + ->SetType(PluginPropertyType::STRING) + ->SetDefaultValue("group"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_MAIL) + ->SetLabel("Group mail field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the group object listing all identities (email addresses) of the group") + ->SetDefaultValue("mail"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_NAME) + ->SetLabel("Group name field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the group object with the name") + ->SetDefaultValue("cn"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_MEMBER) + ->SetLabel("Group member field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the group object with all member DNs") + ->SetDefaultValue("member"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_SENDER_FORMAT) + ->SetLabel("Group mail sender format") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The sender name format for group addresses. Available template values: #USER# for the user name and #GROUP# for the group name") + ->SetDefaultValue("#USER# || #GROUP#"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_BASE) + ->SetLabel("Group base DN") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The base DN to search in for groups") + ]; + } +} diff --git a/plugins/ldap-login-mapping/LICENSE b/plugins/ldap-login-mapping/LICENSE new file mode 100644 index 0000000000..335b3845ab --- /dev/null +++ b/plugins/ldap-login-mapping/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 RainLoop Team +Copyright (c) 2018 <ludovic.pouzenc@mines-albi.fr> +Copyright (c) 2022 <zephone@protonmail.com> + +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/ldap-login-mapping/README b/plugins/ldap-login-mapping/README new file mode 100644 index 0000000000..b12d3a77ec --- /dev/null +++ b/plugins/ldap-login-mapping/README @@ -0,0 +1 @@ +Plugin which allows you to set up LDAP username by email address (when IMAP/SMTP requires it) diff --git a/plugins/ldap-login-mapping/VERSION b/plugins/ldap-login-mapping/VERSION new file mode 100644 index 0000000000..cd5ac039d6 --- /dev/null +++ b/plugins/ldap-login-mapping/VERSION @@ -0,0 +1 @@ +2.0 diff --git a/plugins/ldap-login-mapping/index.php b/plugins/ldap-login-mapping/index.php new file mode 100644 index 0000000000..73d8fae9c1 --- /dev/null +++ b/plugins/ldap-login-mapping/index.php @@ -0,0 +1,253 @@ +<?php + +use RainLoop\Enumerations\PluginPropertyType; +use RainLoop\Plugins\AbstractPlugin; +use RainLoop\Plugins\Property; + +class LDAPLoginMappingPlugin extends AbstractPlugin +{ + const + NAME = 'LDAP login mapping', + VERSION = '2.3', + AUTHOR = 'RainLoop Team, Ludovic Pouzenc<ludovic@pouzenc.fr>, ZephOne<zephone@protonmail.com>', + RELEASE = '2024-09-20', + REQUIRED = '2.36.1', + CATEGORY = 'Login', + DESCRIPTION = 'Enable custom mapping using ldap field'; + /** + * @var array + */ + private $aDomains = array(); + + /** + * @var string + */ + private $sSearchDomain = ''; + + /** + * @var string + */ + private $sHostName = '127.0.0.1'; + + /** + * @var int + */ + private $iHostPort = 389; + + /** + * @var string + */ + private $sUsersDn = ''; + + /** + * @var string + */ + private $sObjectClass = 'inetOrgPerson'; + + /** + * @var string + */ + private $sLoginField = 'uid'; + + /** + * @var string + */ + private $sEmailField = 'mail'; + + /** + * @var \MailSo\Log\Logger + */ + private $oLogger = null; + + public function Init(): void + { + $this->addHook('login.credentials', 'FilterLoginСredentials'); + } + + /** + * @return string + */ + public function Supported(): string + { + if (!\function_exists('ldap_connect')) + { + return 'The LDAP PHP extension must be installed to use this plugin'; + } + + return ''; + } + + /** + * @param string $sEmail + * @param string $sImapUser + * @param string $sPassword + * @param string $sSmtpUser + * + * @throws \RainLoop\Exceptions\ClientException + */ + public function FilterLoginСredentials(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser) + { + $this->oLogger = \RainLoop\Api::Logger(); + + $this->aDomains = explode(',', $this->Config()->Get('plugin', 'domains', '')); + $this->sSearchDomain = trim($this->Config()->Get('plugin', 'search_domain', '')); + $this->sHostName = trim($this->Config()->Get('plugin', 'hostname', '')); + $this->iHostPort = (int) $this->Config()->Get('plugin', 'port', 389); + $this->sUsersDn = trim($this->Config()->Get('plugin', 'users_dn', '')); + $this->sObjectClass = trim($this->Config()->Get('plugin', 'object_class', '')); + $this->sLoginField = trim($this->Config()->Get('plugin', 'login_field', '')); + $this->sEmailField = trim($this->Config()->Get('plugin', 'mail_field', '')); + + if (0 < \strlen($this->sObjectClass) && 0 < \strlen($this->sEmailField)) + { + $sIP = $_SERVER['REMOTE_ADDR']; + $sResult = $this->ldapSearch($sEmail); + if ( is_array($sResult) ) { + $sImapUser = $sResult['login']; + $sEmail = $sResult['email']; + } + syslog(LOG_WARNING, "plugins/ldap-login-mapping/index.php:FilterLoginСredentials() auth try: $sIP/$sEmail, resolved as $sImapUser/$sEmail"); + } + } + + /** + * @return array + */ + public function configMapping(): array + { + return [ + Property::NewInstance('domains') + ->SetLabel('LDAP enabled domains') + ->SetDefaultValue('example1.com,example2.com'), + + Property::NewInstance('search_domain') + ->SetLabel('Forced domain') + ->SetDescription('Force this domain email for LDAP search') + ->SetDefaultValue('example.com'), + + Property::NewInstance('hostname') + ->SetLabel('LDAP hostname') + ->SetDefaultValue('127.0.0.1'), + + Property::NewInstance('port') + ->SetLabel('LDAP port') + ->SetType(PluginPropertyType::INT) + ->SetDefaultValue(389), + + Property::NewInstance('users_dn') + ->SetLabel('Search base DN') + ->SetDescription('LDAP users search base DN. No tokens.') + ->SetDefaultValue('ou=People,dc=domain,dc=com'), + + Property::NewInstance('object_class') + ->SetLabel('objectClass value') + ->SetDefaultValue('inetOrgPerson'), + + Property::NewInstance('login_field') + ->SetLabel('Login field') + ->SetDefaultValue('uid'), + + Property::NewInstance('mail_field') + ->SetLabel('Mail field') + ->SetDefaultValue('mail'), + ]; + } + + /** + * @param string $sEmailOrLogin + * + * @return string + */ + private function ldapSearch($sEmail) + { + $bFound = FALSE; + foreach ( $this->aDomains as $sDomain ) { + $sRegex = '/^[a-z0-9._-]+@' . preg_quote(trim($sDomain)) . '$/i'; + $this->oLogger->Write('DEBUG regex ' . $sRegex, \LOG_INFO, 'LDAP'); + if ( preg_match($sRegex, $sEmail) === 1) { + $bFound = TRUE; + break; + } + } + if ( !$bFound ) { + $this->oLogger->Write( + 'preg_match: no match in "' . $sEmail . '" for /^[a-z0-9._-]+@{configured-domains}$/i', + \LOG_INFO, + 'LDAP'); + return FALSE; + } + $sLogin = \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail); + + $this->oLogger->Write('ldap_connect: trying...', \LOG_INFO, 'LDAP'); + + $oCon = @\ldap_connect($this->sHostName, $this->iHostPort); + if (!$oCon) return FALSE; + + $this->oLogger->Write('ldap_connect: connected', \LOG_INFO, 'LDAP'); + + @\ldap_set_option($oCon, LDAP_OPT_PROTOCOL_VERSION, 3); + + if (!@\ldap_bind($oCon)) { + $this->logLdapError($oCon, 'ldap_bind'); + return FALSE; + } + $sSearchDn = $this->sUsersDn; + $aItems = array($this->sLoginField, $this->sEmailField); + if ( 0 < \strlen($this->sSearchDomain) ) { + $sFilter = '(&(objectclass='.$this->sObjectClass.')(|('.$this->sEmailField.'='.$sLogin.'@'.$this->sSearchDomain.')('.$this->sLoginField.'='.$sLogin.')))'; + + } else { + $sFilter = '(&(objectclass='.$this->sObjectClass.')(|('.$this->sEmailField.'='.$sEmail.')('.$this->sLoginField.'='.$sLogin.')))'; + } + $this->oLogger->Write('ldap_search: start: '.$sSearchDn.' / '.$sFilter, \LOG_INFO, 'LDAP'); + $oS = @\ldap_search($oCon, $sSearchDn, $sFilter, $aItems, 0, 30, 30); + if (!$oS) { + $this->logLdapError($oCon, 'ldap_search'); + return FALSE; + } + $aEntries = @\ldap_get_entries($oCon, $oS); + if (!is_array($aEntries)) { + $this->logLdapError($oCon, 'ldap_get_entries'); + return FALSE; + } + if (!isset($aEntries[0])) { + $this->logLdapError($oCon, 'ldap_get_entries (no result)'); + return FALSE; + } + if (!isset($aEntries[0][$this->sLoginField][0])) { + $this->logLdapError($oCon, 'ldap_get_entries (no login)'); + return FALSE; + } + if (!isset($aEntries[0][$this->sEmailField][0])) { + $this->logLdapError($oCon, 'ldap_get_entries (no mail)'); + return FALSE; + } + $sLogin = $aEntries[0][$this->sLoginField][0]; + $sEmail = $aEntries[0][$this->sEmailField][0]; + $this->oLogger->Write('ldap_search: found "' . $this->sLoginField . ': '.$sLogin . '" and "' . $this->sEmailField . ': '.$sEmail . '"'); + + return array( + 'login' => $sLogin, + 'email' => $sEmail, + ); + } + + /** + * @param mixed $oCon + * @param string $sCmd + * + * @return string + */ + private function logLdapError($oCon, $sCmd) + { + if ($this->oLogger) + { + $sError = $oCon ? @\ldap_error($oCon) : ''; + $iErrno = $oCon ? @\ldap_errno($oCon) : 0; + + $this->oLogger->Write($sCmd.' error: '.$sError.' ('.$iErrno.')', + \LOG_WARNING, 'LDAP'); + } + } + +} diff --git a/plugins/ldap-mail-accounts/LdapMailAccounts.php b/plugins/ldap-mail-accounts/LdapMailAccounts.php new file mode 100644 index 0000000000..5dc0c4eb6a --- /dev/null +++ b/plugins/ldap-mail-accounts/LdapMailAccounts.php @@ -0,0 +1,532 @@ +<?php + +use RainLoop\Enumerations\Capa; +use MailSo\Log\Logger; +use RainLoop\Actions; +use RainLoop\Model\AdditionalAccount; +use RainLoop\Model\MainAccount; +use RainLoop\Providers\Storage\Enumerations\StorageType; + +class LdapMailAccounts +{ + /** @var resource */ + private $ldap; + + /** @var bool */ + private $ldapAvailable = true; + /** @var bool */ + private $ldapConnected = false; + /** @var bool */ + private $ldapBound = false; + + /** @var LdapMailAccountsConfig */ + private $config; + + /** @var Logger */ + private $logger; + + private const LOG_KEY = "LDAP MAIL ACCOUNTS PLUGIN"; + + /** + * LdapMailAccount constructor. + * + * @param LdapMailAccountsConfig $config LdapMailAccountsConfig object containing the admin configuration for this plugin + * @param Logger $logger Used to write to the logfile + */ + public function __construct(LdapMailAccountsConfig $config, Logger $logger) + { + $this->config = $config; + $this->logger = $logger; + + // Check if LDAP is available + if (!extension_loaded('ldap') || !function_exists('ldap_connect')) { + $this->ldapAvailable = false; + $logger->Write("The LDAP extension is not available!", \LOG_WARNING, self::LOG_KEY); + return; + } + + $this->Connect(); + } + + + /** + * @inheritDoc + * + * Overwrite the MainAccount mail address by looking up the new one in the ldap directory + * + * The ldap search string has to be configured in the plugin configuration of the extension (in the SnappyMail Admin Panel) + * + * @param string &$sEmail + */ + public function overwriteEmail(&$sEmail) + { + try { + $this->EnsureBound(); + } catch (LdapMailAccountsException $e) { + return false; // exceptions are only thrown from the handle error function that does logging already + } + + // Try to get account information. IncLogin() returns the username of the user + // and removes the domainname if this was configured inside the domain config. + $username = $sEmail; + $oActions = \RainLoop\Api::Actions(); + $oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::getEmailAddressDomain($sEmail), true); + if ($oDomain->ImapSettings()->shortLogin){ + $username = @ldap_escape($this->RemoveEventualDomainPart($sEmail), "", LDAP_ESCAPE_FILTER); + } + + $searchString = $this->config->search_string; + + // Replace placeholders inside the ldap search string with actual values + $searchString = str_replace("#USERNAME#", $username, $searchString); + $searchString = str_replace("#BASE_DN#", $this->config->base, $searchString); + + $this->logger->Write("ldap search string after replacement of placeholders: $searchString", \LOG_NOTICE, self::LOG_KEY); + + try { + $mailAddressResults = $this->FindLdapResults( + $this->config->field_search, + $searchString, + $this->config->base, + $this->config->objectclass, + $this->config->field_name, + $this->config->field_username, + $this->config->field_domain, + true, + $this->config->field_mail_address_main_account, + ); + } + catch (LdapMailAccountsException $e) { + return false; // exceptions are only thrown from the handle error function that does logging already + } + if (count($mailAddressResults) < 1) { + $this->logger->Write("Could not find user $username in LDAP! Overwriting of main mail address not possible.", \LOG_NOTICE, self::LOG_KEY); + return false; + } + + foreach($mailAddressResults as $mailAddressResult) + { + if($mailAddressResult->username === $username) { + //$sImapUser and $sSmtpUser are already set to be the same as $sEmail by function "resolveLoginCredentials" in /RainLoop/Actions/UserAuth.php + //that called this hook, so we just have to overwrite the mail address + $sEmail = $mailAddressResult->mailMainAccount; + } + } + } + + /** + * @inheritDoc + * + * Add additional mail accounts to the given primary account by looking up the ldap directory + * + * The ldap search string has to be configured in the plugin configuration of the extension (in the SnappyMail Admin Panel) + * + * @param MainAccount $oAccount + * @return bool true if additional accounts have been added or no additional accounts where found in ldap. false if an error occured + */ + public function AddLdapMailAccounts(MainAccount $oAccount): bool + { + try { + $this->EnsureBound(); + } catch (LdapMailAccountsException $e) { + return false; // exceptions are only thrown from the handle error function that does logging already + } + + //Basing on https://github.com/the-djmaze/snappymail/issues/616 + + $oActions = \RainLoop\Api::Actions(); + + //Check if SnappyMail is configured to allow additional accounts + if (!$oActions->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) { + return $oActions->FalseResponse(__FUNCTION__); + } + + // Try to get account information. ImapUser() returns the username of the user + // and removes the domainname if this was configured inside the domain config. + $username = @ldap_escape($oAccount->ImapUser(), "", LDAP_ESCAPE_FILTER); + + $searchString = $this->config->search_string; + + // Replace placeholders inside the ldap search string with actual values + $searchString = str_replace("#USERNAME#", $username, $searchString); + $searchString = str_replace("#BASE_DN#", $this->config->base, $searchString); + + $this->logger->Write("ldap search string after replacement of placeholders: $searchString", \LOG_NOTICE, self::LOG_KEY); + + try { + $mailAddressResults = $this->FindLdapResults( + $this->config->field_search, + $searchString, + $this->config->base, + $this->config->objectclass, + $this->config->field_name, + $this->config->field_username, + $this->config->field_domain, + false, + $this->config->field_mail_address_additional_account + ); + } + catch (LdapMailAccountsException $e) { + return false; // exceptions are only thrown from the handle error function that does logging already + } + + if (count($mailAddressResults) < 1) { + $this->logger->Write("Could not find user $username", \LOG_NOTICE, self::LOG_KEY); + return false; + } + + $aAccounts = $oActions->GetAccounts($oAccount); + + //Search for accounts with suffix " (LDAP)" at the end of the name that were created by this plugin and initially remove them from the + //account array. This only removes the visibility but does not delete the config done by the user. So if a user looses access to a + //mailbox the user will not see the account anymore but the configuration can be restored when the user regains access to it + foreach($aAccounts as $key => $aAccount) + { + if (preg_match("/\s\(LDAP\)$/", $aAccount['name'])) + { + unset($aAccounts[$key]); + } + } + + //SnappyMail saves the passwords of the additional accounts by encrypting them using a cryptkey that is saved in the file .cryptkey + //When the password of the main account changes, SnappyMail asks the user for the old password to reencrypt the keys with the new userpassword. + //On a password change using ldap (or when the password has been forgotten by the user) this makes us some problems. Therefore overwrite + //the .cryptkey file in order to always accept the actual ldap password of the user. This has side effects on pgp keys! + //See https://github.com/the-djmaze/snappymail/issues/1570#issuecomment-2085528061 + if ($this->config->bool_overwrite_cryptkey) { + if (!$oActions->StorageProvider()->Put($oAccount, StorageType::ROOT, '.cryptkey', "")) { + $this->logger->Write("Could not overwrite the .cryptkey file!", \LOG_WARNING, self::LOG_KEY); + return $oActions->FalseResponse(__FUNCTION__); + } + } + + if (count($mailAddressResults) == 1) { + $this->logger->Write("Found only one match for user $username, no additional mail adresses found", \LOG_NOTICE, self::LOG_KEY); + //Write back the accounts even if no additional account was found. This ensures, that previous additional accounts are always removed + $oActions->SetAccounts($oAccount, $aAccounts); + return true; + } + + foreach($mailAddressResults as $mailAddressResult) + { + $sUsername = $mailAddressResult->username; + $sDomain = $mailAddressResult->domain; + $sName = $mailAddressResult->name; + $sEmail = $mailAddressResult->mailAdditionalAccount; + + //Check if the domain of the found mail address is in the list of configured domains + if ($oActions->DomainProvider()->Load($sDomain, true)) + { + //only execute if the found account isn't already in the list of additional accounts + //and if the found account is different from the main account. + //The check if the address is different from the one of the main account when using the Nextcloud integration needs + //to be done twice: directly on the mail address (when Nextcloud is configured to log the user in by mail address) + //or on "$sUsername@$sDomain" for the case Nextcloud logs the user in to SnappyMail by his username and a default domain. + if (!isset($aAccounts[$sEmail]) && $oAccount->Email() !== $sEmail && $oAccount->Email() !== "$sUsername@$sDomain") + { + //Try to login the user with the same password as the primary account has + //if this fails the user will see the new mail addresses but will be asked for the correct password + $sPass = new \SnappyMail\SensitiveString($oAccount->IncPassword()); + //After creating the accounts here $sUsername is used as username to login to the IMAP server (see Account.php) + //$oNewAccount = RainLoop\Model\AdditionalAccount::NewInstanceFromCredentials($oActions, $sEmail, $sUsername, $sPass); + + $oDomain = $oActions->DomainProvider()->Load($sDomain, false); + + $oNewAccount = new AdditionalAccount; + $oNewAccount->setCredentials( + $oDomain, + $sEmail, + $sUsername, + $sPass, + $sUsername + ); + + $aAccounts[$sEmail] = $oNewAccount->asTokenArray($oAccount); + } + + //Always inject/update the found mailbox names into the array (also if the mailbox already existed) + if (isset($aAccounts[$sEmail])) + { + $aAccounts[$sEmail]['name'] = $sName . " (LDAP)"; + } + } + else { + $this->logger->Write("Domain $sDomain is not part of configured domains in SnappyMail Admin Panel - mail address $sEmail will not be added.", \LOG_NOTICE, self::LOG_KEY); + } + } + + if ($aAccounts) + { + $oActions->SetAccounts($oAccount, $aAccounts); + return true; + } + + return false; + } + + /** + * Checks if a connection to the LDAP was possible + * + * @throws LdapMailAccountsException + * + * */ + private function EnsureConnected(): void + { + if ($this->ldapConnected) return; + + $res = $this->Connect(); + if (!$res) + $this->HandleLdapError("Connect"); + } + + /** + * Connect to the LDAP using the server address and protocol version defined inside the configuration of the plugin + */ + private function Connect(): bool + { + // Set up connection + $ldap = @ldap_connect($this->config->server); + if ($ldap === false) { + $this->ldapAvailable = false; + return false; + } + + // Set protocol version + $option = @ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, $this->config->protocol); + if (!$option) { + $this->ldapAvailable = false; + return false; + } + + $this->ldap = $ldap; + $this->ldapConnected = true; + return true; + } + + /** + * Ensures the plugin has been authenticated at the LDAP + * + * @throws LdapMailAccountsException + * + * */ + private function EnsureBound(): void + { + if ($this->ldapBound) return; + $this->EnsureConnected(); + + $res = $this->Bind(); + if (!$res) + $this->HandleLdapError("Bind"); + } + + /** + * Authenticates the plugin at the LDAP using the username and password defined inside the configuration of the plugin + * + * @return bool true if authentication was successful + */ + private function Bind(): bool + { + // Bind to LDAP here + $bindResult = @ldap_bind($this->ldap, $this->config->bind_user, $this->config->bind_password); + if (!$bindResult) { + $this->ldapAvailable = false; + return false; + } + + $this->ldapBound = true; + return true; + } + + /** + * Handles and logs an eventual LDAP error + * + * @param string $op + * @throws LdapMailAccountsException + */ + private function HandleLdapError(string $op = ""): void + { + // Obtain LDAP error and write logs + $errorNo = @ldap_errno($this->ldap); + $errorMsg = @ldap_error($this->ldap); + + $message = empty($op) ? "LDAP Error: {$errorMsg} ({$errorNo})" : "LDAP Error during {$op}: {$errorMsg} ({$errorNo})"; + $this->logger->Write($message, \LOG_ERR, self::LOG_KEY); + throw new LdapMailAccountsException($message, $errorNo); + } + + /** + * Looks up the LDAP for additional mail accounts + * + * The search for additional mail accounts is done by a ldap search using the defined fields inside the configuration of the plugin (SnappyMail Admin Panel) + * + * @param string $searchField + * @param string $searchString + * @param string $searchBase + * @param string $objectClass + * @param string $nameField + * @param string $usernameField + * @param string $domainField + * @param bool $overwriteMailMainAccount true if the mail address of the main account should be looked up for overwriting. false if additional mail accounts should be searched + * @param string $mailAddressField The field containing the mail address (of main account or additional mail account) + * @return LdapMailAccountResult[] + * @throws LdapMailAccountsException + */ + private function FindLdapResults( + string $searchField, + string $searchString, + string $searchBase, + string $objectClass, + string $nameField, + string $usernameField, + string $domainField, + bool $overwriteMailMainAccount, + string $mailAddressField): array + { + $this->EnsureBound(); + $nameField = strtolower($nameField); + $usernameField = strtolower($usernameField); + $domainField = strtolower($domainField); + + $filter = "(&(objectclass=$objectClass)($searchField=$searchString))"; + $this->logger->Write("Used ldap filter to search for mail account(s): $filter", \LOG_NOTICE, self::LOG_KEY); + + //Set together the attributes to search inside the LDAP + $ldapAttributes = ['dn', $usernameField, $nameField, $domainField, $mailAddressField]; + + $ldapResult = @ldap_search($this->ldap, $searchBase, $filter, $ldapAttributes); + if (!$ldapResult) { + $this->HandleLdapError("Fetch $objectClass"); + return []; + } + + $entries = @ldap_get_entries($this->ldap, $ldapResult); + if (!$entries) { + $this->HandleLdapError("Fetch $objectClass"); + return []; + } + + // Save the found ldap entries into a LdapMailAccountResult object and return them + $results = []; + for ($i = 0; $i < $entries["count"]; $i++) { + $entry = $entries[$i]; + + $result = new LdapMailAccountResult(); + $result->dn = $entry["dn"]; + $result->name = $this->LdapGetAttribute($entry, $nameField, true, true); + + $result->username = $this->LdapGetAttribute($entry, $usernameField, true, true); + $result->username = $this->RemoveEventualDomainPart($result->username); + + $result->domain = $this->LdapGetAttribute($entry, $domainField, true, true); + $result->domain = $this->RemoveEventualLocalPart($result->domain); + + if($overwriteMailMainAccount) { + $result->mailMainAccount = $this->LdapGetAttribute($entry, $mailAddressField, true, true); + } + else { + $result->mailAdditionalAccount = $this->LdapGetAttribute($entry, $mailAddressField, true, true); + } + + $results[] = $result; + } + + return $results; + } + + /** + * Removes an eventually found domain-part of an email address + * + * If the input string contains an '@' character the function returns the local-part before the '@'\ + * If no '@' character can be found the input string is returned. + * + * @param string $sInput + * @return string + */ + public static function RemoveEventualDomainPart(string $sInput) : string + { + // Copy of \MailSo\Base\Utils::getEmailAddressLocalPart to make sure that also after eventual future + // updates the input string gets returned when no '@' is found (getEmailAddressDomain already doesn't do this) + $sResult = ''; + if (\strlen($sInput)) + { + $iPos = \strrpos($sInput, '@'); + $sResult = (false === $iPos) ? $sInput : \substr($sInput, 0, $iPos); + } + + return $sResult; + } + + + /** + * Removes an eventually found local-part of an email address + * + * If the input string contains an '@' character the function returns the domain-part behind the '@'\ + * If no '@' character can be found the input string is returned. + * + * @param string $sInput + * @return string + */ + public static function RemoveEventualLocalPart(string $sInput) : string + { + $sResult = ''; + if (\strlen($sInput)) + { + $iPos = \strrpos($sInput, '@'); + $sResult = (false === $iPos) ? $sInput : \substr($sInput, $iPos + 1); + } + + return $sResult; + } + + + /** + * Gets LDAP attributes out of the input array + * + * @param array $entry Array containing the result of a ldap search + * @param string $attribute The name of the attribute to return + * @param bool $single If true the function checks if exact one value for this attribute is inside the input array. If false an array is returned. Default true. + * @param bool $required If true the attribute has to exist inside the input array. Default false. + * @return string|string[] + */ + private function LdapGetAttribute(array $entry, string $attribute, bool $single = true, bool $required = false) + { + if (!isset($entry[$attribute])) { + if ($required) + $this->logger->Write("Attribute $attribute not found on object {$entry['dn']} while required", \LOG_NOTICE, self::LOG_KEY); + + return $single ? "" : []; + } + + if ($single) { + if ($entry[$attribute]["count"] > 1) + $this->logger->Write("Attribute $attribute is multivalues while only a single value is expected", \LOG_NOTICE, self::LOG_KEY); + + return $entry[$attribute][0]; + } + + $result = $entry[$attribute]; + unset($result["count"]); + return array_values($result); + } +} + +class LdapMailAccountResult +{ + /** @var string */ + public $dn; + + /** @var string */ + public $name; + + /** @var string */ + public $username; + + /** @var string */ + public $domain; + + /** @var string */ + public $mailMainAccount; + + /** @var string */ + public $mailAdditionalAccount; +} diff --git a/plugins/ldap-mail-accounts/LdapMailAccountsConfig.php b/plugins/ldap-mail-accounts/LdapMailAccountsConfig.php new file mode 100644 index 0000000000..8b674e4f25 --- /dev/null +++ b/plugins/ldap-mail-accounts/LdapMailAccountsConfig.php @@ -0,0 +1,63 @@ +<?php + + +use RainLoop\Config\Plugin; + +class LdapMailAccountsConfig +{ + public const CONFIG_SERVER = "server"; + public const CONFIG_PROTOCOL_VERSION = "server_version"; + + public const CONFIG_BIND_USER = "bind_user"; + public const CONFIG_BIND_PASSWORD = "bind_password"; + + public const CONFIG_BASE = "base"; + public const CONFIG_OBJECTCLASS = "objectclass"; + public const CONFIG_FIELD_NAME = "field_name"; + public const CONFIG_FIELD_SEARCH = "field_search"; + public const CONFIG_FIELD_USERNAME = "field_username"; + public const CONFIG_SEARCH_STRING = "search_string"; + public const CONFIG_FIELD_MAIL_DOMAIN = "field_domain"; + public const CONFIG_FIELD_MAIL_ADDRESS_ADDITIONAL_ACCOUNT = "field_mail_address_additional_account"; + public const CONFIG_BOOL_OVERWRITE_MAIL_ADDRESS_MAIN_ACCOUNT = "bool_overwrite_mail_address_main_account"; + public const CONFIG_FIELD_MAIL_ADDRESS_MAIN_ACCOUNT = "field_mail_address_main_account"; + public const CONFIG_BOOL_OVERWRITE_CRYPTKEY = "bool_overwrite_cryptkey"; + + public $server; + public $protocol; + public $bind_user; + public $bind_password; + public $base; + public $objectclass; + public $field_name; + public $field_search; + public $field_username; + public $search_string; + public $field_domain; + public $field_mail_address_main_account; + public $field_mail_address_additional_account; + public $bool_overwrite_mail_address_main_account; + public $bool_overwrite_cryptkey; + + public static function MakeConfig(Plugin $config): LdapMailAccountsConfig + { + $ldap = new self(); + $ldap->server = trim($config->Get("plugin", self::CONFIG_SERVER)); + $ldap->protocol = (int)trim($config->Get("plugin", self::CONFIG_PROTOCOL_VERSION, 3)); + $ldap->bind_user = trim($config->Get("plugin", self::CONFIG_BIND_USER)); + $ldap->bind_password = trim($config->Get("plugin", self::CONFIG_BIND_PASSWORD)); + $ldap->base = trim($config->Get("plugin", self::CONFIG_BASE)); + $ldap->objectclass = trim($config->Get("plugin", self::CONFIG_OBJECTCLASS)); + $ldap->field_name = trim($config->Get("plugin", self::CONFIG_FIELD_NAME)); + $ldap->field_search = trim($config->Get("plugin", self::CONFIG_FIELD_SEARCH)); + $ldap->field_username = trim($config->Get("plugin", self::CONFIG_FIELD_USERNAME)); + $ldap->search_string = trim($config->Get("plugin", self::CONFIG_SEARCH_STRING)); + $ldap->field_domain = trim($config->Get("plugin", self::CONFIG_FIELD_MAIL_DOMAIN)); + $ldap->field_mail_address_additional_account = trim($config->Get("plugin", self::CONFIG_FIELD_MAIL_ADDRESS_ADDITIONAL_ACCOUNT)); + $ldap->bool_overwrite_mail_address_main_account = $config->Get("plugin", self::CONFIG_BOOL_OVERWRITE_MAIL_ADDRESS_MAIN_ACCOUNT); + $ldap->field_mail_address_main_account = trim($config->Get("plugin", self::CONFIG_FIELD_MAIL_ADDRESS_MAIN_ACCOUNT)); + $ldap->bool_overwrite_cryptkey = $config->Get("plugin", self::CONFIG_BOOL_OVERWRITE_CRYPTKEY); + + return $ldap; + } +} \ No newline at end of file diff --git a/plugins/ldap-mail-accounts/LdapMailAccountsException.php b/plugins/ldap-mail-accounts/LdapMailAccountsException.php new file mode 100644 index 0000000000..3135cc14bb --- /dev/null +++ b/plugins/ldap-mail-accounts/LdapMailAccountsException.php @@ -0,0 +1,5 @@ +<?php + +class LdapMailAccountsException extends \RainLoop\Exceptions\ClientException +{ +} \ No newline at end of file diff --git a/plugins/ldap-mail-accounts/README.md b/plugins/ldap-mail-accounts/README.md new file mode 100644 index 0000000000..d26d6302dc --- /dev/null +++ b/plugins/ldap-mail-accounts/README.md @@ -0,0 +1,29 @@ +## LDAP Mail Accounts Plugin for SnappyMail + +### Description +This plugin can be used to add additional mail accounts to SnappyMail when a user logs in successfully. The list of additional accounts is retrieved by a ldap query that can be configured inside the plugin settings.\ +On a successful login the username of the SnappyMail user is passed to the plugin and will be searched in the ldap. If additional mail accounts are found, the username and domain-part of those will be used to add the new mail account. The plugin tries to log in the user with the same password used to login to SnappyMail - if this fails SnappyMail asks the user to insert his credentials. + +Version 2.0.0 changes the way additional mail accounts get their e-mail address: the mail address connected with additional mail accounts is now always the address found inside the ldap. +Now it is also possible to overwrite the mail address of the main account: if a user logs into SnappyMail with a username and SnappyMail added the configured default domain the mail address of the main account could have been some not existing address like "username@default-domain.com". This could have happend when using the Nextcloud SnappyMail integration that offers an automatic login using the Nextcloud username. +The plugin now can be configured to overwrite the username or mail address used at login with a mail address found inside ldap. + +Version 2.2.0 adds compatibility with SnappyMail 2.36.1 and later where some changes were introduced that made the plugin unusable. This version also adds the possibility to delete the .cryptkey file on evere login. For more information see the `Configuration` section below. + +### Configuration +- Install and activate the plugin using the SnappyMail Admin Panel -> menu Extensions. +- Click on the gear symbol beside the plugin to open the configration dialog. + - Insert the connection data to access your LDAP. Leave username and password empty for an anonymous login. + - The fields `Object class`, `Base DN`, `Search field` and `LDAP search string` are used to put together a ldap search filter which will return the additional mail accounts of a user:\ + `(&(objectclass=<YOUR OBJECT CLASS>)(<YOUR SEARCH FIELD>=<YOUR SEARCH STRING>))`.\ + This filter will be executed on the `Base DN` you have defined. `LDAP search string` can contain the placeholders `#USERNAME#` (will be replaced with the username the user logged in to Snappymail) and `#BASE_DN#` (will be replaced with the value you inserted into the field `Base DN` inside the plugin settings). This will allow you to create more complex search strings like `uid=#USERNAME#`. + - `Username field`, `Domain name field of additional account`, `Mail address field for additional account` and `Additional account name field` are used to define the ldap attributes to read when the ldap search was successful. For example insert `mail` into the `Username field` and the `Domain name field of additional account` to use the [local-part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the mail address as username and the [domain-part](https://en.wikipedia.org/wiki/Email_address#Domain) as domain for the additional account.\ + `Username field` and `Domain name field of additional account` before use get checked by the plugin if they contain a mail address and if true only the local-part or domain-part is returned. If no `@` is found the content of the found ldap attribute is returned without modification. This can be usefull if your user should login with something different than the mail address (a username that is diffrent from the local-part of the mail address). + + Section `Overwrite mail address of main account` can be used to overwrite the username or mail address used at login with a value found in ldap. If activated, the username or mail address used at login will be looked up inside the `Username field` in ldap (for details see how a search for additional accounts is made). If the username is found, the value of the field `Mail address field for main account` will be used to overwrite the mail address of the main account. + + `Overwrite user cryptkey` can be activated to prevent SnappyMail from asking the user for his old LDAP password when this password was changed or reseted. SnappyMail saves the passwords of the additional accounts by encrypting them using a cryptkey that is saved in the file `.cryptkey`. When the password of the main account changes, SnappyMail asks the user for the old password to reencrypt the keys with the new userpassword. + On a password change using ldap (or when the password has been forgotten by the user) this makes problems and asks the user to insert the old password. Therefore activating this option overwrites the .cryptkey file on login in order to always accept the actual ldap password of the user. + **ATTENTION:** This has side effects on pgp keys because these are also secured by the cryptkey and could therefore not be accessible anymore! See https://github.com/the-djmaze/snappymail/issues/1570#issuecomment-2085528061 . This has also an impact on additional mail accounts that aren't created by this plugin because without the cryptkey saved passwords of additional mail accounts can not be decrypted anymore. + +**Important:** SnappyMail normally needs a mail address as username. This plugin handles some special circumstances (login with an ldap username, not a mail address) so that you can login to your IMAP server with the ldap username but send mails with a mail address connected to this ldap user. diff --git a/plugins/ldap-mail-accounts/index.php b/plugins/ldap-mail-accounts/index.php new file mode 100644 index 0000000000..bc53ad4f4e --- /dev/null +++ b/plugins/ldap-mail-accounts/index.php @@ -0,0 +1,192 @@ +<?php + +use RainLoop\Enumerations\Capa; +use RainLoop\Enumerations\PluginPropertyType; +use RainLoop\Plugins\AbstractPlugin; +use RainLoop\Plugins\Property; +use RainLoop\Model\MainAccount; +use RainLoop\Actions; + + +class LdapMailAccountsPlugin extends AbstractPlugin +{ + const + NAME = 'LDAP Mail Accounts', + VERSION = '2.2.1', + AUTHOR = 'cm-schl', + URL = 'https://github.com/cm-sch', + RELEASE = '2024-09-20', + REQUIRED = '2.36.1', + CATEGORY = 'Accounts', + DESCRIPTION = 'Add additional mail accounts the SnappyMail user has access to by a LDAP query. Basing on the work of FWest98 (https://github.com/FWest98).'; + + public function __construct() + { + include_once __DIR__ . '/LdapMailAccounts.php'; + include_once __DIR__ . '/LdapMailAccountsConfig.php'; + include_once __DIR__ . '/LdapMailAccountsException.php'; + + parent::__construct(); + } + + public function Init(): void + { + $this->addHook("login.success", 'AddAdditionalLdapMailAccounts'); + $this->addHook('login.credentials', 'overwriteMainAccountEmail'); + } + + // Function gets called by RainLoop/Actions/UserAuth.php + /** + * Overwrite the MainAccount mail address by looking up the new one in the ldap directory + * + * @param string &$sEmail + * @param string &$sImapUser + * @param string &$sPassword + * @param string &$sSmtpUser + */ + public function overwriteMainAccountEmail(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser) + { + $this->Manager()->Actions()->Logger()->Write("Login DATA: login: $sImapUser email: $sEmail", \LOG_WARNING, "LDAP MAIL ACCOUNTS PLUGIN"); + + // Set up config + $config = LdapMailAccountsConfig::MakeConfig($this->Config()); + + if ($config->bool_overwrite_mail_address_main_account) + { + $oldapMailAccounts = new LdapMailAccounts($config, $this->Manager()->Actions()->Logger()); + $oldapMailAccounts->overwriteEmail($sEmail); + } + + $this->Manager()->Actions()->Logger()->Write("Login DATA: login: $sImapUser email: $sEmail", \LOG_WARNING, "LDAP MAIL ACCOUNTS PLUGIN"); + } + + // Function gets called by RainLoop/Actions/User.php + /** + * Add additional mail accounts to the webinterface of the user by looking up the ldap directory + * + * @param MainAccount $oAccount + */ + public function AddAdditionalLdapMailAccounts(MainAccount $oAccount) + { + // Set up config + $config = LdapMailAccountsConfig::MakeConfig($this->Config()); + $oldapMailAccounts = new LdapMailAccounts($config, $this->Manager()->Actions()->Logger()); + $oldapMailAccounts->AddLdapMailAccounts($oAccount); + } + + /** + * Defines the content of the plugin configuration page inside the Admin Panel of SnappyMail + */ + protected function configMapping(): array + { + $groupOverwriteMainAccount = new \RainLoop\Plugins\PropertyCollection('Overwrite mail address of main account'); + $groupOverwriteMainAccount->exchangeArray([ + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_BOOL_OVERWRITE_MAIL_ADDRESS_MAIN_ACCOUNT)->SetLabel('Enabled') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(false), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_MAIL_ADDRESS_MAIN_ACCOUNT) + ->SetLabel("Mail address field for main account") + ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDescription("The ldap field containing the mail address to use on the SnappyMail main account. + \nThe value found inside ldap will overwrite the mail address of the SnappyMail main account (the account the user logged in at SnappyMail) + \nThe mail address used at login will still be used to login to the servers.") + ->SetDefaultValue("mail"), + ]); + + $groupAdditionalSettings = new \RainLoop\Plugins\PropertyCollection('Additional settings'); + $groupAdditionalSettings->exchangeArray([ + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_BOOL_OVERWRITE_CRYPTKEY)->SetLabel('Overwrite user cryptkey') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDescription("SnappyMail saves the passwords of the additional accounts by encrypting them using a cryptkey that is saved in the file \".cryptkey\". When the password of the main account changes, SnappyMail asks the user for the old password to reencrypt the keys with the new userpassword. + \nOn a password change using ldap (or when the password has been forgotten by the user) this makes problems and asks the user to insert the old password. Therefore activating this option overwrites the .cryptkey file on login in order to always accept the actual ldap password of the user. + \nATTENTION: This has side effects on pgp keys because these are also secured by the cryptkey and could therefore not be accessible anymore! + \nSee https://github.com/the-djmaze/snappymail/issues/1570#issuecomment-2085528061") + ->SetDefaultValue(false), + ]); + + return [ + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_SERVER) + ->SetLabel("LDAP Server URL") + ->SetPlaceholder("ldap://server:port") + ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_PROTOCOL_VERSION) + ->SetLabel("LDAP Protocol Version") + ->SetType(RainLoop\Enumerations\PluginPropertyType::SELECTION) + ->SetDefaultValue([2, 3]), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_BIND_USER) + ->SetLabel("LDAP Username") + ->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind.") + ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_BIND_PASSWORD) + ->SetLabel("LDAP Password") + ->SetDescription("Leave empty for anonymous bind.") + ->SetType(RainLoop\Enumerations\PluginPropertyType::PASSWORD), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_OBJECTCLASS) + ->SetLabel("Object class") + ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDescription("The object class to use when searching for additional mail accounts of the logged in SnappyMail user") + ->SetDefaultValue("user"), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_BASE) + ->SetLabel("Base DN") + ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDescription("The base DN to search in for additional mail accounts of the logged in SnappyMail user"), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_SEARCH) + ->SetLabel("Search field") + ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDescription("The name of the ldap attribute that has to contain the here defined 'LDAP search string'.") + ->SetDefaultValue("member"), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_SEARCH_STRING) + ->SetLabel("LDAP search string") + ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDescription("The search string used to find ldap objects of mail accounts the user has access to. + \nPossible placeholers:\n#USERNAME# - replaced with the username of the actual SnappyMail user + \n#BASE_DN# - replaced with the value inside the field 'User base DN'.") + ->SetDefaultValue("uid=#USERNAME#"), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_USERNAME) + ->SetLabel("Username field") + ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDescription("Used when searching for additional accounts or when overwriting the mail address of the main account. + \nThe field containing the username of the mail account. + \nWhen looking up additional accounts: + \nIf this field contains an email address, only the local-part before the @ is used. The domain part is retrieved configuring the field below. This username gets used by SnappyMail to login to the additional mail account + \nWhen overwriting the main account mail address: + \nThe username from SnappyMail login gets used to search an LDAP entry containig a field with the same username.") + ->SetDefaultValue("uid"), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_MAIL_DOMAIN) + ->SetLabel("Domain name field of additional account") + ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDescription("The field containing the domain name of the found additional mail account. + \nThis domain gets looked up by SnappyMail to choose the right connection parameters at logging in to the additional mail account. + \nIf this field contains an email address, only the domain-part after the @ is used.") + ->SetDefaultValue("mail"), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_MAIL_ADDRESS_ADDITIONAL_ACCOUNT) + ->SetLabel("Mail address field for additional account") + ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDescription("The ldap field containing the mail address to use on the found additional mail account. + \nThe value found inside ldap will be used as mail address of the additional mail accounts created by this plugin. + \nIn most cases this could be the same ldap field as in \"Domain name field of additional account\"") + ->SetDefaultValue("mail"), + + \RainLoop\Plugins\Property::NewInstance(LdapMailAccountsConfig::CONFIG_FIELD_NAME) + ->SetLabel("Additional account name field") + ->SetType(RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetDescription("The field containing the default sender name of the found additional mail account.") + ->SetDefaultValue("displayName"), + + $groupOverwriteMainAccount, + + $groupAdditionalSettings + ]; + } +} diff --git a/plugins/login-autoconfig/LICENSE b/plugins/login-autoconfig/LICENSE new file mode 100644 index 0000000000..61aa05084c --- /dev/null +++ b/plugins/login-autoconfig/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2024 SnappyMail + +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/login-autoconfig/index.php b/plugins/login-autoconfig/index.php new file mode 100644 index 0000000000..ba23a9b922 --- /dev/null +++ b/plugins/login-autoconfig/index.php @@ -0,0 +1,78 @@ +<?php +/** + * https://datatracker.ietf.org/doc/draft-bucksch-autoconfig/ + */ + +use MailSo\Net\Enumerations\ConnectionSecurityType; + +class LoginAutoconfigPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Login Autoconfig', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '1.1', + RELEASE = '2024-03-12', + REQUIRED = '2.35.3', + CATEGORY = 'Login', + LICENSE = 'MIT', + DESCRIPTION = 'Tries to login using the domain autoconfig'; + + public function Init() : void + { + $this->addHook('login.credentials.step-1', 'detect'); + } + + public function detect(string $sEmail) : void + { + if (\str_contains($sEmail, '@')) { + $oProvider = $this->Manager()->Actions()->DomainProvider(); + $sDomain = \MailSo\Base\Utils::getEmailAddressDomain($sEmail); + $oDomain = $oProvider->Load($sDomain, false); + if (!$oDomain) { + $result = \RainLoop\Providers\Domain\Autoconfig::discover($sEmail); + if ($result) { + $typeIMAP = ConnectionSecurityType::AUTO_DETECT; + if ('STARTTLS' === $result['incomingServer'][0]['socketType']) { + $typeIMAP = ConnectionSecurityType::STARTTLS; + } else if ('SSL' === $result['incomingServer'][0]['socketType']) { + $typeIMAP = ConnectionSecurityType::SSL; + } + $typeSMTP = ConnectionSecurityType::AUTO_DETECT; + if ('STARTTLS' === $result['outgoingServer'][0]['socketType']) { + $typeSMTP = ConnectionSecurityType::STARTTLS; + } else if ('SSL' === $result['outgoingServer'][0]['socketType']) { + $typeSMTP = ConnectionSecurityType::SSL; + } + $oDomain = \RainLoop\Model\Domain::fromArray($sDomain, [ + 'IMAP' => [ + 'host' => $result['incomingServer'][0]['hostname'], + 'port' => $result['incomingServer'][0]['port'], + 'type' => $typeIMAP, + 'shortLogin' => '%EMAILADDRESS%' !== $result['incomingServer'][0]['username'], +// 'ssl' => [] + ], + 'SMTP' => [ + 'host' => $result['outgoingServer'][0]['hostname'], + 'port' => $result['outgoingServer'][0]['port'], + 'type' => $typeSMTP, + 'shortLogin' => '%EMAILADDRESS%' !== $result['outgoingServer'][0]['username'], +// 'useAuth' => !empty($result['authentication']), + ], + 'Sieve' => [ + 'host' => $result['incomingServer'][0]['hostname'], + 'port' => $result['incomingServer'][0]['port'], + 'type' => $typeIMAP, + 'shortLogin' => '%EMAILADDRESS%' !== $result['incomingServer'][0]['username'], + 'enabled' => false + ], + 'whiteList' => '' + ]); + $oProvider->Save($oDomain); + \SnappyMail\Log::notice('Autoconfig', "Domain setup for '{$sDomain}' is created and active"); + } + } + } + } + +} diff --git a/plugins/login-cpanel/LICENSE b/plugins/login-cpanel/LICENSE new file mode 100644 index 0000000000..f709b02e27 --- /dev/null +++ b/plugins/login-cpanel/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +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 +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/login-cpanel/index.php b/plugins/login-cpanel/index.php new file mode 100644 index 0000000000..9bc5ebf7a7 --- /dev/null +++ b/plugins/login-cpanel/index.php @@ -0,0 +1,77 @@ +<?php + +class LogincPanelPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Login cPanel', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '1.4', + RELEASE = '2024-03-27', + REQUIRED = '2.36.1', + CATEGORY = 'Login', + LICENSE = 'MIT', + DESCRIPTION = 'Tries to login using the cPanel $_ENV["REMOTE_*"] variables'; + + public function Init() : void + { + $this->addPartHook('cPanelAutoLogin', 'AutoLogin'); + $this->addHook('filter.app-data', 'FilterAppData'); + $this->addHook('login.credentials', 'FilterLoginCredentials'); + } + + public function FilterAppData($bAdmin, &$aResult) + { + if (!$bAdmin && \is_array($aResult) && empty($aResult['Auth']) && isset($_ENV['REMOTE_USER'])) { + $aResult['DevEmail'] = $_ENV['REMOTE_USER']; +// $aResult['DevPassword'] = $_ENV['REMOTE_PASSWORD']; + } + } + + private static bool $login = false; + public function AutoLogin() : bool + { + $oActions = \RainLoop\Api::Actions(); + + $oException = null; + $oAccount = null; + + $sEmail = $_ENV['REMOTE_USER'] ?? ''; + $sPassword = $_ENV['REMOTE_PASSWORD'] ?? ''; + + if (\strlen($sEmail) && \strlen($sPassword)) { + try + { + static::$login = true; + $oAccount = $oActions->LoginProcess($sEmail, $sPassword); + } + catch (\Throwable $oException) + { + $oLogger = $oActions->Logger(); + $oLogger && $oLogger->WriteException($oException); + } + } + + \MailSo\Base\Http::Location('./'); + return true; + } + + public function FilterLoginCredentials(&$sEmail, &$sImapUser, &$sPassword, &$sSmtpUser) + { + // cPanel https://github.com/the-djmaze/snappymail/issues/697 +// && !empty($_ENV['CPANEL']) + if (static::$login/* && $sImapUser == $_ENV['REMOTE_USER']*/) { + if (empty($_ENV['REMOTE_TEMP_USER'])) { + $iPos = \strpos($sPassword, '[::cpses::]'); + if ($iPos) { + $_ENV['REMOTE_TEMP_USER'] = \substr($sPassword, 0, $iPos); + } + } + if (!empty($_ENV['REMOTE_TEMP_USER'])) { + $sImapUser = $_ENV['REMOTE_USER'] . '/' . $_ENV['REMOTE_TEMP_USER']; + $sSmtpUser = $_ENV['REMOTE_USER'] . '/' . $_ENV['REMOTE_TEMP_USER']; + } + } + } + +} diff --git a/plugins/login-external-sso/LICENSE b/plugins/login-external-sso/LICENSE new file mode 100644 index 0000000000..f709b02e27 --- /dev/null +++ b/plugins/login-external-sso/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +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 +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/login-external-sso/index.php b/plugins/login-external-sso/index.php new file mode 100644 index 0000000000..f3a215b046 --- /dev/null +++ b/plugins/login-external-sso/index.php @@ -0,0 +1,56 @@ +<?php + +class LoginExternalSsoPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Login External SSO', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '1.0', + RELEASE = '2022-11-11', + REQUIRED = '2.21.0', + CATEGORY = 'Login', + LICENSE = 'MIT', + DESCRIPTION = 'Login with $_POST "Email", "Password" and "SsoKey". It returns an SSO hash to use with "?Sso&hash="'; + + public function Init() : void + { + $this->addPartHook('ExternalSso', 'ServiceExternalSso'); + } + + public function ServiceExternalSso() : string + { + $oActions = \RainLoop\Api::Actions(); + $oActions->Http()->ServerNoCache(); + $sKey = $this->Config()->Get('plugin', 'key', ''); + $sEmail = isset($_POST['Email']) ? $_POST['Email'] : ''; + $sPassword = isset($_POST['Password']) ? $_POST['Password'] : ''; + if ($sEmail && $sPassword && $sKey && isset($_POST['SsoKey']) && $_POST['SsoKey'] == $sKey) { + $sResult = \RainLoop\Api::CreateUserSsoHash($sEmail, $sPassword); + if (isset($_POST['Output']) && 'json' === \strtolower($_POST['Output'])) { + \header('Content-Type: application/json; charset=utf-8'); + echo \json_encode(array( + 'Action' => 'ExternalSso', + 'Result' => $sResult + )); + } else { + \header('Content-Type: text/plain'); + echo $sResult; + } + } + return true; + } + + /** + * @return array + */ + public function configMapping() : array + { + return array( + // Was application.ini external_sso_key + \RainLoop\Plugins\Property::NewInstance('key')->SetLabel('SSO key') + ->SetDefaultValue(''), + ); + } + +} diff --git a/plugins/login-external/LICENSE b/plugins/login-external/LICENSE new file mode 100644 index 0000000000..f709b02e27 --- /dev/null +++ b/plugins/login-external/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +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 +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/login-external/index.php b/plugins/login-external/index.php new file mode 100644 index 0000000000..9929d5b2e0 --- /dev/null +++ b/plugins/login-external/index.php @@ -0,0 +1,69 @@ +<?php + +class LoginExternalPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Login External', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '1.4', + RELEASE = '2024-07-01', + REQUIRED = '2.36.1', + CATEGORY = 'Login', + LICENSE = 'MIT', + DESCRIPTION = 'Login with $_POST["Email"] and $_POST["Password"] from anywhere'; + + public function Init() : void + { + $this->addPartHook('ExternalLogin', 'ServiceExternalLogin'); + } + + public function ServiceExternalLogin() : bool + { + $oActions = \RainLoop\Api::Actions(); + $oActions->Http()->ServerNoCache(); + + $oAccount = null; + $oException = null; + + $sEmail = isset($_POST['Email']) ? $_POST['Email'] : ''; + $sPassword = isset($_POST['Password']) ? $_POST['Password'] : ''; + + try + { + // Convert password to SensitiveString type + $oPassword = new \SnappyMail\SensitiveString($sPassword); + $oAccount = $oActions->LoginProcess($sEmail, $oPassword); + if (!$oAccount instanceof \RainLoop\Model\MainAccount) { + $oAccount = null; + \SnappyMail\LOG::info(\get_class($this), 'LoginProcess failed'); + } + } + catch (\Throwable $oException) + { + $oLogger = $oActions->Logger(); + $oLogger && $oLogger->WriteException($oException); + } + + if (isset($_POST['Output']) && 'json' === \strtolower($_POST['Output'])) { + \header('Content-Type: application/json; charset=utf-8'); + $aResult = array( + 'Action' => 'ExternalLogin', + 'Result' => $oAccount ? true : false, + 'ErrorCode' => 0 + ); + if (!$oAccount) { + if ($oException instanceof \RainLoop\Exceptions\ClientException) { + $aResult['ErrorCode'] = $oException->getCode(); + } else { + $aResult['ErrorCode'] = \RainLoop\Notifications::AuthError; + } + } + echo \json_encode($aResult); + } else { + \MailSo\Base\Http::Location('./'); + } + return true; + } + +} diff --git a/plugins/login-gmail/LoginOAuth2.js b/plugins/login-gmail/LoginOAuth2.js new file mode 100644 index 0000000000..efee7ad60d --- /dev/null +++ b/plugins/login-gmail/LoginOAuth2.js @@ -0,0 +1,46 @@ +(rl => { + const client_id = rl.pluginSettingsGet('login-gmail', 'client_id'), + login = () => { + document.location = 'https://accounts.google.com/o/oauth2/auth?' + (new URLSearchParams({ + response_type: 'code', + client_id: client_id, + redirect_uri: document.location.href + '?LoginGMail', + scope: [ + // Primary Google Account email address + 'https://www.googleapis.com/auth/userinfo.email', + // Personal info + 'https://www.googleapis.com/auth/userinfo.profile', + // Associate personal info + 'openid', + // Access IMAP and SMTP through OAUTH + 'https://mail.google.com/' + ].join(' '), + state: 'gmail', // + rl.settings.app('token') + localStorage.getItem('smctoken') + // Force authorize screen, so we always get a refresh_token + access_type: 'offline', + prompt: 'consent' + })); + }; + + if (client_id) { + addEventListener('sm-user-login', e => { + if (event.detail.get('Email').includes('@gmail.com')) { + e.preventDefault(); + login(); + } + }); + + addEventListener('rl-view-model', e => { + if ('Login' === e.detail.viewModelTemplateID) { + const + container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'), + btn = Element.fromHTML('<button type="button">GMail</button>'), + div = Element.fromHTML('<div class="controls"></div>'); + btn.onclick = login; + div.append(btn); + container && container.append(div); + } + }); + } + +})(window.rl); diff --git a/plugins/login-gmail/index.php b/plugins/login-gmail/index.php new file mode 100644 index 0000000000..a6b8d3f9d3 --- /dev/null +++ b/plugins/login-gmail/index.php @@ -0,0 +1,223 @@ +<?php + +/** + * https://developers.google.com/gmail/imap/imap-smtp + * https://developers.google.com/gmail/imap/xoauth2-protocol + * https://console.cloud.google.com/apis/dashboard + */ + +use RainLoop\Model\MainAccount; +use RainLoop\Providers\Storage\Enumerations\StorageType; + +class LoginGMailPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Login GMail OAuth2', + VERSION = '2.37', + RELEASE = '2024-07-15', + REQUIRED = '2.36.1', + CATEGORY = 'Login', + DESCRIPTION = 'GMail IMAP, Sieve & SMTP login using RFC 7628 OAuth2'; + + const + LOGIN_URI = 'https://accounts.google.com/o/oauth2/auth', + TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'; + + private static ?array $auth = null; + + public function Init() : void + { + $this->UseLangs(true); + $this->addJs('LoginOAuth2.js'); + $this->addHook('imap.before-login', 'clientLogin'); + $this->addHook('smtp.before-login', 'clientLogin'); + $this->addHook('sieve.before-login', 'clientLogin'); + + $this->addPartHook('LoginGMail', 'ServiceLoginGMail'); + + // Prevent Disallowed Sec-Fetch Dest: document Mode: navigate Site: cross-site User: true + $this->addHook('filter.http-paths', 'httpPaths'); + } + + public function httpPaths(array $aPaths) : void + { + if (!empty($aPaths[0]) && 'LoginGMail' === $aPaths[0]) { + $oConfig = \RainLoop\Api::Config(); + $oConfig->Set('security', 'secfetch_allow', + \trim($oConfig->Get('security', 'secfetch_allow', '') . ';site=cross-site', ';') + ); + } + } + + public function ServiceLoginGMail() : string + { + $oActions = \RainLoop\Api::Actions(); + $oHttp = $oActions->Http(); + $oHttp->ServerNoCache(); + + $uri = \preg_replace('/.LoginGMail.*$/D', '', $_SERVER['REQUEST_URI']); + + try + { + if (isset($_GET['error'])) { + throw new \RuntimeException($_GET['error']); + } + if (isset($_GET['code']) && isset($_GET['state']) && 'gmail' === $_GET['state']) { + $oGMail = $this->gmailConnector(); + } + if (empty($oGMail)) { + $oActions->Location($uri); + exit; + } + + $iExpires = \time(); + $aResponse = $oGMail->getAccessToken( + static::TOKEN_URI, + 'authorization_code', + array( + 'code' => $_GET['code'], + 'redirect_uri' => $oHttp->GetFullUrl().'?LoginGMail' + ) + ); + if (200 != $aResponse['code']) { + if (isset($aResponse['result']['error'])) { + throw new \RuntimeException( + $aResponse['code'] + . ': ' + . $aResponse['result']['error'] + . ' / ' + . $aResponse['result']['error_description'] + ); + } + throw new \RuntimeException("HTTP: {$aResponse['code']}"); + } + $aResponse = $aResponse['result']; + if (empty($aResponse['access_token'])) { + throw new \RuntimeException('access_token missing'); + } + if (empty($aResponse['refresh_token'])) { + throw new \RuntimeException('refresh_token missing'); + } + + $sAccessToken = $aResponse['access_token']; + $iExpires += $aResponse['expires_in']; + + $oGMail->setAccessToken($sAccessToken); + $aUserInfo = $oGMail->fetch('https://www.googleapis.com/oauth2/v2/userinfo'); + if (200 != $aUserInfo['code']) { + throw new \RuntimeException("HTTP: {$aResponse['code']}"); + } + $aUserInfo = $aUserInfo['result']; + if (empty($aUserInfo['id'])) { + throw new \RuntimeException('unknown id'); + } + if (empty($aUserInfo['email'])) { + throw new \RuntimeException('unknown email address'); + } + + static::$auth = [ + 'access_token' => $sAccessToken, + 'refresh_token' => $aResponse['refresh_token'], + 'expires_in' => $aResponse['expires_in'], + 'expires' => $iExpires + ]; + + $oPassword = new \SnappyMail\SensitiveString($aUserInfo['id']); + $oAccount = $oActions->LoginProcess($aUserInfo['email'], $oPassword); +// $oAccount = MainAccount::NewInstanceFromCredentials($oActions, $aUserInfo['email'], $aUserInfo['email'], $oPassword, true); + if ($oAccount) { +// $oActions->SetMainAuthAccount($oAccount); +// $oActions->SetAuthToken($oAccount); + $oActions->StorageProvider()->Put($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken(), + \SnappyMail\Crypt::EncryptToJSON(static::$auth, $oAccount->CryptKey()) + ); + } + } + catch (\Exception $oException) + { + $oActions->Logger()->WriteException($oException, \LOG_ERR); + } + $oActions->Location($uri); + exit; + } + + public function configMapping() : array + { + return [ + \RainLoop\Plugins\Property::NewInstance('client_id') + ->SetLabel('Client ID') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetAllowedInJs() + ->SetDescription('https://github.com/the-djmaze/snappymail/wiki/FAQ#gmail'), + \RainLoop\Plugins\Property::NewInstance('client_secret') + ->SetLabel('Client Secret') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetEncrypted() + ]; + } + + public function clientLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void + { + if ($oAccount instanceof MainAccount && \str_ends_with($oAccount->Email(), '@gmail.com')) { + $oActions = \RainLoop\Api::Actions(); + try { + $aData = static::$auth ?: \SnappyMail\Crypt::DecryptFromJSON( + $oActions->StorageProvider()->Get($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken()), + $oAccount->CryptKey() + ); + } catch (\Throwable $oException) { +// $oActions->Logger()->WriteException($oException, \LOG_ERR); + return; + } + if (!empty($aData['expires']) && !empty($aData['access_token']) && !empty($aData['refresh_token'])) { + if (\time() >= $aData['expires']) { + $iExpires = \time(); + $oGMail = $this->gmailConnector(); + if ($oGMail) { + $aRefreshTokenResponse = $oGMail->getAccessToken( + static::TOKEN_URI, + 'refresh_token', + array('refresh_token' => $aData['refresh_token']) + ); + if (!empty($aRefreshTokenResponse['result']['access_token'])) { + $aData['access_token'] = $aRefreshTokenResponse['result']['access_token']; + $aResponse['expires'] = $iExpires + $aResponse['expires_in']; + $oActions->StorageProvider()->Put($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken(), + \SnappyMail\Crypt::EncryptToJSON($aData, $oAccount->CryptKey()) + ); + } + } + } + $oSettings->passphrase = $aData['access_token']; + \array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER', 'XOAUTH2'); + } + } + } + + protected function gmailConnector() : ?\OAuth2\Client + { + $client_id = \trim($this->Config()->Get('plugin', 'client_id', '')); + $client_secret = \trim($this->Config()->getDecrypted('plugin', 'client_secret', '')); + if ($client_id && $client_secret) { + try + { + $oGMail = new \OAuth2\Client($client_id, $client_secret); + $oActions = \RainLoop\Api::Actions(); + $sProxy = $oActions->Config()->Get('labs', 'curl_proxy', ''); + if (\strlen($sProxy)) { + $oGMail->setCurlOption(CURLOPT_PROXY, $sProxy); + $sProxyAuth = $oActions->Config()->Get('labs', 'curl_proxy_auth', ''); + if (\strlen($sProxyAuth)) { + $oGMail->setCurlOption(CURLOPT_PROXYUSERPWD, $sProxyAuth); + } + } + return $oGMail; + } + catch (\Exception $oException) + { + $oActions->Logger()->WriteException($oException, \LOG_ERR); + } + } + return null; + } +} diff --git a/plugins/login-o365/LoginOAuth2.js b/plugins/login-o365/LoginOAuth2.js new file mode 100644 index 0000000000..dd8c843eda --- /dev/null +++ b/plugins/login-o365/LoginOAuth2.js @@ -0,0 +1,54 @@ +(rl => { + const client_id = rl.pluginSettingsGet('login-o365', 'client_id'), + // https://learn.microsoft.com/en-us/entra/identity-platform/reply-url#query-parameter-support-in-redirect-uris + query = rl.pluginSettingsGet('login-o365', 'personal') ? '' : '?', + tenant = rl.pluginSettingsGet('login-o365', 'tenant'), + login = () => { + document.location = 'https://login.microsoftonline.com/'+tenant+'/oauth2/v2.0/authorize?' + (new URLSearchParams({ + response_type: 'code', + client_id: client_id, + redirect_uri: document.location.href.replace(/\/$/, '') + '/' + query + 'LoginO365', + scope: [ + // Associate personal info + 'openid', + 'offline_access', + 'email', + 'profile', + // Access IMAP and SMTP through OAUTH + 'https://graph.microsoft.com/IMAP.AccessAsUser.All', +// 'https://graph.microsoft.com/Mail.ReadWrite' + 'https://graph.microsoft.com/Mail.Send' +/* // Legacy: + 'https://outlook.office.com/SMTP.Send', + 'https://outlook.office.com/IMAP.AccessAsUser.All' +*/ + ].join(' '), + state: 'o365', // + rl.settings.app('token') + localStorage.getItem('smctoken') + // Force authorize screen, so we always get a refresh_token + access_type: 'offline', + prompt: 'consent' + })); + }; + + if (client_id) { + addEventListener('sm-user-login', e => { + if (event.detail.get('Email').includes('@hotmail.com')) { + e.preventDefault(); + login(); + } + }); + + addEventListener('rl-view-model', e => { + if ('Login' === e.detail.viewModelTemplateID) { + const + container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'), + btn = Element.fromHTML('<button type="button">Outlook</button>'), + div = Element.fromHTML('<div class="controls"></div>'); + btn.onclick = login; + div.append(btn); + container && container.append(div); + } + }); + } + +})(window.rl); diff --git a/plugins/login-o365/index.php b/plugins/login-o365/index.php new file mode 100644 index 0000000000..62050f429b --- /dev/null +++ b/plugins/login-o365/index.php @@ -0,0 +1,248 @@ +<?php +/** + * Microsoft requires an Azure account that has an active subscription + * I'm not going to pay, so feel free to fix this code yourself. + * https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth + * https://answers.microsoft.com/en-us/msoffice/forum/all/configuration-for-imap-pop-and-smtp-with-oauth-in/3db47d43-25ac-4e0b-b957-22585e6caf15 + * + * https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps + * + * https://learn.microsoft.com/en-us/entra/identity-platform/reply-url#query-parameter-support-in-redirect-uris + * Azure: redirect_uri=https://{DOMAIN}/?LoginO365 + * Personal: redirect_uri=https://{DOMAIN}/LoginO365 + */ + +use RainLoop\Model\MainAccount; +use RainLoop\Providers\Storage\Enumerations\StorageType; + +class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Office365/Outlook OAuth2', + VERSION = '0.3', + RELEASE = '2024-09-29', + REQUIRED = '2.36.1', + CATEGORY = 'Login', + DESCRIPTION = 'Office365/Outlook IMAP, Sieve & SMTP login using RFC 7628 OAuth2'; + + // https://login.microsoftonline.com/{{tenant}}/v2.0/.well-known/openid-configuration + const + LOGIN_URI = 'https://login.microsoftonline.com/{{tenant}}/oauth2/v2.0/auth', + TOKEN_URI = 'https://login.microsoftonline.com/{{tenant}}/oauth2/v2.0/token'; + + private static ?array $auth = null; + + public function Init() : void + { + $this->UseLangs(true); + $this->addJs('LoginOAuth2.js'); + $this->addHook('imap.before-login', 'clientLogin'); + $this->addHook('smtp.before-login', 'clientLogin'); + $this->addHook('sieve.before-login', 'clientLogin'); + + $this->addPartHook('LoginO365', 'ServiceLoginO365'); + + // Prevent Disallowed Sec-Fetch Dest: document Mode: navigate Site: cross-site User: true + $this->addHook('filter.http-paths', 'httpPaths'); + } + + public function httpPaths(array &$aPaths) : void + { + // Personal accounts workaround + if (!empty($_SERVER['PATH_INFO']) && \str_ends_with($_SERVER['PATH_INFO'], 'LoginO365')) { + $aPaths = ['LoginO365']; + } + + if (!empty($aPaths[0]) && 'LoginO365' === $aPaths[0]) { + $oConfig = \RainLoop\Api::Config(); + $oConfig->Set('security', 'secfetch_allow', + \trim($oConfig->Get('security', 'secfetch_allow', '') . ';site=cross-site', ';') + ); + } + } + + public function ServiceLoginO365() : string + { + $oActions = \RainLoop\Api::Actions(); + $oHttp = $oActions->Http(); + $oHttp->ServerNoCache(); + + try + { + if (isset($_GET['error'])) { + throw new \RuntimeException("{$_GET['error']}: {$_GET['error_description']}"); + } + if (!isset($_GET['code']) || empty($_GET['state']) || 'o365' !== $_GET['state']) { + $oActions->Location(\RainLoop\Utils::WebPath()); + exit; + } + $oO365 = $this->o365Connector(); + if (!$oO365) { + $oActions->Location(\RainLoop\Utils::WebPath()); + exit; + } + + $iExpires = \time(); + $aResponse = $oO365->getAccessToken( + \str_replace('{{tenant}}', $this->Config()->Get('plugin', 'tenant', 'common'), static::TOKEN_URI), + 'authorization_code', + array( + 'code' => $_GET['code'], + 'redirect_uri' => $oHttp->GetFullUrl().'?LoginO365' + ) + ); + if (200 != $aResponse['code']) { + if (isset($aResponse['result']['error'])) { + throw new \RuntimeException( + $aResponse['code'] + . ': ' + . $aResponse['result']['error'] + . ' / ' + . $aResponse['result']['error_description'] + ); + } + throw new \RuntimeException("HTTP: {$aResponse['code']}"); + } + $aResponse = $aResponse['result']; + if (empty($aResponse['access_token'])) { + throw new \RuntimeException('access_token missing'); + } + if (empty($aResponse['refresh_token'])) { + throw new \RuntimeException('refresh_token missing'); + } + + $sAccessToken = $aResponse['access_token']; + $iExpires += $aResponse['expires_in']; + + $oO365->setAccessToken($sAccessToken); + $aUserInfo = $oO365->fetch('https://graph.microsoft.com/oidc/userinfo'); + if (200 != $aUserInfo['code']) { + throw new \RuntimeException("HTTP: {$aResponse['code']}"); + } + $aUserInfo = $aUserInfo['result']; + if (empty($aUserInfo['id'])) { + throw new \RuntimeException('unknown id'); + } + if (empty($aUserInfo['email'])) { + throw new \RuntimeException('unknown email address'); + } + + static::$auth = [ + 'access_token' => $sAccessToken, + 'refresh_token' => $aResponse['refresh_token'], + 'expires_in' => $aResponse['expires_in'], + 'expires' => $iExpires + ]; + + $oPassword = new \SnappyMail\SensitiveString($aUserInfo['id']); + $oAccount = $oActions->LoginProcess($aUserInfo['email'], $oPassword); +// $oAccount = MainAccount::NewInstanceFromCredentials($oActions, $aUserInfo['email'], $aUserInfo['email'], $oPassword, true); + if ($oAccount) { +// $oActions->SetMainAuthAccount($oAccount); +// $oActions->SetAuthToken($oAccount); + $oActions->StorageProvider()->Put($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken(), + \SnappyMail\Crypt::EncryptToJSON(static::$auth, $oAccount->CryptKey()) + ); + } + } + catch (\Exception $oException) + { + $oActions->Logger()->WriteException($oException, \LOG_ERR); + } + $oActions->Location(\RainLoop\Utils::WebPath()); + exit; + } + + public function configMapping() : array + { + return [ + \RainLoop\Plugins\Property::NewInstance('personal') + ->SetLabel('Use with personal accounts') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(true) + ->SetAllowedInJs() + ->SetDescription('Sign in users with personal Microsoft accounts such as Outlook.com (Hotmail)'), + \RainLoop\Plugins\Property::NewInstance('client_id') + ->SetLabel('Client ID') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetAllowedInJs() + ->SetDescription('https://github.com/the-djmaze/snappymail/wiki/FAQ#o365'), + \RainLoop\Plugins\Property::NewInstance('client_secret') + ->SetLabel('Client Secret') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) + ->SetEncrypted(), + \RainLoop\Plugins\Property::NewInstance('tenant_id') + ->SetLabel('Tenant ID') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING), + \RainLoop\Plugins\Property::NewInstance('tenant')->SetLabel('Tenant') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) + ->SetDefaultValue(['common','consumers','organizations']) + ->SetAllowedInJs() + ]; + } + + public function clientLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void + { + if ($oAccount instanceof MainAccount && \str_ends_with($oAccount->Email(), '@hotmail.com')) { + $oActions = \RainLoop\Api::Actions(); + try { + $aData = static::$auth ?: \SnappyMail\Crypt::DecryptFromJSON( + $oActions->StorageProvider()->Get($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken()), + $oAccount->CryptKey() + ); + } catch (\Throwable $oException) { +// $oActions->Logger()->WriteException($oException, \LOG_ERR); + return; + } + if (!empty($aData['expires']) && !empty($aData['access_token']) && !empty($aData['refresh_token'])) { + if (\time() >= $aData['expires']) { + $iExpires = \time(); + $oO365 = $this->o365Connector(); + if ($oO365) { + $aRefreshTokenResponse = $oO365->getAccessToken( + \str_replace('{{tenant}}', $this->Config()->Get('plugin', 'tenant', 'common'), static::TOKEN_URI), + 'refresh_token', + array('refresh_token' => $aData['refresh_token']) + ); + if (!empty($aRefreshTokenResponse['result']['access_token'])) { + $aData['access_token'] = $aRefreshTokenResponse['result']['access_token']; + $aResponse['expires'] = $iExpires + $aResponse['expires_in']; + $oActions->StorageProvider()->Put($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken(), + \SnappyMail\Crypt::EncryptToJSON($aData, $oAccount->CryptKey()) + ); + } + } + } + $oSettings->passphrase = $aData['access_token']; + \array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER', 'XOAUTH2'); + } + } + } + + protected function o365Connector() : ?\OAuth2\Client + { + $client_id = \trim($this->Config()->Get('plugin', 'client_id', '')); + $client_secret = \trim($this->Config()->getDecrypted('plugin', 'client_secret', '')); + if ($client_id && $client_secret) { + try + { + $oO365 = new \OAuth2\Client($client_id, $client_secret); + $oActions = \RainLoop\Api::Actions(); + $sProxy = $oActions->Config()->Get('labs', 'curl_proxy', ''); + if (\strlen($sProxy)) { + $oO365->setCurlOption(CURLOPT_PROXY, $sProxy); + $sProxyAuth = $oActions->Config()->Get('labs', 'curl_proxy_auth', ''); + if (\strlen($sProxyAuth)) { + $oO365->setCurlOption(CURLOPT_PROXYUSERPWD, $sProxyAuth); + } + } + return $oO365; + } + catch (\Exception $oException) + { + $oActions->Logger()->WriteException($oException, \LOG_ERR); + } + } + return null; + } +} diff --git a/plugins/login-oauth2/LoginOAuth2.js b/plugins/login-oauth2/LoginOAuth2.js new file mode 100644 index 0000000000..f39bbf12e1 --- /dev/null +++ b/plugins/login-oauth2/LoginOAuth2.js @@ -0,0 +1,19 @@ +(rl => { + if (rl) { + addEventListener('rl-view-model', e => { + // instanceof LoginUserView + if (e.detail && 'Login' === e.detail.viewModelTemplateID) { + const LoginUserView = e.detail, + submitCommand = LoginUserView.submitCommand; + LoginUserView.submitCommand = (self, event) => { + if (LoginUserView.email().includes('@gmail.com')) { + // TODO: redirect to google + } else { + submitCommand.call(LoginUserView, self, event); + } + }; + } + }); + } + +})(window.rl); diff --git a/plugins/login-oauth2/OAuth2/Client.php b/plugins/login-oauth2/OAuth2/Client.php new file mode 100644 index 0000000000..176240d5b6 --- /dev/null +++ b/plugins/login-oauth2/OAuth2/Client.php @@ -0,0 +1,526 @@ +<?php +/** + * Note : Code is released under the GNU LGPL + * + * Please do not change the header of this file + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * See the GNU Lesser General Public License for more details. + */ + +/** + * Light PHP wrapper for the OAuth 2.0 protocol. + * + * This client is based on the OAuth2 specification draft v2.15 + * http://tools.ietf.org/html/draft-ietf-oauth-v2-15 + * + * @author Pierrick Charron <pierrick@webstart.fr> + * @author Anis Berejeb <anis.berejeb@gmail.com> + * @version 1.3.1-dev + */ +namespace OAuth2; + +class Client +{ + /** + * Different AUTH method + */ + const AUTH_TYPE_URI = 0; + const AUTH_TYPE_AUTHORIZATION_BASIC = 1; + const AUTH_TYPE_FORM = 2; + + /** + * Different Access token type + */ + const ACCESS_TOKEN_URI = 0; + const ACCESS_TOKEN_BEARER = 1; + const ACCESS_TOKEN_OAUTH = 2; + const ACCESS_TOKEN_MAC = 3; + + /** + * Different Grant types + */ + const GRANT_TYPE_AUTH_CODE = 'authorization_code'; + const GRANT_TYPE_PASSWORD = 'password'; + const GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials'; + const GRANT_TYPE_REFRESH_TOKEN = 'refresh_token'; + + /** + * HTTP Methods + */ + const HTTP_METHOD_GET = 'GET'; + const HTTP_METHOD_POST = 'POST'; + const HTTP_METHOD_PUT = 'PUT'; + const HTTP_METHOD_DELETE = 'DELETE'; + const HTTP_METHOD_HEAD = 'HEAD'; + const HTTP_METHOD_PATCH = 'PATCH'; + + /** + * HTTP Form content types + */ + const HTTP_FORM_CONTENT_TYPE_APPLICATION = 0; + const HTTP_FORM_CONTENT_TYPE_MULTIPART = 1; + + /** + * Client ID + * + * @var string + */ + protected $client_id = null; + + /** + * Client Secret + * + * @var string + */ + protected $client_secret = null; + + /** + * Client Authentication method + * + * @var int + */ + protected $client_auth = self::AUTH_TYPE_URI; + + /** + * Access Token + * + * @var string + */ + protected $access_token = null; + + /** + * Access Token Type + * + * @var int + */ + protected $access_token_type = self::ACCESS_TOKEN_URI; + + /** + * Access Token Secret + * + * @var string + */ + protected $access_token_secret = null; + + /** + * Access Token crypt algorithm + * + * @var string + */ + protected $access_token_algorithm = null; + + /** + * Access Token Parameter name + * + * @var string + */ + protected $access_token_param_name = 'access_token'; + + /** + * The path to the certificate file to use for https connections + * + * @var string Defaults to . + */ + protected $certificate_file = null; + + /** + * cURL options + * + * @var array + */ + protected $curl_options = array(); + + /** + * Construct + * + * @param string $client_id Client ID + * @param string $client_secret Client Secret + * @param int $client_auth (AUTH_TYPE_URI, AUTH_TYPE_AUTHORIZATION_BASIC, AUTH_TYPE_FORM) + * @param string $certificate_file Indicates if we want to use a certificate file to trust the server. Optional, defaults to null. + * @return void + */ + public function __construct($client_id, $client_secret, $client_auth = self::AUTH_TYPE_URI, $certificate_file = null) + { + if (!extension_loaded('curl')) { + throw new Exception('The PHP exention curl must be installed to use this library.', Exception::CURL_NOT_FOUND); + } + + $this->client_id = $client_id; + $this->client_secret = $client_secret; + $this->client_auth = $client_auth; + $this->certificate_file = $certificate_file; + if (!empty($this->certificate_file) && !is_file($this->certificate_file)) { + throw new InvalidArgumentException('The certificate file was not found', InvalidArgumentException::CERTIFICATE_NOT_FOUND); + } + } + + /** + * Get the client Id + * + * @return string Client ID + */ + public function getClientId() + { + return $this->client_id; + } + + /** + * Get the client Secret + * + * @return string Client Secret + */ + public function getClientSecret() + { + return $this->client_secret; + } + + /** + * getAuthenticationUrl + * + * @param string $auth_endpoint Url of the authentication endpoint + * @param string $redirect_uri Redirection URI + * @param array $extra_parameters Array of extra parameters like scope or state (Ex: array('scope' => null, 'state' => '')) + * @return string URL used for authentication + */ + public function getAuthenticationUrl($auth_endpoint, $redirect_uri, array $extra_parameters = array()) + { + $parameters = array_merge(array( + 'response_type' => 'code', + 'client_id' => $this->client_id, + 'redirect_uri' => $redirect_uri + ), $extra_parameters); + return $auth_endpoint . '?' . http_build_query($parameters, '', '&'); + } + + /** + * getAccessToken + * + * @param string $token_endpoint Url of the token endpoint + * @param string $grant_type Grant Type ('authorization_code', 'password', 'client_credentials', 'refresh_token', or a custom code (@see GrantType Classes) + * @param array $parameters Array sent to the server (depend on which grant type you're using) + * @param array $extra_headers Array of extra headers + * @return array Array of parameters required by the grant_type (CF SPEC) + */ + public function getAccessToken($token_endpoint, $grant_type, array $parameters, array $extra_headers = array()) + { + if (!$grant_type) { + throw new InvalidArgumentException('The grant_type is mandatory.', InvalidArgumentException::INVALID_GRANT_TYPE); + } + $grantTypeClassName = $this->convertToCamelCase($grant_type); + $grantTypeClass = __NAMESPACE__ . '\\GrantType\\' . $grantTypeClassName; + if (!class_exists($grantTypeClass)) { + throw new InvalidArgumentException('Unknown grant type \'' . $grant_type . '\'', InvalidArgumentException::INVALID_GRANT_TYPE); + } + $grantTypeObject = new $grantTypeClass(); + $grantTypeObject->validateParameters($parameters); + if (!defined($grantTypeClass . '::GRANT_TYPE')) { + throw new Exception('Unknown constant GRANT_TYPE for class ' . $grantTypeClassName, Exception::GRANT_TYPE_ERROR); + } + $parameters['grant_type'] = $grantTypeClass::GRANT_TYPE; + $http_headers = $extra_headers; + switch ($this->client_auth) { + case self::AUTH_TYPE_URI: + case self::AUTH_TYPE_FORM: + $parameters['client_id'] = $this->client_id; + $parameters['client_secret'] = $this->client_secret; + break; + case self::AUTH_TYPE_AUTHORIZATION_BASIC: + $parameters['client_id'] = $this->client_id; + $http_headers['Authorization'] = 'Basic ' . base64_encode($this->client_id . ':' . $this->client_secret); + break; + default: + throw new Exception('Unknown client auth type.', Exception::INVALID_CLIENT_AUTHENTICATION_TYPE); + break; + } + + return $this->executeRequest($token_endpoint, $parameters, self::HTTP_METHOD_POST, $http_headers, self::HTTP_FORM_CONTENT_TYPE_APPLICATION); + } + + /** + * setToken + * + * @param string $token Set the access token + * @return void + */ + public function setAccessToken($token) + { + $this->access_token = $token; + } + + /** + * Check if there is an access token present + * + * @return bool Whether the access token is present + */ + public function hasAccessToken() + { + return !!$this->access_token; + } + + /** + * Set the client authentication type + * + * @param string $client_auth (AUTH_TYPE_URI, AUTH_TYPE_AUTHORIZATION_BASIC, AUTH_TYPE_FORM) + * @return void + */ + public function setClientAuthType($client_auth) + { + $this->client_auth = $client_auth; + } + + /** + * Set an option for the curl transfer + * + * @param int $option The CURLOPT_XXX option to set + * @param mixed $value The value to be set on option + * @return void + */ + public function setCurlOption($option, $value) + { + $this->curl_options[$option] = $value; + } + + /** + * Set multiple options for a cURL transfer + * + * @param array $options An array specifying which options to set and their values + * @return void + */ + public function setCurlOptions($options) + { + $this->curl_options = array_merge($this->curl_options, $options); + } + + /** + * Set the access token type + * + * @param int $type Access token type (ACCESS_TOKEN_BEARER, ACCESS_TOKEN_MAC, ACCESS_TOKEN_URI) + * @param string $secret The secret key used to encrypt the MAC header + * @param string $algorithm Algorithm used to encrypt the signature + * @return void + */ + public function setAccessTokenType($type, $secret = null, $algorithm = null) + { + $this->access_token_type = $type; + $this->access_token_secret = $secret; + $this->access_token_algorithm = $algorithm; + } + + /** + * Fetch a protected ressource + * + * @param string $protected_ressource_url Protected resource URL + * @param array $parameters Array of parameters + * @param string $http_method HTTP Method to use (POST, PUT, GET, HEAD, DELETE) + * @param array $http_headers HTTP headers + * @param int $form_content_type HTTP form content type to use + * @return array + */ + public function fetch($protected_resource_url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, array $http_headers = array(), $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART) + { + if ($this->access_token) { + switch ($this->access_token_type) { + case self::ACCESS_TOKEN_URI: + if (is_array($parameters)) { + $parameters[$this->access_token_param_name] = $this->access_token; + } else { + throw new InvalidArgumentException( + 'You need to give parameters as array if you want to give the token within the URI.', + InvalidArgumentException::REQUIRE_PARAMS_AS_ARRAY + ); + } + break; + case self::ACCESS_TOKEN_BEARER: + $http_headers['Authorization'] = 'Bearer ' . $this->access_token; + break; + case self::ACCESS_TOKEN_OAUTH: + $http_headers['Authorization'] = 'OAuth ' . $this->access_token; + break; + case self::ACCESS_TOKEN_MAC: + $http_headers['Authorization'] = 'MAC ' . $this->generateMACSignature($protected_resource_url, $parameters, $http_method); + break; + default: + throw new Exception('Unknown access token type.', Exception::INVALID_ACCESS_TOKEN_TYPE); + break; + } + } + return $this->executeRequest($protected_resource_url, $parameters, $http_method, $http_headers, $form_content_type); + } + + /** + * Generate the MAC signature + * + * @param string $url Called URL + * @param array $parameters Parameters + * @param string $http_method Http Method + * @return string + */ + private function generateMACSignature($url, $parameters, $http_method) + { + $timestamp = time(); + $nonce = uniqid(); + $parsed_url = parse_url($url); + if (!isset($parsed_url['port'])) + { + $parsed_url['port'] = ($parsed_url['scheme'] == 'https') ? 443 : 80; + } + if ($http_method == self::HTTP_METHOD_GET) { + if (is_array($parameters)) { + $parsed_url['path'] .= '?' . http_build_query($parameters, '', '&'); + } elseif ($parameters) { + $parsed_url['path'] .= '?' . $parameters; + } + } + + $signature = base64_encode(hash_hmac($this->access_token_algorithm, + $timestamp . "\n" + . $nonce . "\n" + . $http_method . "\n" + . $parsed_url['path'] . "\n" + . $parsed_url['host'] . "\n" + . $parsed_url['port'] . "\n\n" + , $this->access_token_secret, true)); + + return 'id="' . $this->access_token . '", ts="' . $timestamp . '", nonce="' . $nonce . '", mac="' . $signature . '"'; + } + + /** + * Execute a request (with curl) + * + * @param string $url URL + * @param mixed $parameters Array of parameters + * @param string $http_method HTTP Method + * @param array $http_headers HTTP Headers + * @param int $form_content_type HTTP form content type to use + * @return array + */ + private function executeRequest($url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, ?array $http_headers = null, $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART) + { + $curl_options = array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_CUSTOMREQUEST => $http_method + ); + + switch($http_method) { + case self::HTTP_METHOD_POST: + $curl_options[CURLOPT_POST] = true; + /* No break */ + case self::HTTP_METHOD_PUT: + case self::HTTP_METHOD_PATCH: + + /** + * Passing an array to CURLOPT_POSTFIELDS will encode the data as multipart/form-data, + * while passing a URL-encoded string will encode the data as application/x-www-form-urlencoded. + * http://php.net/manual/en/function.curl-setopt.php + */ + if(is_array($parameters) && self::HTTP_FORM_CONTENT_TYPE_APPLICATION === $form_content_type) { + $parameters = http_build_query($parameters, '', '&'); + } + $curl_options[CURLOPT_POSTFIELDS] = $parameters; + break; + case self::HTTP_METHOD_HEAD: + $curl_options[CURLOPT_NOBODY] = true; + /* No break */ + case self::HTTP_METHOD_DELETE: + case self::HTTP_METHOD_GET: + if (is_array($parameters) && count($parameters) > 0) { + $url .= '?' . http_build_query($parameters, '', '&'); + } elseif ($parameters) { + $url .= '?' . $parameters; + } + break; + default: + break; + } + + $curl_options[CURLOPT_URL] = $url; + + if (is_array($http_headers)) { + $header = array(); + foreach($http_headers as $key => $parsed_urlvalue) { + $header[] = "$key: $parsed_urlvalue"; + } + $curl_options[CURLOPT_HTTPHEADER] = $header; + } + + $ch = curl_init(); + curl_setopt_array($ch, $curl_options); + // https handling + if (!empty($this->certificate_file)) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_CAINFO, $this->certificate_file); + } else { + // bypass ssl verification + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + } + if (!empty($this->curl_options)) { + curl_setopt_array($ch, $this->curl_options); + } + $result = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + if ($curl_error = curl_error($ch)) { + throw new Exception($curl_error, Exception::CURL_ERROR); + } else { + $json_decode = json_decode($result, true); + } + curl_close($ch); + + return array( + 'result' => (null === $json_decode) ? $result : $json_decode, + 'code' => $http_code, + 'content_type' => $content_type + ); + } + + /** + * Set the name of the parameter that carry the access token + * + * @param string $name Token parameter name + * @return void + */ + public function setAccessTokenParamName($name) + { + $this->access_token_param_name = $name; + } + + /** + * Converts the class name to camel case + * + * @param mixed $grant_type the grant type + * @return string + */ + private function convertToCamelCase($grant_type) + { + $parts = explode('_', $grant_type); + array_walk($parts, function(&$item) { $item = ucfirst($item);}); + return implode('', $parts); + } +} + +class Exception extends \Exception +{ + const CURL_NOT_FOUND = 0x01; + const CURL_ERROR = 0x02; + const GRANT_TYPE_ERROR = 0x03; + const INVALID_CLIENT_AUTHENTICATION_TYPE = 0x04; + const INVALID_ACCESS_TOKEN_TYPE = 0x05; +} + +class InvalidArgumentException extends \InvalidArgumentException +{ + const INVALID_GRANT_TYPE = 0x01; + const CERTIFICATE_NOT_FOUND = 0x02; + const REQUIRE_PARAMS_AS_ARRAY = 0x03; + const MISSING_PARAMETER = 0x04; +} diff --git a/rainloop/v/0.0.0/app/libraries/PHP-OAuth2/GrantType/AuthorizationCode.php b/plugins/login-oauth2/OAuth2/GrantType/AuthorizationCode.php similarity index 100% rename from rainloop/v/0.0.0/app/libraries/PHP-OAuth2/GrantType/AuthorizationCode.php rename to plugins/login-oauth2/OAuth2/GrantType/AuthorizationCode.php diff --git a/rainloop/v/0.0.0/app/libraries/PHP-OAuth2/GrantType/ClientCredentials.php b/plugins/login-oauth2/OAuth2/GrantType/ClientCredentials.php similarity index 100% rename from rainloop/v/0.0.0/app/libraries/PHP-OAuth2/GrantType/ClientCredentials.php rename to plugins/login-oauth2/OAuth2/GrantType/ClientCredentials.php diff --git a/rainloop/v/0.0.0/app/libraries/PHP-OAuth2/GrantType/IGrantType.php b/plugins/login-oauth2/OAuth2/GrantType/IGrantType.php similarity index 100% rename from rainloop/v/0.0.0/app/libraries/PHP-OAuth2/GrantType/IGrantType.php rename to plugins/login-oauth2/OAuth2/GrantType/IGrantType.php diff --git a/rainloop/v/0.0.0/app/libraries/PHP-OAuth2/GrantType/Password.php b/plugins/login-oauth2/OAuth2/GrantType/Password.php similarity index 100% rename from rainloop/v/0.0.0/app/libraries/PHP-OAuth2/GrantType/Password.php rename to plugins/login-oauth2/OAuth2/GrantType/Password.php diff --git a/rainloop/v/0.0.0/app/libraries/PHP-OAuth2/GrantType/RefreshToken.php b/plugins/login-oauth2/OAuth2/GrantType/RefreshToken.php similarity index 100% rename from rainloop/v/0.0.0/app/libraries/PHP-OAuth2/GrantType/RefreshToken.php rename to plugins/login-oauth2/OAuth2/GrantType/RefreshToken.php diff --git a/plugins/login-oauth2/OAuth2/LICENSE b/plugins/login-oauth2/OAuth2/LICENSE new file mode 100644 index 0000000000..8000a6faac --- /dev/null +++ b/plugins/login-oauth2/OAuth2/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/plugins/login-oauth2/OAuth2/README.md b/plugins/login-oauth2/OAuth2/README.md new file mode 100644 index 0000000000..9de3570e04 --- /dev/null +++ b/plugins/login-oauth2/OAuth2/README.md @@ -0,0 +1,105 @@ +# Light PHP wrapper for the OAuth 2.0 + +[![Latest Stable Version](https://poser.pugx.org/adoy/fastcgi-client/v/stable)](https://packagist.org/packages/adoy/fastcgi-client) +[![GitHub](https://img.shields.io/github/license/adoy/PHP-OAuth2)](LICENSE) +[![Total Downloads](https://poser.pugx.org/adoy/fastcgi-client/downloads)](https://packagist.org/packages/adoy/fastcgi-client) + + +## How can I use it ? + +```php +<?php + +require('Client.php'); +require('GrantType/IGrantType.php'); +require('GrantType/AuthorizationCode.php'); + +const CLIENT_ID = 'your client id'; +const CLIENT_SECRET = 'your client secret'; + +const REDIRECT_URI = 'http://url/of/this.php'; +const AUTHORIZATION_ENDPOINT = 'https://graph.facebook.com/oauth/authorize'; +const TOKEN_ENDPOINT = 'https://graph.facebook.com/oauth/access_token'; + +$client = new OAuth2\Client(CLIENT_ID, CLIENT_SECRET); +if (!isset($_GET['code'])) +{ + $auth_url = $client->getAuthenticationUrl(AUTHORIZATION_ENDPOINT, REDIRECT_URI); + header('Location: ' . $auth_url); + die('Redirect'); +} +else +{ + $params = array('code' => $_GET['code'], 'redirect_uri' => REDIRECT_URI); + $response = $client->getAccessToken(TOKEN_ENDPOINT, 'authorization_code', $params); + parse_str($response['result'], $info); + $client->setAccessToken($info['access_token']); + $response = $client->fetch('https://graph.facebook.com/me'); + var_dump($response, $response['result']); +} +``` + +## How can I add a new Grant Type ? + +Simply write a new class in the namespace OAuth2\GrantType. You can place the class file under GrantType. +Here is an example : + +```php +<?php + +namespace OAuth2\GrantType; + +/** + * MyCustomGrantType Grant Type + */ +class MyCustomGrantType implements IGrantType +{ + /** + * Defines the Grant Type + * + * @var string Defaults to 'my_custom_grant_type'. + */ + const GRANT_TYPE = 'my_custom_grant_type'; + + /** + * Adds a specific Handling of the parameters + * + * @return array of Specific parameters to be sent. + * @param mixed $parameters the parameters array (passed by reference) + */ + public function validateParameters(&$parameters) + { + if (!isset($parameters['first_mandatory_parameter'])) + { + throw new \Exception('The \'first_mandatory_parameter\' parameter must be defined for the Password grant type'); + } + elseif (!isset($parameters['second_mandatory_parameter'])) + { + throw new \Exception('The \'seconde_mandatory_parameter\' parameter must be defined for the Password grant type'); + } + } +} +``` + +call the OAuth client getAccessToken with the grantType you defined in the GRANT_TYPE constant, As following : + +``` +$response = $client->getAccessToken(TOKEN_ENDPOINT, 'my_custom_grant_type', $params); +``` + +## LICENSE + +This Code is released under the GNU LGPL + +Please do not change the header of the file(s). + +This library is free software; you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This library is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU Lesser General Public License for more details. diff --git a/plugins/login-oauth2/index.php b/plugins/login-oauth2/index.php new file mode 100644 index 0000000000..99351c81b6 --- /dev/null +++ b/plugins/login-oauth2/index.php @@ -0,0 +1,329 @@ +<?php + +class LoginOAuth2Plugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'OAuth2', + VERSION = '1.3', + RELEASE = '2024-03-27', + REQUIRED = '2.36.1', + CATEGORY = 'Login', + DESCRIPTION = 'IMAP, Sieve & SMTP login using RFC 7628 OAuth2'; + + const + LOGIN_URI = 'https://accounts.google.com/o/oauth2/auth', + TOKEN_URI = 'https://accounts.google.com/o/oauth2/token', + GMAIL_TOKENS_PREFIX = ':GAT:'; + + public function Init() : void + { + $this->UseLangs(true); + $this->addJs('LoginOAuth2.js'); +// $this->addHook('imap.before-connect', array($this, $oImapClient, $oSettings)); +// $this->addHook('imap.after-connect', array($this, $oImapClient, $oSettings)); + $this->addHook('imap.before-login', 'clientLogin'); +// $this->addHook('imap.after-login', array($this, $oImapClient, $oSettings)); +// $this->addHook('smtp.before-connect', array($this, $oSmtpClient, $oSettings)); +// $this->addHook('smtp.after-connect', array($this, $oSmtpClient, $oSettings)); + $this->addHook('smtp.before-login', 'clientLogin'); +// $this->addHook('smtp.after-login', array($this, $oSmtpClient, $oSettings)); +// $this->addHook('sieve.before-connect', array($this, $oSieveClient, $oSettings)); +// $this->addHook('sieve.after-connect', array($this, $oSieveClient, $oSettings)); + $this->addHook('sieve.before-login', 'clientLogin'); +// $this->addHook('sieve.after-login', array($this, $oSieveClient, $oSettings)); + $this->addHook('filter.account', 'filterAccount'); + +// set_include_path(get_include_path() . PATH_SEPARATOR . __DIR__); + spl_autoload_register(function($classname){ + if (str_starts_with($classname, 'OAuth2\\')) { + include_once __DIR__ . strtr("\\{$sClassName}", '\\', DIRECTORY_SEPARATOR) . '.php'; + } + }); + } + + public function configMapping() : array + { + return [ + \RainLoop\Plugins\Property::NewInstance('client_id') + ->SetLabel('Client ID') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING), + \RainLoop\Plugins\Property::NewInstance('client_secret') + ->SetLabel('Client Secret') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING), + ]; + } + + public function clientLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void + { + $sPassword = $oSettings->passphrase; + $iGatLen = \strlen(static::GMAIL_TOKENS_PREFIX); + if ($sPassword && static::GMAIL_TOKENS_PREFIX === \substr($sPassword, 0, $iGatLen)) { + $aTokens = \json_decode(\substr($sPassword, $iGatLen)); + $sAccessToken = !empty($aTokens[0]) ? $aTokens[0] : ''; + $sRefreshToken = !empty($aTokens[1]) ? $aTokens[1] : ''; + } + if ($sAccessToken && $sRefreshToken) { + $oSettings->passphrase = $this->gmailRefreshToken($sAccessToken, $sRefreshToken); + \array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER', 'XOAUTH2'); + } + } + + public function filterAccount(\RainLoop\Model\Account $oAccount) : void + { + if ($oAccount instanceof \RainLoop\Model\MainAccount) { + /** + * TODO + * Because password rotates, so does the CryptKey. + * So we need to securely save a cryptkey. + * Encrypted using the old/new refresh token is an option: + * 1. decrypt cryptkey with the old refresh token + * 2. encrypt cryptkey with the new refresh token + * = $oAccount->resealCryptKey(new \SnappyMail\SensitiveString('old refresh token')) + */ + } + } + + protected function loginProcess(&$oAccount, $sEmail, $sPassword) : int + { + $oActions = \RainLoop::Actions(); + $iErrorCode = \RainLoop\Notifications::UnknownError; + try + { + $oAccount = $oActions->LoginProcess($sEmail, $sPassword); + if ($oAccount instanceof \RainLoop\Model\Account) { + $iErrorCode = 0; + } else { + $oAccount = null; + $iErrorCode = \RainLoop\Notifications::AuthError; + } + } + catch (\RainLoop\Exceptions\ClientException $oException) + { + $iErrorCode = $oException->getCode(); + } + catch (\Exception $oException) + { + unset($oException); + $iErrorCode = \RainLoop\Notifications::UnknownError; + } + + return $iErrorCode; + } + + /** + * GMail + */ + + protected static function gmailTokensPassword($sAccessToken, $sRefreshToken) : string + { + return static::GMAIL_TOKENS_PREFIX . \json_encode(array($sAccessToken, $sRefreshToken)); + } + + protected function gmailStoreTokens($oCache, $sAccessToken, $sRefreshToken) : void + { + $sCacheKey = 'tokens='.\sha1($sRefreshToken); + $oCache->Set($sCacheKey, $sAccessToken); + $oCache->SetTimer($sCacheKey); + } + + protected function gmailRefreshToken($sAccessToken, $sRefreshToken) : string + { + $oActions = \RainLoop::Actions(); + $oAccount = $oActions->getAccountFromToken(false); + $oDomain = $oAccount->Domain(); + $oLogger = $oImapClient->Logger(); + if ($oAccount && $oActions->GetIsJson()) { + $oCache = $oActions->Cacher($oAccount); + $sCacheKey = 'tokens='.\sha1($sRefreshToken); + + $sCachedAccessToken = $oCache->Get($sCacheKey); + $iTime = $oCache->GetTimer($sCacheKey); + + if (!$sCachedAccessToken || !$iTime) { + $this->gmailStoreTokens($oCache, $sAccessToken, $sRefreshToken); + } else if (\time() - 1200 > $iTime) { // 20min + $oGMail = $this->gmailConnector(); + if ($oGMail) { + $aRefreshTokenResponse = $oGMail->getAccessToken( + static::TOKEN_URI, + 'refresh_token', + array('refresh_token' => $sRefreshToken) + ); + if (!empty($aRefreshTokenResponse['result']['access_token'])) { + $sCachedAccessToken = $aRefreshTokenResponse['result']['access_token']; + $this->gmailStoreTokens($oCache, $sCachedAccessToken, $sRefreshToken); +/* + $oAccount->SetPassword(static::gmailTokensPassword($sCachedAccessToken, $sRefreshToken)); + $oActions->AuthToken($oAccount); +// $oActions->SetUpdateAuthToken($oActions->GetSpecAuthToken()); + $oActions->sUpdateAuthToken = $oActions->GetSpecAuthToken(); + $sUpdateToken = $oActions->GetUpdateAuthToken(); + if ($sUpdateToken) { + $aResponseItem['UpdateToken'] = $sUpdateToken; + } +*/ + } + } + } + + if ($sCachedAccessToken) { + return $sCachedAccessToken; + } + } + + return $sAccessToken; + } + + protected function gmailConnector() : ?\OAuth2\Client + { + $oGMail = null; + $oActions = \RainLoop::Actions(); + $client_id = \trim($this->Config()->Get('plugin', 'client_id', '')); + $client_secret = \trim($this->Config()->Get('plugin', 'client_secret', '')); + if ($client_id && $client_secret) { +// include_once __DIR__ . '/OAuth2/Client.php'; +// include_once __DIR__ . '/OAuth2/GrantType/IGrantType.php'; +// include_once __DIR__ . '/OAuth2/GrantType/AuthorizationCode.php'; +// include_once __DIR__ . '/OAuth2/GrantType/RefreshToken.php'; + + try + { + $oGMail = new \OAuth2\Client($client_id, $client_secret); + $sProxy = $oActions->Config()->Get('labs', 'curl_proxy', ''); + if (\strlen($sProxy)) { + $oGMail->setCurlOption(CURLOPT_PROXY, $sProxy); + $sProxyAuth = $oActions->Config()->Get('labs', 'curl_proxy_auth', ''); + if (\strlen($sProxyAuth)) { + $oGMail->setCurlOption(CURLOPT_PROXYUSERPWD, $sProxyAuth); + } + } + } + catch (\Exception $oException) + { + $oActions->Logger()->WriteException($oException, \LOG_ERR); + } + } + + return $oGMail; + } + + protected function gmailPopupService() : string + { + $sLoginUrl = ''; + $oAccount = null; + $oActions = \RainLoop::Actions(); + $oHttp = $oActions->Http(); + + $bLogin = false; + $iErrorCode = \RainLoop\Notifications::UnknownError; + + try + { + $oGMail = $this->gmailConnector(); + if ($oHttp->HasQuery('error')) { + $iErrorCode = ('access_denied' === $oHttp->GetQuery('error')) ? + \RainLoop\Notifications::SocialGMailLoginAccessDisable : \RainLoop\Notifications::UnknownError; + } else if ($oGMail) { + $oAccount = $oActions->GetAccount(); + $bLogin = !$oAccount; + + $sCheckToken = ''; + $sCheckAuth = ''; + $sState = $oHttp->GetQuery('state'); + if (!empty($sState)) { + $aParts = explode('|', $sState, 3); + $sCheckToken = !empty($aParts[1]) ? $aParts[1] : ''; + $sCheckAuth = !empty($aParts[2]) ? $aParts[2] : ''; + } + + $sRedirectUrl = $oHttp->GetFullUrl().'?SocialGMail'; + if (!$oHttp->HasQuery('code')) { + $aParams = array( + 'scope' => \trim(\implode(' ', array( + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://mail.google.com/' + ))), + 'state' => '1|'.\RainLoop\Utils::GetConnectionToken().'|'.$oActions->GetSpecAuthToken(), + 'response_type' => 'code' + ); + + $aParams['access_type'] = 'offline'; + // $aParams['prompt'] = 'consent'; + + $sLoginUrl = $oGMail->getAuthenticationUrl(static::LOGIN_URI, $sRedirectUrl, $aParams); + } else if (!empty($sState) && $sCheckToken === \RainLoop\Utils::GetConnectionToken()) { + if (!empty($sCheckAuth)) { + $oActions->SetSpecAuthToken($sCheckAuth); + $oAccount = $oActions->GetAccount(); + $bLogin = !$oAccount; + } + + $aAuthorizationCodeResponse = $oGMail->getAccessToken( + static::TOKEN_URI, + 'authorization_code', + array( + 'code' => $oHttp->GetQuery('code'), + 'redirect_uri' => $sRedirectUrl + ) + ); + + $sAccessToken = !empty($aAuthorizationCodeResponse['result']['access_token']) ? $aAuthorizationCodeResponse['result']['access_token'] : ''; + $sRefreshToken = !empty($aAuthorizationCodeResponse['result']['refresh_token']) ? $aAuthorizationCodeResponse['result']['refresh_token'] : ''; + + if (!empty($sAccessToken)) { + $oGMail->setAccessToken($sAccessToken); + $aUserInfoResponse = $oGMail->fetch('https://www.googleapis.com/oauth2/v2/userinfo'); + + if (!empty($aUserInfoResponse['result']['id'])) { + if ($bLogin) { + $aUserData = null; + if (!empty($aUserInfoResponse['result']['email'])) { + $aUserData = array( + 'Email' => $aUserInfoResponse['result']['email'], + 'Password' => static::gmailTokensPassword($sAccessToken, $sRefreshToken) + ); + } + + if ($aUserData && \is_array($aUserData) && !empty($aUserData['Email']) && isset($aUserData['Password'])) { + $iErrorCode = $this->loginProcess($oAccount, $aUserData['Email'], $aUserData['Password']); + } else { + $iErrorCode = \RainLoop\Notifications::SocialGMailLoginAccessDisable; + } + } + } + } + } + } + } + catch (\Exception $oException) + { + $oActions->Logger()->WriteException($oException, \LOG_ERR); + } + + $oActions = \RainLoop::Actions(); + $oActions->Http()->ServerNoCache(); + \header('Content-Type: text/html; charset=utf-8'); + $sHtml = \file_get_contents(APP_VERSION_ROOT_PATH.'app/templates/Social.html'); + if ($sLoginUrl) { + $sHtml = \strtr($sHtml, array( + '{{RefreshMeta}}' => '<meta http-equiv="refresh" content="0; URL='.$sLoginUrl.'" />', + '{{Script}}' => '' + )); + } else { + $sCallBackType = $bLogin ? '_login' : ''; + $sConnectionFunc = 'rl_'.\md5(\RainLoop\Utils::GetConnectionToken()).'_gmail'.$sCallBackType.'_service'; + $sHtml = \strtr($sHtml, array( + '{{RefreshMeta}}' => '', + '{{Script}}' => '<script data-cfasync="false">opener && opener.'.$sConnectionFunc.' && opener.'. + $sConnectionFunc.'('.$iErrorCode.'); self && self.close && self.close();</script>' + )); + } + + $bAppCssDebug = $oActions->Config()->Get('labs', 'use_app_debug_css', false); + return \strtr($sHtml, array( + '{{Stylesheet}}' => $oActions->StaticPath('css/social'.($bAppCssDebug ? '' : '.min').'.css'), + '{{Icon}}' => 'gmail' + )); + } +} diff --git a/plugins/hmailserver-change-password/LICENSE b/plugins/login-override/LICENSE similarity index 100% rename from plugins/hmailserver-change-password/LICENSE rename to plugins/login-override/LICENSE diff --git a/plugins/login-override/index.php b/plugins/login-override/index.php new file mode 100644 index 0000000000..cef399dd9f --- /dev/null +++ b/plugins/login-override/index.php @@ -0,0 +1,86 @@ +<?php + +class LoginOverridePlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Login Override', + VERSION = '2.4', + RELEASE = '2024-09-20', + REQUIRED = '2.36.1', + CATEGORY = 'Filters', + DESCRIPTION = 'Override IMAP/SMTP login credentials for specific users.'; + + public function Init() : void + { + $this->addHook('login.credentials', 'MapEmailAddress'); + $this->addHook('imap.before-login', 'MapImapCredentials'); + $this->addHook('smtp.before-login', 'MapSmtpCredentials'); + } + + public function MapEmailAddress(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser) + { + $sMapping = \trim($this->Config()->Get('plugin', 'email_mapping', '')); + if (!empty($sMapping)) { + $aList = \preg_split('/\\R/', $sMapping); + foreach ($aList as $sLine) { + $aData = \explode(':', $sLine, 2); + if (!empty($aData[1]) && $sEmail === \trim($aData[0])) { + $sEmail = \trim($aData[1]); + break; + } + } + } + } + + public function MapImapCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Imap\ImapClient $oSmtpClient, \MailSo\Imap\Settings $oSettings) + { + static::MapCredentials($oAccount, $oSettings, $this->Config()->getDecrypted('plugin', 'imap_mapping', '')); + } + + public function MapSmtpCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, \MailSo\Smtp\Settings $oSettings) + { + static::MapCredentials($oAccount, $oSettings, $this->Config()->getDecrypted('plugin', 'smtp_mapping', '')); + } + + private static function MapCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Net\ConnectSettings $oSettings, string $sMapping) + { + $sEmail = $oAccount->Email(); + $aList = \preg_split('/\\R/', \trim($sMapping)); + foreach ($aList as $line) { + $line = \explode(':', $line, 3); + if (!empty($line[0]) && $line[0] === $sEmail) { + if (!empty($line[1])) { + $oSettings->username = $line[1]; + } + if (!empty($line[2])) { + $oSettings->passphrase = $line[2]; + } + break; + } + } + } + + protected function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('email_mapping') + ->SetLabel('Email mapping') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Changes email address as login@example.com:email@example.com') + ->SetDefaultValue('john-user1@example.com:john.doe@example.com') + ->SetEncrypted(), + \RainLoop\Plugins\Property::NewInstance('imap_mapping') + ->SetLabel('IMAP mapping') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Each line as email:loginname:password, loginname or password may be empty to use default') + ->SetDefaultValue('user@example.com:user1:password1') + ->SetEncrypted(), + \RainLoop\Plugins\Property::NewInstance('smtp_mapping') + ->SetLabel('SMTP mapping') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Each line as email:loginname:password, loginname or password may be empty to use default') + ->SetDefaultValue('user@example.com:user1:password1') + ->SetEncrypted() + ); + } +} diff --git a/plugins/login-proxyauth-example/LICENSE b/plugins/login-proxyauth-example/LICENSE new file mode 100644 index 0000000000..f709b02e27 --- /dev/null +++ b/plugins/login-proxyauth-example/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +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 +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/login-proxyauth-example/index.php b/plugins/login-proxyauth-example/index.php new file mode 100644 index 0000000000..7b3ebcf4ba --- /dev/null +++ b/plugins/login-proxyauth-example/index.php @@ -0,0 +1,33 @@ +<?php + +class LoginProxyauthExamplePlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Login PROXYAUTH', + VERSION = '1.0', + RELEASE = '2024-03-12', + REQUIRED = '2.35.3', + CATEGORY = 'Login', + DESCRIPTION = 'IMAP login using PROXYAUTH'; + + public function Init() : void + { + $this->addHook('imap.before-login', 'beforeLogin'); + $this->addHook('imap.after-login', 'afterLogin'); + } + + public function beforeLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oImapClient, \MailSo\Net\ConnectSettings $oSettings) : void + { + if ('example.com' === $oAccount->Domain()->Name()) { + $oSettings->username = 'AdminUsername'; + $oSettings->passphrase = 'AdminPassword'; + } + } + + public function afterLogin(\RainLoop\Model\Account $oAccount, \MailSo\Imap\ImapClient $oImapClient, bool $bSuccess) + { + if ($bSuccess && 'example.com' === $oAccount->Domain()->Name()) { + $oImapClient->SendRequestGetResponse('PROXYAUTH', array($oImapClient->EscapeString($oAccount->IncLogin()))); + } + } +} diff --git a/plugins/login-register/LoginRegister.js b/plugins/login-register/LoginRegister.js new file mode 100644 index 0000000000..939095ce33 --- /dev/null +++ b/plugins/login-register/LoginRegister.js @@ -0,0 +1,33 @@ + +(rl => { + if (rl) { + const + forgotUrl = rl.settings.get('forgotPasswordLinkUrl'), + registerUrl = rl.settings.get('registrationLinkUrl'); + + if (forgotUrl || registerUrl) { + addEventListener('rl-view-model', e => { + if (e.detail && 'Login' === e.detail.viewModelTemplateID) { + const container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'), + forgot = 'LOGIN/LABEL_FORGOT_PASSWORD', + register = 'LOGIN/LABEL_REGISTRATION'; + if (container) { + let html = ''; + if (forgotUrl) { + html = html + '<p class="forgot-link">' + + '<a href="'+forgotUrl+'" target="_blank" class="g-ui-link" data-i18n="'+forgot+'">'+rl.i18n(forgot)+'</a>' + + '</p>'; + } + if (registerUrl) { + html = html + '<p class="registration-link">' + + '<a href="'+registerUrl+'" target="_blank" class="g-ui-link" data-i18n="'+register+'">'+rl.i18n(register)+'</a>' + + '</p>'; + } + container.append(Element.fromHTML('<div class="controls clearfix">' + html + '</div>')); + } + } + }); + } + } + +})(window.rl); diff --git a/plugins/login-register/index.php b/plugins/login-register/index.php new file mode 100644 index 0000000000..7d7a62e118 --- /dev/null +++ b/plugins/login-register/index.php @@ -0,0 +1,42 @@ +<?php + +class LoginRegisterPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Register and Forgot', + VERSION = '2.2', + RELEASE = '2024-03-29', + REQUIRED = '2.36.0', + CATEGORY = 'Login', + DESCRIPTION = 'Links on login screen for registration and forgotten password'; + + public function Init() : void + { + $this->UseLangs(true); + $this->addJs('LoginRegister.js'); + $this->addHook('filter.app-data', 'FilterAppData'); + } + + public function configMapping() : array + { + return [ + \RainLoop\Plugins\Property::NewInstance("forgot_password_link_url") +// ->SetLabel('TAB_LOGIN/LABEL_FORGOT_PASSWORD_LINK_URL') + ->SetLabel('Forgot password url') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::URL), + \RainLoop\Plugins\Property::NewInstance("registration_link_url") +// ->SetLabel('TAB_LOGIN/LABEL_REGISTRATION_LINK_URL') + ->SetLabel('Register url') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::URL), + ]; + } + + public function FilterAppData($bAdmin, &$aResult) + { + if (!$bAdmin && \is_array($aResult) && empty($aResult['Auth'])) { + $aResult['forgotPasswordLinkUrl'] = \trim($this->Config()->Get('plugin', 'forgot_password_link_url', '')); + $aResult['registrationLinkUrl'] = \trim($this->Config()->Get('plugin', 'registration_link_url', '')); + } + } + +} diff --git a/plugins/login-register/langs/ar.json b/plugins/login-register/langs/ar.json new file mode 100644 index 0000000000..86692483a1 --- /dev/null +++ b/plugins/login-register/langs/ar.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "نسيت كلمة السر", + "LABEL_REGISTRATION": "التسجيل" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/bg.json b/plugins/login-register/langs/bg.json new file mode 100644 index 0000000000..223d1fd6cd --- /dev/null +++ b/plugins/login-register/langs/bg.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Забравена парола", + "LABEL_REGISTRATION": "Регистрация" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/cs.json b/plugins/login-register/langs/cs.json new file mode 100644 index 0000000000..53c7e3baf8 --- /dev/null +++ b/plugins/login-register/langs/cs.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Zapomenout heslo", + "LABEL_REGISTRATION": "Registrace" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/da.json b/plugins/login-register/langs/da.json new file mode 100644 index 0000000000..4ee9f84288 --- /dev/null +++ b/plugins/login-register/langs/da.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Glemt adgangskode", + "LABEL_REGISTRATION": "Registrering" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/de.json b/plugins/login-register/langs/de.json new file mode 100644 index 0000000000..a2ef7cc6fd --- /dev/null +++ b/plugins/login-register/langs/de.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Passwort vergessen", + "LABEL_REGISTRATION": "Registrieren" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/el.json b/plugins/login-register/langs/el.json new file mode 100644 index 0000000000..fa1fa7e1e3 --- /dev/null +++ b/plugins/login-register/langs/el.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Ξέχασα τον κωδικό μου", + "LABEL_REGISTRATION": "Εγγραφή" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/en.json b/plugins/login-register/langs/en.json new file mode 100644 index 0000000000..20b0498715 --- /dev/null +++ b/plugins/login-register/langs/en.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Forgot password", + "LABEL_REGISTRATION": "Register" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/es.json b/plugins/login-register/langs/es.json new file mode 100644 index 0000000000..b4bf0354b6 --- /dev/null +++ b/plugins/login-register/langs/es.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Olvidé mi contraseña", + "LABEL_REGISTRATION": "Registrarse" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/et.json b/plugins/login-register/langs/et.json new file mode 100644 index 0000000000..fdbd13832a --- /dev/null +++ b/plugins/login-register/langs/et.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Unustasin salasõna", + "LABEL_REGISTRATION": "Registreerimine" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/fa.json b/plugins/login-register/langs/fa.json new file mode 100644 index 0000000000..62280f2e17 --- /dev/null +++ b/plugins/login-register/langs/fa.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "فراموشی گذرواژه", + "LABEL_REGISTRATION": "ثبت‌نام" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/fi.json b/plugins/login-register/langs/fi.json new file mode 100644 index 0000000000..b288c5a3e5 --- /dev/null +++ b/plugins/login-register/langs/fi.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Unohdin salasanani", + "LABEL_REGISTRATION": "Rekisteröidy" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/fr.json b/plugins/login-register/langs/fr.json new file mode 100644 index 0000000000..d317903f80 --- /dev/null +++ b/plugins/login-register/langs/fr.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Mot de passe oublié", + "LABEL_REGISTRATION": "S'enregistrer" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "URL du mot de passe oublié", + "LABEL_REGISTRATION_LINK_URL": "URL d'enregistrement" + } +} diff --git a/plugins/login-register/langs/hu.json b/plugins/login-register/langs/hu.json new file mode 100644 index 0000000000..87545bf50c --- /dev/null +++ b/plugins/login-register/langs/hu.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Elfelejtett jelszó", + "LABEL_REGISTRATION": "Regisztráció" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/id.json b/plugins/login-register/langs/id.json new file mode 100644 index 0000000000..602afa99d5 --- /dev/null +++ b/plugins/login-register/langs/id.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Lupa sandi", + "LABEL_REGISTRATION": "Registrasi" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/is.json b/plugins/login-register/langs/is.json new file mode 100644 index 0000000000..bdfa0cc202 --- /dev/null +++ b/plugins/login-register/langs/is.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Gleymdi lykilorði", + "LABEL_REGISTRATION": "Nýskráning" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/it.json b/plugins/login-register/langs/it.json new file mode 100644 index 0000000000..2b5f1a967b --- /dev/null +++ b/plugins/login-register/langs/it.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Ho dimenticato la password", + "LABEL_REGISTRATION": "Registrazione" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/ja.json b/plugins/login-register/langs/ja.json new file mode 100644 index 0000000000..76f01f0bf6 --- /dev/null +++ b/plugins/login-register/langs/ja.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "パスワードを忘れた", + "LABEL_REGISTRATION": "初回登録" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/ko.json b/plugins/login-register/langs/ko.json new file mode 100644 index 0000000000..2004d699d9 --- /dev/null +++ b/plugins/login-register/langs/ko.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "비밀번호 분실", + "LABEL_REGISTRATION": "가입" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/lt.json b/plugins/login-register/langs/lt.json new file mode 100644 index 0000000000..447c1aefa4 --- /dev/null +++ b/plugins/login-register/langs/lt.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Pamiršau slaptažodį", + "LABEL_REGISTRATION": "Registracija" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/lv.json b/plugins/login-register/langs/lv.json new file mode 100644 index 0000000000..60df5f0e0c --- /dev/null +++ b/plugins/login-register/langs/lv.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Forgot password", + "LABEL_REGISTRATION": "Registration" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/nb.json b/plugins/login-register/langs/nb.json new file mode 100644 index 0000000000..b7c7fe9151 --- /dev/null +++ b/plugins/login-register/langs/nb.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Glemt passord", + "LABEL_REGISTRATION": "Registrering" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/nl.json b/plugins/login-register/langs/nl.json new file mode 100644 index 0000000000..69fe6c8147 --- /dev/null +++ b/plugins/login-register/langs/nl.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Wachtwoord vergeten", + "LABEL_REGISTRATION": "Registreren" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/pl.json b/plugins/login-register/langs/pl.json new file mode 100644 index 0000000000..85cc607e2c --- /dev/null +++ b/plugins/login-register/langs/pl.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Zapomniane hasło", + "LABEL_REGISTRATION": "Rejestracja" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/pt.json b/plugins/login-register/langs/pt.json new file mode 100644 index 0000000000..c3ba8aebee --- /dev/null +++ b/plugins/login-register/langs/pt.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Esqueceu a senha?", + "LABEL_REGISTRATION": "Registre-se" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/ro.json b/plugins/login-register/langs/ro.json new file mode 100644 index 0000000000..ba4ad338ac --- /dev/null +++ b/plugins/login-register/langs/ro.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Am uitat parola", + "LABEL_REGISTRATION": "Înregistrare" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/ru.json b/plugins/login-register/langs/ru.json new file mode 100644 index 0000000000..19235d5dcb --- /dev/null +++ b/plugins/login-register/langs/ru.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Забытый пароль", + "LABEL_REGISTRATION": "Регистрация" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/sk.json b/plugins/login-register/langs/sk.json new file mode 100644 index 0000000000..a7e3c15654 --- /dev/null +++ b/plugins/login-register/langs/sk.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Zabudol som heslo", + "LABEL_REGISTRATION": "Registrácia" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/sl.json b/plugins/login-register/langs/sl.json new file mode 100644 index 0000000000..be3c403aa5 --- /dev/null +++ b/plugins/login-register/langs/sl.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Pozabljeno geslo", + "LABEL_REGISTRATION": "Registracija" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/sv.json b/plugins/login-register/langs/sv.json new file mode 100644 index 0000000000..04d0b0907b --- /dev/null +++ b/plugins/login-register/langs/sv.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Glömt lösenord", + "LABEL_REGISTRATION": "Registrera" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/tr.json b/plugins/login-register/langs/tr.json new file mode 100644 index 0000000000..162c06a3e4 --- /dev/null +++ b/plugins/login-register/langs/tr.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Şifremi Unuttum", + "LABEL_REGISTRATION": "Registration" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/uk.json b/plugins/login-register/langs/uk.json new file mode 100644 index 0000000000..ed2f61b716 --- /dev/null +++ b/plugins/login-register/langs/uk.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Забули пароль?", + "LABEL_REGISTRATION": "Реєстрація" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/zh-TW.json b/plugins/login-register/langs/zh-TW.json new file mode 100644 index 0000000000..60df5f0e0c --- /dev/null +++ b/plugins/login-register/langs/zh-TW.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "Forgot password", + "LABEL_REGISTRATION": "Registration" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url", + "LABEL_REGISTRATION_LINK_URL": "Register url" + } +} diff --git a/plugins/login-register/langs/zh.json b/plugins/login-register/langs/zh.json new file mode 100644 index 0000000000..08823a0cb7 --- /dev/null +++ b/plugins/login-register/langs/zh.json @@ -0,0 +1,10 @@ +{ + "LOGIN": { + "LABEL_FORGOT_PASSWORD": "忘记密码", + "LABEL_REGISTRATION": "注册" + }, + "TAB_LOGIN": { + "LABEL_FORGOT_PASSWORD_LINK_URL": "忘记密码 Url", + "LABEL_REGISTRATION_LINK_URL": "注册 Url" + } +} diff --git a/plugins/login-remote/LICENSE b/plugins/login-remote/LICENSE new file mode 100644 index 0000000000..f709b02e27 --- /dev/null +++ b/plugins/login-remote/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +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 +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/login-remote/index.php b/plugins/login-remote/index.php new file mode 100644 index 0000000000..ab1550571e --- /dev/null +++ b/plugins/login-remote/index.php @@ -0,0 +1,77 @@ +<?php + +class LoginRemotePlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Login Remote', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '1.5', + RELEASE = '2024-09-20', + REQUIRED = '2.36.1', + CATEGORY = 'Login', + LICENSE = 'MIT', + DESCRIPTION = 'Tries to login using the $_ENV["REMOTE_*"] variables'; + + public function Init() : void + { + $this->addPartHook('RemoteAutoLogin', 'ServiceRemoteAutoLogin'); + $this->addHook('filter.app-data', 'FilterAppData'); + $this->addHook('login.credentials', 'FilterLoginCredentials'); + } + + public function FilterAppData($bAdmin, &$aResult) + { + if (!$bAdmin && \is_array($aResult) && empty($aResult['Auth']) && isset($_ENV['REMOTE_USER'])) { + $aResult['DevEmail'] = $_ENV['REMOTE_USER']; +// $aResult['DevPassword'] = $_ENV['REMOTE_PASSWORD']; + } + } + + private static bool $login = false; + public function ServiceRemoteAutoLogin() : bool + { + $oActions = \RainLoop\Api::Actions(); + + $oException = null; + $oAccount = null; + + $sEmail = $_ENV['REMOTE_USER'] ?? ''; + $sPassword = $_ENV['REMOTE_PASSWORD'] ?? ''; + + if (\strlen($sEmail) && \strlen($sPassword)) { + try + { + static::$login = true; + $oAccount = $oActions->LoginProcess($sEmail, $sPassword); + } + catch (\Throwable $oException) + { + $oLogger = $oActions->Logger(); + $oLogger && $oLogger->WriteException($oException); + } + } + + \MailSo\Base\Http::Location('./'); + return true; + } + + public function FilterLoginCredentials(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser) + { + // cPanel https://github.com/the-djmaze/snappymail/issues/697 +// && !empty($_ENV['CPANEL']) + if (static::$login/* && $sImapUser == $_ENV['REMOTE_USER']*/) { + if (empty($_ENV['REMOTE_TEMP_USER'])) { + $iPos = \strpos($sPassword, '[::cpses::]'); + if ($iPos) { + $_ENV['REMOTE_TEMP_USER'] = \substr($sPassword, 0, $iPos); + } + } + if (!empty($_ENV['REMOTE_TEMP_USER'])) { + $sImapUser = $_ENV['REMOTE_USER'] . '/' . $_ENV['REMOTE_TEMP_USER']; + $sSmtpUser = $_ENV['REMOTE_USER'] . '/' . $_ENV['REMOTE_TEMP_USER']; + } + } + } + +} diff --git a/plugins/login-virtuser/index.php b/plugins/login-virtuser/index.php new file mode 100644 index 0000000000..88207baec4 --- /dev/null +++ b/plugins/login-virtuser/index.php @@ -0,0 +1,71 @@ +<?php + +class LoginVirtuserPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Virtuser Login', + AUTHOR = 'v20z', + URL = 'https://github.com/the-djmaze/snappymail/issues/1691', + VERSION = '0.2', + RELEASE = '2024-09-20', + REQUIRED = '2.36.1', + CATEGORY = 'Login', + DESCRIPTION = 'File based Email-to-User lookup.'; + + public function Init() : void + { + $this->addHook('login.credentials', 'ParseVirtuserFiles'); + } + + /** + * @param string $sEmail + * @param string $sImapUser + * @param string $sPassword + * @param string $sSmtpUser + * + * @throws \RainLoop\Exceptions\ClientException + */ + public function ParseVirtuserFiles(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser) + { + $sFiles = \trim($this->Config()->Get('plugin', 'virtuser_files', '')); + if (!empty($sFiles)) + { + $aFiles = \explode("\n", \preg_replace('/[\r\n\t\s]+/', "\n", $sFiles)); + foreach ($aFiles as $sFile) + { + $fContent = file("$sFile"); + if (empty($fContent)) { + continue; + } + + foreach($fContent as $sLine) { + $sLine = trim($sLine); + if (empty($sLine) || $sLine[0] == '#') { + continue; + } + + $aData = preg_split( '/[[:blank:]]+/', $sLine, 3, PREG_SPLIT_NO_EMPTY); + if (is_array($aData) && !empty($aData[0]) && isset($aData[1])) { + if ($sEmail === $aData[0] && 0 < strlen($aData[1])) { + $sImapUser = $aData[1]; + return; + } + } + } + } + } + } + + /** + * @return array + */ + protected function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('virtuser_files')->SetLabel('Virtuser files') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Each line is an absolute path to virtuser file') + ->SetDefaultValue("/etc/postfix/virtual\n/etc/mail/virtusertable") + ); + } +} diff --git a/plugins/mailbox-detect/index.php b/plugins/mailbox-detect/index.php new file mode 100644 index 0000000000..04ffe68d00 --- /dev/null +++ b/plugins/mailbox-detect/index.php @@ -0,0 +1,228 @@ +<?php + +use MailSo\Imap\Enumerations\FolderType; +use MailSo\Imap\Enumerations\MetadataKeys; + +class MailboxDetectPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'MailboxDetect', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '2.6', + RELEASE = '2023-03-13', + REQUIRED = '2.27.0', + CATEGORY = 'General', + LICENSE = 'MIT', + DESCRIPTION = 'Autodetect system folders and/or create them when needed'; + + public function Init() : void + { + $this->addHook('json.after-folders', 'AfterFolders'); + } + + protected function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('autocreate_system_folders')->SetLabel('Autocreate system folders') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(false), + ); + } + + protected function Logger() : \MailSo\Log\Logger + { + return $this->Manager()->Actions()->Logger(); + } + + public function AfterFolders(array &$aResponse) + { + if (!empty($aResponse['Result']['@Collection'])) { + $oActions = \RainLoop\Api::Actions(); + $oAccount = $oActions->getAccountFromToken(); + if (!$oAccount) { + $this->Logger()->Write('No Account'); + return; + } + $oSettingsLocal = $oActions->SettingsProvider(true)->Load($oAccount); + $roles = [ + 'inbox' => false, + 'sent' => !!$oSettingsLocal->GetConf('SentFolder', ''), + 'drafts' => !!$oSettingsLocal->GetConf('DraftsFolder', ''), + 'junk' => !!$oSettingsLocal->GetConf('JunkFolder', ''), + 'trash' => !!$oSettingsLocal->GetConf('TrashFolder', ''), + 'archive' => !!$oSettingsLocal->GetConf('ArchiveFolder', '') + ]; + $types = [ + FolderType::SENT => 'sent', + FolderType::DRAFTS => 'drafts', + FolderType::JUNK => 'junk', + FolderType::TRASH => 'trash', + FolderType::ARCHIVE => 'archive' + ]; + $found = [ + 'inbox' => [], + 'sent' => [], + 'drafts' => [], + 'junk' => [], + 'trash' => [], + 'archive' => [] + ]; + $aMap = $this->systemFoldersNames($oAccount); + $sDelimiter = ''; + foreach ($aResponse['Result']['@Collection'] as $i => $folder) { + $sDelimiter || ($sDelimiter = $folder['delimiter']); + if ($folder['role']) { + $roles[$folder['role']] = true; + } else if (\in_array('\\sentmail', $folder['attributes'])) { + $found['sent'][] = $i; + } else if (\in_array('\\spam', $folder['attributes'])) { + $found['junk'][] = $i; + } else if (\in_array('\\bin', $folder['attributes'])) { + $found['trash'][] = $i; + } else if (\in_array('\\starred', $folder['attributes'])) { + $found['flagged'][] = $i; + } else { + // Kolab + $kolab = $folder['metadata'][MetadataKeys::KOLAB_CTYPE] + ?? $folder['metadata'][MetadataKeys::KOLAB_CTYPE_SHARED] + ?? ''; + if ('mail.inbox' === $kolab) { + $found['inbox'][] = $i; + } else if ('mail.sentitems' === $kolab /*|| 'mail.outbox' === $kolab*/) { + $found['sent'][] = $i; + } else if ('mail.drafts' === $kolab) { + $found['drafts'][] = $i; + } else if ('mail.junkemail' === $kolab) { + $found['junk'][] = $i; + } else if ('mail.wastebasket' === $kolab) { + $found['trash'][] = $i; + } else { + $iFolderType = 0; + if (isset($aMap[$folder['fullName']])) { + $iFolderType = $aMap[$folder['fullName']]; + } else if (isset($aMap[$folder['name']]) || isset($aMap["INBOX{$folder['delimiter']}{$folder['name']}"])) { + $iFolderType = $aMap[$folder['name']]; + } + if ($iFolderType && isset($types[$iFolderType])) { + $found[$types[$iFolderType]][] = $i; + } + } + } + } + foreach ($roles as $role => $hasRole) { + if ($hasRole) { + unset($found[$role]); + } + } + if ($found) { + foreach ($found as $role => $folders) { + if (isset($folders[0])) { + // Set the first as default + $aFolder = &$aResponse['Result']['@Collection'][$folders[0]]; + $this->Logger()->Write("Set {$role} mailbox to {$aFolder['fullName']}"); + $aFolder['role'] = $role; + } else if ($this->Config()->Get('plugin', 'autocreate_system_folders', false)) { + try + { + $sParent = \substr($aResponse['Result']['namespace'], 0, -1); + $sFolderNameToCreate = \ucfirst($role); +/* + $this->Manager()->RunHook('filter.folders-system-types', array($oAccount, &$aList)); + $iPos = \strrpos($sFolderNameToCreate, $sDelimiter); + if (false !== $iPos) { + $mNewParent = \substr($sFolderNameToCreate, 0, $iPos); + $mNewFolderNameToCreate = \substr($sFolderNameToCreate, $iPos + 1); + if (\strlen($mNewFolderNameToCreate)) { + $sFolderNameToCreate = $mNewFolderNameToCreate; + } + + if (\strlen($mNewParent)) { + $sParent = \strlen($sParent) ? $sParent.$sDelimiter.$mNewParent : $mNewParent; + } + } +*/ + $this->Logger()->Write("Create {$role} mailbox {$sFolderNameToCreate}"); + $aFolder = $oActions->MailClient()->FolderCreate( + $sFolderNameToCreate, + $sParent, + true, + $sDelimiter + )->jsonSerialize(); + $aFolder['role'] = $role; + $aResponse['Result']['@Collection'][] = $aFolder; + } + catch (\Throwable $oException) + { + $this->Logger()->WriteException($oException); + } + } else { + $this->Logger()->Write("Mailbox for {$role} not created"); + } + } + } + } + } + + /** + * @staticvar array $aCache + */ + private function systemFoldersNames(\RainLoop\Model\Account $oAccount) : array + { + static $aCache = null; + if (null === $aCache) { + $aCache = array( + 'Sent' => FolderType::SENT, + 'Send' => FolderType::SENT, + 'Outbox' => FolderType::SENT, + 'Out box' => FolderType::SENT, + 'Sent Item' => FolderType::SENT, + 'Sent Items' => FolderType::SENT, + 'Send Item' => FolderType::SENT, + 'Send Items' => FolderType::SENT, + 'Sent Mail' => FolderType::SENT, + 'Sent Mails' => FolderType::SENT, + 'Send Mail' => FolderType::SENT, + 'Send Mails' => FolderType::SENT, + + 'Drafts' => FolderType::DRAFTS, + 'Draft' => FolderType::DRAFTS, + 'Draft Mail' => FolderType::DRAFTS, + 'Draft Mails' => FolderType::DRAFTS, + 'Drafts Mail' => FolderType::DRAFTS, + 'Drafts Mails' => FolderType::DRAFTS, + + 'Junk' => FolderType::JUNK, + 'Junk E-mail' => FolderType::JUNK, + 'Spam' => FolderType::JUNK, + 'Spams' => FolderType::JUNK, + 'Bulk Mail' => FolderType::JUNK, + 'Bulk Mails' => FolderType::JUNK, + + 'Trash' => FolderType::TRASH, + 'Deleted' => FolderType::TRASH, + 'Deleted Items' => FolderType::TRASH, + 'Bin' => FolderType::TRASH, + + 'Archive' => FolderType::ARCHIVE, + 'Archives' => FolderType::ARCHIVE, + + 'All' => FolderType::ALL, + 'All Mail' => FolderType::ALL, + 'All Mails' => FolderType::ALL, + ); + + $aNewCache = array(); + foreach ($aCache as $sKey => $iType) { + $aNewCache[$sKey] = $iType; + $aNewCache[\str_replace(' ', '', $sKey)] = $iType; + } + + $aCache = $aNewCache; + + $this->Manager()->RunHook('filter.system-folders-names', array($oAccount, &$aCache)); + } + + return $aCache; + } +} diff --git a/plugins/mailcow-change-password/LICENSE b/plugins/mailcow-change-password/LICENSE deleted file mode 100644 index 3a897637bf..0000000000 --- a/plugins/mailcow-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Caleb Blankemeyer (https://github.com/zikeji) - -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/mailcow-change-password/MailcowChangePasswordDriver.php b/plugins/mailcow-change-password/MailcowChangePasswordDriver.php deleted file mode 100644 index cac750df55..0000000000 --- a/plugins/mailcow-change-password/MailcowChangePasswordDriver.php +++ /dev/null @@ -1,162 +0,0 @@ -<?php - -class MailcowChangePasswordDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sDsn = ''; - - /** - * @var string - */ - private $sUser = ''; - - /** - * @var string - */ - private $sPassword = ''; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sDsn - * @param string $sUser - * @param string $sPassword - * - * @return \IspConfigChangePasswordDriver - */ - public function SetConfig($sDsn, $sUser, $sPassword) - { - $this->sDsn = $sDsn; - $this->sUser = $sUser; - $this->sPassword = $sPassword; - - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \IspConfigChangePasswordDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \IspConfigChangePasswordDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('Mailcow: Try to change password for '.$oAccount->Email()); - } - - $bResult = false; - if (!empty($this->sDsn) && 0 < \strlen($this->sUser) && 0 < \strlen($this->sPassword) && $oAccount) - { - try - { - $oPdo = new \PDO($this->sDsn, $this->sUser, $this->sPassword); - $oPdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - - $oStmt = $oPdo->prepare('SELECT password, username FROM mailbox WHERE username = ? LIMIT 1'); - if ($oStmt->execute(array($oAccount->IncLogin()))) - { - $aFetchResult = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetchResult) && isset($aFetchResult[0]['password'], $aFetchResult[0]['username'])) - { - $sDbPassword = $aFetchResult[0]['password']; - if (\substr($sDbPassword, 0, 14) === '{SHA512-CRYPT}') { - $sDbSalt = \substr($sDbPassword, 17, 16); - } else { - $sDbSalt = \substr($sDbPassword, 3, 16); - } - - if ('{SHA512-CRYPT}'.\crypt($sPrevPassword, '$6$'.$sDbSalt) === $sDbPassword) - { - $oStmt = $oPdo->prepare('UPDATE mailbox SET password = ? WHERE username = ?'); - if ($oStmt->execute(array($this->cryptPassword($sNewPassword), $aFetchResult[0]['username']))) { - $oStmt = $oPdo ->prepare('UPDATE users SET digesta1=MD5(CONCAT(?, ":SabreDAV:", ?)) WHERE username=?'); - if ($oStmt->execute(array($aFetchResult[0]['username'],$sNewPassword,$aFetchResult[0]['username']))) { - //the MailCow & SabreDav have been updated, now update the doveadm password - exec("/usr/bin/doveadm pw -s SHA512-CRYPT -p $sNewPassword", $hash, $return); - $bResult = true; - } - } - } - } - } - } - catch (\Exception $oException) - { - if ($this->oLogger) - { - $this->oLogger->WriteException($oException); - } - } - } - - return $bResult; - } - - /** - * @param string $sPassword - * @return string - */ - private function cryptPassword($sPassword) - { - $sSalt = ''; - $sBase64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - - for ($iIndex = 0; $iIndex < 16; $iIndex++) - { - $sSalt .= $sBase64[\rand(0, 63)]; - } - - $crypted = \crypt($sPassword, '$6$'.$sSalt); - return '{SHA512-CRYPT}'.$crypted; - } -} diff --git a/plugins/mailcow-change-password/README b/plugins/mailcow-change-password/README deleted file mode 100644 index 2d75466b90..0000000000 --- a/plugins/mailcow-change-password/README +++ /dev/null @@ -1,5 +0,0 @@ -Plugin that adds functionality to change the email account password with the email frontend [mailcow](https://github.com/andryyy/mailcow). - -Changes the SQL password (for the default ui for users), the dovecot password, and the SabreDAV password. Essentially does what their own PHP script does. It also ensures the old password matches the actual old password (something their Roundcube plugin does not do). - -This plugin is a modification of the [ispconfig-change-password](https://github.com/RainLoop/rainloop-webmail/tree/master/plugins/ipsconfig-change-password) plugin. diff --git a/plugins/mailcow-change-password/VERSION b/plugins/mailcow-change-password/VERSION deleted file mode 100644 index d3827e75a5..0000000000 --- a/plugins/mailcow-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 diff --git a/plugins/mailcow-change-password/index.php b/plugins/mailcow-change-password/index.php deleted file mode 100644 index ae6c0443f5..0000000000 --- a/plugins/mailcow-change-password/index.php +++ /dev/null @@ -1,76 +0,0 @@ -<?php - -class MailcowChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @return string - */ - public function Supported() - { - if (!extension_loaded('pdo') || !class_exists('PDO')) - { - return 'The PHP extension PDO (mysql) must be installed to use this plugin'; - } - - $aDrivers = \PDO::getAvailableDrivers(); - if (!is_array($aDrivers) || !in_array('mysql', $aDrivers)) - { - return 'The PHP extension PDO (mysql) must be installed to use this plugin'; - } - - return ''; - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - $sDsn = \trim($this->Config()->Get('plugin', 'pdo_dsn', '')); - $sUser = (string) $this->Config()->Get('plugin', 'user', ''); - $sPassword = (string) $this->Config()->Get('plugin', 'password', ''); - - if (!empty($sDsn) && 0 < \strlen($sUser) && 0 < \strlen($sPassword)) - { - include_once __DIR__.'/MailcowChangePasswordDriver.php'; - - $oProvider = new MailcowChangePasswordDriver(); - $oProvider->SetLogger($this->Manager()->Actions()->Logger()); - $oProvider->SetConfig($sDsn, $sUser, $sPassword); - $oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))); - } - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('pdo_dsn')->SetLabel('Mailcow PDO dsn') - ->SetDefaultValue('mysql:host=127.0.0.1;dbname=mailcow'), - \RainLoop\Plugins\Property::NewInstance('user')->SetLabel('DB User') - ->SetDefaultValue('mailcow'), - \RainLoop\Plugins\Property::NewInstance('password')->SetLabel('DB Password') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} diff --git a/plugins/mailinabox-change-password/LICENSE b/plugins/mailinabox-change-password/LICENSE deleted file mode 100644 index 6ebe205bfc..0000000000 --- a/plugins/mailinabox-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Marius Gripsgard <marius@ubports.com> - -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/mailinabox-change-password/MailInABoxChangePasswordDriver.php b/plugins/mailinabox-change-password/MailInABoxChangePasswordDriver.php deleted file mode 100644 index 95534f5431..0000000000 --- a/plugins/mailinabox-change-password/MailInABoxChangePasswordDriver.php +++ /dev/null @@ -1,157 +0,0 @@ -<?php -/* - * Mail-in-a-box Password Change Plugin - * - * Based on VirtualminChangePasswordDriver - * - * Author: Marius Gripsgard - */ -class MailInABoxChangePasswordDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sAllowedEmails = ''; - /** - * @var string - */ - private $sHost = ''; - /** - * @var string - */ - private $sAdminUser = ''; - /** - * @var string - */ - private $sAdminPassword = ''; - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - /** - * @param string $sHost - * @param string $sAdminUser - * @param string $sAdminPassword - * - * @return \MailInABoxChangePasswordDriver - */ - public function SetConfig($sHost, $sAdminUser, $sAdminPassword) - { - $this->sHost = $sHost; - $this->sAdminUser = $sAdminUser; - $this->sAdminPassword = $sAdminPassword; - return $this; - } - /** - * @param string $sAllowedEmails - * - * @return \MailInABoxChangePasswordDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \MailInABoxChangePasswordDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - return $this; - } - /** - * @param string $sDesc - * @param int $iType = \MailSo\Log\Enumerations\Type::INFO - * - * @return \MailInABoxChangePasswordDriver - */ - public function WriteLog($sDesc, $iType = \MailSo\Log\Enumerations\Type::INFO) - { - if ($this->oLogger) - { - $this->oLogger->Write($sDesc, $iType); - } - return $this; - } - /** - * @param \RainLoop\Model\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - $this->WriteLog('Mail-in-a-box: Try to change password for '.$oAccount->Email()); - $bResult = false; - if (!empty($this->sHost) && !empty($this->sAdminUser) && !empty($this->sAdminPassword) && $oAccount) - { - $this->WriteLog('Mail-in-a-box:[Check] Required Fields Present'); - $sEmail = \trim(\strtolower($oAccount->Email())); - $sHost = \rtrim(\trim($this->sHost), '/'); - $sUrl = $sHost.'/admin/mail/users/password'; - - $sAdminUser = $this->sAdminUser; - $sAdminPassword = $this->sAdminPassword; - $iCode = 0; - $aPost = array( - 'email' => $sEmail, - 'password' => $sNewPassword, - ); - $aOptions = array( - CURLOPT_URL => $sUrl, - CURLOPT_HEADER => false, - CURLOPT_FAILONERROR => true, - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => \http_build_query($aPost, '', '&'), - CURLOPT_TIMEOUT => 20, - CURLOPT_SSL_VERIFYHOST => false, - CURLOPT_USERPWD => $sAdminUser.':'.$sAdminPassword, - CURLOPT_HTTPAUTH => CURLAUTH_BASIC - ); - $oCurl = \curl_init(); - \curl_setopt_array($oCurl, $aOptions); - $this->WriteLog('Mail-in-a-box: Send post request: '.$sUrl); - $mResult = \curl_exec($oCurl); - $iCode = (int) \curl_getinfo($oCurl, CURLINFO_HTTP_CODE); - $sContentType = (string) \curl_getinfo($oCurl, CURLINFO_CONTENT_TYPE); - $this->WriteLog('Mail-in-a-box: Post request result: (Status: '.$iCode.', ContentType: '.$sContentType.')'); - if (false === $mResult || 200 !== $iCode) - { - $this->WriteLog('Mail-in-a-box: Error: '.\curl_error($oCurl), \MailSo\Log\Enumerations\Type::WARNING); - } - if (\is_resource($oCurl)) - { - \curl_close($oCurl); - } - if (false !== $mResult && 200 === $iCode) - { - $this->WriteLog('Mail-in-a-box: Password Change Status: Success'); - $bResult = true; - } - else - { - $this->WriteLog('Mail-in-a-box[Error]: Empty Response: Code: '.$iCode); - } - } - return $bResult; - } -} diff --git a/plugins/mailinabox-change-password/README b/plugins/mailinabox-change-password/README deleted file mode 100644 index 9f419ce620..0000000000 --- a/plugins/mailinabox-change-password/README +++ /dev/null @@ -1 +0,0 @@ -Plugin that adds functionality to change the email account password (Mail-in-a-Box). diff --git a/plugins/mailinabox-change-password/VERSION b/plugins/mailinabox-change-password/VERSION deleted file mode 100644 index d3827e75a5..0000000000 --- a/plugins/mailinabox-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 diff --git a/plugins/mailinabox-change-password/index.php b/plugins/mailinabox-change-password/index.php deleted file mode 100644 index b88235e541..0000000000 --- a/plugins/mailinabox-change-password/index.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php -/* - * Mail-in-a-box Password Change Plugin - * - * Based on VirtualminChangePassword - * - * Author: Marius Gripsgard - */ -class MailInABoxChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - include_once __DIR__.'/MailInABoxChangePasswordDriver.php'; - $sHost = \trim($this->Config()->Get('plugin', 'host', '')); - $sAdminUser = (string) $this->Config()->Get('plugin', 'admin_user', ''); - $sAdminPassword = (string) $this->Config()->Get('plugin', 'admin_password', ''); - $oProvider = new \MailInABoxChangePasswordDriver(); - $oProvider->SetLogger($this->Manager()->Actions()->Logger()); - $oProvider->SetConfig($sHost, $sAdminUser, $sAdminPassword); - $oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))); - break; - } - } - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('host')->SetLabel('Mail-in-a-box Host') - ->SetDefaultValue('https://box.mailinabox.email') - ->SetDescription('Mail-in-a-box host URL. Example: https://box.mailinabox.email'), - \RainLoop\Plugins\Property::NewInstance('admin_user')->SetLabel('Admin User') - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('admin_password')->SetLabel('Admin Password') - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} diff --git a/plugins/nextcloud/LICENSE b/plugins/nextcloud/LICENSE new file mode 100644 index 0000000000..9e5e56cdd9 --- /dev/null +++ b/plugins/nextcloud/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2022 SnappyMail + +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/nextcloud/NextcloudContactsSuggestions.php b/plugins/nextcloud/NextcloudContactsSuggestions.php new file mode 100644 index 0000000000..34d1cbe496 --- /dev/null +++ b/plugins/nextcloud/NextcloudContactsSuggestions.php @@ -0,0 +1,80 @@ +<?php + +class NextcloudContactsSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions +{ + use \MailSo\Log\Inherit; + + private bool $ignoreSystemAddressbook; + + function __construct(bool $ignoreSystemAddressbook = true) + { + $this->ignoreSystemAddressbook = $ignoreSystemAddressbook; + } + + public function Process(\RainLoop\Model\Account $oAccount, string $sQuery, int $iLimit = 20): array + { + try + { + $sQuery = \trim($sQuery); + if ('' === $sQuery) { + return []; + } + + $cm = \OC::$server->getContactsManager(); + if (!$cm || !$cm->isEnabled()) { + return []; + } + + // Unregister system addressbook so as to return only contacts in user's addressbooks + if ($this->ignoreSystemAddressbook) { + foreach($cm->getUserAddressBooks() as $addressBook) { + if($addressBook->isSystemAddressBook()) { + $cm->unregisterAddressBook($addressBook); + } + } + } + + $aSearchResult = $cm->search($sQuery, array('FN', 'NICKNAME', 'TITLE', 'EMAIL')); + + //$this->oLogger->WriteDump($aSearchResult); + + if (\is_array($aSearchResult) && 0 < \count($aSearchResult)) { + $iInputLimit = $iLimit; + $aResult = array(); + foreach ($aSearchResult as $aContact) { + if (0 >= $iLimit) { + break; + } + if (!empty($aContact['UID'])) { + $sUid = $aContact['UID']; + $mEmails = isset($aContact['EMAIL']) ? $aContact['EMAIL'] : ''; + + $sFullName = isset($aContact['FN']) ? \trim($aContact['FN']) : ''; + if (empty($sFullName) && isset($aContact['NICKNAME'])) { + $sFullName = \trim($aContact['NICKNAME']); + } + + if (!\is_array($mEmails)) { + $mEmails = array($mEmails); + } + + foreach ($mEmails as $sEmail) { + $sHash = $sFullName.'|'.$sEmail; + if (!isset($aResult[$sHash])) { + $aResult[$sHash] = array($sEmail, $sFullName); + --$iLimit; + } + } + } + } + return \array_slice(\array_values($aResult), 0, $iInputLimit); + } + } + catch (\Exception $oException) + { + $this->logException($oException); + } + + return []; + } +} diff --git a/plugins/nextcloud/index.php b/plugins/nextcloud/index.php new file mode 100644 index 0000000000..c49524b33c --- /dev/null +++ b/plugins/nextcloud/index.php @@ -0,0 +1,416 @@ +<?php + +class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Nextcloud', + VERSION = '2.38.1', + RELEASE = '2024-10-08', + CATEGORY = 'Integrations', + DESCRIPTION = 'Integrate with Nextcloud v20+', + REQUIRED = '2.38.0'; + + public function Init() : void + { + if (static::IsIntegrated()) { + \SnappyMail\Log::debug('Nextcloud', 'integrated'); + $this->UseLangs(true); + + $this->addHook('main.fabrica', 'MainFabrica'); + $this->addHook('filter.app-data', 'FilterAppData'); + $this->addHook('filter.language', 'FilterLanguage'); + + $this->addCss('style.css'); + + $this->addJs('js/webdav.js'); + + $this->addJs('js/message.js'); + $this->addHook('json.attachments', 'DoAttachmentsActions'); + $this->addJsonHook('NextcloudSaveMsg', 'NextcloudSaveMsg'); + + $this->addJs('js/composer.js'); + $this->addJsonHook('NextcloudAttachFile', 'NextcloudAttachFile'); + + $this->addJs('js/messagelist.js'); + + $this->addTemplate('templates/PopupsNextcloudFiles.html'); + $this->addTemplate('templates/PopupsNextcloudCalendars.html'); + +// $this->addHook('login.credentials.step-2', 'loginCredentials2'); +// $this->addHook('login.credentials', 'loginCredentials'); + $this->addHook('imap.before-login', 'beforeLogin'); + $this->addHook('smtp.before-login', 'beforeLogin'); + $this->addHook('sieve.before-login', 'beforeLogin'); + } else { + \SnappyMail\Log::debug('Nextcloud', 'NOT integrated'); + // \OC::$server->getConfig()->getAppValue('snappymail', 'snappymail-no-embed'); + $this->addHook('main.content-security-policy', 'ContentSecurityPolicy'); + } + } + + public function ContentSecurityPolicy(\SnappyMail\HTTP\CSP $CSP) + { + if (\method_exists($CSP, 'add')) { + $CSP->add('frame-ancestors', "'self'"); + } + } + + public function Supported() : string + { + return static::IsIntegrated() ? '' : 'Nextcloud not found to use this plugin'; + } + + public static function IsIntegrated() + { + return \class_exists('OC') && isset(\OC::$server); + } + + public static function IsLoggedIn() + { + return static::IsIntegrated() && \OC::$server->getUserSession()->isLoggedIn(); + } + + public function loginCredentials(string &$sEmail, string &$sLogin, ?string &$sPassword = null) : void + { + /** + * This has an issue. + * When user changes email address, all settings are gone as the new + * _data_/_default_/storage/{domain}/{local-part} is used + */ +// $ocUser = \OC::$server->getUserSession()->getUser(); +// $sEmail = $ocUser->getEMailAddress() ?: $ocUser->getPrimaryEMailAddress() ?: $sEmail; + } + + public function loginCredentials2(string &$sEmail, ?string &$sPassword = null) : void + { + $ocUser = \OC::$server->getUserSession()->getUser(); + $sEmail = $ocUser->getEMailAddress() ?: $ocUser->getPrimaryEMailAddress() ?: $sEmail; + } + + public function beforeLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void + { + // Only login with OIDC access token if + // it is enabled in config, the user is currently logged in with OIDC, + // the current snappymail account is the OIDC account and no account defined explicitly + if ($oAccount instanceof \RainLoop\Model\MainAccount + && \OCA\SnappyMail\Util\SnappyMailHelper::isOIDCLogin() +// && $oClient->supportsAuthType('OAUTHBEARER') // v2.28 + && \str_starts_with($oSettings->passphrase, 'oidc_login|') + ) { +// $oSettings->passphrase = \OC::$server->getSession()->get('snappymail-passphrase'); + $oSettings->passphrase = \OC::$server->getSession()->get('oidc_access_token'); + \array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER'); + } + } + + /* + \OC::$server->getCalendarManager(); + \OC::$server->getLDAPProvider(); + */ + + public function NextcloudAttachFile() : array + { + $aResult = [ + 'success' => false, + 'tempName' => '' + ]; + $sFile = $this->jsonParam('file', ''); + $oFiles = \OCP\Files::getStorage('files'); + if ($oFiles && $oFiles->is_file($sFile) && $fp = $oFiles->fopen($sFile, 'rb')) { + $oActions = \RainLoop\Api::Actions(); + $oAccount = $oActions->getAccountFromToken(); + if ($oAccount) { + $sSavedName = 'nextcloud-file-' . \sha1($sFile . \microtime()); + if (!$oActions->FilesProvider()->PutFile($oAccount, $sSavedName, $fp)) { + $aResult['error'] = 'failed'; + } else { + $aResult['tempName'] = $sSavedName; + $aResult['success'] = true; + } + } + } + return $this->jsonResponse(__FUNCTION__, $aResult); + } + + public function NextcloudSaveMsg() : array + { + $sSaveFolder = \ltrim($this->jsonParam('folder', ''), '/'); +// $aValues = \RainLoop\Api::Actions()->decodeRawKey($this->jsonParam('msgHash', '')); + $msgHash = $this->jsonParam('msgHash', ''); + $aValues = \json_decode(\MailSo\Base\Utils::UrlSafeBase64Decode($msgHash), true); + $aResult = [ + 'folder' => '', + 'filename' => '', + 'success' => false + ]; + if ($sSaveFolder && !empty($aValues['folder']) && !empty($aValues['uid'])) { + $oActions = \RainLoop\Api::Actions(); + $oMailClient = $oActions->MailClient(); + if (!$oMailClient->IsLoggined()) { + $oAccount = $oActions->getAccountFromToken(); + $oAccount->ImapConnectAndLogin($oActions->Plugins(), $oMailClient->ImapClient(), $oActions->Config()); + } + + $sSaveFolder = $sSaveFolder ?: 'Emails'; + $oFiles = \OCP\Files::getStorage('files'); + if ($oFiles) { + $oFiles->is_dir($sSaveFolder) || $oFiles->mkdir($sSaveFolder); + } + $aResult['folder'] = $sSaveFolder; + $aResult['filename'] = \MailSo\Base\Utils::SecureFileName( + \mb_substr($this->jsonParam('filename', '') ?: \date('YmdHis'), 0, 100) + ) . '.' . \md5($msgHash) . '.eml'; + + + $oMailClient->MessageMimeStream( + function ($rResource) use ($oFiles, $aResult) { + if (\is_resource($rResource)) { + $aResult['success'] = $oFiles->file_put_contents("{$aResult['folder']}/{$aResult['filename']}", $rResource); + } + }, + (string) $aValues['folder'], + (int) $aValues['uid'], + isset($aValues['mimeIndex']) ? (string) $aValues['mimeIndex'] : '' + ); + } + + return $this->jsonResponse(__FUNCTION__, $aResult); + } + + public function DoAttachmentsActions(\SnappyMail\AttachmentsAction $data) + { + if (static::isLoggedIn() && 'nextcloud' === $data->action) { + $oFiles = \OCP\Files::getStorage('files'); + if ($oFiles && \method_exists($oFiles, 'file_put_contents')) { + $sSaveFolder = \ltrim($this->jsonParam('NcFolder', ''), '/'); + $sSaveFolder = $sSaveFolder ?: 'Attachments'; + $oFiles->is_dir($sSaveFolder) || $oFiles->mkdir($sSaveFolder); + $data->result = true; + foreach ($data->items as $aItem) { + $sSavedFileName = empty($aItem['fileName']) ? 'file.dat' : $aItem['fileName']; + if (!empty($aItem['data'])) { + $sSavedFileNameFull = static::SmartFileExists($sSaveFolder.'/'.$sSavedFileName, $oFiles); + if (!$oFiles->file_put_contents($sSavedFileNameFull, $aItem['data'])) { + $data->result = false; + } + } else if (!empty($aItem['fileHash'])) { + $fFile = $data->filesProvider->GetFile($data->account, $aItem['fileHash'], 'rb'); + if (\is_resource($fFile)) { + $sSavedFileNameFull = static::SmartFileExists($sSaveFolder.'/'.$sSavedFileName, $oFiles); + if (!$oFiles->file_put_contents($sSavedFileNameFull, $fFile)) { + $data->result = false; + } + if (\is_resource($fFile)) { + \fclose($fFile); + } + } + } + } + } + } + } + + public function FilterAppData($bAdmin, &$aResult) : void + { + if (!$bAdmin && \is_array($aResult)) { + $ocUser = \OC::$server->getUserSession()->getUser(); + $sUID = $ocUser->getUID(); + $oUrlGen = \OC::$server->getURLGenerator(); + $sWebDAV = $oUrlGen->getAbsoluteURL($oUrlGen->linkTo('', 'remote.php') . '/dav'); +// $sWebDAV = \OCP\Util::linkToRemote('dav'); + $aResult['Nextcloud'] = [ + 'UID' => $sUID, + 'WebDAV' => $sWebDAV, + 'CalDAV' => $this->Config()->Get('plugin', 'calendar', false) +// 'WebDAV_files' => $sWebDAV . '/files/' . $sUID + ]; + if (empty($aResult['Auth'])) { + $config = \OC::$server->getConfig(); + $sEmail = ''; + // Only store the user's password in the current session if they have + // enabled auto-login using Nextcloud username or email address. + if ($config->getAppValue('snappymail', 'snappymail-autologin', false)) { + $sEmail = $sUID; + } else if ($config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) { + $sEmail = $config->getUserValue($sUID, 'settings', 'email', ''); + } else { + \SnappyMail\Log::debug('Nextcloud', 'snappymail-autologin is off'); + } + // If the user has set credentials for SnappyMail in their personal + // settings, override everything before and use those instead. + $sCustomEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email', ''); + if ($sCustomEmail) { + $sEmail = $sCustomEmail; + } + if (!$sEmail) { + $sEmail = $ocUser->getEMailAddress(); +// ?: $ocUser->getPrimaryEMailAddress(); + } +/* + if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) { + if (\OC::$server->getSession()->get('is_oidc')) { + $sEmail = "{$sUID}@nextcloud"; + $aResult['DevPassword'] = \OC::$server->getSession()->get('oidc_access_token'); + } else { + \SnappyMail\Log::debug('Nextcloud', 'Not an OIDC login'); + } + } else { + \SnappyMail\Log::debug('Nextcloud', 'OIDC is off'); + } +*/ + $aResult['DevEmail'] = $sEmail ?: ''; + } else if (!empty($aResult['ContactsSync'])) { + $bSave = false; + if (empty($aResult['ContactsSync']['Url'])) { + $aResult['ContactsSync']['Url'] = "{$sWebDAV}/addressbooks/users/{$sUID}/contacts/"; + $bSave = true; + } + if (empty($aResult['ContactsSync']['User'])) { + $aResult['ContactsSync']['User'] = $sUID; + $bSave = true; + } + $pass = \OC::$server->getSession()['snappymail-passphrase']; + if ($pass/* && empty($aResult['ContactsSync']['Password'])*/) { + $pass = \SnappyMail\Crypt::DecryptUrlSafe($pass, $sUID); + if ($pass) { + $aResult['ContactsSync']['Password'] = $pass; + $bSave = true; + } + } + if ($bSave) { + $oActions = \RainLoop\Api::Actions(); + $oActions->setContactsSyncData( + $oActions->getAccountFromToken(), + array( + 'Mode' => $aResult['ContactsSync']['Mode'], + 'User' => $aResult['ContactsSync']['User'], + 'Password' => $aResult['ContactsSync']['Password'], + 'Url' => $aResult['ContactsSync']['Url'] + ) + ); + } + } + } + } + + public function FilterLanguage(&$sLanguage, $bAdmin) : void + { + if (!\RainLoop\Api::Config()->Get('webmail', 'allow_languages_on_settings', true)) { + $aResultLang = \SnappyMail\L10n::getLanguages($bAdmin); + $userId = \OC::$server->getUserSession()->getUser()->getUID(); + $userLang = \OC::$server->getConfig()->getUserValue($userId, 'core', 'lang', 'en'); + $userLang = \strtr($userLang, '_', '-'); + $sLanguage = $this->determineLocale($userLang, $aResultLang); + // Check if $sLanguage is null + if (!$sLanguage) { + $sLanguage = 'en'; // Assign 'en' if $sLanguage is null + } + } + } + + /** + * Determine locale from user language. + * + * @param string $langCode The name of the input. + * @param array $languagesArray The value of the array. + * + * @return string return locale + */ + private function determineLocale(string $langCode, array $languagesArray) : ?string + { + // Direct check for the language code + if (\in_array($langCode, $languagesArray)) { + return $langCode; + } + + // Check without country code + if (\str_contains($langCode, '-')) { + $langCode = \explode('-', $langCode)[0]; + if (\in_array($langCode, $languagesArray)) { + return $langCode; + } + } + + // Check with uppercase country code + $langCodeWithUpperCase = $langCode . '-' . \strtoupper($langCode); + if (\in_array($langCodeWithUpperCase, $languagesArray)) { + return $langCodeWithUpperCase; + } + + // If no match is found + return null; + } + + /** + * @param mixed $mResult + */ + public function MainFabrica(string $sName, &$mResult) + { + if (static::isLoggedIn()) { + if ('suggestions' === $sName && $this->Config()->Get('plugin', 'suggestions', true)) { + if (!\is_array($mResult)) { + $mResult = array(); + } + include_once __DIR__ . '/NextcloudContactsSuggestions.php'; + $mResult[] = new NextcloudContactsSuggestions( + $this->Config()->Get('plugin', 'ignoreSystemAddressbook', true) + ); + } +/* + if ($this->Config()->Get('plugin', 'storage', false) && ('storage' === $sName || 'storage-local' === $sName)) { + require_once __DIR__ . '/storage.php'; + $oDriver = new \NextcloudStorage(APP_PRIVATE_DATA.'storage', $sName === 'storage-local'); + } +*/ + } + } + + protected function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('suggestions')->SetLabel('Suggestions') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(true), + \RainLoop\Plugins\Property::NewInstance('ignoreSystemAddressbook')->SetLabel('Ignore system addressbook') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(true), +/* + \RainLoop\Plugins\Property::NewInstance('storage')->SetLabel('Use Nextcloud user ID in config storage path') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(false) +*/ + \RainLoop\Plugins\Property::NewInstance('calendar')->SetLabel('Enable "Put ICS in calendar"') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(false) + ); + } + + private static function SmartFileExists(string $sFilePath, $oFiles) : string + { + $sFilePath = \str_replace('\\', '/', \trim($sFilePath)); + + if (!$oFiles->file_exists($sFilePath)) { + return $sFilePath; + } + + $aFileInfo = \pathinfo($sFilePath); + + $iIndex = 0; + + while (true) { + ++$iIndex; + $sFilePathNew = $aFileInfo['dirname'].'/'. + \preg_replace('/\(\d{1,2}\)$/', '', $aFileInfo['filename']). + ' ('.$iIndex.')'. + (empty($aFileInfo['extension']) ? '' : '.'.$aFileInfo['extension']) + ; + if (!$oFiles->file_exists($sFilePathNew)) { + return $sFilePathNew; + } + if (10 < $iIndex) { + break; + } + } + return $sFilePath; + } +} diff --git a/plugins/nextcloud/js/composer.js b/plugins/nextcloud/js/composer.js new file mode 100644 index 0000000000..5c2ab7662d --- /dev/null +++ b/plugins/nextcloud/js/composer.js @@ -0,0 +1,68 @@ +(rl => { +// if (rl.settings.get('Nextcloud')) + + addEventListener('rl-view-model.create', e => { + if ('PopupsCompose' === e.detail.viewModelTemplateID) { + let view = e.detail; + view.nextcloudAttach = () => { + rl.nextcloud.selectFiles().then(files => { + let urls = []; + files && files.forEach(file => { + if (file.name) { + let attachment = view.addAttachmentHelper( + Jua?.randomId(), + file.name.replace(/^.*\/([^/]+)$/, '$1'), + file.size + ); + + rl.pluginRemoteRequest( + (iError, data) => { + attachment.uploading(false).complete(true); + if (iError) { + attachment.error(data?.Result?.error || 'failed'); + } else { + attachment.tempName(data.Result.tempName); + } + }, + 'NextcloudAttachFile', + { + 'file': file.name + } + ); + } else if (file.url) { + urls.push(file.url); + } + }); + if (urls.length) { + // TODO: other editors and text/plain + // https://github.com/the-djmaze/snappymail/issues/981 + view.oEditor.editor.squire.insertHTML(urls.join("<br>")); + } + }); + }; + } + }); + + let template = document.getElementById('PopupsCompose'); + const uploadBtn = template.content.querySelector('#composeUploadButton'); + if (uploadBtn) { + uploadBtn.after(Element.fromHTML(`<a class="btn fontastic" data-bind="click: nextcloudAttach" + data-i18n="[title]NEXTCLOUD/ATTACH_FILES">◦◯◦</a>`)); + } + +/** + https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html + POST /ocs/v2.php/apps/files_sharing/api/v1/shares + JSON { + "path":"/Nextcloud intro.mp4", + "shareType":3, // public link + "shareWith":"user@example.com", +// "publicUpload":false, +// "password":null, +// "permissions":1, // default +// "expireDate":"YYYY-MM-DD", +// "note":"Especially for you" + } +*/ + +})(window.rl); diff --git a/plugins/nextcloud/js/message.js b/plugins/nextcloud/js/message.js new file mode 100644 index 0000000000..f5154b7455 --- /dev/null +++ b/plugins/nextcloud/js/message.js @@ -0,0 +1,188 @@ +(rl => { +// if (rl.settings.get('Nextcloud')) + const templateId = 'MailMessageView'; + + addEventListener('rl-view-model.create', e => { + if (templateId === e.detail.viewModelTemplateID) { + + const + template = document.getElementById(templateId), + cfg = rl.settings.get('Nextcloud'), + attachmentsControls = template.content.querySelector('.attachmentsControls'), + msgMenu = template.content.querySelector('#more-view-dropdown-id + menu'); + + if (attachmentsControls) { + attachmentsControls.append(Element.fromHTML(`<span> + <i class="fontastic iconcolor-red" data-bind="visible: saveNextcloudError">✖</i> + <i class="fontastic" data-bind="visible: !saveNextcloudError(), + css: {'icon-spinner': saveNextcloudLoading()}">💾</i> + <span class="g-ui-link" data-bind="click: saveNextcloud" data-i18n="NEXTCLOUD/SAVE_ATTACHMENTS"></span> + </span>`)); + + // https://github.com/nextcloud/calendar/issues/4684 + if (cfg.CalDAV) { + attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICS" data-icon="📅"> + <span class="g-ui-link" data-bind="click: nextcloudSaveICS" data-i18n="NEXTCLOUD/SAVE_ICS"></span> + </span>`)); + } + } +/* + // https://github.com/the-djmaze/snappymail/issues/592 + if (cfg.CalDAV) { + const attachmentsPlace = template.content.querySelector('.attachmentsPlace'); + attachmentsPlace.after(Element.fromHTML(` + <table data-bind="if: nextcloudICS, visible: nextcloudICS"><tbody style="white-space:pre"> + <tr><td>Summary</td><td data-icon="📅" data-bind="text: nextcloudICS().SUMMARY"></td></tr> + <tr><td>Organizer</td><td data-bind="text: nextcloudICS().ORGANIZER"></td></tr> + <tr><td>Start</td><td data-bind="text: nextcloudICS().DTSTART"></td></tr> + <tr><td>End</td><td data-bind="text: nextcloudICS().DTEND"></td></tr> + <tr><td>Transparency</td><td data-bind="text: nextcloudICS().TRANSP"></td></tr> + <tr data-bind="foreach: nextcloudICS().ATTENDEE"> + <td></td><td data-bind="text: $data.replace(/;/g,';\\n')"></td> + </tr> + </tbody></table>`)); + } +*/ + if (msgMenu) { + msgMenu.append(Element.fromHTML(`<li role="presentation"> + <a href="#" tabindex="-1" data-icon="📥" data-bind="click: nextcloudSaveMsg" data-i18n="NEXTCLOUD/SAVE_EML"></a> + </li>`)); + } + + let view = e.detail; + view.saveNextcloudError = ko.observable(false).extend({ falseTimeout: 7000 }); + view.saveNextcloudLoading = ko.observable(false); + view.saveNextcloud = () => { + const + hashes = (view.message()?.attachments || []) + .map(item => item?.checked() /*&& !item?.isLinked()*/ ? item.download : '') + .filter(v => v); + if (hashes.length) { + view.saveNextcloudLoading(true); + rl.nextcloud.selectFolder().then(folder => { + if (folder) { + rl.fetchJSON('./?/Json/&q[]=/0/', {}, { + Action: 'AttachmentsActions', + target: 'nextcloud', + hashes: hashes, + NcFolder: folder + }) + .then(result => { + view.saveNextcloudLoading(false); + if (result?.Result) { + // success + } else { + view.saveNextcloudError(true); + } + }) + .catch(() => { + view.saveNextcloudLoading(false); + view.saveNextcloudError(true); + }); + } else { + view.saveNextcloudLoading(false); + } + }); + } + }; + + view.nextcloudSaveMsg = () => { + rl.nextcloud.selectFolder().then(folder => { + let msg = view.message(); + folder && rl.pluginRemoteRequest( + (iError, data) => { + console.dir({ + iError:iError, + data:data + }); + }, + 'NextcloudSaveMsg', + { + 'msgHash': msg.requestHash, + 'folder': folder, + 'filename': msg.subject() + } + ); + }); + }; + + view.nextcloudICS = ko.observable(null); + + view.nextcloudSaveICS = () => { + let VEVENT = view.nextcloudICS(); + VEVENT && rl.nextcloud.selectCalendar() + .then(href => href && rl.nextcloud.calendarPut(href, VEVENT)); + } + + /** + * TODO + */ + view.message.subscribe(msg => { + view.nextcloudICS(null); + if (msg && cfg.CalDAV) { +// let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType); + let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType); + if (ics && ics.download) { + // fetch it and parse the VEVENT + rl.fetch(ics.linkDownload()) + .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) + .then(text => { + let VEVENT, + VALARM, + multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE', + 'EXRULE','RSTATUS','RELATED','RESOURCES','RDATE','RRULE'], + lines = text.split(/\r?\n/), + i = lines.length; + while (i--) { + let line = lines[i]; + if (VEVENT) { + while (line.startsWith(' ') && i--) { + line = lines[i] + line.slice(1); + } + if (line.startsWith('END:VALARM')) { + VALARM = {}; + continue; + } else if (line.startsWith('BEGIN:VALARM')) { + VEVENT.VALARM || (VEVENT.VALARM = []); + VEVENT.VALARM.push(VALARM); + VALARM = null; + continue; + } else if (line.startsWith('BEGIN:VEVENT')) { + break; + } + line = line.match(/^([^:;]+)[:;](.+)$/); + if (line) { + if (VALARM) { + VALARM[line[1]] = line[2]; + } else if (multiple.includes(line[1]) || 'X-' == line[1].slice(0,2)) { + VEVENT[line[1]] || (VEVENT[line[1]] = []); + VEVENT[line[1]].push(line[2]); + } else { + VEVENT[line[1]] = line[2]; + } + } + } else if (line.startsWith('END:VEVENT')) { + VEVENT = {}; + } + } +// METHOD:REPLY || METHOD:REQUEST +// console.dir({VEVENT:VEVENT}); + if (VEVENT) { + VEVENT.rawText = text; + VEVENT.isCancelled = () => VEVENT.STATUS?.includes('CANCELLED'); + VEVENT.isConfirmed = () => VEVENT.STATUS?.includes('CONFIRMED'); + VEVENT.shouldReply = () => VEVENT.METHOD?.includes('REPLY'); + console.dir({ + isCancelled: VEVENT.isCancelled(), + shouldReply: VEVENT.shouldReply() + }); + view.nextcloudICS(VEVENT); + } + }); + } + } + }); + } + }); + +})(window.rl); diff --git a/plugins/nextcloud/js/messagelist.js b/plugins/nextcloud/js/messagelist.js new file mode 100644 index 0000000000..8b8681afac --- /dev/null +++ b/plugins/nextcloud/js/messagelist.js @@ -0,0 +1,39 @@ +(rl => { +// if (rl.settings.get('Nextcloud')) + + addEventListener('rl-view-model.create', e => { + if ('MailMessageList' === e.detail.viewModelTemplateID) { + let view = e.detail; + view.nextcloudSaveMsgs = () => { + view.messageList.hasChecked() + && rl.nextcloud.selectFolder().then(folder => { + folder && view.messageList.forEach(msg => { + msg.checked() && rl.pluginRemoteRequest( + (iError, data) => { + console.dir({ + iError:iError, + data:data + }); + }, + 'NextcloudSaveMsg', + { + 'msgHash': msg.requestHash, + 'folder': folder, + 'filename': msg.subject() + } + ); + }); + }); + }; + } + }); + + const msgMenu = document.getElementById('MailMessageList') + .content.querySelector('#more-list-dropdown-id + menu [data-bind*="forwardCommand"]'); + if (msgMenu) { + msgMenu.after(Element.fromHTML(`<li role="presentation" data-bind="css:{disabled:!messageList.hasChecked()}"> + <a href="#" tabindex="-1" data-icon="📥" data-bind="click: nextcloudSaveMsgs" data-i18n="NEXTCLOUD/SAVE_EML"></a> + </li>`)); + } + +})(window.rl); diff --git a/plugins/nextcloud/js/webdav.js b/plugins/nextcloud/js/webdav.js new file mode 100644 index 0000000000..24aa1f2260 --- /dev/null +++ b/plugins/nextcloud/js/webdav.js @@ -0,0 +1,502 @@ +(rl => { + +const + nsDAV = 'DAV:', + nsNC = 'http://nextcloud.org/ns', + nsOC = 'http://owncloud.org/ns', + nsOCS = 'http://open-collaboration-services.org/ns', + nsCalDAV = 'urn:ietf:params:xml:ns:caldav', + + OC = () => parent.OC, + + // Nextcloud 19 deprecated generateUrl, but screw `import { generateUrl } from "@nextcloud/router"` + shareUrl = () => OC().webroot + '/ocs/v2.php/apps/files_sharing/api/v1/shares', + generateUrl = path => OC().webroot + (OC().config.modRewriteWorking ? '' : '/index.php') + path, + generateRemoteUrl = path => location.protocol + '//' + location.host + generateUrl(path), + +// shareTypes = {0 = user, 1 = group, 3 = public link} + + propfindFiles = `<?xml version="1.0"?> +<propfind xmlns="DAV:" xmlns:oc="${nsOC}" xmlns:ocs="${nsOCS}" xmlns:nc="${nsNC}"> + <prop> + <oc:fileid/> + <oc:size/> + <resourcetype/> + <getcontentlength/> + <ocs:share-permissions/> + <oc:share-types/> + </prop> +</propfind>`, + + propfindCal = `<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:" xmlns:x1="http://apple.com/ns/ical/"> + <d:prop> + <d:resourcetype/> + <d:current-user-privilege-set/> + <d:displayname/> + <x1:calendar-color/> + </d:prop> +</d:propfind>`, + + xmlParser = new DOMParser(), + pathRegex = /.*\/remote.php\/dav\/[^/]+\/[^/]+/g, + + getElementsByTagName = (parent, namespace, localName) => parent.getElementsByTagNameNS(namespace, localName), + getDavElementsByTagName = (parent, localName) => getElementsByTagName(parent, nsDAV, localName), + getDavElementByTagName = (parent, localName) => getDavElementsByTagName(parent, localName)?.item(0), + getElementByTagName = (parent, localName) => +parent.getElementsByTagName(localName)?.item(0), + + ncFetch = (path, options) => { + if (!OC().requestToken) { + return Promise.reject(new Error('OC.requestToken missing')); + } + options = Object.assign({ + mode: 'same-origin', + cache: 'no-cache', + redirect: 'error', + credentials: 'same-origin', + headers: {} + }, options); + options.headers.requesttoken = OC().requestToken; + return fetch(path, options); + }, + + davFetch = (mode, path, options) => { + let cfg = rl.settings.get('Nextcloud'); +// cfg.UID = document.head.dataset.user + return ncFetch(cfg.WebDAV + '/' + mode + '/' + cfg.UID + path, options); + }, + + davFetchFiles = (path, options) => davFetch('files', path, options), + + createDirectory = path => davFetchFiles(path, { method: 'MKCOL' }), + + fetchFiles = path => { + if (!OC().requestToken) { + return Promise.reject(new Error('OC.requestToken missing')); + } + return davFetchFiles(path, { + method: 'PROPFIND', + headers: { + 'Content-Type': 'application/xml; charset=utf-8' + }, + body: propfindFiles + }) + .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) + .then(text => { + const + elemList = [], + responseList = getDavElementsByTagName( + xmlParser.parseFromString(text, 'application/xml').documentElement, + 'response' + ); + path = path.replace(/\/$/, ''); + for (let i = 0; i < responseList.length; ++i) { + const + e = responseList.item(i), + elem = { + name: decodeURIComponent(getDavElementByTagName(e, 'href').textContent + .replace(pathRegex, '').replace(/\/$/, '')), + isFile: false + }; + if (getDavElementsByTagName(getDavElementByTagName(e, 'resourcetype'), 'collection').length) { + // skip current directory + if (elem.name === path) { + continue; + } + } else { + elem.isFile = true; + elem.id = e.getElementsByTagNameNS(nsOC, 'fileid')?.item(0)?.textContent; + elem.size = getDavElementByTagName(e, 'getcontentlength')?.textContent + || getElementByTagName(e, 'oc:size')?.textContent; + elem.shared = [...e.getElementsByTagNameNS(nsOC, 'share-type')].some(node => '3' == node.textContent); + } + elemList.push(elem); + } + // https://github.com/the-djmaze/snappymail/issues/1177 +// elemList.sort((a, b) => a.isFile != b.isFile ? (a.isFile ? 1 : -1) : a.name.localeCompare(b.name)); + return Promise.resolve(elemList); + }); + }, + + buildTree = (view, parent, items, path) => { + if (items.length) { + try { + // https://github.com/the-djmaze/snappymail/issues/1109 + let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'}); + items.sort((a, b) => collator.compare(a.name, b.name)); + } catch (e) { + console.error(e); + } + items.forEach(item => { + if (!item.isFile) { + let li = document.createElement('li'), + details = document.createElement('details'), + summary = document.createElement('summary'), + ul = document.createElement('ul'); + details.addEventListener('toggle', () => { + ul.children.length + || fetchFiles(item.name).then(items => buildTree(view, ul, items, item.name)); + }); + summary.textContent = item.name.replace(/^.*\/([^/]+)$/, '$1'); + summary.dataset.icon = '📁'; + if (!view.files()) { + let btn = document.createElement('button'); + btn.name = 'select'; + btn.textContent = 'select'; + btn.className = 'button-vue'; + summary.append(btn); + summary.item_name = item.name; + } + details.append(summary); + details.append(ul); +// a.append('- ' + item.name.replace(/^\/+/, '')); + li.append(details); + parent.append(li); + } + }); + if (view.files()) { + items.forEach(item => { + if (item.isFile) { + let li = document.createElement('li'), + cb = document.createElement('input'); + + li.item = item; + li.textContent = item.name.replace(/^.*\/([^/]+)$/, '$1'); + li.dataset.icon = '🗎'; + + cb.type = 'checkbox'; + li.append(cb); + + parent.append(li); + } + }); + } + } + if (!view.files()) { + let li = document.createElement('li'), + input = document.createElement('input'), + btn = document.createElement('button'); + btn.name = 'create'; + btn.textContent = 'create & select'; + btn.className = 'button-vue'; + btn.input = input; + li.item_path = path; + li.append(input); + li.append(btn); + parent.append(li); + } + }; + +class NextcloudFilesPopupView extends rl.pluginPopupView { + constructor() { + super('NextcloudFiles'); + this.addObservables({ + files: false + }); + } + + attach() { + this.select = []; + this.tree.querySelectorAll('input').forEach(input => + input.checked && this.select.push(input.parentNode.item) + ); + this.close(); + } + + shareInternal() { + this.select = []; + this.tree.querySelectorAll('input').forEach(input => + input.checked && this.select.push({url:generateRemoteUrl(`/f/${input.parentNode.item.id}`)}) + ); + this.close(); + } + + sharePublic() { + const inputs = [...this.tree.querySelectorAll('input')], + loop = () => { + if (!inputs.length) { + this.close(); + return; + } + const input = inputs.pop(); + if (!input.checked) { + loop(); + } else { + const item = input.parentNode.item; + if (item.shared) { + ncFetch( + shareUrl() + `?format=json&path=${encodeURIComponent(item.name)}&reshares=true` + ) + .then(response => (response.status < 400) ? response.json() : Promise.reject(new Error({ response }))) + .then(json => { + this.select.push({url:json.ocs.data[0].url}); + loop(); +// json.data[0].password + }); + } else { + ncFetch( + shareUrl(), + { + method:'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + path:item.name, + shareType:3, + attributes:"[]" + }) + } + ) + .then(response => (response.status < 400) ? response.json() : Promise.reject(new Error({ response }))) + .then(json => { +// PUT /ocs/v2.php/apps/files_sharing/api/v1/shares/2 {"password":"ABC09"} + this.select.push({url:json.ocs.data.url}); + loop(); + }); + } + } + }; + + this.select = []; + loop(); + } + + onBuild(dom) { + this.tree = dom.querySelector('#sm-nc-files-tree'); + this.tree.addEventListener('click', event => { + let el = event.target; + if (el.matches('button')) { + let parent = el.parentNode; + if ('select' == el.name) { + this.select = parent.item_name; + this.close(); + } else if ('create' == el.name) { + let name = el.input.value.replace(/[|\\?*<":>+[]\/&\s]/g, ''); + if (name.length) { + name = parent.item_path + '/' + name; + createDirectory(name).then(response => { + if (response.status == 201) { + this.select = name; + this.close(); + } + }); + } + } + } + }); + } + + // Happens after showModal() + beforeShow(files, fResolve) { + this.select = ''; + this.files(!!files); + this.fResolve = fResolve; + + this.tree.innerHTML = ''; + fetchFiles('/').then(items => { + buildTree(this, this.tree, items, '/'); + }).catch(err => console.error(err)) + } + + onHide() { + this.fResolve(this.select); + } +/* +beforeShow() {} // Happens before showModal() +onShow() {} // Happens after showModal() +afterShow() {} // Happens after showModal() animation transitionend +onHide() {} // Happens before animation transitionend +afterHide() {} // Happens after animation transitionend +close() {} +*/ +} + +class NextcloudCalendarsPopupView extends rl.pluginPopupView { + constructor() { + super('NextcloudCalendars'); + } + + onBuild(dom) { + this.tree = dom.querySelector('#sm-nc-calendars'); + this.tree.addEventListener('click', event => { + let el = event.target; + if (el.matches('button')) { + this.select = el.href; + this.close(); + } + }); + } + createCalendarListItem(calendarData, treeElement) { + const { + displayName, + href, + calendarColor + } = calendarData; + + const li = document.createElement('li'); + li.style.display = 'flex'; + + const span = document.createElement('span'); + span.setAttribute('role', 'img'); + span.className = 'material-design-icon checkbox-blank-circle-icon'; + span.style.fill = calendarColor; + span.style.width = '20px'; + span.style.height = '20px'; + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', '20'); + svg.setAttribute('height', '20'); + svg.setAttribute('viewBox', '0 0 24 24'); + + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', 'M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z'); + svg.appendChild(path); + + span.appendChild(svg); + + const button = document.createElement('button'); + button.className = 'button-vue'; + button.style.backgroundColor = 'transparent'; + button.style.border = '0'; + button.style.fontSize = 'large'; + button.style.padding = '0'; + button.style.cursor = 'pointer'; + button.style.marginLeft = '5px'; + button.href = href.replace(pathRegex, '').replace(/\/$/, ''); + button.textContent = displayName; + button.style.color = '#1968DF'; + + li.appendChild(span); + li.appendChild(button); + + treeElement.appendChild(li); + } + // Happens after showModal() + beforeShow(fResolve) { + this.select = ''; + this.fResolve = fResolve; + this.tree.innerHTML = ''; + davFetch('calendars', '/', { + method: 'PROPFIND', + headers: { + 'Content-Type': 'application/xml; charset=utf-8' + }, + body: propfindCal + }) + .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) + .then(text => { + // Parse the XML text + const xmlDoc = xmlParser.parseFromString(text, 'application/xml').documentElement; + const responseList = getElementsInNamespaces(xmlDoc, 'response'); + for (let i = 0; i < responseList.length; ++i) { + const e = responseList[i]; + if (getDavElementByTagName(e, 'resourcetype').getElementsByTagNameNS(nsCalDAV, 'calendar').length) { + const displayNameElement = getElementsInNamespaces(e, 'displayname')[0]; + const displayName = displayNameElement ? displayNameElement.textContent.trim() : ''; + + const hrefElement = getElementsInNamespaces(e, 'href')[0]; + const href = hrefElement ? hrefElement.textContent.trim() : ''; + + const calendarColorElement = getElementsInNamespaces(e, 'calendar-color')[0]; + const calendarColor = calendarColorElement ? calendarColorElement.textContent.trim() : '#000000'; + + // Create an object to hold calendar data + const calendarData = { + displayName, + href, + calendarColor + }; + + // Call the function to create and append the list item + this.createCalendarListItem(calendarData, this.tree); + } + } + }) + .catch(err => console.error(err)); + } + + onHide() { + this.fResolve(this.select); + } +/* +beforeShow() {} // Happens before showModal() +onShow() {} // Happens after showModal() +afterShow() {} // Happens after showModal() animation transitionend +onHide() {} // Happens before animation transitionend +afterHide() {} // Happens after animation transitionend +close() {} +*/ +} + +rl.nextcloud = { + selectCalendar: () => + new Promise(resolve => { + NextcloudCalendarsPopupView.showModal([ + href => resolve(href), + ]); + }), + + calendarPut: (path, event) => { + davFetch('calendars', path + '/' + event.UID + '.ics', { + method: 'PUT', + headers: { + 'Content-Type': 'text/calendar' + }, + // Validation error in iCalendar: A calendar object on a CalDAV server MUST NOT have a METHOD property. + body: event.rawText + .replace('METHOD:', 'X-METHOD:') + // https://github.com/nextcloud/calendar/issues/4684 + .replace('ATTENDEE:', 'X-ATTENDEE:') + .replace('ORGANIZER:', 'X-ORGANIZER:') + .replace(/RSVP=TRUE/g, 'RSVP=FALSE') + .replace(/\r?\n/g, '\r\n') + }) + .then(response => { + if (201 == response.status) { + // Created + } else if (204 == response.status) { + // Not modified + } else { +// response.text().then(text => console.error({status:response.status, body:text})); + Promise.reject(new Error({ response })); + } + }); + }, + + selectFolder: () => + new Promise(resolve => { + NextcloudFilesPopupView.showModal([ + false, + folder => resolve(folder), + ]); + }), + + selectFiles: () => + new Promise(resolve => { + NextcloudFilesPopupView.showModal([ + true, + files => resolve(files), + ]); + }) +}; + +})(window.rl); + +function getElementsInNamespaces(xmlDocument, tagName) { + const namespaces = { + d: 'DAV:', + x1: 'http://apple.com/ns/ical/', + }; + const results = []; + for (const prefix in namespaces) { + const namespaceURI = namespaces[prefix]; + const elements = xmlDocument.getElementsByTagNameNS(namespaceURI, tagName); + for (const element of elements) { + results.push(element); + } + } + return results; +} diff --git a/plugins/nextcloud/langs/de.json b/plugins/nextcloud/langs/de.json new file mode 100644 index 0000000000..5f43f1357f --- /dev/null +++ b/plugins/nextcloud/langs/de.json @@ -0,0 +1,14 @@ +{ + "NEXTCLOUD": { + "SAVE_ATTACHMENTS": "Speichern Sie in Nextcloud", + "SAVE_EML": "Als .eml in Nextcloud speichern", + "SAVE_ICS": "Kalender eintragen", + "SELECT_FOLDER": "Ordner auswählen", + "SELECT_FILES": "Datei(en) auswählen", + "ATTACH_FILES": "Nextcloud-Dateien anhängen", + "SELECT_CALENDAR": "Kalender auswählen", + "FILE_ATTACH": "anfügen", + "FILE_INTERNAL": "intern", + "FILE_PUBLIC": "öffentlich" + } +} diff --git a/plugins/nextcloud/langs/en.json b/plugins/nextcloud/langs/en.json new file mode 100644 index 0000000000..2c19d11c23 --- /dev/null +++ b/plugins/nextcloud/langs/en.json @@ -0,0 +1,14 @@ +{ + "NEXTCLOUD": { + "SAVE_ATTACHMENTS": "Save in Nextcloud", + "SAVE_EML": "Save as .eml in Nextcloud", + "SAVE_ICS": "Add to calendar", + "SELECT_FOLDER": "Select folder", + "SELECT_FILES": "Select file(s)", + "ATTACH_FILES": "Attach Nextcloud files", + "SELECT_CALENDAR": "Select calendar", + "FILE_ATTACH": "attach", + "FILE_INTERNAL": "internal", + "FILE_PUBLIC": "public" + } +} diff --git a/plugins/nextcloud/langs/pl.json b/plugins/nextcloud/langs/pl.json new file mode 100644 index 0000000000..82e0d4f181 --- /dev/null +++ b/plugins/nextcloud/langs/pl.json @@ -0,0 +1,14 @@ +{ + "NEXTCLOUD": { + "SAVE_ATTACHMENTS": "Zapisz w Nextcloudzie", + "SAVE_EML": "Zapisz jako .eml w Nextcloudzie", + "SAVE_ICS": "Dodaj do Kalendarza", + "SELECT_FOLDER": "Wybierz folder", + "SELECT_FILES": "Wybierz plik(i)", + "ATTACH_FILES": "Dodaj załącznik z Nextclouda", + "SELECT_CALENDAR": "Wybierz kalendarz", + "FILE_ATTACH": "dołącz", + "FILE_INTERNAL": "link wewnętrzny", + "FILE_PUBLIC": "link publiczny" + } +} diff --git a/plugins/nextcloud/langs/ru.json b/plugins/nextcloud/langs/ru.json new file mode 100644 index 0000000000..a8b28372d3 --- /dev/null +++ b/plugins/nextcloud/langs/ru.json @@ -0,0 +1,14 @@ +{ + "NEXTCLOUD": { + "SAVE_ATTACHMENTS": "Сохранить в Nextcloud", + "SAVE_EML": "Сохранить как .eml в Nextcloud", + "SAVE_ICS": "Вставить в календарь", + "SELECT_FOLDER": "Выбрите папку", + "SELECT_FILES": "Выберите файл(ы)", + "ATTACH_FILES": "Прикрепить файлы из Nextcloud", + "SELECT_CALENDAR": "Выбрать календарь", + "FILE_ATTACH": "Прикрепить с ПК", + "FILE_INTERNAL": "Внутреняя", + "FILE_PUBLIC": "Публичная" + } +} diff --git a/plugins/nextcloud/langs/zh-TW.json b/plugins/nextcloud/langs/zh-TW.json new file mode 100644 index 0000000000..292d022868 --- /dev/null +++ b/plugins/nextcloud/langs/zh-TW.json @@ -0,0 +1,14 @@ +{ + "NEXTCLOUD": { + "SAVE_ATTACHMENTS": "儲存到 Nextcloud", + "SAVE_EML": "以 .eml 檔案儲存到 Nextcloud", + "SAVE_ICS": "新增到日曆", + "SELECT_FOLDER": "選擇檔案夾", + "SELECT_FILES": "選擇檔案", + "ATTACH_FILES": "新增 Nextcloud 檔案", + "SELECT_CALENDAR": "選擇日曆", + "FILE_ATTACH": "附加", + "FILE_INTERNAL": "內部", + "FILE_PUBLIC": "公開" + } +} diff --git a/plugins/nextcloud/langs/zh.json b/plugins/nextcloud/langs/zh.json new file mode 100644 index 0000000000..39b20ac2eb --- /dev/null +++ b/plugins/nextcloud/langs/zh.json @@ -0,0 +1,14 @@ +{ + "NEXTCLOUD": { + "SAVE_ATTACHMENTS": "保存到 Nextcloud", + "SAVE_EML": "以 .eml 文件保存到 Nextcloud", + "SAVE_ICS": "添加到日历", + "SELECT_FOLDER": "选择文件夹", + "SELECT_FILES": "选择文件", + "ATTACH_FILES": "添加 Nextcloud 文件", + "SELECT_CALENDAR": "选择日历", + "FILE_ATTACH": "附加", + "FILE_INTERNAL": "内部", + "FILE_PUBLIC": "公开" + } +} diff --git a/plugins/nextcloud/storage.php b/plugins/nextcloud/storage.php new file mode 100644 index 0000000000..c8c71e1fc7 --- /dev/null +++ b/plugins/nextcloud/storage.php @@ -0,0 +1,22 @@ +<?php + +use RainLoop\Providers\Storage\Enumerations\StorageType; + +class NextcloudStorage extends \RainLoop\Providers\Storage\FileStorage +{ + /** + * @param \RainLoop\Model\Account|string|null $mAccount + */ + public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false) : string + { + $sDataPath = parent::GenerateFilePath($mAccount, $iStorageType, $bMkDir); + if (StorageType::CONFIG === $iStorageType) { + $sUID = \OC::$server->getUserSession()->getUser()->getUID(); + $sDataPath .= ".config/{$sUID}/"; + if ($bMkDir && !\is_dir($sDataPath) && !\mkdir($sDataPath, 0700, true)) { + throw new \RainLoop\Exceptions\Exception('Can\'t make storage directory "'.$sDataPath.'"'); + } + } + return $sDataPath; + } +} diff --git a/plugins/nextcloud/style.css b/plugins/nextcloud/style.css new file mode 100644 index 0000000000..4dd1429926 --- /dev/null +++ b/plugins/nextcloud/style.css @@ -0,0 +1,17 @@ +#V-PopupsNextcloudFiles li:hover { + background-color: var(--color-background-hover); +} +#V-PopupsNextcloudFiles li { + clear: right; + line-height: 2.5em; +} +#V-PopupsNextcloudFiles li input, +#V-PopupsNextcloudFiles li button { + cursor: pointer; + float: right; + margin: 0 0 0 0.5em; +} + +#rl-app .squire-wysiwyg a { + text-decoration: underline; +} diff --git a/plugins/nextcloud/templates/PopupsNextcloudCalendars.html b/plugins/nextcloud/templates/PopupsNextcloudCalendars.html new file mode 100644 index 0000000000..879340c8f1 --- /dev/null +++ b/plugins/nextcloud/templates/PopupsNextcloudCalendars.html @@ -0,0 +1,7 @@ +<header> + <a class="close" href="#" data-bind="click: close">×</a> + <h3 data-i18n="NEXTCLOUD/SELECT_CALENDAR"></h3> +</header> +<div class="modal-body form-horizontal"> + <ul id="sm-nc-calendars"></ul> +</div> diff --git a/plugins/nextcloud/templates/PopupsNextcloudFiles.html b/plugins/nextcloud/templates/PopupsNextcloudFiles.html new file mode 100644 index 0000000000..dcc03d7e86 --- /dev/null +++ b/plugins/nextcloud/templates/PopupsNextcloudFiles.html @@ -0,0 +1,18 @@ +<header> + <a class="close" href="#" data-bind="click: close">×</a> + <h3 data-i18n="NEXTCLOUD/SELECT_FOLDER" data-bind="hidden:files"></h3> + <h3 data-i18n="NEXTCLOUD/SELECT_FILES" data-bind="visible:files"></h3> +</header> +<div class="modal-body form-horizontal"> +<!-- + TODO: built tree of directories and optional files + In case of directories: radiobutton to select one OR click selects + In case of files: checkbox to select (multiple) +--> + <ul id="sm-nc-files-tree"></ul> +</div> +<footer data-bind="visible:files"> + <button data-bind="click:attach" name="select" data-icon="📎" class="button-vue" data-i18n="NEXTCLOUD/FILE_ATTACH">attach</button> + <button data-bind="click:shareInternal" name="share-internal" data-icon="🔗" class="button-vue" data-i18n="NEXTCLOUD/FILE_INTERNAL">internal</button> + <button data-bind="click:sharePublic" name="share-public" data-icon="🔗" class="button-vue" data-i18n="NEXTCLOUD/FILE_PUBLIC">public</button> +</footer> diff --git a/plugins/override-smtp-credentials/README b/plugins/override-smtp-credentials/README deleted file mode 100644 index 291c3e8d3c..0000000000 --- a/plugins/override-smtp-credentials/README +++ /dev/null @@ -1 +0,0 @@ -Plugin which allows you to override smtp credentials specified users. \ No newline at end of file diff --git a/plugins/override-smtp-credentials/VERSION b/plugins/override-smtp-credentials/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/override-smtp-credentials/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/override-smtp-credentials/index.php b/plugins/override-smtp-credentials/index.php index ed26330c6c..e524bef719 100644 --- a/plugins/override-smtp-credentials/index.php +++ b/plugins/override-smtp-credentials/index.php @@ -2,54 +2,80 @@ class OverrideSmtpCredentialsPlugin extends \RainLoop\Plugins\AbstractPlugin { - public function Init() + const + NAME = 'Override SMTP Credentials', + VERSION = '2.5', + RELEASE = '2024-03-12', + REQUIRED = '2.35.3', + CATEGORY = 'Filters', + DESCRIPTION = 'Override SMTP credentials for specific users.'; + + public function Init() : void { - $this->addHook('filter.smtp-credentials', 'FilterSmtpCredentials'); + $this->addHook('smtp.before-connect', 'FilterSmtpConnect'); + $this->addHook('smtp.before-login', 'FilterSmtpCredentials'); } /** * @param \RainLoop\Model\Account $oAccount - * @param array $aSmtpCredentials + * @param \MailSo\Smtp\SmtpClient $oSmtpClient + * @param \MailSo\Smtp\Settings $oSettings */ - public function FilterSmtpCredentials($oAccount, &$aSmtpCredentials) + public function FilterSmtpConnect(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, \MailSo\Smtp\Settings $oSettings) { - if ($oAccount instanceof \RainLoop\Model\Account && \is_array($aSmtpCredentials)) - { - $sEmail = $oAccount->Email(); - + $sEmail = $oAccount->Email(); + $sWhiteList = \trim($this->Config()->Get('plugin', 'override_users', '')); + $sFoundValue = ''; + if (\strlen($sWhiteList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sWhiteList, $sFoundValue)) { + \SnappyMail\LOG::debug('SMTP Override', "{$sEmail} matched {$sFoundValue}"); + $oSettings->usePhpMail = false; $sHost = \trim($this->Config()->Get('plugin', 'smtp_host', '')); - $sWhiteList = \trim($this->Config()->Get('plugin', 'override_users', '')); - - if (0 < strlen($sWhiteList) && 0 < \strlen($sHost) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sWhiteList)) - { - $aSmtpCredentials['Host'] = $sHost; - $aSmtpCredentials['Port'] = (int) $this->Config()->Get('plugin', 'smtp_port', 25); - + if (\strlen($sHost)) { + $oSettings->host = $sHost; + $oSettings->port = (int) $this->Config()->Get('plugin', 'smtp_port', 25); $sSecure = \trim($this->Config()->Get('plugin', 'smtp_secure', 'None')); switch ($sSecure) { case 'SSL': - $aSmtpCredentials['Secure'] = MailSo\Net\Enumerations\ConnectionSecurityType::SSL; + $oSettings->type = MailSo\Net\Enumerations\ConnectionSecurityType::SSL; break; case 'TLS': - $aSmtpCredentials['Secure'] = MailSo\Net\Enumerations\ConnectionSecurityType::STARTTLS; + case 'STARTTLS': + $oSettings->type = MailSo\Net\Enumerations\ConnectionSecurityType::STARTTLS; + break; + case 'Detect': + $oSettings->type = MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT; break; default: - $aSmtpCredentials['Secure'] = MailSo\Net\Enumerations\ConnectionSecurityType::NONE; + $oSettings->type = MailSo\Net\Enumerations\ConnectionSecurityType::NONE; break; } - - $aSmtpCredentials['UseAuth'] = (bool) $this->Config()->Get('plugin', 'smtp_auth', true); - $aSmtpCredentials['Login'] = \trim($this->Config()->Get('plugin', 'smtp_user', '')); - $aSmtpCredentials['Password'] = (string) $this->Config()->Get('plugin', 'smtp_password', ''); } + } else { + \SnappyMail\LOG::debug('SMTP Override', "{$sEmail} no match"); + } + } + + /** + * @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) + { + $sWhiteList = \trim($this->Config()->Get('plugin', 'override_users', '')); + $sFoundValue = ''; + if (\strlen($sWhiteList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $sWhiteList, $sFoundValue)) { + $oSettings->useAuth = (bool) $this->Config()->Get('plugin', 'smtp_auth', true); + $oSettings->username = \trim($this->Config()->Get('plugin', 'smtp_user', '')); + $oSettings->passphrase = (string) $this->Config()->Get('plugin', 'smtp_password', ''); } } /** * @return array */ - public function configMapping() + protected function configMapping() : array { return array( \RainLoop\Plugins\Property::NewInstance('smtp_host')->SetLabel('SMTP Host') @@ -59,7 +85,7 @@ public function configMapping() ->SetDefaultValue(25), \RainLoop\Plugins\Property::NewInstance('smtp_secure')->SetLabel('SMTP Secure') ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) - ->SetDefaultValue(array('None', 'SSL', 'TLS')), + ->SetDefaultValue(array('None', 'Detect', 'SSL', 'STARTTLS')), \RainLoop\Plugins\Property::NewInstance('smtp_auth')->SetLabel('Use auth') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) ->SetDefaultValue(true), diff --git a/plugins/piwik-analytics/LICENSE b/plugins/piwik-analytics/LICENSE deleted file mode 100644 index 271342337d..0000000000 --- a/plugins/piwik-analytics/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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 -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/piwik-analytics/README b/plugins/piwik-analytics/README deleted file mode 100644 index 9b69b68ca0..0000000000 --- a/plugins/piwik-analytics/README +++ /dev/null @@ -1 +0,0 @@ -Embed Piwik Analytics (Open source web analytics platform) code into your webmail installation pages. \ No newline at end of file diff --git a/plugins/piwik-analytics/VERSION b/plugins/piwik-analytics/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/piwik-analytics/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/piwik-analytics/index.php b/plugins/piwik-analytics/index.php deleted file mode 100644 index 1932a07172..0000000000 --- a/plugins/piwik-analytics/index.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php - -class PiwikAnalyticsPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - /** - * @return void - */ - public function Init() - { - if ('' !== $this->Config()->Get('plugin', 'piwik_url', '') && - '' !== $this->Config()->Get('plugin', 'site_id', '')) - { - $this->addJs('js/include.js'); - } - } - - /** - * @return array - */ - public function configMapping() - { - $oUrl = \RainLoop\Plugins\Property::NewInstance('piwik_url')->SetLabel('Piwik URL') - ->SetAllowedInJs(true); - - $oSiteID = \RainLoop\Plugins\Property::NewInstance('site_id')->SetLabel('Site ID') - ->SetAllowedInJs(true); - - if (\method_exists($oUrl, 'SetPlaceholder')) - { - $oUrl->SetPlaceholder('http://'); - $oSiteID->SetPlaceholder(''); - } - - return array($oUrl, $oSiteID); - } -} diff --git a/plugins/piwik-analytics/js/include.js b/plugins/piwik-analytics/js/include.js deleted file mode 100644 index a5ab3328ae..0000000000 --- a/plugins/piwik-analytics/js/include.js +++ /dev/null @@ -1,37 +0,0 @@ - -$(function () { - - if (!window.rl) - { - return; - } - - var - sPiwikURL = '' + window.rl.pluginSettingsGet('piwik-analytics', 'piwik_url'), - sSiteID = '' + window.rl.pluginSettingsGet('piwik-analytics', 'site_id') - ; - - if ('' !== sPiwikURL && '' !== sSiteID) - { - sPiwikURL = sPiwikURL.replace(/[\\\/\s]+$/, '') + '/'; - if (!/^https?:/i.test(sPiwikURL)) - { - sPiwikURL = 'http://' + sPiwikURL; - } - - window._paq = window._paq || []; - (function(window){ - window._paq.push(['setSiteId', sSiteID]); - window._paq.push(['setTrackerUrl', sPiwikURL + 'piwik.php']); - window._paq.push(['trackPageView']); - window.setInterval(function () { - window._paq.push(['trackPageView']); - }, 1000 * 60 * 2); - var d = window.document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0]; - g.type = 'text/javascript'; g.defer = true; g.async = true; g.src = sPiwikURL + 'piwik.js'; - if (s && s.parentNode) { - s.parentNode.insertBefore(g, s); - } - }(window)); - } -}); \ No newline at end of file diff --git a/plugins/poppassd-change-password/ChangePasswordPoppassdDriver.php b/plugins/poppassd-change-password/ChangePasswordPoppassdDriver.php deleted file mode 100644 index 9cb2851c25..0000000000 --- a/plugins/poppassd-change-password/ChangePasswordPoppassdDriver.php +++ /dev/null @@ -1,119 +0,0 @@ -<?php - -class ChangePasswordPoppassdDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sHost = '127.0.0.1'; - - /** - * @var int - */ - private $iPort = 106; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sHost - * - * @return \ChangePasswordPoppassdDriver - */ - public function SetHost($sHost) - { - $this->sHost = $sHost; - return $this; - } - - /** - * @param int $iPort - * - * @return \ChangePasswordPoppassdDriver - */ - public function SetPort($iPort) - { - $this->iPort = (int) $iPort; - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \ChangePasswordPoppassdDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \ChangePasswordPoppassdDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Model\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - $bResult = false; - - try - { - $oPoppassdClient = \MailSo\Poppassd\PoppassdClient::NewInstance(); - if ($this->oLogger instanceof \MailSo\Log\Logger) - { - $oPoppassdClient->SetLogger($this->oLogger); - } - - $oPoppassdClient - ->Connect($this->sHost, $this->iPort) - ->Login($oAccount->Login(), $oAccount->Password()) - ->NewPass($sNewPassword) - ->LogoutAndDisconnect() - ; - - $bResult = true; - } - catch (\Exception $oException) - { - $bResult = false; - } - - return $bResult; - } -} \ No newline at end of file diff --git a/plugins/poppassd-change-password/LICENSE b/plugins/poppassd-change-password/LICENSE deleted file mode 100644 index 271342337d..0000000000 --- a/plugins/poppassd-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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 -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/poppassd-change-password/README b/plugins/poppassd-change-password/README deleted file mode 100644 index 6be76f7138..0000000000 --- a/plugins/poppassd-change-password/README +++ /dev/null @@ -1 +0,0 @@ -Plugin that adds functionality to change the email account password (POPPASSD). \ No newline at end of file diff --git a/plugins/poppassd-change-password/VERSION b/plugins/poppassd-change-password/VERSION deleted file mode 100644 index b123147e2a..0000000000 --- a/plugins/poppassd-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 \ No newline at end of file diff --git a/plugins/poppassd-change-password/index.php b/plugins/poppassd-change-password/index.php deleted file mode 100644 index cb90487f32..0000000000 --- a/plugins/poppassd-change-password/index.php +++ /dev/null @@ -1,52 +0,0 @@ -<?php - -class PoppassdChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - include_once __DIR__.'/ChangePasswordPoppassdDriver.php'; - - $oProvider = new ChangePasswordPoppassdDriver(); - - $oProvider - ->SetHost($this->Config()->Get('plugin', 'host', '')) - ->SetPort((int) $this->Config()->Get('plugin', 'port', 106)) - ->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))) - ->SetLogger($this->Manager()->Actions()->Logger()) - ; - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('host')->SetLabel('POPPASSD Host') - ->SetDefaultValue('127.0.0.1'), - \RainLoop\Plugins\Property::NewInstance('port')->SetLabel('POPPASSD Port') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) - ->SetDefaultValue(106), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} diff --git a/plugins/postfixadmin-change-password/ChangePasswordPostfixAdminDriver.php b/plugins/postfixadmin-change-password/ChangePasswordPostfixAdminDriver.php deleted file mode 100755 index 384c1c996e..0000000000 --- a/plugins/postfixadmin-change-password/ChangePasswordPostfixAdminDriver.php +++ /dev/null @@ -1,346 +0,0 @@ -<?php - -class ChangePasswordPostfixAdminDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sEngine = 'MySQL'; - - /** - * @var string - */ - private $sHost = '127.0.0.1'; - - /** - * @var int - */ - private $iPort = 3306; - - /** - * @var string - */ - private $sDatabase = 'postfixadmin'; - - /** - * @var string - */ - private $sTable = 'mailbox'; - - /** - * @var string - */ - private $sUsercol = 'username'; - - /** - * @var string - */ - private $sPasscol = 'password'; - - /** - * @var string - */ - private $sUser = 'postfixadmin'; - - /** - * @var string - */ - private $sPassword = ''; - - /** - * @var string - */ - private $sEncrypt = ''; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sEngine - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetEngine($sEngine) - { - $this->sEngine = $sEngine; - return $this; - } - - /** - * @param string $sHost - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetHost($sHost) - { - $this->sHost = $sHost; - return $this; - } - - /** - * @param int $iPort - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetPort($iPort) - { - $this->iPort = (int) $iPort; - return $this; - } - - /** - * @param string $sDatabase - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetDatabase($sDatabase) - { - $this->sDatabase = $sDatabase; - return $this; - } - - /** - * @param string $sTable - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetTable($sTable) - { - $this->sTable = $sTable; - return $this; - } - - /** - * @param string $sUsercol - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetUserColumn($sUsercol) - { - $this->sUsercol = $sUsercol; - return $this; - } - - /** - * @param string $sPasscol - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetPasswordColumn($sPasscol) - { - $this->sPasscol = $sPasscol; - return $this; - } - - /** - * @param string $sUser - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetUser($sUser) - { - $this->sUser = $sUser; - return $this; - } - - /** - * @param string $sPassword - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetPassword($sPassword) - { - $this->sPassword = $sPassword; - return $this; - } - - /** - * @param string $sEncrypt - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetEncrypt($sEncrypt) - { - $this->sEncrypt = $sEncrypt; - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \ChangePasswordPostfixAdminDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Model\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('Postfix: Try to change password for '.$oAccount->Email()); - } - - unset($sPrevPassword); - - $bResult = false; - - if (0 < \strlen($sNewPassword)) - { - try - { - $sDsn = ''; - switch($this->sEngine){ - case 'MySQL': - $sDsn = 'mysql:host='.$this->sHost.';port='.$this->iPort.';dbname='.$this->sDatabase; - break; - case 'PostgreSQL': - $sDsn = 'pgsql:host='.$this->sHost.';port='.$this->iPort.';dbname='.$this->sDatabase; - break; - default: - $sDsn = 'mysql:host='.$this->sHost.';port='.$this->iPort.';dbname='.$this->sDatabase; - break; - } - - - $oPdo = new \PDO($sDsn, $this->sUser, $this->sPassword); - $oPdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - - $sUpdatePassword = $this->cryptPassword($sNewPassword, $oPdo); - if (0 < \strlen($sUpdatePassword)) - { - $oStmt = $oPdo->prepare("UPDATE {$this->sTable} SET {$this->sPasscol} = ? WHERE {$this->sUsercol} = ?"); - $bResult = (bool) $oStmt->execute(array($sUpdatePassword, $oAccount->Email())); - } - else - { - if ($this->oLogger) - { - $this->oLogger->Write('Postfix: Encrypted password is empty', - \MailSo\Log\Enumerations\Type::ERROR); - } - } - - $oPdo = null; - } - catch (\Exception $oException) - { - if ($this->oLogger) - { - $this->oLogger->WriteException($oException); - } - } - } - - return $bResult; - } - - /** - * @param string $sPassword - * @param \PDO $oPdo - * - * @return string - */ - private function cryptPassword($sPassword, $oPdo) - { - $sResult = ''; - if (function_exists('random_bytes')) { - $sSalt = substr(base64_encode(random_bytes(32)), 0, 16); - } else { - $sSalt = substr(str_shuffle('./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'), 0, 16); - } - switch (strtolower($this->sEncrypt)) - { - default: - case 'plain': - case 'cleartext': - $sResult = '{PLAIN}' . $sPassword; - break; - - case 'md5crypt': - include_once __DIR__.'/md5crypt.php'; - $sResult = '{MD5-CRYPT}' . md5crypt($sPassword); - break; - - case 'md5': - $sResult = '{PLAIN-MD5}' . md5($sPassword); - break; - - case 'system': - $sResult = '{CRYPT}' . crypt($sPassword); - break; - - case 'sha256-crypt': - $sResult = '{SHA256-CRYPT}' . crypt($sPassword,'$5$'.$sSalt); - break; - - case 'sha512-crypt': - $sResult = '{SHA512-CRYPT}' . crypt($sPassword,'$6$'.$sSalt); - break; - - case 'argon2i': - $sResult = '{ARGON2I}' . password_hash($sPassword, PASSWORD_ARGON2I); - break; - - case 'mysql_encrypt': - if($this->sEngine == 'MySQL'){ - $oStmt = $oPdo->prepare('SELECT ENCRYPT(?) AS encpass'); - if ($oStmt->execute(array($sPassword))) - { - $aFetchResult = $oStmt->fetchAll(\PDO::FETCH_ASSOC); - if (\is_array($aFetchResult) && isset($aFetchResult[0]['encpass'])) - { - $sResult = $aFetchResult[0]['encpass']; - } - } - }else{ - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::CouldNotSaveNewPassword); - } - break; - } - - return $sResult; - } -} diff --git a/plugins/postfixadmin-change-password/LICENSE b/plugins/postfixadmin-change-password/LICENSE deleted file mode 100644 index c6cb63dbfe..0000000000 --- a/plugins/postfixadmin-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 RainLoop Team, @zaffkea - -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/postfixadmin-change-password/README b/plugins/postfixadmin-change-password/README deleted file mode 100644 index 342f4d4b5d..0000000000 --- a/plugins/postfixadmin-change-password/README +++ /dev/null @@ -1 +0,0 @@ -Plugin that adds functionality to change the email account password (PostfixAdmin). \ No newline at end of file diff --git a/plugins/postfixadmin-change-password/VERSION b/plugins/postfixadmin-change-password/VERSION deleted file mode 100644 index 7e32cd5698..0000000000 --- a/plugins/postfixadmin-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.3 diff --git a/plugins/postfixadmin-change-password/index.php b/plugins/postfixadmin-change-password/index.php deleted file mode 100755 index 24cfa061e4..0000000000 --- a/plugins/postfixadmin-change-password/index.php +++ /dev/null @@ -1,100 +0,0 @@ -<?php - -class PostfixadminChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @return string - */ - public function Supported() - { - if (!extension_loaded('pdo') || !class_exists('PDO')) - { - return 'The PHP extension PDO must be installed to use this plugin'; - } - - $aDrivers = \PDO::getAvailableDrivers(); - if (!is_array($aDrivers) || (!in_array('mysql', $aDrivers) && !in_array('pgsql', $aDrivers))) - { - return 'The PHP extension PDO (mysql or pgsql) must be installed to use this plugin'; - } - - return ''; - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - include_once __DIR__.'/ChangePasswordPostfixAdminDriver.php'; - - $oProvider = new ChangePasswordPostfixAdminDriver(); - - $oProvider - ->SetEngine($this->Config()->Get('plugin', 'engine','')) - ->SetHost($this->Config()->Get('plugin', 'host', '')) - ->SetPort((int) $this->Config()->Get('plugin', 'port', 3306)) - ->SetDatabase($this->Config()->Get('plugin', 'database', '')) - ->SetTable($this->Config()->Get('plugin', 'table', '')) - ->SetUserColumn($this->Config()->Get('plugin', 'usercol', '')) - ->SetPasswordColumn($this->Config()->Get('plugin', 'passcol', '')) - ->SetUser($this->Config()->Get('plugin', 'user', '')) - ->SetPassword($this->Config()->Get('plugin', 'password', '')) - ->SetEncrypt($this->Config()->Get('plugin', 'encrypt', '')) - ->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))) - ->SetLogger($this->Manager()->Actions()->Logger()) - ; - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('engine')->SetLabel('Engine') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) - ->SetDefaultValue(array('MySQL', 'PostgreSQL')) - ->SetDescription('Database Engine'), - \RainLoop\Plugins\Property::NewInstance('host')->SetLabel('Host') - ->SetDefaultValue('127.0.0.1'), - \RainLoop\Plugins\Property::NewInstance('port')->SetLabel('Port') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) - ->SetDefaultValue(3306), - \RainLoop\Plugins\Property::NewInstance('database')->SetLabel('Database') - ->SetDefaultValue('postfixadmin'), - \RainLoop\Plugins\Property::NewInstance('table')->SetLabel('table') - ->SetDefaultValue('mailbox'), - \RainLoop\Plugins\Property::NewInstance('usercol')->SetLabel('username column') - ->SetDefaultValue('username'), - \RainLoop\Plugins\Property::NewInstance('passcol')->SetLabel('password column') - ->SetDefaultValue('password'), - \RainLoop\Plugins\Property::NewInstance('user')->SetLabel('User') - ->SetDefaultValue('postfixadmin'), - \RainLoop\Plugins\Property::NewInstance('password')->SetLabel('Password') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('encrypt')->SetLabel('Encrypt') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) - ->SetDefaultValue(array('md5crypt', 'md5', 'system', 'cleartext', 'argon2i', 'mysql_encrypt', 'SHA256-CRYPT', 'SHA512-CRYPT')) - ->SetDescription('In what way do you want the passwords to be crypted ?'), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} diff --git a/plugins/postfixadmin-change-password/md5crypt.php b/plugins/postfixadmin-change-password/md5crypt.php deleted file mode 100644 index 13f878eca8..0000000000 --- a/plugins/postfixadmin-change-password/md5crypt.php +++ /dev/null @@ -1,139 +0,0 @@ -<?php - -// md5crypt -// Action: Creates MD5 encrypted password -// Call: md5crypt (string cleartextpassword) - -function md5crypt($pw, $salt = "", $magic = "") -{ - $MAGIC = "$1$"; - - if ($magic == "") - { - $magic = $MAGIC; - } - - if ($salt == "") - { - $salt = create_salt(); - } - - $slist = explode("$", $salt); - if (isset($slist[0]) && $slist[0] == "1") - { - $salt = $slist[1]; - } - - $salt = substr($salt, 0, 8); - $ctx = $pw.$magic.$salt; - $final = hex2bin(md5($pw.$salt.$pw)); - - for ($i = strlen($pw); $i > 0; $i -= 16) - { - if ($i > 16) - { - $ctx .= substr($final,0,16); - } - else - { - $ctx .= substr($final,0,$i); - } - } - - $i = strlen($pw); - - while ($i > 0) - { - if ($i & 1) - { - $ctx .= chr(0); - } - else - { - $ctx .= $pw[0]; - } - - $i = $i >> 1; - } - - $final = hex2bin(md5($ctx)); - - for ($i=0; $i<1000; $i++) - { - $ctx1 = ""; - if ($i & 1) - { - $ctx1 .= $pw; - } - else - { - $ctx1 .= substr($final,0,16); - } - if ($i % 3) - { - $ctx1 .= $salt; - } - if ($i % 7) - { - $ctx1 .= $pw; - } - if ($i & 1) - { - $ctx1 .= substr($final, 0, 16); - } - else - { - $ctx1 .= $pw; - } - - $final = hex2bin(md5($ctx1)); - } - - $passwd = ""; - $passwd .= to64(((ord($final[0]) << 16) | (ord($final[6]) << 8) | (ord($final[12]))), 4); - $passwd .= to64(((ord($final[1]) << 16) | (ord($final[7]) << 8) | (ord($final[13]))), 4); - $passwd .= to64(((ord($final[2]) << 16) | (ord($final[8]) << 8) | (ord($final[14]))), 4); - $passwd .= to64(((ord($final[3]) << 16) | (ord($final[9]) << 8) | (ord($final[15]))), 4); - $passwd .= to64(((ord($final[4]) << 16) | (ord($final[10]) << 8) | (ord($final[5]))), 4); - $passwd .= to64(ord($final[11]), 2); - - return $magic.$salt.'$'.$passwd; -} - -function create_salt() -{ - srand((double) microtime() * 1000000); - return substr(md5(rand(0,9999999)), 0, 8); -} - -// PHP around 5.3.8 includes hex2bin as native function - http://php.net/hex2bin -if (!function_exists('hex2bin')) -{ - function hex2bin($str) - { - $len = strlen($str); - $nstr = ""; - for ($i = 0; $i < $len; $i += 2) - { - $num = sscanf(substr($str, $i, 2), "%x"); - $nstr .= chr($num[0]); - } - - return $nstr; - } -} - -function to64($v, $n) -{ - $ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - $ret = ""; - - while (($n - 1) >= 0) - { - $n--; - $ret .= $ITOA64[$v & 0x3f]; - $v = $v >> 6; - } - - return $ret; -} \ No newline at end of file diff --git a/plugins/proxy-auth/LICENSE b/plugins/proxy-auth/LICENSE new file mode 100644 index 0000000000..172df18fd2 --- /dev/null +++ b/plugins/proxy-auth/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2023 Philipp Mundhenk + +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/proxy-auth/README.md b/plugins/proxy-auth/README.md new file mode 100644 index 0000000000..7a3004d702 --- /dev/null +++ b/plugins/proxy-auth/README.md @@ -0,0 +1,82 @@ +# SnappyMail Proxy Auth + +This plugin allows to authenticate a user through the remote user header, effectively allowing single-sign on. +This is achieved through "master user"-like functionality. + +## Example Configuration + +The exact setup depends on your mailserver, reverse proxy, authentication solution, etc. +The following example is for Traefik with Authelia and Dovecot as mailserver. + +### SnappyMail + +The following steps are require in SnappyMail: + +- To open SnappyMail through a reverse proxy server (with redirect of authentication system), make sure to enable the correct secfetch policies: ```mode=navigate,dest=document,site=cross-site,user=true;mode=navigate,dest=document,site=same-site,user=true``` in the admin panel -> Config -> Security -> secfetch_allow. +- Activate plugin in admin panel -> Extensions +- Configure the plugin with the required data: + - Master User Separator is dependent on Dovecot config (see below) + - Master User is dependent on Dovecot config (see below) + - Master User Password is dependent on Dovecot config (see below) + - Header Name is dependent on authentication solution. This is the header containing the name of currently logged in user. In case of Authelia, this is "Remote-User". + - Check Proxy: Since this plugin partially bypasses authentication, it is important to only allow this access from well-defined hosts. It is highly recommended to activate this option! + - When checking for reverse proxy, it is required to set the IP filter to either an IP address or a subnet. + - Automatic Login: Automatically logs in the user of user header is present (see below) + +This concludes the setup of SnappyMail. + +### Dovecot + +In Dovecot, you need to enable Master User. +Enable ```!include auth-master.conf.ext``` in /etc/dovecot/conf.d/10-auth.conf. +The file /etc/dovecot/conf.d/auth-master.conf.ext should contain: +``` +# Authentication for master users. Included from auth.conf. + +# By adding master=yes setting inside a passdb you make the passdb a list +# of "master users", who can log in as anyone else. +# <doc/wiki/Authentication.MasterUsers.txt> + +# Example master user passdb using passwd-file. You can use any passdb though. +passdb { + driver = passwd-file + master = yes + args = /etc/dovecot/master-users + + # Unless you're using PAM, you probably still want the destination user to + # be looked up from passdb that it really exists. pass=yes does that. + pass = yes +} +``` + +You then need to create a master user in /etc/dovecot/master-users: +``` +admin:PASSWORD::::::allow_nets=local,172.17.0.0/16 +``` +where the encrypted password ```PASSWORD``` can be created from a cleartext password with ```doveadm pw -s CRYPT```. +It should start with ```{CRYPT}```. +Username and password need to configured in the SnappyMail ProxyAuth plugin (see above). + +You likely also want to limit the access by an IP address filter, e.g., to ```local,172.17.0.0/16```, if you are running Postfix (```local```) and within a default Docker environment (```172.17.0.0/16```). +Otherwise, master user login (assuming password is known) is possible from every connectable system. +This is an unnecessary security risk. + +Additionally, you need to set the master user separator in /etc/dovecot/conf.d/10-auth.conf, e.g., ```auth_master_user_separator = *```. +The separator needs to be configured in the SnappyMail ProxyAuth plugin (see above). + +## Test + +Once configured correctly, you should be able to access SnappyMail through your reverse proxy at ```https://snappymail.tld/?ProxyAuth```. +If your reverse proxy provides the username in the configured header (e.g., Remote-User), you will automatically be logged in to your account. +If not, you will be redirected to the login page. + +## Automatic Login + +By default, automatic login is activated. +Behind the scenes, this checks for the existence of the configured user header (through ```/?UserHeaderSet```) and automatically redirects to ```https://snappymail.tld/?ProxyAuth```, trying to log in the user. +Note that due to this implementation, logout is impossible, as once logged out, the user will automatically be logged in again. +The user is always considered logged in, as authentication is handled through reverse proxy and authentication system. + +Auto login can be disabled in the plugin settings. +You can also change the logout link in admin panel -> Config -> custom_logout_link to the one of your authentication system, e.g., ```https://auth.yourdomain.com/logout```. +In this case, you can log out from your overall system via SnappyMail. diff --git a/plugins/proxy-auth/index.php b/plugins/proxy-auth/index.php new file mode 100644 index 0000000000..ad2fea0f45 --- /dev/null +++ b/plugins/proxy-auth/index.php @@ -0,0 +1,211 @@ +<?php + +class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Proxy Auth', + AUTHOR = 'Philipp', + URL = 'https://www.mundhenk.org/', + VERSION = '0.5', + RELEASE = '2024-09-20', + REQUIRED = '2.36.1', + CATEGORY = 'Login', + LICENSE = 'MIT', + DESCRIPTION = 'Uses HTTP Remote-User and (Dovecot) master user for login'; + + public function Init() : void + { + $this->addJs('js/auto-login.js'); + $this->addPartHook('ProxyAuth', 'ServiceProxyAuth'); + $this->addPartHook('UserHeaderSet', 'ServiceUserHeaderSet'); + $this->addHook('login.credentials', 'MapEmailAddress'); + } + + /* by https://gist.github.com/tott/7684443 */ + /** + * Check if a given ip is in a network + * @param string $ip IP to check in IPV4 format eg. 127.0.0.1 + * @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed + * @return boolean true if the ip is in this range / false if not. + */ + private function ip_in_range( $ip, $range ) { + if ( strpos( $range, '/' ) == false ) { + $range .= '/32'; + } + // $range is in IP/CIDR format eg 127.0.0.1/24 + list( $range, $netmask ) = explode( '/', $range, 2 ); + $range_decimal = ip2long( $range ); + $ip_decimal = ip2long( $ip ); + $wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1; + $netmask_decimal = ~ $wildcard_decimal; + return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) ); + } + + public function MapEmailAddress(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser) + { + $oActions = \RainLoop\Api::Actions(); + $oLogger = $oActions->Logger(); + $sPrefix = "ProxyAuth"; + $sLevel = LOG_DEBUG; + $sMsg = "sEmail= " . $sEmail; + $oLogger->Write($sMsg, $sLevel, $sPrefix); + + $sMasterUser = \trim($this->Config()->getDecrypted('plugin', 'master_user', '')); + $sMasterSeparator = \trim($this->Config()->getDecrypted('plugin', 'master_separator', '')); + + /* remove superuser from email for proper UI */ + if (static::$login) { + $sEmail = str_replace($sMasterUser, "", $sEmail); + $sEmail = str_replace($sMasterSeparator, "", $sEmail); + } + } + + private static bool $login = false; + public function ServiceProxyAuth() : bool + { + $oActions = \RainLoop\Api::Actions(); + + $oException = null; + $oAccount = null; + + $oLogger = $oActions->Logger(); + $sLevel = LOG_DEBUG; + $sPrefix = "ProxyAuth"; + + $sMasterUser = \trim($this->Config()->getDecrypted('plugin', 'master_user', '')); + $sMasterSeparator = \trim($this->Config()->getDecrypted('plugin', 'master_separator', '')); + $sHeaderName = \trim($this->Config()->getDecrypted('plugin', 'header_name', '')); + + $sRemoteUser = $this->Manager()->Actions()->Http()->GetHeader($sHeaderName); + $sMsg = "Remote User: " . $sRemoteUser; + $oLogger->Write($sMsg, $sLevel, $sPrefix); + + $sProxyIP = $this->Config()->getDecrypted('plugin', 'proxy_ip', ''); + $sMsg = "ProxyIP: " . $sProxyIP; + $oLogger->Write($sMsg, $sLevel, $sPrefix); + + $sProxyCheck = $this->Config()->getDecrypted('plugin', 'proxy_check', ''); + $sClientIPs = $this->Manager()->Actions()->Http()->GetClientIP(true); + + /* make sure that remote user is only set by authorized proxy to avoid security risks */ + if ($sProxyCheck) { + $sProxyRequest = false; + $sMsg = "checking client IPs: " . $sClientIPs; + $oLogger->Write($sMsg, $sLevel, $sPrefix); + + $sClientIPs = explode(", ", $sClientIPs); + if (is_array($sClientIPs)) { + foreach ($sClientIPs as &$sIP) { + $sMsg = "checking client IP: " . $sIP; + $oLogger->Write($sMsg, $sLevel, $sPrefix); + + if ($this->ip_in_range($sIP, $sProxyIP)) { + $sProxyRequest = true; + } + } + } else { + $sMsg = "checking client IP: " . $sClientIPs; + $oLogger->Write($sMsg, $sLevel, $sPrefix); + + if ($this->ip_in_range($sClientIPs, $sProxyIP)) { + $sProxyRequest = true; + } + } + } else { + $sProxyRequest = true; + } + + if ($sProxyRequest) { + /* create master user login from remote user header and settings */ + $sEmail = $sRemoteUser . $sMasterSeparator . $sMasterUser; + $sPassword = new \SnappyMail\SensitiveString(\trim($this->Config()->getDecrypted('plugin', 'master_password', ''))); + + try + { + static::$login = true; + $oAccount = $oActions->LoginProcess($sEmail, $sPassword); + } + catch (\Throwable $oException) + { + $oLogger = $oActions->Logger(); + $oLogger && $oLogger->WriteException($oException); + } + + \MailSo\Base\Http::Location('./'); + return true; + } + + \MailSo\Base\Http::Location('./'); + return true; + } + + public function ServiceUserHeaderSet() : bool + { + $oActions = \RainLoop\Api::Actions(); + + $oLogger = $oActions->Logger(); + $sLevel = LOG_DEBUG; + $sPrefix = "ProxyAuth"; + + $sHeaderName = \trim($this->Config()->getDecrypted('plugin', 'header_name', '')); + + $sRemoteUser = $this->Manager()->Actions()->Http()->GetHeader($sHeaderName); + $sMsg = "Remote User: " . $sRemoteUser; + $oLogger->Write($sMsg, $sLevel, $sPrefix); + + if (strlen($sRemoteUser) > 0) { + \MailSo\Base\Http::StatusHeader('200'); + } else { + \MailSo\Base\Http::StatusHeader('401'); + } + return true; + } + + protected function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('master_separator') + ->SetLabel('Master User separator') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Sets the master user separator (format: <username><separator><master username>)') + ->SetDefaultValue('*') + ->SetEncrypted(), + \RainLoop\Plugins\Property::NewInstance('master_user') + ->SetLabel('Master User') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Username of master user') + ->SetDefaultValue('admin') + ->SetEncrypted(), + \RainLoop\Plugins\Property::NewInstance('master_password') + ->SetLabel('Master Password') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Password for master user') + ->SetDefaultValue('adminpassword') + ->SetEncrypted(), + \RainLoop\Plugins\Property::NewInstance('header_name') + ->SetLabel('Header Name') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Name of header containing username') + ->SetDefaultValue('Remote-User') + ->SetEncrypted(), + \RainLoop\Plugins\Property::NewInstance('check_proxy') + ->SetLabel('Check Proxy') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDescription('Activates check if proxy is connecting') + ->SetDefaultValue(true) + ->SetEncrypted(), + \RainLoop\Plugins\Property::NewInstance('proxy_ip') + ->SetLabel('Proxy IPNet') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('IP or Subnet of proxy, auth header will only be accepted from this address') + ->SetDefaultValue('10.1.0.0/24') + ->SetEncrypted(), + \RainLoop\Plugins\Property::NewInstance('auto_login') + ->SetAllowedInJs(true) + ->SetLabel('Activate automatic login') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDescription('Activates automatic login, if User Header is set (note: Use custom_logout_link to enable logout, see plugin README)') + ->SetDefaultValue(true) + ); + } +} diff --git a/plugins/proxy-auth/js/auto-login.js b/plugins/proxy-auth/js/auto-login.js new file mode 100644 index 0000000000..ba5e0e6173 --- /dev/null +++ b/plugins/proxy-auth/js/auto-login.js @@ -0,0 +1,32 @@ +(rl => { + + rl && addEventListener('rl-view-model', e => { + const id = e.detail.viewModelTemplateID; + if (e.detail && ('Login' === id)) { + let + auto_login = window.rl.pluginSettingsGet('proxy-auth', 'auto_login'); + ; + + const + ForwardProxyAuth = () => { + if (auto_login) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "?UserHeaderSet", true); + + xhr.onreadystatechange = function () { + if (xhr.readyState == 4 && xhr.status == 200) { + window.location.href = "?ProxyAuth"; + } + }; + + xhr.send(); + } + }; + + window.ForwardProxyAuth = ForwardProxyAuth; + + ForwardProxyAuth(); + } + }); +})(window.rl); + diff --git a/plugins/proxyauth-login-example/LICENSE b/plugins/proxyauth-login-example/LICENSE deleted file mode 100644 index 4a4ca8d815..0000000000 --- a/plugins/proxyauth-login-example/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 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 -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/proxyauth-login-example/VERSION b/plugins/proxyauth-login-example/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/proxyauth-login-example/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/proxyauth-login-example/index.php b/plugins/proxyauth-login-example/index.php deleted file mode 100644 index 747d219d13..0000000000 --- a/plugins/proxyauth-login-example/index.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php - -class ProxyauthLoginExamplePlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('event.login-post-login-provide', 'EventLoginPostLoginProvide'); - } - - /** - * @param string $sLogin - * @param string $sPassword - */ - public function isValidAccount($sLogin, $sPassword) - { - return !empty($sLogin) && !empty($sPassword); - } - - /** - * @param \RainLoop\Model\Account $oAccount - */ - public function EventLoginPostLoginProvide(&$oAccount) - { - if ($oAccount instanceof \RainLoop\Model\Account) - { - // Verify logic - $bValid = $this->isValidAccount($oAccount->Login(), $oAccount->Password()); - - /** - * $oAccount->Email(); // Email (It is not a IMAP login) - * $oAccount->Login(); // IMAP login - * $oAccount->Password(); // IMAP password - * $oAccount->DomainIncHost(); // IMAP host - * - * @see \RainLoo\Model\Account for more - */ - - if (!$bValid) // if verify failed - { - // throw a Auth Error Exception - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AuthError); - } - else // Or setup your proxyauth admin account credentials - { - $oAccount->SetProxyAuthUser('admin@domain.com'); - $oAccount->SetProxyAuthPassword('secret-admin-password'); - } - } - } -} diff --git a/plugins/recaptcha/README b/plugins/recaptcha/README deleted file mode 100644 index ee29dbafcf..0000000000 --- a/plugins/recaptcha/README +++ /dev/null @@ -1,3 +0,0 @@ -A CAPTCHA (v2) is a program that can generate and grade tests that humans can pass but current computer programs cannot. -For example, humans can read distorted text as the one shown below, but current computer programs can't. -More info at http://www.google.com/recaptcha \ No newline at end of file diff --git a/plugins/recaptcha/VERSION b/plugins/recaptcha/VERSION deleted file mode 100644 index 616187889b..0000000000 --- a/plugins/recaptcha/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.2 \ No newline at end of file diff --git a/plugins/recaptcha/index.php b/plugins/recaptcha/index.php index bffda14b1c..9245bd5127 100644 --- a/plugins/recaptcha/index.php +++ b/plugins/recaptcha/index.php @@ -2,23 +2,32 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin { + const + NAME = 'reCaptcha', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '2.16', + RELEASE = '2024-03-12', + REQUIRED = '2.35.3', + CATEGORY = 'General', + LICENSE = 'MIT', + DESCRIPTION = 'A CAPTCHA (v2) is a program that can generate and grade tests that humans can pass but current computer programs cannot. For example, humans can read distorted text as the one shown below, but current computer programs can\'t. More info at https://developers.google.com/recaptcha'; + /** * @return void */ - public function Init() + public function Init() : void { $this->UseLangs(true); $this->addJs('js/recaptcha.js'); - $this->addHook('ajax.action-pre-call', 'AjaxActionPreCall'); - $this->addHook('filter.ajax-response', 'FilterAjaxResponse'); + $this->addHook('json.before-login', 'BeforeLogin'); + $this->addHook('json.after-login', 'AfterLogin'); + $this->addHook('main.content-security-policy', 'ContentSecurityPolicy'); } - /** - * @return array - */ - public function configMapping() + protected function configMapping() : array { return array( \RainLoop\Plugins\Property::NewInstance('public_key')->SetLabel('Site key') @@ -32,7 +41,7 @@ public function configMapping() ->SetDefaultValue(array('light', 'dark')), \RainLoop\Plugins\Property::NewInstance('error_limit')->SetLabel('Limit') ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) - ->SetDefaultValue(array(0, 1, 2, 3, 4, 5)) + ->SetDefaultValue(array('0', 1, 2, 3, 4, 5)) ->SetDescription('') ); } @@ -51,13 +60,11 @@ private function getCaptchaCacherKey() private function getLimit() { $iConfigLimit = $this->Config()->Get('plugin', 'error_limit', 0); - if (0 < $iConfigLimit) - { + if (0 < $iConfigLimit) { $oCacher = $this->Manager()->Actions()->Cacher(); $sLimit = $oCacher && $oCacher->IsInited() ? $oCacher->Get($this->getCaptchaCacherKey()) : '0'; - if (0 < \strlen($sLimit) && \is_numeric($sLimit)) - { + if (\is_numeric($sLimit)) { $iConfigLimit -= (int) $sLimit; } } @@ -68,84 +75,75 @@ private function getLimit() /** * @return void */ - public function FilterAppDataPluginSection($bAdmin, $bAuth, &$aData) + public function FilterAppDataPluginSection(bool $bAdmin, bool $bAuth, array &$aConfig) : void { - if (!$bAdmin && !$bAuth && \is_array($aData)) - { - $aData['show_captcha_on_login'] = 1 > $this->getLimit(); + if (!$bAdmin && !$bAuth) { + $aConfig['show_captcha_on_login'] = 1 > $this->getLimit();; } } - /** - * @param string $sAction - */ - public function AjaxActionPreCall($sAction) + public function BeforeLogin() { - if ('Login' === $sAction && 0 >= $this->getLimit()) - { + if (0 >= $this->getLimit()) { $bResult = false; - $sResult = $this->Manager()->Actions()->Http()->SendPostRequest( - 'https://www.google.com/recaptcha/api/siteverify', - array( - 'secret' => $this->Config()->Get('plugin', 'private_key', ''), - 'response' => $this->Manager()->Actions()->GetActionParam('RecaptchaResponse', '') - ) - ); - - if ($sResult) - { - $aResp = @\json_decode($sResult, true); - if (\is_array($aResp) && isset($aResp['success']) && $aResp['success']) - { + $HTTP = \SnappyMail\HTTP\Request::factory(); + $oResponse = $HTTP->doRequest('POST', 'https://www.recaptcha.net/recaptcha/api/siteverify', array( + 'secret' => $this->Config()->Get('plugin', 'private_key', ''), + 'response' => $this->Manager()->Actions()->GetActionParam('RecaptchaResponse', '') + )); + + if ($oResponse) { + $aResp = \json_decode($oResponse->body, true); + if (\is_array($aResp) && isset($aResp['success']) && $aResp['success']) { $bResult = true; } } - if (!$bResult) - { + if (!$bResult) { $this->Manager()->Actions()->Logger()->Write('RecaptchaResponse:'.$sResult); - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::CaptchaError); + throw new \RainLoop\Exceptions\ClientException(105); } } } /** * @param string $sAction - * @param array $aResponseItem + * @param array $aResponse */ - public function FilterAjaxResponse($sAction, &$aResponseItem) + public function AfterLogin(array &$aResponse) { - if ('Login' === $sAction && $aResponseItem && isset($aResponseItem['Result'])) - { + if (isset($aResponse['Result'])) { $oCacher = $this->Manager()->Actions()->Cacher(); $iConfigLimit = (int) $this->Config()->Get('plugin', 'error_limit', 0); $sKey = $this->getCaptchaCacherKey(); - if (0 < $iConfigLimit && $oCacher && $oCacher->IsInited()) - { - if (false === $aResponseItem['Result']) - { + if (0 < $iConfigLimit && $oCacher && $oCacher->IsInited()) { + if (false === $aResponse['Result']) { $iLimit = 0; $sLimut = $oCacher->Get($sKey); - if (0 < \strlen($sLimut) && \is_numeric($sLimut)) - { + if (\is_numeric($sLimut)) { $iLimit = (int) $sLimut; } $oCacher->Set($sKey, ++$iLimit); - if ($iConfigLimit <= $iLimit) - { - $aResponseItem['Captcha'] = true; + if ($iConfigLimit <= $iLimit) { + $aResponse['Captcha'] = true; } - } - else - { + } else { $oCacher->Delete($sKey); } } } } + + public function ContentSecurityPolicy(\SnappyMail\HTTP\CSP $CSP) + { + $CSP->add('script-src', 'https://www.gstatic.com/recaptcha/'); + $CSP->add('script-src', 'https://www.recaptcha.net/recaptcha/'); + $CSP->add('frame-src', 'https://www.recaptcha.net/recaptcha/'); + } + } diff --git a/plugins/recaptcha/js/recaptcha.js b/plugins/recaptcha/js/recaptcha.js index 49d20bd603..591d265983 100644 --- a/plugins/recaptcha/js/recaptcha.js +++ b/plugins/recaptcha/js/recaptcha.js @@ -1,95 +1,68 @@ -(function ($, window) { - $(function () { +(rl => { - var - nId = null, - bStarted = false - ; + rl && addEventListener('rl-view-model', e => { + const id = e.detail.viewModelTemplateID; + if (e.detail && ('AdminLogin' === id || 'Login' === id) + && rl.pluginSettingsGet('recaptcha', 'show_captcha_on_login')) { + let + nId = null, + script; - function ShowRecaptcha() - { - if (window.grecaptcha && window.rl) - { - if (null === nId) - { - var - oEl = null, - oLink = $('.plugin-mark-Login-BottomControlGroup') - ; + const + mode = 'Login' === id ? 'user' : 'admin', - if (oLink && oLink[0]) - { - oEl = $('<div class="controls"></div>'); + doc = document, - $(oLink[0]).after(oEl); + container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'), - nId = window.grecaptcha.render(oEl[0], { - 'sitekey': window.rl.pluginSettingsGet('recaptcha', 'public_key'), - 'theme': window.rl.pluginSettingsGet('recaptcha', 'theme') + ShowRecaptcha = () => { + if (window.grecaptcha && null === nId && container) { + const oEl = doc.createElement('div'); + oEl.className = 'controls'; + + container.after(oEl); + + nId = window.grecaptcha.render(oEl, { + 'sitekey': rl.pluginSettingsGet('recaptcha', 'public_key'), + 'theme': rl.pluginSettingsGet('recaptcha', 'theme') }); } - } - } - } - - window.__globalShowRecaptcha = ShowRecaptcha; + }, - function StartRecaptcha() - { - if (!window.grecaptcha && window.rl) - { - $.getScript('https://www.google.com/recaptcha/api.js?onload=__globalShowRecaptcha&render=explicit&hl=' + window.rl.settingsGet('Language')); - } - else - { - ShowRecaptcha(); - } - } + StartRecaptcha = () => { + if (window.grecaptcha) { + ShowRecaptcha(); + } else if (!script) { + script = doc.createElement('script'); +// script.onload = ShowRecaptcha; + script.src = 'https://www.recaptcha.net/recaptcha/api.js?onload=ShowRecaptcha&render=explicit&hl=' + doc.documentElement.lang; + doc.head.append(script); + } + }; - if (window.rl) - { - window.rl.addHook('user-login-submit', function (fSubmitResult) { - if (null !== nId && !window.grecaptcha.getResponse(nId)) - { - fSubmitResult(105); - } - }); + window.ShowRecaptcha = ShowRecaptcha; - window.rl.addHook('view-model-on-show', function (sName, oViewModel) { - if (!bStarted && oViewModel && - ('View:RainLoop:Login' === sName || 'View/App/Login' === sName || 'LoginViewModel' === sName || 'LoginAppView' === sName) && - window.rl.pluginSettingsGet('recaptcha', 'show_captcha_on_login')) - { - bStarted = true; - StartRecaptcha(); - } - }); + StartRecaptcha(); - window.rl.addHook('ajax-default-request', function (sAction, oParameters) { - if ('Login' === sAction && oParameters && null !== nId && window.grecaptcha) - { - oParameters['RecaptchaResponse'] = window.grecaptcha.getResponse(nId); + addEventListener(`sm-${mode}-login`, e => { + if (null !== nId && window.grecaptcha) { + e.detail.set('RecaptchaResponse', window.grecaptcha.getResponse(nId)); + } else { + e.preventDefault(); } }); - window.rl.addHook('ajax-default-response', function (sAction, oData, sType) { - if ('Login' === sAction) - { - if (!oData || 'success' !== sType || !oData['Result']) - { - if (null !== nId && window.grecaptcha) - { - window.grecaptcha.reset(nId); - } - else if (oData && oData['Captcha']) - { - StartRecaptcha(); - } + addEventListener(`sm-${mode}-login-response`, e => { + if (e.detail.error) { + if (null !== nId && window.grecaptcha) { + window.grecaptcha.reset(nId); + } else if (e.detail.data && e.detail.data.Captcha) { + StartRecaptcha(); } } }); } }); -}($, window)); \ No newline at end of file +})(window.rl); diff --git a/plugins/rest-change-password/LICENSE b/plugins/rest-change-password/LICENSE deleted file mode 100644 index 271342337d..0000000000 --- a/plugins/rest-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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 -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/rest-change-password/README b/plugins/rest-change-password/README deleted file mode 100644 index f045f42e7b..0000000000 --- a/plugins/rest-change-password/README +++ /dev/null @@ -1 +0,0 @@ -Plugin that adds functionality to change the email account password (Generic REST). \ No newline at end of file diff --git a/plugins/rest-change-password/RestChangePasswordDriver.php b/plugins/rest-change-password/RestChangePasswordDriver.php deleted file mode 100644 index cc7a2f41cc..0000000000 --- a/plugins/rest-change-password/RestChangePasswordDriver.php +++ /dev/null @@ -1,174 +0,0 @@ -<?php - -class RestChangePasswordDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sUrl = ''; - - /** - * @var string - */ - private $sKey = ''; - - /** - * @var string - */ - private $sFieldEmail = ''; - - /** - * @var string - */ - private $sFieldOldpassword = ''; - - /** - * @var string - */ - private $sFieldNewpassword = ''; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sUrl - * @param string $sKey - * - * @return \RestChangePasswordDriver - */ - public function SetConfig($sUrl, $sKey) - { - $this->sUrl = $sUrl; - $this->sKey = $sKey; - - return $this; - } - - /** - * @param string $sFieldEmail - * @param string $sFieldOldpassword - * @param string $sFieldNewpassword - * - * @return \RestChangePasswordDriver - */ - public function SetFieldNames($sFieldEmail, $sFieldOldpassword, $sFieldNewpassword) - { - $this->sFieldEmail = $sFieldEmail; - $this->sFieldOldpassword = $sFieldOldpassword; - $this->sFieldNewpassword = $sFieldNewpassword; - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \RestChangePasswordDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \RestChangePasswordDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('Rest: Try to change password for '.$oAccount->Email()); - } - - $bResult = false; - if (!empty($this->sUrl) && $oAccount) - { - $sEmail = \trim(\strtolower($oAccount->Email())); - - $sUrl = $this->sUrl; - # Adding the REST Api key to the url, try to use always https - if (!empty($this->sKey)) - { - $sUrl = str_replace('://', '://'+$this->sKey+"@", $this->sUrl); - } - - $iCode = 0; - $oHttp = \MailSo\Base\Http::SingletonInstance(); - - if ($this->oLogger) - { - $this->oLogger->Write('Rest[Api Request]:'.$sUrl); - } - - $mResult = $oHttp->SendPostRequest($sUrl, - array( - $this->sFieldEmail => $sEmail, - $this->sFieldOldpassword => $sPrevPassword, - $this->sFieldNewpassword => $sNewPassword, - ), 'MailSo Http User Agent (v1)', $iCode, $this->oLogger); - - if (false !== $mResult && 200 === $iCode) - { - $aRes = null; - @\parse_str($mResult, $aRes); - if (is_array($aRes) && (!isset($aRes['error']) || (int) $aRes['error'] !== 1)) - { - $bResult = true; - } - else - { - if ($this->oLogger) - { - $this->oLogger->Write('Rest[Error]: Response: '.$mResult); - } - } - } - else - { - if ($this->oLogger) - { - $this->oLogger->Write('Rest[Error]: Empty Response: Code:'.$iCode); - } - } - } - - return $bResult; - } -} diff --git a/plugins/rest-change-password/VERSION b/plugins/rest-change-password/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/rest-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/rest-change-password/index.php b/plugins/rest-change-password/index.php deleted file mode 100644 index 7fd973bfa4..0000000000 --- a/plugins/rest-change-password/index.php +++ /dev/null @@ -1,78 +0,0 @@ -<?php - -class RestChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - $sUrl = \trim($this->Config()->Get('plugin', 'rest_url', '')); - $sKey = \trim($this->Config()->Get('plugin', 'rest_key', '')); - - $sFieldEmail = \trim($this->Config()->Get('plugin', 'rest_field_email', '')); - $sFieldOldpassword = \trim($this->Config()->Get('plugin', 'rest_field_oldpassword', '')); - $sFieldNewpassword = \trim($this->Config()->Get('plugin', 'rest_field_newpassword', '')); - - if (!empty($sUrl)) - { - include_once __DIR__.'/RestChangePasswordDriver.php'; - - $oProvider = new RestChangePasswordDriver(); - $oProvider->SetLogger($this->Manager()->Actions()->Logger()); - $oProvider->SetConfig($sUrl, $sKey); - $oProvider->SetFieldNames($sFieldEmail, $sFieldOldpassword, $sFieldNewpassword); - $oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))); - } - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('rest_url') - ->SetLabel('REST API Url') - ->SetDefaultValue('') - ->SetDescription('Ex: http://localhost:8080/api/change_password or https://domain.com/api/user/passsword_update'), - \RainLoop\Plugins\Property::NewInstance('rest_key') - ->SetLabel('REST API key') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) - ->SetDescription('REST API Key for authentication, if you have "user" and "passsword" enter it as "user:password"') - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('rest_field_email') - ->SetLabel('Field "email" name') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) - ->SetDescription('Enter the name of the REST field name for email') - ->SetDefaultValue('email'), - \RainLoop\Plugins\Property::NewInstance('rest_field_oldpassword') - ->SetLabel('Field "oldpassword" name') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) - ->SetDescription('Enter the name of the REST field name for oldpassword') - ->SetDefaultValue('oldpassword'), - \RainLoop\Plugins\Property::NewInstance('rest_field_newpassword') - ->SetLabel('Field "newpassword" name') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) - ->SetDescription('Enter the name of the REST field name for newpassword') - ->SetDefaultValue('newpassword'), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} diff --git a/plugins/search-filters/index.php b/plugins/search-filters/index.php new file mode 100644 index 0000000000..090f311414 --- /dev/null +++ b/plugins/search-filters/index.php @@ -0,0 +1,213 @@ +<?php + +class SearchFiltersPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + public const + NAME = 'Search Filters', + AUTHOR = 'AbdoBnHesham', + URL = 'https://github.com/the-djmaze/snappymail/pull/1673', + VERSION = '0.2', + RELEASE = '2024-06-28', + REQUIRED = '2.36.3', + CATEGORY = 'General', + LICENSE = 'MIT', + DESCRIPTION = 'Add filters to search queries'; + + public function Init(): void + { + $this->UseLangs(true); + + $this->addHook('imap.after-login', 'ApplyFilters'); + + $this->addTemplate('templates/STabSearchFilters.html'); + + $this->addTemplate('templates/PopupsSearchFilters.html'); + $this->addTemplate('templates/PopupsSTabAdvancedSearch.html'); + $this->addJs('js/SearchFilters.js'); + + $this->addJsonHook('SGetFilters', 'GetFilters'); + $this->addJsonHook('SAddEditFilter', 'AddEditFilter'); + $this->addJsonHook('SUpdateSearchQ', 'UpdateSearchQ'); + $this->addJsonHook('SDeleteFilter', 'DeleteFilter'); + } + + public function ApplyFilters( + \RainLoop\Model\Account $oAccount, + \MailSo\Imap\ImapClient $oImapClient, + bool $bSuccess, + \MailSo\Imap\Settings $oSettings + ) { + if (!$bSuccess) { + return; + } + + $aSettings = $this->getUserSettings(); + if (empty($aSettings['SFilters'])) { + $aSettings['SFilters'] = []; + $this->saveUserSettings($aSettings); + return; + } + + $Filters = $aSettings['SFilters']; + + foreach ($Filters as $filter) { + $this->Manager()->logWrite(json_encode([ + 'filter' => $filter, + ]), LOG_WARNING); + + $folder = 'INBOX'; + $searchQ = $filter['searchQ']; + $uids = $this->searchMessages($oImapClient, $searchQ, "INBOX"); + + //Mark as read/seen + if ($filter['fSeen']) { + foreach ($uids as $uid) { + $oRange = new MailSo\Imap\SequenceSet([$uid]); + $this->Manager()->Actions()->MailClient()->MessageSetFlag( + $folder, + $oRange, + MailSo\Imap\Enumerations\MessageFlag::SEEN + ); + } + } + + //Flag/Star message + if ($filter['fFlag']) { + foreach ($uids as $uid) { + $oRange = new MailSo\Imap\SequenceSet([$uid]); + $this->Manager()->Actions()->MailClient()->MessageSetFlag( + $folder, + $oRange, + MailSo\Imap\Enumerations\MessageFlag::FLAGGED + ); + } + } + + // Move to folder + if ($filter['fFolder']) { + $folder = $filter['fFolder']; + foreach ($uids as $uid) { + $oRange = new MailSo\Imap\SequenceSet([$uid]); + $oImapClient->MessageMove("INBOX", $folder, $oRange); + } + } + } + } + + private function searchMessages( + \MailSo\Imap\ImapClient $imapClient, + string $search, + string $folder = "INBOX" + ): array { + $oParams = new \MailSo\Mail\MessageListParams(); + $oParams->sSearch = $search; + $oParams->sFolderName = $folder; + + $bUseCache = false; + $oSearchCriterias = \MailSo\Imap\SearchCriterias::fromString( + $imapClient, + $folder, + $search, + true, + $bUseCache + ); + + $imapClient->FolderSelect($folder); + return $imapClient->MessageSearch($oSearchCriterias, true); + } + + public function GetFilters() + { + $aSettings = $this->getUserSettings(); + $Filters = $aSettings['SFilters'] ?? []; + + $Search = $this->jsonParam('SSearchQ'); + if (!$Search) { + return $this->jsonResponse(__FUNCTION__, ['SFilters' => $Filters]); + } + + $Filter = null; + foreach ($aSettings['SFilters'] as $filter) { + if ($filter['searchQ'] == $Search) { + $Filter = $filter; + } + } + + return $this->jsonResponse(__FUNCTION__, ['SFilter' => $Filter]); + } + + public function AddEditFilter() + { + $SFilter = $this->jsonParam('SFilter'); + $newFilter = [ + 'searchQ' => $SFilter['searchQ'], + 'priority' => $SFilter['priority'] ?? 1, + 'fFolder' => $SFilter['fFolder'], + 'fSeen' => $SFilter['fSeen'], + 'fFlag' => $SFilter['fFlag'], + ]; + + $aSettings = $this->getUserSettings(); + $aSettings['SFilters'] = $aSettings['SFilters'] ?? []; + + $foundIndex = null; + foreach ($aSettings['SFilters'] as $index => $filter) { + if ($filter['searchQ'] == $SFilter['searchQ']) { + if ($filter['priority'] != $SFilter['priority']) { + array_splice($aSettings['SFilters'], $index, 1); + } else { + $foundIndex = $index; + } + } + } + + if ($foundIndex === null) { + $insertIndex = 0; + foreach ($aSettings['SFilters'] as $index => $filter) + if ($filter['priority'] >= $newFilter['priority']) + $insertIndex = $index + 1; + else + break; + + array_splice($aSettings['SFilters'], $insertIndex, 0, [$newFilter]); + } else { + $aSettings['SFilters'][$foundIndex] = $newFilter; + } + + return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings($aSettings)); + } + + public function UpdateSearchQ() + { + $SFilter = $this->jsonParam('SFilter'); + + $aSettings = $this->getUserSettings(); + $aSettings['SFilters'] = $aSettings['SFilters'] ?? []; + + foreach ($aSettings['SFilters'] as $index => $filter) { + if ($filter['searchQ'] == $SFilter['oldSearchQ']) { + $filter['searchQ'] = $SFilter['searchQ']; + $aSettings['SFilters'][$index] = $filter; + break; + } + } + + return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings($aSettings)); + } + + public function DeleteFilter() + { + $Search = $this->jsonParam('SSearchQ'); + + $aSettings = $this->getUserSettings(); + $aSettings['SFilters'] = $aSettings['SFilters'] ?? []; + + foreach ($aSettings['SFilters'] as $index => $filter) { + if ($filter['searchQ'] == $Search) { + array_splice($aSettings['SFilters'], $index, 1); + } + } + + return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings($aSettings)); + } +} diff --git a/plugins/search-filters/js/SearchFilters.js b/plugins/search-filters/js/SearchFilters.js new file mode 100644 index 0000000000..d2d5385f2f --- /dev/null +++ b/plugins/search-filters/js/SearchFilters.js @@ -0,0 +1,340 @@ +((rl) => { + const Folders = ko.computed(() => { + const + aResult = [{ + id: -1, + name: '' + }], + sDeepPrefix = '\u00A0\u00A0\u00A0', + showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/, + foldersWalk = folders => { + folders.forEach(oItem => { + if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) { + aResult.push({ + id: oItem.fullName, + name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName() + }); + } + + if (oItem.subFolders.length) { + foldersWalk(oItem.subFolders()); + } + }); + }; + foldersWalk(rl.app.folderList()); + return aResult; + }); + + const Priorities = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const searchQ = ko.observable(''), + priority = ko.observable(1), + oldSearchQ = ko.observable(''), + fFolder = ko.observable(''), + fSeen = ko.observable(false), + fFlag = ko.observable(false), + ioFilters = ko.observableArray([]); + + const i18n = (val) => rl.i18n(`SFILTERS/${val}`); + + const ServerActions = { + GetFilters(loading) { + ioFilters([]); + rl.pluginRemoteRequest((iError, oData) => { + if (iError) return console.error(iError); + + oData.Result.SFilters.forEach((f) => ioFilters.push(ko.observable(f))); + + if (loading) loading(false); + }, 'SGetFilters'); + }, + GetFilter() { + rl.pluginRemoteRequest( + (iError, oData) => { + if (iError) return console.error(iError); + + const filter = oData.Result.SFilter || {}; + priority(filter.priority || 1); + fFolder(filter.fFolder); + fSeen(filter.fSeen); + fFlag(filter.fFlag); + }, + 'SGetFilters', + { + SSearchQ: searchQ() + } + ); + }, + AddOrEditFilter() { + rl.pluginRemoteRequest( + (iError, oData) => { + if (!iError) this.GetFilters(); + }, + 'SAddEditFilter', + { + SFilter: { + searchQ: oldSearchQ() || searchQ(), + priority: priority(), + fFolder: fFolder(), + fSeen: fSeen(), + fFlag: fFlag() + } + } + ); + }, + UpdateSearchQ(newSearchQ) { + rl.pluginRemoteRequest( + (iError, oData) => { + if (!iError) this.GetFilters(); + }, + 'SUpdateSearchQ', + { + SFilter: { + oldSearchQ: oldSearchQ(), + searchQ: newSearchQ + } + } + ); + }, + RemoveFilter(searchQToRemove) { + rl.pluginRemoteRequest( + (iError, oData) => { + if (iError) return console.error(iError, oData); + const index = ioFilters().findIndex((f) => f.searchQ() === searchQToRemove); + ioFilters.splice(index, 1); + }, + 'SDeleteFilter', + { + SSearchQ: searchQToRemove + } + ); + } + }; + + addEventListener('rl-view-model', (event) => { + const advS = event.detail; + if (advS.viewModelTemplateID == 'PopupsAdvancedSearch') { + const button = document.createElement('button'); + button.setAttribute('class', 'btn'); + button.setAttribute('data-i18n', 'SFILTERS/CREATE_FILTER'); + button.addEventListener('click', function () { + searchQ(advS.buildSearchString()); + if (searchQ()) SearchFiltersPopupView.showModal(); + }); + + const footer = advS.querySelector('footer'); + footer.style.display = 'flex'; + footer.style.justifyContent = 'space-between'; + + footer.prepend(button); + } + }); + + class SearchFiltersSettingsTab { + constructor() { + this.folders = Folders; + this.Priorities = Priorities; + this.ioFilters = ioFilters; + + this.loading = ko.observable(false); + this.saving = ko.observable(false); + + this.i18n = i18n; + + this.savingOrLoading = ko.computed(() => { + return this.loading() || this.saving(); + }); + } + + edit(filter) { + oldSearchQ(filter.searchQ); + searchQ(filter.searchQ); + priority(filter.priority || 1); + fFolder(filter.fFolder); + fSeen(filter.fSeen); + fFlag(filter.fFlag); + AdvancedSearchPopupView.showModal(); + } + + remove(filter) { + ServerActions.RemoveFilter(filter.searchQ); + } + + onShow() { + this.clear(); + this.loading(true); + ServerActions.GetFilters(this.loading); + } + + clear() { + this.ioFilters([]); + this.loading(false); + this.saving(false); + } + } + + rl.addSettingsViewModel(SearchFiltersSettingsTab, 'STabSearchFilters', 'Search Filters', 'searchfilters'); + + class SearchFiltersPopupView extends rl.pluginPopupView { + constructor() { + super('SearchFilters'); + + this.folders = Folders; + this.Priorities = Priorities; + + this.priority = priority; + this.fFolder = fFolder; + this.fSeen = fSeen; + this.fFlag = fFlag; + + this.usefFolder = ko.observable(false); + this.fFolder.subscribe((v) => this.usefFolder(!!v)); + } + + submitForm() { + if (!this.usefFolder()) fFolder(''); + ServerActions.AddOrEditFilter(); + this.close(); + } + + beforeShow() { + if (!oldSearchQ()) { + ServerActions.GetFilter(); + } else { + this.usefFolder(!!fFolder()); + } + } + } + + class AdvancedSearchPopupView extends rl.pluginPopupView { + constructor() { + super('STabAdvancedSearch'); + + this.addObservables({ + from: '', + to: '', + subject: '', + text: '', + repliedValue: -1, + selectedDateValue: -1, + selectedTreeValue: '', + + hasAttachment: false, + starred: false, + unseen: false + }); + + this.addComputables({ + repliedOptions: () => { + return [ + { id: -1, name: '' }, + { id: 1, name: rl.i18n('GLOBAL/YES') }, + { id: 0, name: rl.i18n('GLOBAL/NO') } + ]; + }, + + selectedDates: () => { + let prefix = 'SEARCH/DATE_'; + return [ + { id: -1, name: rl.i18n(prefix + 'ALL') }, + { id: 3, name: rl.i18n(prefix + '3_DAYS') }, + { id: 7, name: rl.i18n(prefix + '7_DAYS') }, + { id: 30, name: rl.i18n(prefix + 'MONTH') }, + { id: 90, name: rl.i18n(prefix + '3_MONTHS') }, + { id: 180, name: rl.i18n(prefix + '6_MONTHS') }, + { id: 365, name: rl.i18n(prefix + 'YEAR') } + ]; + }, + + selectedTree: () => { + let prefix = 'SEARCH/SUBFOLDERS_'; + return [ + { id: '', name: rl.i18n(prefix + 'NONE') }, + { id: 'subtree-one', name: rl.i18n(prefix + 'SUBTREE_ONE') }, + { id: 'subtree', name: rl.i18n(prefix + 'SUBTREE') } + ]; + } + }); + } + + submitForm() { + const newSearchQ = this.buildSearchString(); + if (newSearchQ != oldSearchQ()) ServerActions.UpdateSearchQ(newSearchQ); + this.close(); + } + + editFilters() { + SearchFiltersPopupView.showModal(); + } + + buildSearchString() { + const self = this, + data = new FormData(), + append = (key, value) => value.length && data.append(key, value); + + append('from', self.from().trim()); + append('to', self.to().trim()); + append('subject', self.subject().trim()); + append('text', self.text().trim()); + append('in', self.selectedTreeValue()); + if (-1 < self.selectedDateValue()) { + let d = new Date(); + d.setDate(d.getDate() - self.selectedDateValue()); + append('since', d.toISOString().split('T')[0]); + } + + let result = decodeURIComponent(new URLSearchParams(data).toString()); + + if (self.hasAttachment()) { + result += '&attachment'; + } + if (self.unseen()) { + result += '&unseen'; + } + if (self.starred()) { + result += '&flagged'; + } + if (1 == self.repliedValue()) { + result += '&answered'; + } + if (0 == self.repliedValue()) { + result += '&unanswered'; + } + + return result.replace(/^&+/, ''); + } + + onShow() { + const pString = (value) => (null != value ? '' + value : ''); + + const self = this, + params = new URLSearchParams('?' + searchQ()); + self.from(pString(params.get('from'))); + self.to(pString(params.get('to'))); + self.subject(pString(params.get('subject'))); + self.text(pString(params.get('text'))); + self.selectedTreeValue(pString(params.get('in'))); + self.selectedDateValue(-1); + self.hasAttachment(params.has('attachment')); + self.starred(params.has('flagged')); + self.unseen(params.has('unseen')); + if (params.has('answered')) { + self.repliedValue(1); + } else if (params.has('unanswered')) { + self.repliedValue(0); + } + } + + clear() { + oldSearchQ(''); + searchQ(''); + fFolder(''); + priority(1); + fSeen(false); + fFlag(false); + } + + onHide() { + this.clear(); + } + } +})(window.rl); diff --git a/plugins/search-filters/langs/en.json b/plugins/search-filters/langs/en.json new file mode 100644 index 0000000000..6fb25bbf31 --- /dev/null +++ b/plugins/search-filters/langs/en.json @@ -0,0 +1,21 @@ +{ + "SFILTERS": { + "WHEN_MESSAGE_MATCH_SEARCH_CRITERIA": "When a message is an exact match for your search criteria:", + "FILTERS": "Filters", + "FILTER": "Filter", + "MOVE_TO_FOLDER": "Move to folder: ", + "CREATE_FILTER": "Create filter", + "EDIT_FILTERS": "Edit filters", + "MATCHES": "Matches: ", + "DO_THIS": "Do this: ", + "SAVE": "Save", + "EDIT": "Edit", + "REMOVE": "Remove", + "NO_FILTERS": "You haven't added any filters yet.", + "THE_FOLLOWING_FILTERS_ARE_APPLIED": "The following filters are applied to all incoming mail: ", + "MARK_AS_READ": "Mark as read", + "STAR_IT": "Star it", + "PRIORITY_LABLE": "Priority points, Higher means it'll apply first", + "PRIORITY_POINTS": "Priority points" + } +} \ No newline at end of file diff --git a/plugins/search-filters/templates/PopupsSTabAdvancedSearch.html b/plugins/search-filters/templates/PopupsSTabAdvancedSearch.html new file mode 100644 index 0000000000..d1a260551a --- /dev/null +++ b/plugins/search-filters/templates/PopupsSTabAdvancedSearch.html @@ -0,0 +1,82 @@ +<header> + <a href="#" class="close" data-bind="click: close">×</a> + <h3 data-i18n="SEARCH/TITLE_ADV"></h3> +</header> +<form id="advsearchform" class="modal-body form-horizontal" action="#/" autocomplete="off" spellcheck="false" + data-bind="submit: submitForm"> + <div> + <div class="control-group"> + <label data-i18n="GLOBAL/FROM"></label> + <input type="text" autofocus="" autocomplete="off" autocorrect="off" autocapitalize="off" + data-bind="value: from"> + </div> + <div class="control-group"> + <label data-i18n="GLOBAL/TO"></label> + <input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" data-bind="value: to"> + </div> + <div class="control-group"> + <label data-i18n="GLOBAL/SUBJECT"></label> + <input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" data-bind="value: subject"> + </div> + <div class="control-group"> + <label data-i18n="SEARCH/TEXT"></label> + <input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" data-bind="value: text"> + </div> + </div> + <div> + <div class="control-group"> + <label data-i18n="SEARCH/DATE"></label> + <div data-bind="component: { + name: 'Select', + params: { + options: selectedDates, + value: selectedDateValue, + optionsText: 'name', + optionsValue: 'id' + } + }"></div> + </div> + <div class="control-group"> + <label data-i18n="SEARCH/REPLIED"></label> + <div data-bind="component: { + name: 'Select', + params: { + options: repliedOptions, + value: repliedValue, + optionsText: 'name', + optionsValue: 'id' + } + }"></div> + </div> + <div class="control-group"> + <label></label> + <div> + <div data-bind="component: { + name: 'Checkbox', + params: { + label: 'SEARCH/UNSEEN', + value: unseen + } + }"></div> + <div data-bind="component: { + name: 'Checkbox', + params: { + label: 'SEARCH/FLAGGED', + value: starred + } + }"></div> + <div data-bind="component: { + name: 'Checkbox', + params: { + label: 'SEARCH/HAS_ATTACHMENT', + value: hasAttachment + } + }"></div> + </div> + </div> + </div> +</form> +<footer style="display: flex; justify-content: space-between;"> + <button class="btn" data-i18n="SFILTERS/EDIT_FILTERS" data-bind="click: editFilters"></button> + <button form="advsearchform" class="btn" data-icon="✔" data-i18n="SFILTERS/SAVE"></button> +</footer> \ 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 0000000000..c85ca8914f --- /dev/null +++ b/plugins/search-filters/templates/PopupsSearchFilters.html @@ -0,0 +1,54 @@ +<header> + <a href="#" class="close" data-bind="click: close">×</a> + <h3 data-i18n="SFILTERS/WHEN_MESSAGE_MATCH_SEARCH_CRITERIA"></h3> +</header> +<form id="search-filters-form" class="modal-body form-horizontal" action="#/" autocomplete="off" spellcheck="false" + data-bind="submit: submitForm"> + <div> + <div class="control-group"> + <div data-bind="component: { + name: 'Checkbox', + params: { + label: 'SFILTERS/MOVE_TO_FOLDER', + value: usefFolder + } + }"></div> + <select data-bind=" + options: folders, + value: fFolder, + optionsText: 'name', + optionsValue: 'id', + enable: usefFolder + "></select> + </div> + <div class="control-group"> + <div data-bind="component: { + name: 'Checkbox', + params: { + label: 'SFILTERS/MARK_AS_READ', + value: fSeen + } + }"></div> + </div> + <div class="control-group"> + <div data-bind="component: { + name: 'Checkbox', + params: { + label: 'SFILTERS/STAR_IT', + value: fFlag + } + }"></div> + </div> + <div class="control-group"> + <label for="priority" data-i18n="SFILTERS/PRIORITY_LABLE"></label> + <select data-bind=" + options: Priorities, + value: priority, + "></select> + </div> + </div> +</form> +<footer> + <button form="search-filters-form" class="btn buttonSearchFilters" data-icon="✔" + data-i18n="SFILTERS/CREATE_FILTER"></button> +</footer> diff --git a/plugins/search-filters/templates/STabSearchFilters.html b/plugins/search-filters/templates/STabSearchFilters.html new file mode 100644 index 0000000000..d006da345b --- /dev/null +++ b/plugins/search-filters/templates/STabSearchFilters.html @@ -0,0 +1,39 @@ +<div> + <div class="form-horizontal"> + <div class="legend"> + <span data-i18n="SFILTERS/THE_FOLLOWING_FILTERS_ARE_APPLIED"></span> +     + <i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: loading"></i> + </div> + + <div data-bind="visible: ioFilters().length == 0"> + <span data-i18n="SFILTERS/NO_FILTERS"> </span> + </div> + + <!-- ko foreach: ioFilters --> + <label class="control-label"> + <span data-bind="text: $root.i18n('MATCHES')"> </span> + <span data-bind="text: searchQ"></span> +   + <span data-bind="text: $root.i18n('PRIORITY_POINTS')"></span> + : +   + <span data-bind="text: priority"></span> + <br> + <span data-bind="text: $root.i18n('DO_THIS')"></span> + <span data-bind="visible: fFolder, text: $root.i18n('MOVE_TO_FOLDER')"></span> + <span data-bind="visible: fFolder, text: fFolder"></span> +   + <span data-bind="visible: fSeen, text: $root.i18n('MARK_AS_READ')"></span> +   + <span data-bind="visible: fFlag, text: $root.i18n('STAR_IT')"></span> + </label> + <div class="controls"> + <button class="btn" data-bind="click: $root.edit, text: $root.i18n('EDIT')"></button> + <button class="btn" data-bind="click: $root.remove, text: $root.i18n('REMOVE')"></button> + </div> + <br> + <!-- /ko --> + + </div> +</div> \ No newline at end of file diff --git a/plugins/send-save-in/LICENSE b/plugins/send-save-in/LICENSE new file mode 100644 index 0000000000..f709b02e27 --- /dev/null +++ b/plugins/send-save-in/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +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 +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/send-save-in/index.php b/plugins/send-save-in/index.php new file mode 100644 index 0000000000..2289c18f3a --- /dev/null +++ b/plugins/send-save-in/index.php @@ -0,0 +1,23 @@ +<?php + +class SendSaveInPlugin extends \RainLoop\Plugins\AbstractPlugin +{ +// use \MailSo\Log\Inherit; + + const + NAME = 'Send Save In', + AUTHOR = 'SnappyMail', + URL = 'https://snappymail.eu/', + VERSION = '0.1', + RELEASE = '2024-10-08', + REQUIRED = '2.38.1', + CATEGORY = 'General', + LICENSE = 'MIT', + DESCRIPTION = 'When composing a message, select the save folder'; + + public function Init() : void + { +// $this->UseLangs(true); // start use langs folder + $this->addJs('savein.js'); + } +} diff --git a/plugins/send-save-in/savein.js b/plugins/send-save-in/savein.js new file mode 100644 index 0000000000..b95e188922 --- /dev/null +++ b/plugins/send-save-in/savein.js @@ -0,0 +1,75 @@ +(rl => { + const templateId = 'PopupsCompose', + folderListOptionsBuilder = () => { + const + aResult = [{ + id: '', + name: '', + system: false, + disabled: false + }], + sDeepPrefix = '\u00A0\u00A0\u00A0', + showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/, + + foldersWalk = folders => { + folders.forEach(oItem => { + if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) { + aResult.push({ + id: oItem.fullName, + name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(), + system: false, + disabled: !oItem.selectable() + }); + } + + if (oItem.subFolders.length) { + foldersWalk(oItem.subFolders()); + } + }); + }; + + + // FolderUserStore.folderList() + foldersWalk(rl.app.folderList() || []); + + return aResult; + }; + + let oldSentFolderFn; + + addEventListener('rl-view-model.create', e => { + if (templateId === e.detail.viewModelTemplateID) { + const view = e.detail; // ComposePopupView + + view.sentFolderValue = ko.observable(''); + view.sentFolderSelectList = ko.computed(folderListOptionsBuilder, {'pure':true}); + view.defaultOptionsAfterRender = (domItem, item) => + item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled); + + oldSentFolderFn = view.sentFolder.bind(view); + view.sentFolder = () => view.sentFolderValue() || oldSentFolderFn(); + + document.getElementById(templateId).content.querySelector('.b-header tbody').append(Element.fromHTML(` + <tr> + <td>Store in</td> + <td> + <select class="span3" data-bind="options: sentFolderSelectList, value: sentFolderValue, + optionsText: 'name', optionsValue: 'id', optionsAfterRender: defaultOptionsAfterRender"></select> + (When send, store a copy of the message in the selected folder) + </td> + </tr>`)); + + view.currentIdentity.subscribe(()=>{ + view.sentFolderValue(oldSentFolderFn()); + }); + } + }); + + addEventListener('rl-vm-visible', e => { + if (templateId === e.detail.viewModelTemplateID) { + const view = e.detail; // ComposePopupView + view.sentFolderValue(oldSentFolderFn()); + } + }); + +})(window.rl); diff --git a/plugins/_depricated/convert-headers-styles/LICENSE b/plugins/set-remote-addr/LICENSE similarity index 100% rename from plugins/_depricated/convert-headers-styles/LICENSE rename to plugins/set-remote-addr/LICENSE diff --git a/plugins/set-remote-addr/index.php b/plugins/set-remote-addr/index.php new file mode 100644 index 0000000000..dbc10f8a6c --- /dev/null +++ b/plugins/set-remote-addr/index.php @@ -0,0 +1,23 @@ +<?php +/** + * Can be used for proxies, like Nginx with: + * proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + * It's an alternative to application.ini http_client_ip_check_proxy + */ + +class SetRemoteAddrPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Set REMOTE_ADDR', + VERSION = '2.0', + DESCRIPTION = 'Sets the $_SERVER[\'REMOTE_ADDR\'] value from HTTP_CLIENT_IP/HTTP_X_FORWARDED_FOR'; + + public function Init() : void + { + if (!empty($_SERVER['HTTP_CLIENT_IP'])) { + $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CLIENT_IP']; + } else if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR']; + } + } +} diff --git a/plugins/smtp-use-from-adr-account/README b/plugins/smtp-use-from-adr-account/README new file mode 100644 index 0000000000..e4a1410076 --- /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 0000000000..cea9eab3b0 --- /dev/null +++ b/plugins/smtp-use-from-adr-account/index.php @@ -0,0 +1,115 @@ +<?php + +class SmtpUseFromAdrAccountPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + + const + NAME = 'Use From-Address-Account for smtp', + AUTHOR = 'attike', + URL = 'https://github.com/attike', + VERSION = '1.1', + RELEASE = '2024-03-12', + REQUIRED = '2.35.3', + CATEGORY = 'Filters', + DESCRIPTION = 'Set smpt-config and -credentials based on selected from-address-account'; + + public $aFromAccount = array(); + + public function Init() : void + { + $this->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/snowfall-on-login-screen/README b/plugins/snowfall-on-login-screen/README deleted file mode 100644 index 86af64815a..0000000000 --- a/plugins/snowfall-on-login-screen/README +++ /dev/null @@ -1 +0,0 @@ -Add snowfall to your login screen :) \ No newline at end of file diff --git a/plugins/snowfall-on-login-screen/VERSION b/plugins/snowfall-on-login-screen/VERSION deleted file mode 100644 index b123147e2a..0000000000 --- a/plugins/snowfall-on-login-screen/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 \ No newline at end of file diff --git a/plugins/snowfall-on-login-screen/index.php b/plugins/snowfall-on-login-screen/index.php index 71eaf9f508..b3ccb1d41b 100644 --- a/plugins/snowfall-on-login-screen/index.php +++ b/plugins/snowfall-on-login-screen/index.php @@ -2,7 +2,13 @@ class SnowfallOnLoginScreenPlugin extends \RainLoop\Plugins\AbstractPlugin { - public function Init() + const + NAME = 'Snowfall on login screen', + VERSION = '2.1', + CATEGORY = 'Fun', + DESCRIPTION = 'Snowfall on login screen (just for fun).'; + + public function Init() : void { $this->addJs('js/snowfall.js'); $this->addJs('js/include.js'); diff --git a/plugins/snowfall-on-login-screen/js/include.js b/plugins/snowfall-on-login-screen/js/include.js index 7598b98204..e8adf55058 100644 --- a/plugins/snowfall-on-login-screen/js/include.js +++ b/plugins/snowfall-on-login-screen/js/include.js @@ -1,18 +1,17 @@ -$(function () { - if (window.snowFall && window.rl && !window.rl.settingsGet('Auth')) +if (!/iphone|ipod|ipad|android/i.test(navigator.userAgent)) +{ + if (window.snowFall && window.rl) { - var - sUserAgent = (navigator.userAgent || '').toLowerCase(), - bIsiOSDevice = -1 < sUserAgent.indexOf('iphone') || -1 < sUserAgent.indexOf('ipod') || -1 < sUserAgent.indexOf('ipad'), - bIsAndroidDevice = -1 < sUserAgent.indexOf('android') - ; - - if (!bIsiOSDevice && !bIsAndroidDevice) - { - window.snowFall.snow(document.getElementsByTagName('body'), { - shadow: true, round: true, minSize: 2, maxSize: 5 - }); - } + let body = document.body; + addEventListener('sm-show-screen', e => { + if ('login' == e.detail) { + window.snowFall.snow(body, { + shadow: true, round: true, minSize: 2, maxSize: 5 + }); + } else if (body.snow) { + body.snow.clear(); + } + }); } -}); \ No newline at end of file +} diff --git a/plugins/snowfall-on-login-screen/js/snowfall.js b/plugins/snowfall-on-login-screen/js/snowfall.js index a60c80abfb..671b44e509 100644 --- a/plugins/snowfall-on-login-screen/js/snowfall.js +++ b/plugins/snowfall-on-login-screen/js/snowfall.js @@ -1,275 +1,260 @@ -/* Snowfall pure js - ==================================================================== - LICENSE - ==================================================================== - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - ==================================================================== - - 1.0 - Wanted to rewrite my snow plugin to use pure JS so you werent necessarily tied to using a framework. - Does not include a selector engine or anything, just pass elements to it using standard JS selectors. - - Does not clear snow currently. Collection portion removed just for ease of testing will add back in next version - - Theres a few ways to call the snow you could do it the following way by directly passing the selector, - - snowFall.snow(document.getElementsByTagName("body"), {options}); - - or you could save the selector results to a variable, and then call it - - var elements = document.getElementsByClassName('yourclass'); - snowFall.snow(elements, {options}); - - Options are all the same as the plugin except clear, and collection - - values for snow options are - - flakeCount, - flakeColor, - flakeIndex, - minSize, - maxSize, - minSpeed, - maxSpeed, - round, true or false, makes the snowflakes rounded if the browser supports it. - shadow true or false, gives the snowflakes a shadow if the browser supports it. - -*/ - -// Paul Irish requestAnimationFrame polyfill -(function(window) { - var lastTime = 0; - var vendors = ['webkit', 'moz']; - for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { - window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; - window.cancelAnimationFrame = - window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; - } - - if (!window.requestAnimationFrame) - window.requestAnimationFrame = function(callback, element) { - var currTime = new Date().getTime(); - var timeToCall = window.Math.max(0, 16 - (currTime - lastTime)); - var id = window.setTimeout(function() { callback(currTime + timeToCall); }, - timeToCall); - lastTime = currTime + timeToCall; - return id; - }; - - if (!window.cancelAnimationFrame) - window.cancelAnimationFrame = function(id) { - clearTimeout(id); - }; -}(window)); - -var snowFall = (function(){ - function jSnow(){ - // local methods - var defaults = { - flakeCount : 35, - flakeColor : '#ffffff', - flakeIndex: 999999, - minSize : 1, - maxSize : 2, - minSpeed : 1, - maxSpeed : 5, - round : false, - shadow : false, - collection : false, - image : false, - collectionHeight : 40 - }, - element = {}, - flakes = [], - flakeId = 0, - elHeight = 0, - elWidth = 0, - elTop = 0, - elLeft = 0, - widthOffset = 0, - snowTimeout = 0, - // For extending the default object with properties - extend = function(obj, extObj){ - for(var i in extObj){ - if(obj.hasOwnProperty(i)){ - obj[i] = extObj[i]; - } - } - }, - // random between range - random = function random(min, max){ - return window.Math.round(min + window.Math.random()*(max-min)); - }, - // Set multiple styles at once. - setStyle = function(element, props) - { - for (var property in props){ - element.style[property] = props[property] + ((property === 'width' || property === 'height') ? 'px' : ''); - } - }, - // snowflake - flake = function(_x, _y, _size, _speed, _id) - { - // Flake properties - this.id = _id; - this.x = _x + elLeft; - this.y = _y + elTop; - this.size = _size; - this.speed = _speed; - this.step = 0; - this.stepSize = random(1,10) / 100; - - if(defaults.collection){ - this.target = defaults.collection[random(0,defaults.collection.length-1)]; - } - - var flakeObj = null; - - if(defaults.image){ - flakeObj = new Image(); - flakeObj.src = defaults.image; - }else{ - flakeObj = window.document.createElement("div"); - setStyle(flakeObj, {'background' : defaults.flakeColor}); - } - - flakeObj.className = 'snowfall-flakes'; - flakeObj.setAttribute('id','flake-' + this.id); - setStyle(flakeObj, {'width' : this.size, 'height' : this.size, 'position' : 'absolute', 'top' : this.y, 'left' : this.x, 'fontSize' : 0, 'zIndex' : defaults.flakeIndex}); - - // This adds the style to make the snowflakes round via border radius property - if(defaults.round){ - setStyle(flakeObj,{'-moz-border-radius' : ~~(defaults.maxSize) + 'px', '-webkit-border-radius' : ~~(defaults.maxSize) + 'px', 'borderRadius' : ~~(defaults.maxSize) + 'px'}); - } - - // This adds shadows just below the snowflake so they pop a bit on lighter colored web pages - if(defaults.shadow){ - setStyle(flakeObj,{'-moz-box-shadow' : '1px 1px 1px #555', '-webkit-box-shadow' : '1px 1px 1px #555', 'boxShadow' : '1px 1px 1px #555'}); - } - - window.document.body.appendChild(flakeObj); - - this.element = flakeObj; - - // Update function, used to update the snow flakes, and checks current snowflake against bounds - this.update = function(){ - this.y += this.speed; - - if(this.y > (elTop + elHeight) - (this.size + 6)){ - this.reset(); - } - - this.element.style.top = this.y + 'px'; - this.element.style.left = ~~this.x + 'px'; - - this.step += this.stepSize; - this.x += window.Math.cos(this.step); - - if(this.x > (elLeft + elWidth) - widthOffset || this.x < widthOffset){ - this.reset(); - } - }; - - // Resets the snowflake once it reaches one of the bounds set - this.reset = function(){ - this.y = elTop; - this.x = elLeft + random(widthOffset, elWidth - widthOffset); - this.stepSize = random(1,10) / 100; - this.size = random((defaults.minSize * 100), (defaults.maxSize * 100)) / 100; - this.speed = random(defaults.minSpeed, defaults.maxSpeed); - }; - }, - // this controls flow of the updating snow - animateSnow = function(){ - for(var i = 0; i < flakes.length; i += 1){ - flakes[i].update(); - } - snowTimeout = requestAnimationFrame(function(){animateSnow();}); - }; - return{ - snow : function(_element, _options){ - extend(defaults, _options); - - //init the element vars - element = _element; - elHeight = element.clientHeight, - elWidth = element.offsetWidth; - elTop = element.offsetTop; - elLeft = element.offsetLeft; - - element.snow = this; - - // if this is the body the offset is a little different - if(element.tagName.toLowerCase() === 'body'){ - widthOffset = 25; - } - - // Bind the window resize event so we can get the innerHeight again - window.onresize = function(){ - elHeight = element.clientHeight; - elWidth = element.offsetWidth; - elTop = element.offsetTop; - elLeft = element.offsetLeft; - }; - - // initialize the flakes - for(var i = 0; i < defaults.flakeCount; i+=1){ - flakeId = flakes.length; - flakes.push(new flake(random(widthOffset,elWidth - widthOffset), random(0, elHeight), random((defaults.minSize * 100), (defaults.maxSize * 100)) / 100, random(defaults.minSpeed, defaults.maxSpeed), flakeId)); - } - // start the snow - animateSnow(); - }, - clear : function(){ - var flakeChildren = null; - - if(!element.getElementsByClassName){ - flakeChildren = element.querySelectorAll('.snowfall-flakes'); - }else{ - flakeChildren = element.getElementsByClassName('snowfall-flakes'); - } - - var flakeChilLen = flakeChildren.length; - while(flakeChilLen--){ - element.removeChild(flakeChildren[flakeChilLen]); - } - - flakes = []; - cancelAnimationFrame(snowTimeout); - } - }; - }; - return{ - snow : function(elements, options){ - if(typeof(options) === 'string'){ - if(elements.length > 0){ - for(var i = 0; i < elements.length; i++){ - if(elements[i].snow){ - elements[i].snow.clear(); - } - } - }else{ - elements.snow.clear(); - } - }else{ - if(elements.length > 0){ - for(var i = 0; i < elements.length; i++){ - new jSnow().snow(elements[i], options); - } - }else{ - new jSnow().snow(elements, options); - } - } - } - }; -})(); \ No newline at end of file +/* Snowfall pure js + ==================================================================== + LICENSE + ==================================================================== + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ==================================================================== + + 1.0 + Wanted to rewrite my snow plugin to use pure JS so you werent necessarily tied to using a framework. + Does not include a selector engine or anything, just pass elements to it using standard JS selectors. + + Does not clear snow currently. Collection portion removed just for ease of testing will add back in next version + + Theres a few ways to call the snow you could do it the following way by directly passing the selector, + + snowFall.snow(document.getElementsByTagName("body"), {options}); + + or you could save the selector results to a variable, and then call it + + var elements = document.getElementsByClassName('yourclass'); + snowFall.snow(elements, {options}); + + Options are all the same as the plugin except clear, and collection + + values for snow options are + + flakeCount, + flakeColor, + flakeIndex, + minSize, + maxSize, + minSpeed, + maxSpeed, + round, true or false, makes the snowflakes rounded if the browser supports it. + shadow true or false, gives the snowflakes a shadow if the browser supports it. + +*/ + +// Paul Irish requestAnimationFrame polyfill +(window => { + var lastTime = 0; + var vendors = ['webkit', 'moz']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = + window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = callback => { + var currTime = new Date().getTime(); + var timeToCall = window.Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(() => callback(currTime + timeToCall), timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; +})(window); + +var snowFall = (function(){ + function jSnow(){ + // local methods + var defaults = { + flakeCount : 35, + flakeColor : '#ffffff', + flakeIndex: 999999, + minSize : 1, + maxSize : 2, + minSpeed : 1, + maxSpeed : 5, + round : false, + shadow : false, + collection : false, + image : false, + collectionHeight : 40 + }, + element = {}, + flakes = [], + flakeId = 0, + elHeight = 0, + elWidth = 0, + elTop = 0, + elLeft = 0, + widthOffset = 0, + snowTimeout = 0, + // For extending the default object with properties + extend = function(obj, extObj){ + for(var i in extObj){ + if(obj.hasOwnProperty(i)){ + obj[i] = extObj[i]; + } + } + }, + // random between range + random = function random(min, max){ + return window.Math.round(min + window.Math.random()*(max-min)); + }, + // Set multiple styles at once. + setStyle = function(element, props) + { + for (var property in props){ + element.style[property] = props[property] + ((property === 'width' || property === 'height') ? 'px' : ''); + } + }, + // snowflake + flake = function(_x, _y, _size, _speed, _id) + { + // Flake properties + this.id = _id; + this.x = _x + elLeft; + this.y = _y + elTop; + this.size = _size; + this.speed = _speed; + this.step = 0; + this.stepSize = random(1,10) / 100; + + if(defaults.collection){ + this.target = defaults.collection[random(0,defaults.collection.length-1)]; + } + + var flakeObj = null; + + if(defaults.image){ + flakeObj = new Image(); + flakeObj.src = defaults.image; + }else{ + flakeObj = window.document.createElement("div"); + setStyle(flakeObj, {'background' : defaults.flakeColor}); + } + + flakeObj.className = 'snowfall-flakes'; + flakeObj.setAttribute('id','flake-' + this.id); + setStyle(flakeObj, {'width' : this.size, 'height' : this.size, 'position' : 'absolute', 'top' : this.y, 'left' : this.x, 'fontSize' : 0, 'zIndex' : defaults.flakeIndex}); + + // This adds the style to make the snowflakes round via border radius property + if(defaults.round){ + setStyle(flakeObj,{'-moz-border-radius' : ~~(defaults.maxSize) + 'px', '-webkit-border-radius' : ~~(defaults.maxSize) + 'px', 'borderRadius' : ~~(defaults.maxSize) + 'px'}); + } + + // This adds shadows just below the snowflake so they pop a bit on lighter colored web pages + if(defaults.shadow){ + setStyle(flakeObj,{'-moz-box-shadow' : '1px 1px 1px #555', '-webkit-box-shadow' : '1px 1px 1px #555', 'boxShadow' : '1px 1px 1px #555'}); + } + + window.document.body.appendChild(flakeObj); + + this.element = flakeObj; + + // Update function, used to update the snow flakes, and checks current snowflake against bounds + this.update = function(){ + this.y += this.speed; + + if(this.y > (elTop + elHeight) - (this.size + 6)){ + this.reset(); + } + + this.element.style.top = this.y + 'px'; + this.element.style.left = ~~this.x + 'px'; + + this.step += this.stepSize; + this.x += window.Math.cos(this.step); + + if(this.x > (elLeft + elWidth) - widthOffset || this.x < widthOffset){ + this.reset(); + } + }; + + // Resets the snowflake once it reaches one of the bounds set + this.reset = function(){ + this.y = elTop; + this.x = elLeft + random(widthOffset, elWidth - widthOffset); + this.stepSize = random(1,10) / 100; + this.size = random((defaults.minSize * 100), (defaults.maxSize * 100)) / 100; + this.speed = random(defaults.minSpeed, defaults.maxSpeed); + }; + }, + // this controls flow of the updating snow + animateSnow = function(){ + for(var i = 0; i < flakes.length; i += 1){ + flakes[i].update(); + } + snowTimeout = requestAnimationFrame(function(){animateSnow();}); + }; + return{ + snow : function(_element, _options){ + extend(defaults, _options); + + //init the element vars + element = _element; + elHeight = element.clientHeight, + elWidth = element.offsetWidth; + elTop = element.offsetTop; + elLeft = element.offsetLeft; + + element.snow = this; + + // if this is the body the offset is a little different + if(element.tagName.toLowerCase() === 'body'){ + widthOffset = 25; + } + + // Bind the window resize event so we can get the innerHeight again + window.onresize = function(){ + elHeight = element.clientHeight; + elWidth = element.offsetWidth; + elTop = element.offsetTop; + elLeft = element.offsetLeft; + }; + + // initialize the flakes + for(var i = 0; i < defaults.flakeCount; i+=1){ + flakeId = flakes.length; + flakes.push(new flake(random(widthOffset,elWidth - widthOffset), random(0, elHeight), random((defaults.minSize * 100), (defaults.maxSize * 100)) / 100, random(defaults.minSpeed, defaults.maxSpeed), flakeId)); + } + // start the snow + animateSnow(); + }, + clear : function(){ + var flakeChildren = null; + + if(!element.getElementsByClassName){ + flakeChildren = element.querySelectorAll('.snowfall-flakes'); + }else{ + flakeChildren = element.getElementsByClassName('snowfall-flakes'); + } + + var flakeChilLen = flakeChildren.length; + while(flakeChilLen--){ + element.removeChild(flakeChildren[flakeChilLen]); + } + + flakes = []; + cancelAnimationFrame(snowTimeout); + } + }; + } + return{ + snow : (elements, options) => { + if(typeof(options) === 'string'){ + elements.snow.clear(); + }else{ + new jSnow().snow(elements, options); + } + } + }; +})(); diff --git a/plugins/two-factor-auth/LICENSE b/plugins/two-factor-auth/LICENSE new file mode 100644 index 0000000000..5b03ee9bd4 --- /dev/null +++ b/plugins/two-factor-auth/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2021 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/two-factor-auth/index.php b/plugins/two-factor-auth/index.php new file mode 100644 index 0000000000..a080ae08ba --- /dev/null +++ b/plugins/two-factor-auth/index.php @@ -0,0 +1,350 @@ +<?php + +use \RainLoop\Exceptions\ClientException; +use \RainLoop\Model\Account; +use \RainLoop\Model\MainAccount; + +class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'Two Factor Authentication', + VERSION = '2.19.0', + RELEASE = '2024-03-29', + REQUIRED = '2.36.0', + CATEGORY = 'Login', + DESCRIPTION = 'Provides support for TOTP 2FA'; + + public function Init() : void + { + $this->UseLangs(true); + + $this->addJs('js/TwoFactorAuthLogin.js'); + $this->addJs('js/TwoFactorAuthSettings.js'); + + $this->addHook('login.success', 'DoLogin'); + $this->addHook('filter.app-data', 'FilterAppData'); + + $this->addJsonHook('GetTwoFactorInfo', 'DoGetTwoFactorInfo'); + $this->addJsonHook('CreateTwoFactorSecret', 'DoCreateTwoFactorSecret'); + $this->addJsonHook('ShowTwoFactorSecret', 'DoShowTwoFactorSecret'); + $this->addJsonHook('EnableTwoFactor', 'DoEnableTwoFactor'); + $this->addJsonHook('VerifyTwoFactorCode', 'DoVerifyTwoFactorCode'); + $this->addJsonHook('ClearTwoFactorInfo', 'DoClearTwoFactorInfo'); + + $this->addTemplate('templates/TwoFactorAuthSettings.html'); + $this->addTemplate('templates/PopupsTwoFactorAuthTest.html'); + } + + public function configMapping() : array + { + return [ + \RainLoop\Plugins\Property::NewInstance("force_two_factor_auth") +// ->SetLabel('PLUGIN_TWO_FACTOR/LABEL_FORCE') + ->SetLabel('Enforce 2-Step Verification') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL), + ]; + } + + public function FilterAppData($bAdmin, &$aResult) + { + if (!$bAdmin && \is_array($aResult)/* && isset($aResult['Auth']) && !$aResult['Auth']*/) { + $aResult['RequireTwoFactor'] = (bool) $this->Config()->Get('plugin', 'force_two_factor_auth', false); + + $aResult['SetupTwoFactor'] = false; + if ($aResult['RequireTwoFactor'] && !empty($aResult['Auth'])) { + $aData = $this->getTwoFactorInfo($this->getMainAccountFromToken()); + $aResult['SetupTwoFactor'] = empty($aData['IsSet']) || empty($aData['Enable']) || empty($aData['Secret']); + } + } + } + + public function DoLogin(MainAccount $oAccount) + { + if ($this->TwoFactorAuthProvider($oAccount)) { + $aData = $this->getTwoFactorInfo($oAccount); + if (isset($aData['IsSet'], $aData['Enable']) && !empty($aData['Secret']) && $aData['IsSet'] && $aData['Enable']) { + $sCode = \trim($this->jsonParam('totp_code', '')); + if (empty($sCode)) { + $this->Logger()->Write("TFA: Code required for {$oAccount->Email()}"); + throw new ClientException(\RainLoop\Notifications::AuthError); + } + + $bUseBackupCode = false; + if (6 < \strlen($sCode) && !empty($aData['BackupCodes'])) { + $aBackupCodes = \explode(' ', \trim(\preg_replace('/[^\d]+/', ' ', $aData['BackupCodes']))); + $bUseBackupCode = \in_array($sCode, $aBackupCodes); + if ($bUseBackupCode) { + $this->removeBackupCodeFromTwoFactorInfo($oAccount, $sCode); + } + } + + if (!$bUseBackupCode && !$this->TwoFactorAuthProvider($oAccount)->VerifyCode($aData['Secret'], $sCode)) { + $this->Manager()->Actions()->LoggerAuthHelper($oAccount); + $this->Logger()->Write("TFA: Code failed for {$oAccount->Email()}"); + throw new ClientException(\RainLoop\Notifications::AuthError); + } + $this->Logger()->Write("TFA: Code verified for {$oAccount->Email()}"); + } + } + } + + public function DoGetTwoFactorInfo() : array + { + $oAccount = $this->getMainAccountFromToken(); + + if (!$this->TwoFactorAuthProvider($oAccount)) { + return $this->jsonResponse(__FUNCTION__, false); + } + + return $this->jsonResponse(__FUNCTION__, $this->getTwoFactorInfo($oAccount, true)); + } + + public function DoCreateTwoFactorSecret() : array + { + $oAccount = $this->getMainAccountFromToken(); + + if (!$this->TwoFactorAuthProvider($oAccount)) { + return $this->jsonResponse(__FUNCTION__, false); + } + + $sEmail = $oAccount->Email(); + + $sSecret = $this->TwoFactorAuthProvider($oAccount)->CreateSecret(); + + $aCodes = \array_map(function(){return \rand(100000000, 900000000);}, \array_fill(0, 8, null)); + + $this->StorageProvider()->Put($oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'two_factor', + \json_encode(array( + 'User' => $sEmail, + 'Enable' => false, + 'Secret' => $sSecret, + 'QRCode' => static::getQRCode($oAccount, $sSecret), + 'BackupCodes' => \implode(' ', $aCodes) + )) + ); + + return $this->jsonResponse(__FUNCTION__, $this->getTwoFactorInfo($oAccount)); + } + + private static function getQRCode(MainAccount $oAccount, string $secret) : string + { + $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}", + "otpauth://totp/{$email}?secret={$secret}", + \SnappyMail\QRCode::ERROR_CORRECT_LEVEL_M + ); + return $QR->__toString(); + } + + public function DoShowTwoFactorSecret() : array + { + $oAccount = $this->getMainAccountFromToken(); + + if (!$this->TwoFactorAuthProvider($oAccount)) { + return $this->jsonResponse(__FUNCTION__, false); + } + + $aResult = $this->getTwoFactorInfo($oAccount); + unset($aResult['BackupCodes']); + + $aResult['QRCode'] = static::getQRCode($oAccount, $aResult['Secret']); + + return $this->jsonResponse(__FUNCTION__, $aResult); + } + + public function DoEnableTwoFactor() : array + { + $oAccount = $this->getMainAccountFromToken(); + + if (!$this->TwoFactorAuthProvider($oAccount)) { + return $this->jsonResponse(__FUNCTION__, false); + } + + $oActions = $this->Manager()->Actions(); + if ($oActions->HasActionParam('EnableTwoFactor')) { + $sValue = $oActions->GetActionParam('EnableTwoFactor', ''); + $oActions->SettingsProvider()->Load($oAccount)->SetConf('EnableTwoFactor', !empty($sValue)); + } + + $sEmail = $oAccount->Email(); + + $bResult = false; + $mData = $this->getTwoFactorInfo($oAccount); + if (isset($mData['Secret'], $mData['BackupCodes'])) { + $bResult = $this->StorageProvider()->Put($oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'two_factor', + \json_encode(array( + 'User' => $sEmail, + 'Enable' => '1' === \trim($this->jsonParam('Enable', '0')), + 'Secret' => $mData['Secret'], + 'BackupCodes' => $mData['BackupCodes'] + )) + ); + } + + return $this->jsonResponse(__FUNCTION__, $bResult); + } + + public function DoVerifyTwoFactorCode() : array + { + $oAccount = $this->getMainAccountFromToken(); + + if (!$this->TwoFactorAuthProvider($oAccount)) { + return $this->jsonResponse(__FUNCTION__, false); + } + + $sCode = \trim($this->jsonParam('Code', '')); + + $aData = $this->getTwoFactorInfo($oAccount); + $sSecret = !empty($aData['Secret']) ? $aData['Secret'] : ''; + + return $this->jsonResponse(__FUNCTION__, + $this->TwoFactorAuthProvider($oAccount)->VerifyCode($sSecret, $sCode)); + } + + public function DoClearTwoFactorInfo() : array + { + $oAccount = $this->getMainAccountFromToken(); + + if (!$this->TwoFactorAuthProvider($oAccount)) { + return $this->jsonResponse(__FUNCTION__, false); + } + + $this->StorageProvider()->Clear($oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'two_factor' + ); + + return $this->jsonResponse(__FUNCTION__, $this->getTwoFactorInfo($oAccount, true)); + } + + protected function Logger() : \MailSo\Log\Logger + { + return $this->Manager()->Actions()->Logger(); + } + protected function getMainAccountFromToken() : MainAccount + { + return $this->Manager()->Actions()->getMainAccountFromToken(); + } + protected function StorageProvider() : \RainLoop\Providers\Storage + { + return $this->Manager()->Actions()->StorageProvider(); + } + + private $oTwoFactorAuthProvider = null; + protected function TwoFactorAuthProvider(MainAccount $oAccount) : ?TwoFactorAuthInterface + { + if (!$this->oTwoFactorAuthProvider) { + require __DIR__ . '/providers/interface.php'; + require __DIR__ . '/providers/totp.php'; + $this->oTwoFactorAuthProvider = new TwoFactorAuthTotp(); + } + return $this->oTwoFactorAuthProvider; + } + + protected function getTwoFactorInfo(MainAccount $oAccount, bool $bRemoveSecret = false) : array + { + $sEmail = $oAccount->Email(); + + $mData = null; + + $aResult = array( + 'User' => '', + 'IsSet' => false, + 'Enable' => false, + 'Secret' => '', + 'BackupCodes' => '' + ); + + if (!empty($sEmail)) { + $aResult['User'] = $sEmail; + + $sData = $this->StorageProvider()->Get($oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'two_factor' + ); + + if ($sData) { + $mData = static::DecodeKeyValues($sData); + } + } + + if (!empty($aResult['User']) && + !empty($mData['User']) && !empty($mData['Secret']) && + !empty($mData['BackupCodes']) && $sEmail === $mData['User']) + { + $aResult['IsSet'] = true; + $aResult['Enable'] = isset($mData['Enable']) ? !!$mData['Enable'] : false; + $aResult['Secret'] = $mData['Secret']; + $aResult['BackupCodes'] = $mData['BackupCodes']; + $aResult['QRCode'] = static::getQRCode($oAccount, $mData['Secret']); + } + + if ($bRemoveSecret) { + if (isset($aResult['Secret'])) { + unset($aResult['Secret']); + } + + if (isset($aResult['BackupCodes'])) { + unset($aResult['BackupCodes']); + } + } + + return $aResult; + } + + protected function removeBackupCodeFromTwoFactorInfo(MainAccount $oAccount, string $sCode) : bool + { + if (!$oAccount || empty($sCode)) { + return false; + } + + $sData = $this->StorageProvider()->Get($oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'two_factor' + ); + + if ($sData) { + $mData = static::DecodeKeyValues($sData); + + if (!empty($mData['BackupCodes'])) { + $sBackupCodes = \preg_replace('/[^\d]+/', ' ', ' '.$mData['BackupCodes'].' '); + $sBackupCodes = \str_replace(' '.$sCode.' ', '', $sBackupCodes); + + $mData['BackupCodes'] = \trim(\preg_replace('/[^\d]+/', ' ', $sBackupCodes)); + + return $this->StorageProvider()->Put($oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'two_factor', + \json_encode($mData) + ); + } + } + + return false; + } + + private static function DecodeKeyValues(string $sData) : array + { + if (!\str_contains($sData, 'User')) { + $sData = \MailSo\Base\Utils::UrlSafeBase64Decode($sData); + if (!\strlen($sData)) { + return ''; + } + $sKey = \md5(APP_SALT); + $sData = \is_callable('xxtea_decrypt') + ? \xxtea_decrypt($sData, $sKey) + : \MailSo\Base\Xxtea::decrypt($sData, $sKey); + } + try { + return \json_decode($sData, true, 512, JSON_THROW_ON_ERROR) ?: array(); + } catch (\Throwable $e) { + return \unserialize($sData) ?: array(); + } + } +} diff --git a/plugins/two-factor-auth/js/TwoFactorAuthLogin.js b/plugins/two-factor-auth/js/TwoFactorAuthLogin.js new file mode 100644 index 0000000000..aa71747c20 --- /dev/null +++ b/plugins/two-factor-auth/js/TwoFactorAuthLogin.js @@ -0,0 +1,36 @@ + +(rl => { + + const + forceTOTP = () => { + if (rl.settings.get('SetupTwoFactor')) { + setTimeout(() => document.location.hash = '#/settings/two-factor-auth', 50); + } + }; + + addEventListener('rl-view-model', e => { + if ('Login' === e.detail.viewModelTemplateID) { + const container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'), + placeholder = 'PLUGIN_2FA/LABEL_TWO_FACTOR_CODE'; + if (container) { + container.prepend(Element.fromHTML('<div class="controls">' + + '<span class="fontastic">⏱</span>' + + '<input name="totp_code" type="text" class="input-block-level"' + + ' pattern="[0-9]*" inputmode="numeric"' + + ' autocomplete="one-time-code" autocorrect="off" autocapitalize="none"' + + ' data-bind="textInput: totp, disable: submitRequest" data-i18n="[placeholder]'+placeholder + + '" placeholder="'+rl.i18n(placeholder)+'">' + + '</div>')); + } + } + }); + + // https://github.com/the-djmaze/snappymail/issues/349 + addEventListener('sm-show-screen', e => { + if (!e.detail.startsWith('settings') && rl.settings.get('SetupTwoFactor')) { + e.preventDefault(); + forceTOTP(); + } + }); + +})(window.rl); diff --git a/plugins/two-factor-auth/js/TwoFactorAuthSettings.js b/plugins/two-factor-auth/js/TwoFactorAuthSettings.js new file mode 100644 index 0000000000..79d8c6bb5d --- /dev/null +++ b/plugins/two-factor-auth/js/TwoFactorAuthSettings.js @@ -0,0 +1,215 @@ +/* +import { trigger as translatorTrigger } from 'Common/Translator'; +*/ + +(rl => { if (rl) { + +const + pString = value => null != value ? '' + value : '', + + Remote = new class { + /** + * @param {?Function} fCallback + * @param {string} sCode + */ + verifyCode(fCallback, sCode) { + rl.pluginRemoteRequest(fCallback, 'VerifyTwoFactorCode', { + Code: sCode + }); + } + + /** + * @param {?Function} fCallback + * @param {boolean} bEnable + */ + enableTwoFactor(fCallback, bEnable) { + rl.pluginRemoteRequest(fCallback, 'EnableTwoFactor', { + Enable: bEnable ? 1 : 0 + }); + } + }; + +class TwoFactorAuthSettings +{ + + constructor() { + this.processing = ko.observable(false); + this.clearing = ko.observable(false); + this.secreting = ko.observable(false); + + this.viewUser = ko.observable(''); + this.twoFactorStatus = ko.observable(false); + + this.twoFactorTested = ko.observable(false); + + this.viewSecret = ko.observable(''); + this.viewQRCode = ko.observable(''); + this.viewBackupCodes = ko.observable(''); + + this.viewEnable_ = ko.observable(false); + + const fn = iError => iError && this.viewEnable_(false); + Object.entries({ + viewEnable: { + read: this.viewEnable_, + write: (value) => { + value = !!value; + if (value && this.twoFactorTested()) { + this.viewEnable_(value); + Remote.enableTwoFactor(iError => { + fn(iError); + rl.settings.get('RequireTwoFactor') && rl.settings.set('SetupTwoFactor', !!iError); + }, value); + } else { + value || this.viewEnable_(value); + Remote.enableTwoFactor(fn, false); + } + } + }, + + viewTwoFactorEnableTooltip: () => { +// translatorTrigger(); + return this.twoFactorTested() || this.viewEnable_() + ? '' + : rl.i18n('PLUGIN_2FA/TWO_FACTOR_SECRET_TEST_BEFORE_DESC'); + }, + + viewTwoFactorStatus: () => { +// translatorTrigger(); + return rl.i18n('PLUGIN_2FA/TWO_FACTOR_SECRET_' + + (this.twoFactorStatus() ? '' : 'NOT_') + + 'CONFIGURED_DESC' + ); + }, + + twoFactorAllowedEnable: () => this.viewEnable() || this.twoFactorTested() + }).forEach(([key, fn]) => this[key] = ko.computed(fn)); + + this.onResult = this.onResult.bind(this); + this.onShowSecretResult = this.onShowSecretResult.bind(this); + } + + showSecret() { + this.secreting(true); + rl.pluginRemoteRequest(this.onShowSecretResult, 'ShowTwoFactorSecret'); + } + + hideSecret() { + this.viewSecret(''); + this.viewQRCode(''); + this.viewBackupCodes(''); + } + + createTwoFactor() { + this.processing(true); + rl.pluginRemoteRequest(this.onResult, 'CreateTwoFactorSecret'); + } + + testTwoFactor() { + TwoFactorAuthTestPopupView.showModal([ + () => { + this.twoFactorTested(true); + this.viewEnable(true); + } + ]); + } + + clearTwoFactor() { + this.hideSecret(); + + this.twoFactorTested(false); + + this.clearing(true); + rl.pluginRemoteRequest(this.onResult, 'ClearTwoFactorInfo'); + } + + onShow() { + this.hideSecret(''); + } + + getQr() { + return 'otpauth://totp/' + encodeURIComponent(this.viewUser()) + + '?secret=' + encodeURIComponent(this.viewSecret()) + + '&issuer=' + encodeURIComponent(''); + } + + onResult(iError, oData) { + this.processing(false); + this.clearing(false); + + if (iError) { + this.viewUser(''); + this.viewEnable_(false); + this.twoFactorStatus(false); + this.twoFactorTested(false); + this.hideSecret(''); + } else { + this.viewUser(pString(oData.Result.User)); + this.viewEnable_(!!oData.Result.Enable); + this.twoFactorStatus(!!oData.Result.IsSet); + this.twoFactorTested(!!oData.Result.Tested); + + this.viewSecret(pString(oData.Result.Secret)); + this.viewQRCode(oData.Result.QRCode); + this.viewBackupCodes(pString(oData.Result.BackupCodes).replace(/[\s]+/g, ' ')); + } + } + + onShowSecretResult(iError, data) { + this.secreting(false); + + if (iError) { + this.viewSecret(''); + this.viewQRCode(''); + } else { + this.viewSecret(pString(data.Result.Secret)); + this.viewQRCode(pString(data.Result.QRCode)); + } + } + + onBuild() { + this.processing(true); + rl.pluginRemoteRequest(this.onResult, 'GetTwoFactorInfo'); + } +} + +class TwoFactorAuthTestPopupView extends rl.pluginPopupView { + constructor() { + super('TwoFactorAuthTest'); + + this.addObservables({ + code: '', + codeStatus: null, + testing: false + }); + + ko.decorateCommands(this, { + testCodeCommand: self => self.code() && !self.testing() + }); + } + + testCodeCommand() { + this.testing(true); + Remote.verifyCode(iError => { + this.testing(false); + this.codeStatus(!iError); + iError || (this.onSuccess() | this.close()); + }, this.code()); + } + + onShow(onSuccess) { + this.code(''); + this.codeStatus(null); + this.testing(false); + this.onSuccess = onSuccess; + } +} + +rl.addSettingsViewModel( + TwoFactorAuthSettings, + 'TwoFactorAuthSettings', + 'PLUGIN_2FA/LEGEND_TWO_FACTOR_AUTH', + 'two-factor-auth' +); + +}})(window.rl); diff --git a/plugins/two-factor-auth/langs/cs.ini b/plugins/two-factor-auth/langs/cs.ini new file mode 100644 index 0000000000..2324476fcf --- /dev/null +++ b/plugins/two-factor-auth/langs/cs.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "Dvoufázové ověření" +LABEL_ENABLE_TWO_FACTOR = "Povolit dvoufázové ověření" +LABEL_TWO_FACTOR_USER = "Uživatel" +LABEL_TWO_FACTOR_STATUS = "Stav" +LABEL_TWO_FACTOR_SECRET = "Tajný klíč" +LABEL_TWO_FACTOR_BACKUP_CODES = "Backup codes" +BUTTON_CREATE = "Create a secret" +BUTTON_ACTIVATE = "Activate" +LINK_TEST = "test" +BUTTON_SHOW_SECRET = "Show Secret" +BUTTON_HIDE_SECRET = "Hide Secret" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured" +TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually." +TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive." +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test." +TITLE_TEST_CODE = "2-Step verification test" +LABEL_CODE = "Code" +LABEL_TWO_FACTOR_CODE = "Verification Code" diff --git a/plugins/two-factor-auth/langs/de.ini b/plugins/two-factor-auth/langs/de.ini new file mode 100644 index 0000000000..90ad5d35c2 --- /dev/null +++ b/plugins/two-factor-auth/langs/de.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "Zwei-Faktor-Authentifizierung" +LABEL_ENABLE_TWO_FACTOR = "Zwei-Faktor-Authentifizierung aktivieren" +LABEL_TWO_FACTOR_USER = "Benutzer" +LABEL_TWO_FACTOR_STATUS = "Status" +LABEL_TWO_FACTOR_SECRET = "Geheimnis" +LABEL_TWO_FACTOR_BACKUP_CODES = "Sicherungscodes" +BUTTON_CREATE = "Neues Geheimnis erstellen" +BUTTON_ACTIVATE = "Aktivieren" +LINK_TEST = "test" +BUTTON_SHOW_SECRET = "Geheimnis einblenden" +BUTTON_HIDE_SECRET = "Geheminis ausblenden" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Konfiguriert" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nicht konfiguriert" +TWO_FACTOR_SECRET_DESC = "Importieren Sie diese Information in Ihre Google-Authenticator-Anwendung (oder andere TOTP-Anwendung), indem Sie den unten bereitgestellten QR-Code verwenden oder den Code manuell eingeben." +TWO_FACTOR_BACKUP_CODES_DESC = "Sollten Sie keine Codes über den Google Authenticator erhalten, können Sie einen Sicherungscode zur Anmeldung verwenden. Der Sicherungscode wird inaktiv, sobald Sie ihn zur Anmeldung verwendet haben." +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Sie können diese Einstellung nicht ohne vorherigen Test verändern." +TITLE_TEST_CODE = "Zwei-Faktor-Authentifizierung" +LABEL_CODE = "Code" +LABEL_TWO_FACTOR_CODE = "Verifizierungscode" diff --git a/plugins/two-factor-auth/langs/en.ini b/plugins/two-factor-auth/langs/en.ini new file mode 100644 index 0000000000..003a50a314 --- /dev/null +++ b/plugins/two-factor-auth/langs/en.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "2-Step Verification (TOTP)" +LABEL_ENABLE_TWO_FACTOR = "Enable 2-Step verification" +LABEL_TWO_FACTOR_USER = "User" +LABEL_TWO_FACTOR_STATUS = "Status" +LABEL_TWO_FACTOR_SECRET = "Secret" +LABEL_TWO_FACTOR_BACKUP_CODES = "Backup codes" +BUTTON_CREATE = "Create a secret" +BUTTON_ACTIVATE = "Activate" +LINK_TEST = "test" +BUTTON_SHOW_SECRET = "Show Secret" +BUTTON_HIDE_SECRET = "Hide Secret" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured" +TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually." +TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive." +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test." +TITLE_TEST_CODE = "2-Step verification test" +LABEL_CODE = "Code" +LABEL_TWO_FACTOR_CODE = "Verification Code" diff --git a/plugins/two-factor-auth/langs/es.ini b/plugins/two-factor-auth/langs/es.ini new file mode 100644 index 0000000000..85fe091911 --- /dev/null +++ b/plugins/two-factor-auth/langs/es.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "Verificación de 2 Pasos" +LABEL_ENABLE_TWO_FACTOR = "Activar la verificación de 2 pasos" +LABEL_TWO_FACTOR_USER = "Usuario" +LABEL_TWO_FACTOR_STATUS = "Estado" +LABEL_TWO_FACTOR_SECRET = "Clave secreta" +LABEL_TWO_FACTOR_BACKUP_CODES = "Códigos de copia de seguridad" +BUTTON_CREATE = "Crear nueva clave secreta" +BUTTON_ACTIVATE = "Activate" +LINK_TEST = "probar" +BUTTON_SHOW_SECRET = "Mostrar clave secreta" +BUTTON_HIDE_SECRET = "Ocultar clave secreta" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configurado" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "No configurado" +TWO_FACTOR_SECRET_DESC = "Importar esta información en su cliente Google Authenticator (u otro cliente TOTP) utilizando el código QR ​​se indica debajo o introduciendo el código manualmente." +TWO_FACTOR_BACKUP_CODES_DESC = "Si usted no puede recibir los códigos a través de Google Authenticator, puede utilizar códigos de copia de seguridad para firmar pulg Después de que usted ha utilizado un código de copia de seguridad para iniciar sesión, se convertirá en inactiva." +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test." +TITLE_TEST_CODE = "Prueba de verificación de 2 pasos" +LABEL_CODE = "Código" +LABEL_TWO_FACTOR_CODE = "Código de verificación" diff --git a/plugins/two-factor-auth/langs/fr.ini b/plugins/two-factor-auth/langs/fr.ini new file mode 100644 index 0000000000..82cbd64dcd --- /dev/null +++ b/plugins/two-factor-auth/langs/fr.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "Authentification en deux étapes" +LABEL_ENABLE_TWO_FACTOR = "Activer l'authentification en deux étapes" +LABEL_TWO_FACTOR_USER = "Utilisateur" +LABEL_TWO_FACTOR_STATUS = "Statut" +LABEL_TWO_FACTOR_SECRET = "Secret" +LABEL_TWO_FACTOR_BACKUP_CODES = "Code de sauvegarde" +BUTTON_CREATE = "Créer une nouvelle clé secrète" +BUTTON_ACTIVATE = "Activer" +LINK_TEST = "test" +BUTTON_SHOW_SECRET = "Montrer la clé secrète" +BUTTON_HIDE_SECRET = "Masquer la clé secrète" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configuré" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Non configuré" +TWO_FACTOR_SECRET_DESC = "Importez ces informations dans votre client Google Authenticator (ou un autre client TOTP) en utilisant le code QR fourni ci-dessous ou en entrant les valeurs manuellement." +TWO_FACTOR_BACKUP_CODES_DESC = "Si vous ne pouvez pas recevoir les codes par Google Authenticator, vous pouvez utiliser les codes de sauvegarde pour vous connecter. Après avoir fait cela, il deviendra inactif." +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Vous ne pouvez pas modifier ce paramètre avant de l'avoir essayé." +TITLE_TEST_CODE = "Test d'authentification en deux étapes" +LABEL_CODE = "Code" +LABEL_TWO_FACTOR_CODE = "Code de vérification" diff --git a/plugins/two-factor-auth/langs/hu.ini b/plugins/two-factor-auth/langs/hu.ini new file mode 100644 index 0000000000..a847b1be40 --- /dev/null +++ b/plugins/two-factor-auth/langs/hu.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "2 lépcsős hitelesítés" +LABEL_ENABLE_TWO_FACTOR = "2 lépcsős hitelesítés engedélyezése" +LABEL_TWO_FACTOR_USER = "Felhasználó" +LABEL_TWO_FACTOR_STATUS = "Állapot" +LABEL_TWO_FACTOR_SECRET = "Titok" +LABEL_TWO_FACTOR_BACKUP_CODES = "Biztonsági kódok" +BUTTON_CREATE = "Új titok létrehozás" +BUTTON_ACTIVATE = "Aktivál" +LINK_TEST = "teszt" +BUTTON_SHOW_SECRET = "Titok megjelenítése" +BUTTON_HIDE_SECRET = "Titok elrejtése" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Beállítva" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nincs beállítva" +TWO_FACTOR_SECRET_DESC = "Importáld ezt az infót a Google Authenticator kliensedbe (vagy más TOTP kliensbe) az alábbi QR kód használatával vagy a kód manuális megadatásával." +TWO_FACTOR_BACKUP_CODES_DESC = "Ha nem kapod meg a kódokat a Google Authenticator kliensből (vagy más TOTP kliensből), akkor a bejelentkezéshez használhatod a biztonsági kódot. A biztonsági kód használata után inaktívvá válik." +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Tesztelés nélkül nem lehet megváltoztatni ezt a beállítást." +TITLE_TEST_CODE = "2-lépéses hitelesítés teszt" +LABEL_CODE = "Kód" +LABEL_TWO_FACTOR_CODE = "Megerősítő kód" diff --git a/plugins/two-factor-auth/langs/it.ini b/plugins/two-factor-auth/langs/it.ini new file mode 100644 index 0000000000..fbd44f45d3 --- /dev/null +++ b/plugins/two-factor-auth/langs/it.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "Verifica a 2 Fattori" +LABEL_ENABLE_TWO_FACTOR = "Abilita la verifica a 2 fattori" +LABEL_TWO_FACTOR_USER = "Utente" +LABEL_TWO_FACTOR_STATUS = "Stato" +LABEL_TWO_FACTOR_SECRET = "Chiave segreta" +LABEL_TWO_FACTOR_BACKUP_CODES = "Codici di backup" +BUTTON_CREATE = "Crea la chiave segreta" +BUTTON_ACTIVATE = "Attiva" +LINK_TEST = "test" +BUTTON_SHOW_SECRET = "Mostra la chiave segreta" +BUTTON_HIDE_SECRET = "Nascondi la chiave segreta" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configurato" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Non configurato" +TWO_FACTOR_SECRET_DESC = "Importa queste informazioni nella tua app Google Authenticator (o un altro programma per l'autenticazione a due fattori) utilizzando il codice QR fornito qui sotto o inserendo il codice manualmente." +TWO_FACTOR_BACKUP_CODES_DESC = "Se non riesci a ricevere i codici tramite Google Authenticator (o un altro programma per l'autenticazione a due fattori), puoi utilizzare i codici di backup per accedere. Dopo che hai utilizzato un codice di backup per accedere, diventerà inattivo." +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Non puoi modificare questa impostazione prima del test." +TITLE_TEST_CODE = "Test di verifica a 2 fattori" +LABEL_CODE = "Codice" +LABEL_TWO_FACTOR_CODE = "Codice di Verifica" diff --git a/plugins/two-factor-auth/langs/nl.ini b/plugins/two-factor-auth/langs/nl.ini new file mode 100644 index 0000000000..425558d5fd --- /dev/null +++ b/plugins/two-factor-auth/langs/nl.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "2-Stap verificatie" +LABEL_ENABLE_TWO_FACTOR = "Gebruik 2-Stap verificatie" +LABEL_TWO_FACTOR_USER = "Gebruikersnaam" +LABEL_TWO_FACTOR_STATUS = "Status" +LABEL_TWO_FACTOR_SECRET = "Geheime sleutel" +LABEL_TWO_FACTOR_BACKUP_CODES = "Backup codes" +BUTTON_CREATE = "Nieuwe geheime sleutel aanmaken" +BUTTON_ACTIVATE = "Activeren" +LINK_TEST = "test" +BUTTON_SHOW_SECRET = "Bekijk geheime sleutel" +BUTTON_HIDE_SECRET = "Verberg geheime sleutel" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Geconfigureerd" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Niet geconfigureerd" +TWO_FACTOR_SECRET_DESC = "Importeer deze informatie in uw Google Authenticator-client (of andere TOTP-client) door gebruik te maken van de QR code hier beneden of door de code handmatig in te voeren." +TWO_FACTOR_BACKUP_CODES_DESC = "Als u geen codes ontvangt via de Google Authenticator kunt u de backup codes gebruiken om in te loggen. Na gebruik van de backup code wordt deze inactief." +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "U kunt 2-stap verificatie niet activeren voordat u het succesvol getest heeft." +TITLE_TEST_CODE = "2-Stap verificatie test" +LABEL_CODE = "Code" +LABEL_TWO_FACTOR_CODE = "Verificatie Code" diff --git a/plugins/two-factor-auth/langs/pl.ini b/plugins/two-factor-auth/langs/pl.ini new file mode 100644 index 0000000000..2e00f76837 --- /dev/null +++ b/plugins/two-factor-auth/langs/pl.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "2FA (TOTP)" +LABEL_ENABLE_TWO_FACTOR = "Włącz weryfikację 2-stopniową" +LABEL_TWO_FACTOR_USER = "Użytkownik" +LABEL_TWO_FACTOR_STATUS = "Status" +LABEL_TWO_FACTOR_SECRET = "Twój sekretny klucz" +LABEL_TWO_FACTOR_BACKUP_CODES = "Kody zapasowe" +BUTTON_CREATE = "Utwórz sekretny klucz" +BUTTON_ACTIVATE = "Aktywuj" +LINK_TEST = "Testuj" +BUTTON_SHOW_SECRET = "Pokaż Twój sekretny klucz" +BUTTON_HIDE_SECRET = "Ukryj klucz" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Skonfigurowano" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nieskonfigurowano" +TWO_FACTOR_SECRET_DESC = "Zaimportuj tę infomację do aplikacji Google Authenticator (lub innego klienta TOTP) używając kodu QR poniżej lub wpisz sekretny klucz ręcznie." +TWO_FACTOR_BACKUP_CODES_DESC = "Jeśli nie możesz uzyskać kodów poprzez aplikację Google Authenticator (lub innego klienta TOTP), możesz użyć kodów zapasowych aby się zalogować. Po użyciu kodu zapasowego do zalogowania, stanie się on nieaktywny." +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Nie możesz zmienić tego ustawienia przed testem." +TITLE_TEST_CODE = "Test weryfikacji 2-stopniowej" +LABEL_CODE = "Kod" +LABEL_TWO_FACTOR_CODE = "Kod weryfikacyjny" diff --git a/plugins/two-factor-auth/langs/sv.ini b/plugins/two-factor-auth/langs/sv.ini new file mode 100644 index 0000000000..7ffd23f21b --- /dev/null +++ b/plugins/two-factor-auth/langs/sv.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "Tvåstegsverifiering (TOTP)" +LABEL_ENABLE_TWO_FACTOR = "Aktivera tvåstegsverifiering" +LABEL_TWO_FACTOR_USER = "Användare" +LABEL_TWO_FACTOR_STATUS = "Status" +LABEL_TWO_FACTOR_SECRET = "Hemlig kod" +LABEL_TWO_FACTOR_BACKUP_CODES = "Backupkoder" +BUTTON_CREATE = "Skapa ny hemlig kod" +BUTTON_ACTIVATE = "Aktivera" +LINK_TEST = "test" +BUTTON_SHOW_SECRET = "Visa hemlig kod" +BUTTON_HIDE_SECRET = "Göm hemlig kod" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Konfigurerad" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Inte konfigurarad" +TWO_FACTOR_SECRET_DESC = "Importera denna information till din Google Authenticator klient (eller annan TOTP klient) med denna QR kod eller manuellt med koden här nedan." +TWO_FACTOR_BACKUP_CODES_DESC = "Om du inte kan ta emot koderna med Google Authenticator, så använd backupkoderna för att logga in. När du använt backupkoderna för att logga in så blir dom inaktiva." +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Du kan inte ändra denna inställning innan test." +TITLE_TEST_CODE = "Tvåstegsverifieringstest" +LABEL_CODE = "Kod" +LABEL_TWO_FACTOR_CODE = "Verifikationskod" diff --git a/plugins/two-factor-auth/langs/uk.ini b/plugins/two-factor-auth/langs/uk.ini new file mode 100644 index 0000000000..5f09cfb09f --- /dev/null +++ b/plugins/two-factor-auth/langs/uk.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "2-етапна перевірка (TOTP)" +LABEL_ENABLE_TWO_FACTOR = "Увімкнути 2-етапну перевірку" +LABEL_TWO_FACTOR_USER = "Ім'я користувача" +LABEL_TWO_FACTOR_STATUS = "Статус" +LABEL_TWO_FACTOR_SECRET = "Ключ" +LABEL_TWO_FACTOR_BACKUP_CODES = "Резервні коди" +BUTTON_CREATE = "Create a secret" +BUTTON_ACTIVATE = "Activate" +LINK_TEST = "Тестувати" +BUTTON_SHOW_SECRET = "Показати ключ" +BUTTON_HIDE_SECRET = "Приховати ключ" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Налаштовано" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Не налаштовано" +TWO_FACTOR_SECRET_DESC = "Імпортуйте цю інформацію у свій клієнт Google Authenticator (або інший клієнт TOTP), використовуючи наданий нижче QR-код або ввівши код вручну" +TWO_FACTOR_BACKUP_CODES_DESC = "Якщо ви не можете отримати код через Google Authenticator (або інший клієнт TOTP), ви можете скористатися резервними кодами для входу. Після того, як ви використаєте резервний код для входу, він стане неактивним. Закреслюйте його" +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Ви не можете змінити це налаштування перед тестуванням." +TITLE_TEST_CODE = "Тест 2-етапної перевірки" +LABEL_CODE = "Код" +LABEL_TWO_FACTOR_CODE = "Код підтвердження" diff --git a/plugins/two-factor-auth/langs/zh.ini b/plugins/two-factor-auth/langs/zh.ini new file mode 100644 index 0000000000..87c530e92b --- /dev/null +++ b/plugins/two-factor-auth/langs/zh.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "两步验证 (TOTP)" +LABEL_ENABLE_TWO_FACTOR = "启用两步验证" +LABEL_TWO_FACTOR_USER = "用户" +LABEL_TWO_FACTOR_STATUS = "状态" +LABEL_TWO_FACTOR_SECRET = "密钥" +LABEL_TWO_FACTOR_BACKUP_CODES = "备用代码" +BUTTON_CREATE = "创建新密钥" +BUTTON_ACTIVATE = "启用" +LINK_TEST = "测试" +BUTTON_SHOW_SECRET = "显示密钥" +BUTTON_HIDE_SECRET = "隐藏密钥" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "已设置" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "未设置" +TWO_FACTOR_SECRET_DESC = "将下面的信息导入 Google Authenticator (或其他 TOTP 客户端)。 扫描下面的二维码或手动输入密钥。" +TWO_FACTOR_BACKUP_CODES_DESC = "如果你无法从验证器获取验证码,你可以使用备用代码登录。 当你使用过某一个备用代码后,它将会失效。" +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "完成测试前你无法更改此设置。" +TITLE_TEST_CODE = "两步验证测试" +LABEL_CODE = "代码" +LABEL_TWO_FACTOR_CODE = "验证码" diff --git a/plugins/two-factor-auth/providers/interface.php b/plugins/two-factor-auth/providers/interface.php new file mode 100644 index 0000000000..678a382c66 --- /dev/null +++ b/plugins/two-factor-auth/providers/interface.php @@ -0,0 +1,8 @@ +<?php + +interface TwoFactorAuthInterface +{ + public function Label() : string; + public function VerifyCode(string $sSecret, string $sCode) : bool; + public function CreateSecret() : string; +} diff --git a/plugins/two-factor-auth/providers/totp.php b/plugins/two-factor-auth/providers/totp.php new file mode 100644 index 0000000000..bffc6b9ce9 --- /dev/null +++ b/plugins/two-factor-auth/providers/totp.php @@ -0,0 +1,20 @@ +<?php + +class TwoFactorAuthTotp implements TwoFactorAuthInterface +{ + public function Label() : string + { + return 'Two Factor Authenticator Code'; + } + + public function VerifyCode(string $sSecret, string $sCode) : bool + { + return \SnappyMail\TOTP::Verify($sSecret, $sCode); + } + + public function CreateSecret() : string + { + return \SnappyMail\TOTP::CreateSecret(); + } + +} diff --git a/plugins/two-factor-auth/templates/PopupsTwoFactorAuthTest.html b/plugins/two-factor-auth/templates/PopupsTwoFactorAuthTest.html new file mode 100644 index 0000000000..f6242c74d3 --- /dev/null +++ b/plugins/two-factor-auth/templates/PopupsTwoFactorAuthTest.html @@ -0,0 +1,18 @@ +<header> + <a class="close" href="#" data-bind="click: close">×</a> + <h3 data-i18n="PLUGIN_2FA/TITLE_TEST_CODE"></h3> +</header> +<div class="modal-body form-horizontal"> + <div class="control-group"> + <label data-i18n="PLUGIN_2FA/LABEL_CODE"></label> + <input type="text" class="uiInput inputName" + autofocus="" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" + data-bind="textInput: code, onEnter: testCodeCommand" /> + </div> +</div> +<footer> + <a class="btn" data-bind="command: testCodeCommand, css: { 'btn-success': true === codeStatus(), 'btn-danger': false === codeStatus() }"> + <i data-bind="css: {'icon-ok': !testing(), 'icon-spinner': testing()}"></i> + <span data-i18n="GLOBAL/TEST"></span> + </a> +</footer> diff --git a/plugins/two-factor-auth/templates/TwoFactorAuthSettings.html b/plugins/two-factor-auth/templates/TwoFactorAuthSettings.html new file mode 100644 index 0000000000..95569afd08 --- /dev/null +++ b/plugins/two-factor-auth/templates/TwoFactorAuthSettings.html @@ -0,0 +1,81 @@ +<div class="b-settings-two-factor form-horizontal"> + <div class="legend" data-i18n="PLUGIN_2FA/LEGEND_TWO_FACTOR_AUTH"></div> + <div class="form-horizontal"> + <div class="control-group" data-bind="visible: twoFactorStatus"> + <div class="controls"> + <div style="display: inline-block" data-bind="attr:{title: viewTwoFactorEnableTooltip}"> + <div data-bind="component: { + name: 'Checkbox', + params: { + label: 'PLUGIN_2FA/LABEL_ENABLE_TWO_FACTOR', + enable: twoFactorAllowedEnable, + value: viewEnable, + inline: true + } + }"></div> + </div> +     + <span class="g-ui-link" data-bind="click: testTwoFactor, visible: twoFactorStatus" + data-i18n="PLUGIN_2FA/LINK_TEST"></span> + </div> + </div> + <div class="control-group"> + <label class="control-label"> + <span data-i18n="PLUGIN_2FA/LABEL_TWO_FACTOR_USER"></span> + </label> + <div class="controls" style="padding-top: 5px;"> + <strong data-bind="text: viewUser"></strong> + </div> + </div> + <div class="control-group" data-bind="visible: '' === viewSecret() && twoFactorStatus() && !clearing()"> + <div class="controls" style="padding-top: 5px;"> + <strong data-bind="visible: secreting">...</strong> + <span class="g-ui-link" data-bind="click: showSecret, visible: !secreting()" + data-i18n="PLUGIN_2FA/BUTTON_SHOW_SECRET"></span> + </div> + </div> + <div class="control-group" data-bind="visible: '' !== viewSecret()"> + <label class="control-label"> + <span data-i18n="PLUGIN_2FA/LABEL_TWO_FACTOR_SECRET"></span> + </label> + <div class="controls" style="padding-top: 5px;"> + <strong data-bind="text: viewSecret" style="user-select:text"></strong> +    + <span class="g-ui-link" data-bind="click: hideSecret" data-i18n="PLUGIN_2FA/BUTTON_HIDE_SECRET"></span> + <br /> + <br /> + <blockquote> + <p class="muted width100-on-mobile" style="width: 550px" data-i18n="PLUGIN_2FA/TWO_FACTOR_SECRET_DESC"></p> + </blockquote> + <pre data-bind="text: viewQRCode" style="line-height:1.1;letter-spacing:-1px;font-family:monospace"></pre> + </div> + </div> + <div class="control-group" data-bind="visible: '' !== viewBackupCodes()"> + <label class="control-label"> + <span data-i18n="PLUGIN_2FA/LABEL_TWO_FACTOR_BACKUP_CODES"></span> + </label> + <div class="controls" style="padding-top: 5px;"> + <pre data-bind="text: viewBackupCodes" style="user-select: text; width: 230px; word-break: break-word;"></pre> + <br /> + <blockquote> + <p class="muted width100-on-mobile" style="width: 550px" data-i18n="PLUGIN_2FA/TWO_FACTOR_BACKUP_CODES_DESC"></p> + </blockquote> + </div> + </div> + </div> + + <a class="btn btn-danger" data-bind="click: clearTwoFactor, visible: twoFactorStatus"> + <i class="fontastic" data-bind="css: {'icon-spinner': clearing()}">✖</i> + <span data-i18n="GLOBAL/CLEAR"></span> + </a> + <a class="btn" data-bind="click: createTwoFactor, visible: !twoFactorStatus()"> + <i class="fontastic" data-bind="css: {'icon-spinner': processing()}">▶</i> + <span data-i18n="PLUGIN_2FA/BUTTON_ACTIVATE"></span> + </a> +<!-- + <a class="btn" data-bind="click: close, visible: viewEnable()"> + <i class="icon-ok" ></i> + <span data-i18n="GLOBAL/DONE"></span> + </a> +--> +</div> diff --git a/plugins/vesta-change-password/LICENSE b/plugins/vesta-change-password/LICENSE deleted file mode 100644 index 271342337d..0000000000 --- a/plugins/vesta-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 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 -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/vesta-change-password/README b/plugins/vesta-change-password/README deleted file mode 100644 index 4f2f06281d..0000000000 --- a/plugins/vesta-change-password/README +++ /dev/null @@ -1 +0,0 @@ -Plugin that adds functionality to change the email account password (Vesta Control Panel). \ No newline at end of file diff --git a/plugins/vesta-change-password/VERSION b/plugins/vesta-change-password/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/vesta-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/vesta-change-password/VestaChangePasswordDriver.php b/plugins/vesta-change-password/VestaChangePasswordDriver.php deleted file mode 100644 index cbb8be5c17..0000000000 --- a/plugins/vesta-change-password/VestaChangePasswordDriver.php +++ /dev/null @@ -1,146 +0,0 @@ -<?php - -class VestaChangePasswordDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sHost = ''; - - /** - * @var string - */ - private $iPort = 8083; - - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sHost - * @param int $iPort - * - * @return \VestaChangePasswordDriver - */ - public function SetConfig($sHost, $iPort) - { - $this->sHost = $sHost; - $this->iPort = $iPort; - - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \VestaChangePasswordDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \VestaChangePasswordDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param \RainLoop\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('Vesta: Try to change password for '.$oAccount->Email()); - } - - $bResult = false; - if (!empty($this->sHost) && 0 < $this->iPort && $oAccount) - { - $sEmail = \trim(\strtolower($oAccount->Email())); - - $sHost = \trim($this->sHost); - $sHost = \str_replace('{user:host-imap}', $oAccount->Domain()->IncHost(), $sHost); - $sHost = \str_replace('{user:host-smtp}', $oAccount->Domain()->OutHost(), $sHost); - $sHost = \str_replace('{user:domain}', \MailSo\Base\Utils::GetDomainFromEmail($sEmail), $sHost); - $sHost = \rtrim($this->sHost, '/\\'); - $sHost = 'https://'.$sHost; - - $sUrl = $sHost.':'.$this->iPort.'/reset/mail/'; - - $iCode = 0; - $oHttp = \MailSo\Base\Http::SingletonInstance(); - - if ($this->oLogger) - { - $this->oLogger->Write('Vesta[Api Request]:'.$sUrl); - } - - $mResult = $oHttp->SendPostRequest($sUrl, - array( - 'email' => $sEmail, - 'password' => $sPrevPassword, - 'new' => $sNewPassword, - ), 'MailSo Http User Agent (v1)', $iCode, $this->oLogger); - - if (false !== $mResult && 200 === $iCode) - { - $aRes = null; - @\parse_str($mResult, $aRes); - if (is_array($aRes) && (!isset($aRes['error']) || (int) $aRes['error'] !== 1)) - { - $bResult = true; - } - else - { - if ($this->oLogger) - { - $this->oLogger->Write('Vesta[Error]: Response: '.$mResult); - } - } - } - else - { - if ($this->oLogger) - { - $this->oLogger->Write('Vesta[Error]: Empty Response: Code:'.$iCode); - } - } - } - - return $bResult; - } -} diff --git a/plugins/vesta-change-password/index.php b/plugins/vesta-change-password/index.php deleted file mode 100644 index 2453541d83..0000000000 --- a/plugins/vesta-change-password/index.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php - -class VestaChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - $sHost = \trim($this->Config()->Get('plugin', 'vesta_host', '')); - $iPort = (int) $this->Config()->Get('plugin', 'vesta_port', 8083); - - if (!empty($sHost) && 0 < $iPort) - { - include_once __DIR__.'/VestaChangePasswordDriver.php'; - - $oProvider = new VestaChangePasswordDriver(); - $oProvider->SetLogger($this->Manager()->Actions()->Logger()); - $oProvider->SetConfig($sHost, $iPort); - $oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))); - } - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('vesta_host')->SetLabel('Vesta Host') - ->SetDefaultValue('') - ->SetDescription('Ex: localhost or domain.com'), - \RainLoop\Plugins\Property::NewInstance('Vesta_port')->SetLabel('Vesta Port') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) - ->SetDefaultValue(8083), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} diff --git a/plugins/video-on-login-screen/LICENSE b/plugins/video-on-login-screen/LICENSE old mode 100644 new mode 100755 index 4aed64b3af..44b915a0c4 --- a/plugins/video-on-login-screen/LICENSE +++ b/plugins/video-on-login-screen/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 RainLoop Team +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 diff --git a/plugins/video-on-login-screen/README b/plugins/video-on-login-screen/README old mode 100644 new mode 100755 diff --git a/plugins/video-on-login-screen/VERSION b/plugins/video-on-login-screen/VERSION old mode 100644 new mode 100755 diff --git a/plugins/video-on-login-screen/index.php b/plugins/video-on-login-screen/index.php old mode 100644 new mode 100755 index 173d4e0400..26f29b9539 --- a/plugins/video-on-login-screen/index.php +++ b/plugins/video-on-login-screen/index.php @@ -2,41 +2,43 @@ class VideoOnLoginScreenPlugin extends \RainLoop\Plugins\AbstractPlugin { + const + NAME = 'Video On Login Screen', + VERSION = '0.1', + RELEASE = '2023-11-09', + REQUIRED = '2.5.0', + CATEGORY = 'Login', + DESCRIPTION = 'Play a simple video on the login screen.'; + /** * @return void */ - public function Init() + public function Init() : void { - $this->addJs('js/vide/jquery.vide.js'); $this->addJs('js/video-on-login.js'); + $this->addHook('main.content-security-policy', 'ContentSecurityPolicy'); } /** * @return array */ - public function configMapping() + 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('webm_file')->SetLabel('Url to a webm file') - ->SetPlaceholder('http://') - ->SetAllowedInJs(true) - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('ogv_file')->SetLabel('Url to a ogv 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%')), - \RainLoop\Plugins\Property::NewInstance('muted')->SetLabel('Muted') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) - ->SetAllowedInJs(true) - ->SetDefaultValue(true), ); } + + 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/vide/CHANGELOG.md b/plugins/video-on-login-screen/js/vide/CHANGELOG.md deleted file mode 100644 index c922ebbd86..0000000000 --- a/plugins/video-on-login-screen/js/vide/CHANGELOG.md +++ /dev/null @@ -1,41 +0,0 @@ -### 0.3.0 -* Added the `resizing` option. -* Updated tests. - -### 0.2.1 -* Code refactoring. -* Updated devDependencies. - -### 0.2.0 -* Lots of updates since 0.1.0. -* Improved code linting. -* Cleaned up the repository. -* Added bower dependencies. -* Updated devDependencies. - -### 0.1.4 -* Fixed wrong URL parsing. -* Changed main files in bower.json. - -### 0.1.3 -* Path argument can receive list of sources. -* Strings with options and pathes can be passed to the constructor directly. -* Added `posterType: none` value. -* Updated README. -* Updated JSDoc. -* Updated tests. -* Updated examples. -* Added CONTRIBUTING.md -* Other small fixes and optimizations. - -### 0.1.2 -* Restored `posterType` option to specify poster image type. - -### 0.1.1 -* Support of absolute URLs (#10). -* Fixed the CORS issue (#11). -* Fixed the destroy method. -* Fixed parsing of empty options. -* Poster and video positions are the same now. -* Syntax and behavior of the position option are similar to the CSS `background-position` property. -* Add support of the `.jpeg` extension. diff --git a/plugins/video-on-login-screen/js/vide/CONTRIBUTING.md b/plugins/video-on-login-screen/js/vide/CONTRIBUTING.md deleted file mode 100644 index 3900d1fe75..0000000000 --- a/plugins/video-on-login-screen/js/vide/CONTRIBUTING.md +++ /dev/null @@ -1,11 +0,0 @@ -CONTRIBUTING -==== - -1. Fork. -2. Run `npm install`. -3. Make your changes on the `src` folder. -4. Update tests. -5. Run `npm test`, make sure everything is okay. -6. Submit a pull request to the master branch. - -Thanks. diff --git a/plugins/video-on-login-screen/js/vide/MIT-LICENSE b/plugins/video-on-login-screen/js/vide/MIT-LICENSE deleted file mode 100644 index f03343d6dd..0000000000 --- a/plugins/video-on-login-screen/js/vide/MIT-LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Ilya Makarov, http://vodkabears.github.io - -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. \ No newline at end of file diff --git a/plugins/video-on-login-screen/js/vide/README.md b/plugins/video-on-login-screen/js/vide/README.md deleted file mode 100644 index 546d81975d..0000000000 --- a/plugins/video-on-login-screen/js/vide/README.md +++ /dev/null @@ -1,140 +0,0 @@ -[![Bower version](https://badge.fury.io/bo/vide.svg)](http://badge.fury.io/bo/vide) -[![Travis](https://travis-ci.org/VodkaBears/Vide.svg?branch=master)](https://travis-ci.org/VodkaBears/Vide) -[![devDependency Status](https://david-dm.org/vodkabears/vide/dev-status.svg)](https://david-dm.org/vodkabears/vide#info=devDependencies) -Vide -==== - -Easy as hell jQuery plugin for video backgrounds. - -## Notes - -* All modern desktop browsers are supported. -* IE9+ -* iOS plays video from a browser only in the native player. So video for iOS is disabled, only fullscreen poster will be used. -* Some android devices play video, some not — go figure. So video for android is disabled, only fullscreen poster will be used. - -## Instructions - -Download it from [GitHub](https://github.com/VodkaBears/Vide/releases/latest) or via Bower: -`bower install vide` - -Include plugin: `<script src="js/jquery.vide.min.js"></script>` - -Prepare your video in several formats like '.webm', '.mp4' for cross browser compatability, also add a poster with `.jpg`, `.png` or `.gif` extension: -``` -path/ -├── to/ -│ ├── video.mp4 -│ ├── video.ogv -│ ├── video.webm -│ └── video.jpg -``` - -Add `data-vide-bg` attribute with a path to the video and poster without extension, video and poster must have the same name. Add `data-vide-options` to pass vide options, if you need it. By default video is muted, looped and starts automatically. -```html -<div style="width: 1000px; height: 500px;" - data-vide-bg="path/to/video" data-vide-options="loop: false, muted: false, position: 0% 0%"> -</div> -``` - -Also you can set extended path: -```html -<div style="width: 1000px; height: 500px;" - data-vide-bg="mp4: path/to/video1, webm: path/to/video2, ogv: path/to/video3, poster: path/to/poster" data-vide-options="posterType: jpg, loop: false, muted: false, position: 0% 0%"> -</div> -``` - -In some situations it can be helpful to initialize it with JS because Vide doesn't have mutation observers: -```js -$("#myBlock").vide("path/to/video"); -$("#myBlock").vide("path/to/video", { -...options... -}); -$("#myBlock").vide({ - mp4: path/to/video1, - webm: path/to/video2, - ogv: path/to/video3, - poster: path/to/poster -}, { -...options... -}); -$("#myBlock").vide("extended path as a string", "options as a string"); -``` - -Easy as hell. - -## Options - -Below is a complete list of options and matching default values: - -```js -$("#yourElement").vide({ - volume: 1, - playbackRate: 1, - muted: true, - loop: true, - autoplay: true, - position: "50% 50%", // Similar to the CSS `background-position` property. - posterType: "detect", // Poster image type. "detect" — auto-detection; "none" — no poster; "jpg", "png", "gif",... - extensions. - resizing: true // Auto-resizing, read: https://github.com/VodkaBears/Vide#resizing -}); -``` - -## Methods - -Below is a complete list of methods: - -```js -// Get instance of the plugin -var instance = $("#yourElement").data("vide"); - -// Get video element of the background. Do what you want. -instance.getVideoObject(); - -// Resize video background. -// It calls automatically, if window resize (or element, if you will use something like https://github.com/cowboy/jquery-resize). -instance.resize(); - -// Destroy plugin instance -instance.destroy(); -``` - -## Resizing - -The Vide plugin resizes if the window resizes. If you will use something like https://github.com/cowboy/jquery-resize, it will resize automatically when the container resizes. Or simply use `resize()` method whenever you need. - -Set the `resizing` option to false to disable auto-resizing. - -## Encoding video - -http://diveintohtml5.info/video.html#miro - -## Ruby Gem - -[Vider](https://github.com/wazery/vider) by Islam Wazery. - -## License - -``` -The MIT License (MIT) - -Copyright (c) 2014 Ilya Makarov, http://vodkabears.github.io - -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/js/vide/bower.json b/plugins/video-on-login-screen/js/vide/bower.json deleted file mode 100644 index 7d4c47379e..0000000000 --- a/plugins/video-on-login-screen/js/vide/bower.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "vide", - "version": "0.3.0", - "homepage": "http://vodkabears.github.io/vide/", - "authors": [ - "Ilya Makarov <dfrost.00@gmail.com>" - ], - "description": "Easy as hell jQuery plugin for video backgrounds.", - "main": "dist/jquery.vide.js", - "ignore": [ - "**/.*", - "examples/", - "libs/", - "src/", - "test/", - "*.md", - "Gruntfile.js", - "package.json", - "vide.jquery.json" - ], - "keywords": [ - "jquery", - "plugin", - "jquery-plugin", - "video", - "background", - "ui", - "responsive", - "declarative" - ], - "license": "MIT", - "dependencies": { - "jquery": "*" - }, - "devDependencies": { - "qunit": "~1.15.0", - "jquery": "jquery#^1.11.1", - "jquery2": "jquery#^2.1.1" - } -} diff --git a/plugins/video-on-login-screen/js/vide/jquery.vide.js b/plugins/video-on-login-screen/js/vide/jquery.vide.js deleted file mode 100644 index cc5f29dfc7..0000000000 --- a/plugins/video-on-login-screen/js/vide/jquery.vide.js +++ /dev/null @@ -1,442 +0,0 @@ -/* - * Vide - v0.3.0 - * Easy as hell jQuery plugin for video backgrounds. - * http://vodkabears.github.io/vide/ - * - * Made by Ilya Makarov - * Under MIT License - */ -!(function($, window, document, navigator) { - "use strict"; - - /** - * Vide settings - * @private - */ - var pluginName = "vide", - defaults = { - volume: 1, - playbackRate: 1, - muted: true, - loop: true, - autoplay: true, - position: "50% 50%", - posterType: "detect", - resizing: true - }, - - // is iOs? - isIOS = /iPad|iPhone|iPod/i.test(navigator.userAgent), - - // is Android? - isAndroid = /Android/i.test(navigator.userAgent); - - /** - * Parse string with options - * @param {String} str - * @returns {Object|String} - * @private - */ - function parseOptions(str) { - var obj = {}, - delimiterIndex, - option, - prop, val, - arr, len, - i; - - // remove spaces around delimiters and split - arr = str.replace(/\s*:\s*/g, ":").replace(/\s*,\s*/g, ",").split(","); - - // parse string - for (i = 0, len = arr.length; i < len; i++) { - option = arr[i]; - - // Ignore urls and string without colon delimiters - if (option.search(/^(http|https|ftp):\/\//) !== -1 || - option.search(":") === -1) - { - break; - } - - delimiterIndex = option.indexOf(":"); - prop = option.substring(0, delimiterIndex); - val = option.substring(delimiterIndex + 1); - - // if val is an empty string, make it undefined - if (!val) { - val = undefined; - } - - // convert string value if it is like a boolean - if (typeof val === "string") { - val = val === "true" || (val === "false" ? false : val); - } - - // convert string value if it is like a number - if (typeof val === "string") { - val = !isNaN(val) ? +val : val; - } - - obj[prop] = val; - } - - // if nothing is parsed - if (prop == null && val == null) { - return str; - } - - return obj; - } - - /** - * Parse position option - * @param {String} str - * @returns {Object} - * @private - */ - function parsePosition(str) { - str = "" + str; - - // default value is a center - var args = str.split(/\s+/), - x = "50%", y = "50%", - len, arg, - i; - - for (i = 0, len = args.length; i < len; i++) { - arg = args[i]; - - // convert values - if (arg === "left") { - x = "0%"; - } else if (arg === "right") { - x = "100%"; - } else if (arg === "top") { - y = "0%"; - } else if (arg === "bottom") { - y = "100%"; - } else if (arg === "center") { - if (i === 0) { - x = "50%"; - } else { - y = "50%"; - } - } else { - if (i === 0) { - x = arg; - } else { - y = arg; - } - } - } - - return { x: x, y: y }; - } - - /** - * Search poster - * @param {String} path - * @param {Function} callback - * @private - */ - function findPoster(path, callback) { - var onLoad = function() { - callback(this.src); - }; - - $("<img src='" + path + ".gif'>").load(onLoad); - $("<img src='" + path + ".jpg'>").load(onLoad); - $("<img src='" + path + ".jpeg'>").load(onLoad); - $("<img src='" + path + ".png'>").load(onLoad); - } - - /** - * Vide constructor - * @param {HTMLElement} element - * @param {Object|String} path - * @param {Object|String} options - * @constructor - */ - function Vide(element, path, options) { - this.$element = $(element); - - // parse path - if (typeof path === "string") { - path = parseOptions(path); - } - - // parse options - if (!options) { - options = {}; - } else if (typeof options === "string") { - options = parseOptions(options); - } - - // remove extension - if (typeof path === "string") { - path = path.replace(/\.\w*$/, ""); - } else if (typeof path === "object") { - for (var i in path) { - if (path.hasOwnProperty(i)) { - path[i] = path[i].replace(/\.\w*$/, ""); - } - } - } - - this.settings = $.extend({}, defaults, options); - this.path = path; - - this.init(); - } - - /** - * Initialization - * @public - */ - Vide.prototype.init = function() { - var vide = this, - position = parsePosition(vide.settings.position), - sources, - poster; - - // Set video wrapper styles - vide.$wrapper = $("<div>").css({ - "position": "absolute", - "z-index": -1, - "top": 0, - "left": 0, - "bottom": 0, - "right": 0, - "overflow": "hidden", - "-webkit-background-size": "cover", - "-moz-background-size": "cover", - "-o-background-size": "cover", - "background-size": "cover", - "background-repeat": "no-repeat", - "background-position": position.x + " " + position.y - }); - - // Get poster path - poster = vide.path; - if (typeof vide.path === "object") { - if (vide.path.poster) { - poster = vide.path.poster; - } else { - if (vide.path.mp4) { - poster = vide.path.mp4; - } else if (vide.path.webm) { - poster = vide.path.webm; - } else if (vide.path.ogv) { - poster = vide.path.ogv; - } - } - } - - // Set video poster -// if (vide.settings.posterType === "detect") { -// findPoster(poster, function(url) { -// vide.$wrapper.css("background-image", "url(" + url + ")"); -// }); -// } else if (vide.settings.posterType !== "none") { -// vide.$wrapper -// .css("background-image", "url(" + poster + "." + vide.settings.posterType + ")"); -// } - - // if parent element has a static position, make it relative - if (vide.$element.css("position") === "static") { - vide.$element.css("position", "relative"); - } - - vide.$element.prepend(vide.$wrapper); - - if (!isIOS && !isAndroid) { - sources = ""; - - if (typeof vide.path === "object") { - if (vide.path.mp4) { - sources += "<source src='" + vide.path.mp4 + ".mp4' type='video/mp4'>"; - } - if (vide.path.webm) { - sources += "<source src='" + vide.path.webm + ".webm' type='video/webm'>"; - } - if (vide.path.ogv) { - sources += "<source src='" + vide.path.ogv + ".ogv' type='video/ogv'>"; - } - - vide.$video = $("<video>" + sources + "</video>"); - } else { - vide.$video = $("<video>" + - "<source src='" + vide.path + ".mp4' type='video/mp4'>" + - "<source src='" + vide.path + ".webm' type='video/webm'>" + - "<source src='" + vide.path + ".ogv' type='video/ogg'>" + - "</video>"); - } - - // Disable visibility, while loading - vide.$video.css("visibility", "hidden"); - - // Set video properties - vide.$video.prop({ - autoplay: vide.settings.autoplay, - loop: vide.settings.loop, - volume: vide.settings.volume, - muted: vide.settings.muted, - playbackRate: vide.settings.playbackRate - }); - - // Append video - vide.$wrapper.append(vide.$video); - - // Video alignment - vide.$video.css({ - "margin": "auto", - "position": "absolute", - "z-index": -1, - "top": position.y, - "left": position.x, - "-webkit-transform": "translate(-" + position.x + ", -" + position.y + ")", - "-ms-transform": "translate(-" + position.x + ", -" + position.y + ")", - "transform": "translate(-" + position.x + ", -" + position.y + ")" - }); - - // resize video, when it's loaded - vide.$video.bind("loadedmetadata." + pluginName, function() { - vide.$video.css("visibility", "visible"); - vide.resize(); - vide.$wrapper.css("background-image", "none"); - }); - - // resize event is available only for 'window', - // use another code solutions to detect DOM elements resizing - vide.$element.bind("resize." + pluginName, function() { - if (vide.settings.resizing) { - vide.resize(); - } - }); - } - }; - - /** - * Get video element of the background - * @returns {HTMLVideoElement|null} - * @public - */ - Vide.prototype.getVideoObject = function() { - return this.$video ? this.$video[0] : null; - }; - - /** - * Resize video background - * @public - */ - Vide.prototype.resize = function() { - if (!this.$video) { - return; - } - - var - // get native video size - videoHeight = this.$video[0].videoHeight, - videoWidth = this.$video[0].videoWidth, - - // get wrapper size - wrapperHeight = this.$wrapper.height(), - wrapperWidth = this.$wrapper.width(); - - if (wrapperWidth / videoWidth > wrapperHeight / videoHeight) { - this.$video.css({ - "width": wrapperWidth + 2, - - // +2 pixels to prevent empty space after transformation - "height": "auto" - }); - } else { - this.$video.css({ - "width": "auto", - - // +2 pixels to prevent empty space after transformation - "height": wrapperHeight + 2 - }); - } - }; - - /** - * Destroy video background - * @public - */ - Vide.prototype.destroy = function() { - this.$element.unbind(pluginName); - - if (this.$video) { - this.$video.unbind(pluginName); - } - - delete $[pluginName].lookup[this.index]; - this.$element.removeData(pluginName); - this.$wrapper.remove(); - }; - - /** - * Special plugin object for instances. - * @type {Object} - * @public - */ - $[pluginName] = { - lookup: [] - }; - - /** - * Plugin constructor - * @param {Object|String} path - * @param {Object|String} options - * @returns {JQuery} - * @constructor - */ - $.fn[pluginName] = function(path, options) { - var instance; - - this.each(function() { - instance = $.data(this, pluginName); - - if (instance) { - - // destroy plugin instance if exists - instance.destroy(); - } - - // create plugin instance - instance = new Vide(this, path, options); - instance.index = $[pluginName].lookup.push(instance) - 1; - $.data(this, pluginName, instance); - }); - - return this; - }; - - $(document).ready(function() { - - // window resize event listener - $(window).bind("resize." + pluginName, function() { - for (var len = $[pluginName].lookup.length, i = 0, instance; i < len; i++) { - instance = $[pluginName].lookup[i]; - - if (instance && instance.settings.resizing) { - instance.resize(); - } - } - }); - - // Auto initialization. - // Add 'data-vide-bg' attribute with a path to the video without extension. - // Also you can pass options throw the 'data-vide-options' attribute. - // 'data-vide-options' must be like "muted: false, volume: 0.5". - $(document).find("[data-" + pluginName + "-bg]").each(function(i, element) { - var $element = $(element), - options = $element.data(pluginName + "-options"), - path = $element.data(pluginName + "-bg"); - - $element[pluginName](path, options); - }); - }); -})(window.jQuery, window, document, navigator); diff --git a/plugins/video-on-login-screen/js/vide/jquery.vide.min.js b/plugins/video-on-login-screen/js/vide/jquery.vide.min.js deleted file mode 100644 index 20f3182cb3..0000000000 --- a/plugins/video-on-login-screen/js/vide/jquery.vide.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Vide - v0.3.0 - * Easy as hell jQuery plugin for video backgrounds. - * http://vodkabears.github.io/vide/ - * - * Made by Ilya Makarov - * Under MIT License - */ -!function(a,b,c,d){"use strict";function e(a){var b,c,d,e,f,g,h,i={};for(f=a.replace(/\s*:\s*/g,":").replace(/\s*,\s*/g,",").split(","),h=0,g=f.length;g>h&&(c=f[h],-1===c.search(/^(http|https|ftp):\/\//)&&-1!==c.search(":"));h++)b=c.indexOf(":"),d=c.substring(0,b),e=c.substring(b+1),e||(e=void 0),"string"==typeof e&&(e="true"===e||("false"===e?!1:e)),"string"==typeof e&&(e=isNaN(e)?e:+e),i[d]=e;return null==d&&null==e?a:i}function f(a){a=""+a;var b,c,d,e=a.split(/\s+/),f="50%",g="50%";for(d=0,b=e.length;b>d;d++)c=e[d],"left"===c?f="0%":"right"===c?f="100%":"top"===c?g="0%":"bottom"===c?g="100%":"center"===c?0===d?f="50%":g="50%":0===d?f=c:g=c;return{x:f,y:g}}function g(b,c){var d=function(){c(this.src)};a("<img src='"+b+".gif'>").load(d),a("<img src='"+b+".jpg'>").load(d),a("<img src='"+b+".jpeg'>").load(d),a("<img src='"+b+".png'>").load(d)}function h(b,c,d){if(this.$element=a(b),"string"==typeof c&&(c=e(c)),d?"string"==typeof d&&(d=e(d)):d={},"string"==typeof c)c=c.replace(/\.\w*$/,"");else if("object"==typeof c)for(var f in c)c.hasOwnProperty(f)&&(c[f]=c[f].replace(/\.\w*$/,""));this.settings=a.extend({},j,d),this.path=c,this.init()}var i="vide",j={volume:1,playbackRate:1,muted:!0,loop:!0,autoplay:!0,position:"50% 50%",posterType:"detect",resizing:!0},k=/iPad|iPhone|iPod/i.test(d.userAgent),l=/Android/i.test(d.userAgent);h.prototype.init=function(){var b,c,d=this,e=f(d.settings.position);d.$wrapper=a("<div>").css({position:"absolute","z-index":-1,top:0,left:0,bottom:0,right:0,overflow:"hidden","-webkit-background-size":"cover","-moz-background-size":"cover","-o-background-size":"cover","background-size":"cover","background-repeat":"no-repeat","background-position":e.x+" "+e.y}),c=d.path,"object"==typeof d.path&&(d.path.poster?c=d.path.poster:d.path.mp4?c=d.path.mp4:d.path.webm?c=d.path.webm:d.path.ogv&&(c=d.path.ogv)),"detect"===d.settings.posterType?g(c,function(a){d.$wrapper.css("background-image","url("+a+")")}):"none"!==d.settings.posterType&&d.$wrapper.css("background-image","url("+c+"."+d.settings.posterType+")"),"static"===d.$element.css("position")&&d.$element.css("position","relative"),d.$element.prepend(d.$wrapper),k||l||(b="","object"==typeof d.path?(d.path.mp4&&(b+="<source src='"+d.path.mp4+".mp4' type='video/mp4'>"),d.path.webm&&(b+="<source src='"+d.path.webm+".webm' type='video/webm'>"),d.path.ogv&&(b+="<source src='"+d.path.ogv+".ogv' type='video/ogv'>"),d.$video=a("<video>"+b+"</video>")):d.$video=a("<video><source src='"+d.path+".mp4' type='video/mp4'><source src='"+d.path+".webm' type='video/webm'><source src='"+d.path+".ogv' type='video/ogg'></video>"),d.$video.css("visibility","hidden"),d.$video.prop({autoplay:d.settings.autoplay,loop:d.settings.loop,volume:d.settings.volume,muted:d.settings.muted,playbackRate:d.settings.playbackRate}),d.$wrapper.append(d.$video),d.$video.css({margin:"auto",position:"absolute","z-index":-1,top:e.y,left:e.x,"-webkit-transform":"translate(-"+e.x+", -"+e.y+")","-ms-transform":"translate(-"+e.x+", -"+e.y+")",transform:"translate(-"+e.x+", -"+e.y+")"}),d.$video.bind("loadedmetadata."+i,function(){d.$video.css("visibility","visible"),d.resize(),d.$wrapper.css("background-image","none")}),d.$element.bind("resize."+i,function(){d.settings.resizing&&d.resize()}))},h.prototype.getVideoObject=function(){return this.$video?this.$video[0]:null},h.prototype.resize=function(){if(this.$video){var a=this.$video[0].videoHeight,b=this.$video[0].videoWidth,c=this.$wrapper.height(),d=this.$wrapper.width();this.$video.css(d/b>c/a?{width:d+2,height:"auto"}:{width:"auto",height:c+2})}},h.prototype.destroy=function(){this.$element.unbind(i),this.$video&&this.$video.unbind(i),delete a[i].lookup[this.index],this.$element.removeData(i),this.$wrapper.remove()},a[i]={lookup:[]},a.fn[i]=function(b,c){var d;return this.each(function(){d=a.data(this,i),d&&d.destroy(),d=new h(this,b,c),d.index=a[i].lookup.push(d)-1,a.data(this,i,d)}),this},a(c).ready(function(){a(b).bind("resize."+i,function(){for(var b,c=a[i].lookup.length,d=0;c>d;d++)b=a[i].lookup[d],b&&b.settings.resizing&&b.resize()}),a(c).find("[data-"+i+"-bg]").each(function(b,c){var d=a(c),e=d.data(i+"-options"),f=d.data(i+"-bg");d[i](f,e)})})}(window.jQuery,window,document,navigator); \ No newline at end of file diff --git a/plugins/video-on-login-screen/js/video-on-login.js b/plugins/video-on-login-screen/js/video-on-login.js old mode 100644 new mode 100755 index 4871289986..d5d497a62c --- a/plugins/video-on-login-screen/js/video-on-login.js +++ b/plugins/video-on-login-screen/js/video-on-login.js @@ -1,10 +1,13 @@ -(function ($, window) { +(rl => { - $(function () { + rl && addEventListener('rl-view-model', e => { + const id = e.detail.viewModelTemplateID; + if (e.detail && ('AdminLogin' === id || 'Login' === id)) { + let + nId = null, + script; - if (window.rl && window.rl && !window.rl.settingsGet('Auth')) - { - var + let iRate = 1, sRate = window.rl.pluginSettingsGet('video-on-login-screen', 'playback_rate') ; @@ -30,18 +33,75 @@ iRate = 2; break; } + const + mode = 'Login' === id ? 'user' : 'admin', - $('#rl-bg').vide({ - 'mp4': window.rl.pluginSettingsGet('video-on-login-screen', 'mp4_file') || '', - 'webm': window.rl.pluginSettingsGet('video-on-login-screen', 'webm_file') || '', - 'ogv': window.rl.pluginSettingsGet('video-on-login-screen', 'ogv_file') || '' - }, { - playbackRate: iRate, - muted: !!window.rl.pluginSettingsGet('video-on-login-screen', 'muted') + 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)); - +})(window.rl); diff --git a/plugins/view-ics/LICENSE b/plugins/view-ics/LICENSE new file mode 100644 index 0000000000..9e5e56cdd9 --- /dev/null +++ b/plugins/view-ics/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2022 SnappyMail + +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/view-ics/ical.js b/plugins/view-ics/ical.js new file mode 100644 index 0000000000..9813ebe7c2 --- /dev/null +++ b/plugins/view-ics/ical.js @@ -0,0 +1,869 @@ +// https://www.rfc-editor.org/rfc/rfc5545 + +(rl => { + + const + paramRegEx = /;([a-zA-Z0-9-]+)=([^";:,]*|"[^"]*")/g, + // eslint-disable-next-line max-len + dateRegEx = /(?<year>[0-9]{4})(?<month>[0-9]{2})(?<day>[0-9]{2})T(?<hour>[0-9]{2})(?<minute>[0-9]{2})(?<second>[0-9]{2})(?<utc>Z?)/, + parseDate = (str, tz) => { + 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)); + tz && (options.timeZone = windowsVTIMEZONEs[tz] || tz); + try { + return date.format(options); + } catch (e) { + console.error(e); + if (options.timeZone) { + options.timeZone = undefined; + return date.format(options); + } + } + }; + + class Property extends String + { + constructor(name, value, params) { + super(value); + this.name = name; +// this.value = value; + this.params = {}; + if (params) { + for (const param of params.matchAll(paramRegEx)) { + if (param[2]) { + this.params[param[1]] = param[2].replace(/^"|"$/); + } + } + } + } +/* + includes(...params) { + return this.value.includes(...params); + } + replace(...params) { + return this.value.replace(...params); + } + toString() { + return this.value; + } +*/ + static fromString(data) { + // Parse component property 'name *(";" param ) ":" value' + const match = data.match(/^([a-zA-Z0-9-]+)((?:;[a-zA-Z0-9-]+=(?:[^";:,]*|"[^"]*"))*):(.+)$/); + return match && new Property(match[1], match[3], match[2]); + } + } + + rl.ICS = { + + parseDate: parseDate, + + parseEvent(text) { + let VEVENT, + VALARM, + multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE', + 'EXRULE','RSTATUS','RELATED','RESOURCES','RDATE','RRULE'], + lines = text.split(/\r?\n/), + i = lines.length; + while (i--) { + let line = lines[i], prop; + if (VEVENT) { + while (line.startsWith(' ') && i--) { + line = lines[i] + line.slice(1); + } + if (line.startsWith('END:VALARM')) { + // Start parsing Alarm Component + VALARM = {}; + continue; + } else if (line.startsWith('BEGIN:VALARM')) { + // End parsing Alarm Component + VEVENT.VALARM || (VEVENT.VALARM = []); + VEVENT.VALARM.push(VALARM); + VALARM = null; + continue; + } else if (line.startsWith('BEGIN:VEVENT')) { + // End parsing + break; + } + prop = Property.fromString(line); + if (prop) { + if (VALARM) { + VALARM[prop.name] = prop; + } else if (multiple.includes(prop.name) || 'X-' == prop.name.slice(0,2)) { + VEVENT[prop.name] || (VEVENT[prop.name] = []); + VEVENT[prop.name].push(prop); + } else { + if ('DTSTART' === prop.name || 'DTEND' === prop.name) { + prop = parseDate(prop, prop.params.TZID); + } + VEVENT[prop.name] = prop; + } + } + } else if (line.startsWith('END:VEVENT')) { + // Start parsing Event Component + VEVENT = {}; + } + } +// METHOD:REPLY || METHOD:REQUEST + if (VEVENT) { + VEVENT.rawText = text; + VEVENT.isCancelled = () => VEVENT.STATUS?.includes('CANCELLED'); + VEVENT.isConfirmed = () => VEVENT.STATUS?.includes('CONFIRMED'); + VEVENT.shouldReply = () => VEVENT.METHOD?.includes('REPLY'); + console.dir({ + VEVENT, + isCancelled: VEVENT.isCancelled(), + shouldReply: VEVENT.shouldReply() + }); + } + + return VEVENT; + } + }; + + // Microsoft Windows timezones + // Subset from https://github.com/unicode-cldr/cldr-core/blob/master/supplemental/windowsZones.json + const windowsVTIMEZONEs = { + // 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" + */], + }; +})(window.rl); diff --git a/plugins/view-ics/index.php b/plugins/view-ics/index.php new file mode 100644 index 0000000000..f12253bc7d --- /dev/null +++ b/plugins/view-ics/index.php @@ -0,0 +1,19 @@ +<?php + +class ViewICSPlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'View ICS', + VERSION = '2.2', + RELEASE = '2024-06-29', + CATEGORY = 'Messages', + DESCRIPTION = 'Display ICS attachment or JSON-LD details', + REQUIRED = '2.34.0'; + + public function Init() : void + { +// $this->UseLangs(true); + $this->addJs('ical.js'); + $this->addJs('message.js'); + } +} diff --git a/plugins/view-ics/message.js b/plugins/view-ics/message.js new file mode 100644 index 0000000000..551cccff9b --- /dev/null +++ b/plugins/view-ics/message.js @@ -0,0 +1,85 @@ +(rl => { + const templateId = 'MailMessageView'; + + addEventListener('rl-view-model.create', e => { + if (templateId === e.detail.viewModelTemplateID) { + + const + template = document.getElementById(templateId), + view = e.detail, + attachmentsPlace = template.content.querySelector('.attachmentsPlace'); + + attachmentsPlace.after(Element.fromHTML(` + <details data-bind="if: viewICS, visible: viewICS"> + <summary data-icon="📅" data-bind="text: viewICS().SUMMARY"></summary> + <table><tbody style="white-space:pre"> + <tr data-bind="visible: viewICS().ORGANIZER"><td>Organizer</td><td data-bind="text: viewICS().ORGANIZER"></td></tr> + <tr><td>Start</td><td data-bind="text: viewICS().DTSTART"></td></tr> + <tr><td>End</td><td data-bind="text: viewICS().DTEND"></td></tr> +<!-- <tr><td>Transparency</td><td data-bind="text: viewICS().TRANSP"></td></tr>--> + <tr data-bind="foreach: viewICS().ATTENDEE"> + <td></td><td data-bind="text: $data.replace(/;/g,';\\n')"></td> + </tr> + </tbody></table> + </details>`)); + + 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: rl.ICS.parseDate(item["ical:dtstart"]), +// DTEND: rl.ICS.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) { + // fetch it and parse the VEVENT + rl.fetch(ics.linkDownload()) + .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) + .then(text => { + let VEVENT = rl.ICS.parseEvent(text); + if (VEVENT) { + view.viewICS(VEVENT); + } + }); + } + } + }); + } + }); + +})(window.rl); diff --git a/plugins/view-ics/style.css b/plugins/view-ics/style.css new file mode 100644 index 0000000000..b3a4e5c8ce --- /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/virtualmin-change-password/LICENSE b/plugins/virtualmin-change-password/LICENSE deleted file mode 100644 index c364cecf0d..0000000000 --- a/plugins/virtualmin-change-password/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 icedman21 - -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/virtualmin-change-password/README b/plugins/virtualmin-change-password/README deleted file mode 100644 index 58f2e03781..0000000000 --- a/plugins/virtualmin-change-password/README +++ /dev/null @@ -1 +0,0 @@ -This plugin utilizes Virtualmin's remote API to change user passwords. The plugin requires the Admin user name and password to succesfully execute the password change. The host and port where Virtualmin listens on is also needed. See https://www.virtualmin.com/documentation/developer/http for more information. \ No newline at end of file diff --git a/plugins/virtualmin-change-password/VERSION b/plugins/virtualmin-change-password/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/virtualmin-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/virtualmin-change-password/VirtualminChangePasswordDriver.php b/plugins/virtualmin-change-password/VirtualminChangePasswordDriver.php deleted file mode 100644 index c4fbd72d63..0000000000 --- a/plugins/virtualmin-change-password/VirtualminChangePasswordDriver.php +++ /dev/null @@ -1,210 +0,0 @@ -<?php - -/* - * This Virtualmin Password Change Plugin was developed by Icedman21 - * http://icedman21.com - */ -class VirtualminChangePasswordDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $sAllowedEmails = ''; - - /** - * @var string - */ - private $sHost = ''; - - /** - * @var string - */ - private $sAdminUser = ''; - - /** - * @var string - */ - private $sAdminPassword = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @param string $sHost - * @param string $sAdminUser - * @param string $sAdminPassword - * - * @return \VirtualminChangePasswordDriver - */ - public function SetConfig($sHost, $sAdminUser, $sAdminPassword) - { - $this->sHost = $sHost; - $this->sAdminUser = $sAdminUser; - $this->sAdminPassword = $sAdminPassword; - - return $this; - } - - /** - * @param string $sAllowedEmails - * - * @return \VirtualminChangePasswordDriver - */ - public function SetAllowedEmails($sAllowedEmails) - { - $this->sAllowedEmails = $sAllowedEmails; - - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \VirtualminChangePasswordDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param string $sDesc - * @param int $iType = \MailSo\Log\Enumerations\Type::INFO - * - * @return \VirtualminChangePasswordDriver - */ - public function WriteLog($sDesc, $iType = \MailSo\Log\Enumerations\Type::INFO) - { - if ($this->oLogger) - { - $this->oLogger->Write($sDesc, $iType); - } - - return $this; - } - - /** - * @param \RainLoop\Model\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return $oAccount && $oAccount->Email() && - \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails); - } - - /** - * @param \RainLoop\Model\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - $this->WriteLog('Virtualmin: Try to change password for '.$oAccount->Email()); - - $bResult = false; - if (!empty($this->sHost) && !empty($this->sAdminUser) && !empty($this->sAdminPassword) && $oAccount) - { - $this->WriteLog('Virtualmin:[Check] Required Fields Present'); - - $sEmail = \trim(\strtolower($oAccount->Email())); - $sEmailUser = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail); - $sEmailDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail); - - $sHost = \rtrim(\trim($this->sHost), '/'); - $sUrl = $sHost.'/virtual-server/remote.cgi'; - - $sAdminUser = $this->sAdminUser; - $sAdminPassword = $this->sAdminPassword; - - $iCode = 0; - - $aPost = array( - 'user' => $sEmailUser, - 'pass' => $sNewPassword, - 'domain' => $sEmailDomain, - 'program' => 'modify-user' - ); - - $aOptions = array( - CURLOPT_URL => $sUrl, - CURLOPT_HEADER => false, - CURLOPT_FAILONERROR => true, - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => \http_build_query($aPost, '', '&'), - CURLOPT_TIMEOUT => 20, - CURLOPT_SSL_VERIFYHOST => false, - CURLOPT_USERPWD => $sAdminUser.':'.$sAdminPassword - ); - - $oCurl = \curl_init(); - \curl_setopt_array($oCurl, $aOptions); - - $this->WriteLog('Virtualmin: Send post request: '.$sUrl); - - $mResult = \curl_exec($oCurl); - - $iCode = (int) \curl_getinfo($oCurl, CURLINFO_HTTP_CODE); - $sContentType = (string) \curl_getinfo($oCurl, CURLINFO_CONTENT_TYPE); - - $this->WriteLog('Virtualmin: Post request result: (Status: '.$iCode.', ContentType: '.$sContentType.')'); - if (false === $mResult || 200 !== $iCode) - { - $this->WriteLog('Virtualmin: Error: '.\curl_error($oCurl), \MailSo\Log\Enumerations\Type::WARNING); - } - - if (\is_resource($oCurl)) - { - \curl_close($oCurl); - } - - if (false !== $mResult && 200 === $iCode) - { - $aRes = null; - @\parse_str($mResult, $aRes); - if (\is_array($aRes) && (!isset($aRes['error']) || (int) $aRes['error'] !== 1)) - { - $iPos = \strpos($mResult, 'Exit status: '); - - if ($iPos !== false) - { - $aStatus = \explode(' ', $mResult); - $sStatus = \trim(\array_pop($aStatus)); - - if ('0' === $sStatus) - { - $this->WriteLog('Virtualmin: Password Change Status: Success'); - $bResult = true; - } - else - { - $this->WriteLog('Virtualmin[Error]: Response: '.$mResult); - } - } - } - else - { - $this->WriteLog('Virtualmin[Error]: Response: '.$mResult); - } - } - else - { - $this->WriteLog('Virtualmin[Error]: Empty Response: Code: '.$iCode); - } - } - - return $bResult; - } -} diff --git a/plugins/virtualmin-change-password/index.php b/plugins/virtualmin-change-password/index.php deleted file mode 100644 index b4c0a86f66..0000000000 --- a/plugins/virtualmin-change-password/index.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php - -class VirtualminChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - include_once __DIR__.'/VirtualminChangePasswordDriver.php'; - - $sHost = \trim($this->Config()->Get('plugin', 'host', '')); - $sAdminUser = (string) $this->Config()->Get('plugin', 'admin_user', ''); - $sAdminPassword = (string) $this->Config()->Get('plugin', 'admin_password', ''); - - $oProvider = new \VirtualminChangePasswordDriver(); - $oProvider->SetLogger($this->Manager()->Actions()->Logger()); - $oProvider->SetConfig($sHost, $sAdminUser, $sAdminPassword); - $oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', '')))); - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('host')->SetLabel('Virtualmin Host') - ->SetDefaultValue('https://localhost:10000') - ->SetDescription('Virtualmin host URL. Example: https://example.com:10000'), - \RainLoop\Plugins\Property::NewInstance('admin_user')->SetLabel('Admin User') - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('admin_password')->SetLabel('Admin Password') - ->SetDefaultValue(''), - \RainLoop\Plugins\Property::NewInstance('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('*') - ); - } -} diff --git a/plugins/vpopmail-change-password/ChangePasswordVpopmailDriver.php b/plugins/vpopmail-change-password/ChangePasswordVpopmailDriver.php deleted file mode 100755 index 7013d226e2..0000000000 --- a/plugins/vpopmail-change-password/ChangePasswordVpopmailDriver.php +++ /dev/null @@ -1,228 +0,0 @@ -<?php - -class ChangePasswordVpopmailDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface -{ - /** - * @var string - */ - private $mHost = 'localhost'; - - /** - * @var string - */ - private $mUser = ''; - - /** - * @var string - */ - private $mPass = ''; - - /** - * @var string - */ - private $mDatabase = ''; - - /** - * @var string - */ - private $mTable = ''; - - /** - * @var string - */ - private $mColumn = ''; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; - - /** - * @var array - */ - private $aDomains = array(); - - /** - * @param string $mHost - * - * @return \ChangePasswordVpopmailDriver - */ - public function SetmHost($mHost) - { - $this->mHost = $mHost; - return $this; - } - - /** - * @param string $mUser - * - * @return \ChangePasswordVpopmailDriver - */ - public function SetmUser($mUser) - { - $this->mUser = $mUser; - return $this; - } - - /** - * @param string $mPass - * - * @return \ChangePasswordVpopmailDriver - */ - public function SetmPass($mPass) - { - $this->mPass = $mPass; - return $this; - } - - /** - * @param string $mDatabase - * - * @return \ChangePasswordVpopmailDriver - */ - public function SetmDatabase($mDatabase) - { - $this->mDatabase = $mDatabase; - return $this; - } - - /** - * @param string $mTable - * - * @return \ChangePasswordVpopmailDriver - */ - public function SetmTable($mTable) - { - $this->mTable = $mTable; - return $this; - } - - /** - * @param string $mColumn - * - * @return \ChangePasswordVpopmailDriver - */ - public function SetmColumn($mColumn) - { - $this->mColumn = $mColumn; - return $this; - } - - /** - * @param \MailSo\Log\Logger $oLogger - * - * @return \ChangePasswordVpopmailDriver - */ - public function SetLogger($oLogger) - { - if ($oLogger instanceof \MailSo\Log\Logger) - { - $this->oLogger = $oLogger; - } - - return $this; - } - - /** - * @param array $aDomains - * - * @return bool - */ - public function SetAllowedDomains($aDomains) - { - if (\is_array($aDomains) && 0 < \count($aDomains)) - { - $this->aDomains = $aDomains; - } - - return $this; - } - - /** - * @param \RainLoop\Account $oAccount - * - * @return bool - */ - public function PasswordChangePossibility($oAccount) - { - return 0 === \count($this->aDomains) || ($oAccount && \in_array(\strtolower( - \MailSo\Base\Utils::GetDomainFromEmail($oAccount->Email)), $this->aDomains)); - } - - /** - * @param \RainLoop\Account $oAccount - * @param string $sPrevPassword - * @param string $sNewPassword - * - * @return bool - */ - public function ChangePassword(\RainLoop\Account $oAccount, $sPrevPassword, $sNewPassword) - { - if ($this->oLogger) - { - $this->oLogger->Write('Try to change password for '.$oAccount->Email()); - } - - if (empty($this->mHost) || empty($this->mDatabase) || empty($this->mColumn) || empty($this->mTable)) - { - return false; - } - - $bResult = false; - - $sDsn = 'mysql:host='.$this->mHost.';dbname='.$this->mDatabase.';charset=utf8'; - $aOptions = array( - PDO::ATTR_EMULATE_PREPARES => false, - PDO::ATTR_PERSISTENT => true, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - ); - - $sLoginPart = \MailSo\Base\Utils::GetAccountNameFromEmail($oAccount->Email()); - $sDomainPart = \MailSo\Base\Utils::GetDomainFromEmail($oAccount->Email()); - - try - { - $oConn = new PDO($sDsn, $this->mUser, $this->mPass, $aOptions); - - $oSelect = $oConn->prepare('SELECT '.$this->mColumn.' FROM '.$this->mTable.' WHERE pw_name=? AND pw_domain=? LIMIT 1'); - $oSelect->execute(array($sLoginPart, $sDomainPart)); - - $aColCrypt = $oSelect->fetchAll(PDO::FETCH_ASSOC); - - $sCryptPass = isset($aColCrypt[0][$this->mColumn]) ? $aColCrypt[0][$this->mColumn] : ''; - if (0 < \strlen($sCryptPass) && \crypt($sPrevPassword, $sCryptPass) === $sCryptPass) - { - $oUpdate = $oConn->prepare('UPDATE '.$this->mTable.' SET '.$this->mColumn.'=ENCRYPT(?,concat("$1$",right(md5(rand()), 8 ),"$")), pw_clear_passwd=\'\' WHERE pw_name=? AND pw_domain=?'); - $oUpdate->execute(array( - $sNewPassword, - $sLoginPart, - $sDomainPart - )); - - $bResult = true; - if ($this->oLogger) - { - $this->oLogger->Write('Success! Password changed.'); - } - } - else - { - $bResult = false; - if ($this->oLogger) - { - $this->oLogger->Write('Something went wrong. Either current password is incorrect, or new password does not match criteria.'); - } - } - } - catch (\Exception $oException) - { - $bResult = false; - if ($this->oLogger) - { - $this->oLogger->WriteException($oException); - } - } - - return $bResult; - } -} diff --git a/plugins/vpopmail-change-password/LICENSE b/plugins/vpopmail-change-password/LICENSE deleted file mode 100755 index 6739d4b4e1..0000000000 --- a/plugins/vpopmail-change-password/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Almas at Dusal.net - -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/vpopmail-change-password/VERSION b/plugins/vpopmail-change-password/VERSION deleted file mode 100755 index 9459d4ba2a..0000000000 --- a/plugins/vpopmail-change-password/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 diff --git a/plugins/vpopmail-change-password/index.php b/plugins/vpopmail-change-password/index.php deleted file mode 100755 index 8eb5fd5fe7..0000000000 --- a/plugins/vpopmail-change-password/index.php +++ /dev/null @@ -1,71 +0,0 @@ -<?php - -class ChangePasswordVpopmailPlugin extends \RainLoop\Plugins\AbstractPlugin -{ - public function Init() - { - $this->addHook('main.fabrica', 'MainFabrica'); - } - - /** - * @param string $sName - * @param mixed $oProvider - */ - public function MainFabrica($sName, &$oProvider) - { - switch ($sName) - { - case 'change-password': - - include_once __DIR__.'/ChangePasswordVpopmailDriver.php'; - - $oProvider = new ChangePasswordVpopmailDriver(); - - $sDomains = \strtolower(\trim(\preg_replace('/[\s;,]+/', ' ', - $this->Config()->Get('plugin', 'domains', '')))); - - if (0 < \strlen($sDomains)) - { - $aDomains = \explode(' ', $sDomains); - $oProvider->SetAllowedDomains($aDomains); - } - - $oProvider - ->SetLogger($this->Manager()->Actions()->Logger()) - ->SetmHost($this->Config()->Get('plugin', 'mHost', '')) - ->SetmUser($this->Config()->Get('plugin', 'mUser', '')) - ->SetmPass($this->Config()->Get('plugin', 'mPass', '')) - ->SetmDatabase($this->Config()->Get('plugin', 'mDatabase', '')) - ->SetmTable($this->Config()->Get('plugin', 'mTable', '')) - ->SetmColumn($this->Config()->Get('plugin', 'mColumn', '')) - ; - - break; - } - } - - /** - * @return array - */ - public function configMapping() - { - return array( - \RainLoop\Plugins\Property::NewInstance('domains')->SetLabel('Allowed Domains') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) - ->SetDescription('Allowed domains, space as delimiter') - ->SetDefaultValue('gmail.com yahoo.com'), - \RainLoop\Plugins\Property::NewInstance('mHost')->SetLabel('MySQL Host') - ->SetDefaultValue('localhost'), - \RainLoop\Plugins\Property::NewInstance('mUser')->SetLabel('MySQL User') - ->SetDefaultValue('vpopmail'), - \RainLoop\Plugins\Property::NewInstance('mPass')->SetLabel('MySQL Password') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD), - \RainLoop\Plugins\Property::NewInstance('mDatabase')->SetLabel('MySQL Database') - ->SetDefaultValue('vpopmail'), - \RainLoop\Plugins\Property::NewInstance('mTable')->SetLabel('MySQL Table') - ->SetDefaultValue('vpopmail'), - \RainLoop\Plugins\Property::NewInstance('mColumn')->SetLabel('MySQL Column') - ->SetDefaultValue('pw_passwd') - ); - } -} diff --git a/plugins/white-list/README b/plugins/white-list/README deleted file mode 100644 index 64fb7dc2a9..0000000000 --- a/plugins/white-list/README +++ /dev/null @@ -1 +0,0 @@ -Simple white list plugin \ No newline at end of file diff --git a/plugins/white-list/VERSION b/plugins/white-list/VERSION deleted file mode 100644 index 9f8e9b69a3..0000000000 --- a/plugins/white-list/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0 \ No newline at end of file diff --git a/plugins/white-list/index.php b/plugins/white-list/index.php index 547c001c1c..2b91b8bcc9 100644 --- a/plugins/white-list/index.php +++ b/plugins/white-list/index.php @@ -2,29 +2,33 @@ class WhiteListPlugin extends \RainLoop\Plugins\AbstractPlugin { - public function Init() + const + NAME = 'Whitelist', + 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('filter.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 + ); } } } @@ -32,13 +36,13 @@ public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword) /** * @return array */ - public function configMapping() + protected function configMapping() : array { return array( \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 0000000000..a26fe51585 --- /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 0000000000..c52d411f3e --- /dev/null +++ b/plugins/wysiwyg-example/index.php @@ -0,0 +1,41 @@ +<?php + +class WysiwygExamplePlugin extends \RainLoop\Plugins\AbstractPlugin +{ + const + NAME = 'WYSIWYG Example', + VERSION = '1.0', + RELEASE = '2024-01-30', + REQUIRED = '2.34.0', + DESCRIPTION = 'Add Example as WYSIWYG editor option'; + + public function Init() : void + { + $path = APP_VERSION_ROOT_PATH . 'static/wysiwyg-example'; + + // Apache AH00037: Symbolic link not allowed or link target not accessible + // That's why we clone the source + if (!\is_dir($path)/* && !\is_link($path)*/) { + $old_mask = umask(0022); + // $active = \symlink(__DIR__ . '/src', $path); + \mkdir($path, 0755); + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator(__DIR__ . '/static', \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($iterator as $item) { + if ($item->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 0000000000..8d1c8b69c3 --- /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 0000000000..30f6613ea0 --- /dev/null +++ b/public_html/translate.php @@ -0,0 +1,197 @@ +<?php + +$lang = (empty($_GET['lang']) || !preg_match('/^[a-z]{2}(-[A-Z]{2})?$/D',$_GET['lang'])) ? '' : $_GET['lang']; + +function toJSON($data) +{ + foreach ($data as $section => $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 = ['<option></option>']; +foreach (glob("{$root}/*", GLOB_ONLYDIR) as $dir) { + $name = basename($dir); + if ('en' !== $name) { + $languages[$name] = "<option value='{$name}'".($lang == $name ? ' selected' : '').">{$name}</option>"; + } +} +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] = "<option value='{$key}'".($lang == $key ? ' selected' : '').">{$name}</option>"; + } +} + +//print_r($languages); + +echo '<!DOCTYPE html> +<html><head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width"> + <title>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 '
'; + +/* + - - - - - - - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Admin/PopupsDomainAlias.html b/rainloop/v/0.0.0/app/templates/Views/Admin/PopupsDomainAlias.html deleted file mode 100644 index 0426d8d1ea..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Admin/PopupsDomainAlias.html +++ /dev/null @@ -1,53 +0,0 @@ -
- -
\ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Admin/PopupsPlugin.html b/rainloop/v/0.0.0/app/templates/Views/Admin/PopupsPlugin.html deleted file mode 100644 index 58c4594a63..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Admin/PopupsPlugin.html +++ /dev/null @@ -1,41 +0,0 @@ -
- -
\ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Common/Cmd.html b/rainloop/v/0.0.0/app/templates/Views/Common/Cmd.html deleted file mode 100644 index 9cd70b5f39..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Common/Cmd.html +++ /dev/null @@ -1,10 +0,0 @@ -
-
-
-
-
-
#
- -
-
-
\ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Common/Pagenator.html b/rainloop/v/0.0.0/app/templates/Views/Common/Pagenator.html deleted file mode 100644 index 7609a9793a..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Common/Pagenator.html +++ /dev/null @@ -1,5 +0,0 @@ - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Common/PopupsAsk.html b/rainloop/v/0.0.0/app/templates/Views/Common/PopupsAsk.html deleted file mode 100644 index 93df1f523b..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Common/PopupsAsk.html +++ /dev/null @@ -1,26 +0,0 @@ -
- -
diff --git a/rainloop/v/0.0.0/app/templates/Views/Common/PopupsKeyboardShortcutsHelp.html b/rainloop/v/0.0.0/app/templates/Views/Common/PopupsKeyboardShortcutsHelp.html deleted file mode 100644 index d8fe5d5d3c..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Common/PopupsKeyboardShortcutsHelp.html +++ /dev/null @@ -1,90 +0,0 @@ -
- -
diff --git a/rainloop/v/0.0.0/app/templates/Views/Common/PopupsLanguages.html b/rainloop/v/0.0.0/app/templates/Views/Common/PopupsLanguages.html deleted file mode 100644 index 03e3297bac..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Common/PopupsLanguages.html +++ /dev/null @@ -1,22 +0,0 @@ -
- -
\ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Common/PopupsWelcomePage.html b/rainloop/v/0.0.0/app/templates/Views/Common/PopupsWelcomePage.html deleted file mode 100644 index 0edb0665b7..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Common/PopupsWelcomePage.html +++ /dev/null @@ -1,22 +0,0 @@ -
- -
\ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Components/Checkbox.html b/rainloop/v/0.0.0/app/templates/Views/Components/Checkbox.html deleted file mode 100644 index d710e0f500..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Components/Checkbox.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -    - - - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Components/CheckboxClassic.html b/rainloop/v/0.0.0/app/templates/Views/Components/CheckboxClassic.html deleted file mode 100644 index b275485c8a..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Components/CheckboxClassic.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Components/CheckboxMaterialDesign.html b/rainloop/v/0.0.0/app/templates/Views/Components/CheckboxMaterialDesign.html deleted file mode 100644 index cbd3dcc489..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Components/CheckboxMaterialDesign.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Components/Date.html b/rainloop/v/0.0.0/app/templates/Views/Components/Date.html deleted file mode 100644 index a680e77966..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Components/Date.html +++ /dev/null @@ -1,14 +0,0 @@ - - -   - -   - - -   -
- \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Components/Input.html b/rainloop/v/0.0.0/app/templates/Views/Components/Input.html deleted file mode 100644 index d05f962a33..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Components/Input.html +++ /dev/null @@ -1,14 +0,0 @@ - - -   - -   - - -   -
- \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Components/Radio.html b/rainloop/v/0.0.0/app/templates/Views/Components/Radio.html deleted file mode 100644 index 4a1e0d0bbf..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Components/Radio.html +++ /dev/null @@ -1,7 +0,0 @@ -
- -
\ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Components/SaveTrigger.html b/rainloop/v/0.0.0/app/templates/Views/Components/SaveTrigger.html deleted file mode 100644 index 44c2f1cb1b..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Components/SaveTrigger.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
\ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Components/Select.html b/rainloop/v/0.0.0/app/templates/Views/Components/Select.html deleted file mode 100644 index 5d65327169..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Components/Select.html +++ /dev/null @@ -1,20 +0,0 @@ -
- - -    - - - -   - -   - - -   -
- -
\ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/Components/TextArea.html b/rainloop/v/0.0.0/app/templates/Views/Components/TextArea.html deleted file mode 100644 index 3565e30b7a..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/Components/TextArea.html +++ /dev/null @@ -1,9 +0,0 @@ - - -   -
- \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/About.html b/rainloop/v/0.0.0/app/templates/Views/User/About.html deleted file mode 100644 index e1b57af096..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/About.html +++ /dev/null @@ -1,6 +0,0 @@ -
-

RainLoop Webmail

-     -

()

-

https://www.rainloop.net

-
\ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/ComposeAttachment.html b/rainloop/v/0.0.0/app/templates/Views/User/ComposeAttachment.html deleted file mode 100644 index 15ac4710c3..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/ComposeAttachment.html +++ /dev/null @@ -1,19 +0,0 @@ -
  • -
    -
    - - -
    -
    -
    -
    -
    - -
    -   -
    -
    -   -
    -
    -
  • \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/Login.html b/rainloop/v/0.0.0/app/templates/Views/User/Login.html deleted file mode 100644 index daaeb1e893..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/Login.html +++ /dev/null @@ -1,170 +0,0 @@ - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/LoginWelcome.html b/rainloop/v/0.0.0/app/templates/Views/User/LoginWelcome.html deleted file mode 100644 index c156b8c647..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/LoginWelcome.html +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/MailFolderList.html b/rainloop/v/0.0.0/app/templates/Views/User/MailFolderList.html deleted file mode 100644 index 3cf924b469..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/MailFolderList.html +++ /dev/null @@ -1,39 +0,0 @@ -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/MailFolderListItem.html b/rainloop/v/0.0.0/app/templates/Views/User/MailFolderListItem.html deleted file mode 100644 index 7ae0e600c1..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/MailFolderListItem.html +++ /dev/null @@ -1,12 +0,0 @@ - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/MailFolderListSystemItem.html b/rainloop/v/0.0.0/app/templates/Views/User/MailFolderListSystemItem.html deleted file mode 100644 index f26adba741..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/MailFolderListSystemItem.html +++ /dev/null @@ -1,12 +0,0 @@ - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/MailMessageList.html b/rainloop/v/0.0.0/app/templates/Views/User/MailMessageList.html deleted file mode 100644 index 478325b678..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/MailMessageList.html +++ /dev/null @@ -1,218 +0,0 @@ -
    -
    -
    -
    - -
    - - - -
    -
    - - - -
    -
     
    - -
    - - - -
    -
     
    -
    - - - -
    -
     
    - - - - - - -
     
    - -
     
    - -
    -
    -
    -
    -
    -
    -
    - × - -
    - - - -
    - -
    -
    -
    -
    -
    -
    -
    -
    - -    - -
    -
    - -
    -
    - -    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - ... -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/MailMessageListItem.html b/rainloop/v/0.0.0/app/templates/Views/User/MailMessageListItem.html deleted file mode 100644 index a83231b3ac..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/MailMessageListItem.html +++ /dev/null @@ -1,49 +0,0 @@ -
    -
    -   -
    -
    -
    -
    - - - - - - - - - -
    -
    - -
    -
    - - - - - - -
    -
    - -
    -
    -   -   - -   -
    -
    - - - -
    -
    - ! - - -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/MailMessageListItemNoPreviewPane.html b/rainloop/v/0.0.0/app/templates/Views/User/MailMessageListItemNoPreviewPane.html deleted file mode 100644 index e16ec903b0..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/MailMessageListItemNoPreviewPane.html +++ /dev/null @@ -1,50 +0,0 @@ -
    -
    -   -
    -
    -
    -
    - -
    -
    - - - - - - -
    -
    - -
    -
    - - - - - - - - - -
    -
    -   -   - -   -
    -
    - - - -
    -
    - ! - - -
    -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/MailMessageView.html b/rainloop/v/0.0.0/app/templates/Views/User/MailMessageView.html deleted file mode 100644 index f1c48973d2..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/MailMessageView.html +++ /dev/null @@ -1,447 +0,0 @@ -
    -
    -
    - -
    -
    - - - -
    -
     
    -
    - - - -
    -
     
    - -
     
    - -
    -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - -
    - -
    - -
    - -
    - -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - - - -
    -
    -
    - - - - - - - - ! - - -
    -
    -
    - -
    -
    - - - -   - - - - - () - -
    -
    -
    - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - -
    - -   - () -
    -
    -
    -
    -
    -
    - -
    - -
    - - - - - - - - - - -
    - -
    - ... -
    -
    - -
    -
    -
    - -    - -
    -
    - -    - -
    -
    - - -
    - -
    - - - - -    - - - - - - -      - - - -    - - - - - - -      - - - -    - - - - - - -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PhotoSwipe.html b/rainloop/v/0.0.0/app/templates/Views/User/PhotoSwipe.html deleted file mode 100644 index f256ed71fd..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PhotoSwipe.html +++ /dev/null @@ -1,33 +0,0 @@ - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsAccount.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsAccount.html deleted file mode 100644 index 3827149d08..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsAccount.html +++ /dev/null @@ -1,50 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsAddOpenPgpKey.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsAddOpenPgpKey.html deleted file mode 100644 index 03dc3f71d8..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsAddOpenPgpKey.html +++ /dev/null @@ -1,27 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsAdvancedSearch.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsAdvancedSearch.html deleted file mode 100644 index 48cac4c09a..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsAdvancedSearch.html +++ /dev/null @@ -1,107 +0,0 @@ -
    - -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsCompose.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsCompose.html deleted file mode 100644 index c8f06827aa..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsCompose.html +++ /dev/null @@ -1,218 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsComposeOpenPgp.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsComposeOpenPgp.html deleted file mode 100644 index 8cedd36484..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsComposeOpenPgp.html +++ /dev/null @@ -1,119 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsContacts.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsContacts.html deleted file mode 100644 index 6b0c3ff4cd..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsContacts.html +++ /dev/null @@ -1,274 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsFilter.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsFilter.html deleted file mode 100644 index 47d2d4c754..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsFilter.html +++ /dev/null @@ -1,64 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsFolderClear.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsFolderClear.html deleted file mode 100644 index 524b935aba..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsFolderClear.html +++ /dev/null @@ -1,34 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsFolderCreate.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsFolderCreate.html deleted file mode 100644 index b1c57bc2ee..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsFolderCreate.html +++ /dev/null @@ -1,42 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsFolderSystem.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsFolderSystem.html deleted file mode 100644 index 265b2a7310..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsFolderSystem.html +++ /dev/null @@ -1,63 +0,0 @@ -
    - -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsIdentity.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsIdentity.html deleted file mode 100644 index 731bccaab8..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsIdentity.html +++ /dev/null @@ -1,99 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsMessageOpenPgp.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsMessageOpenPgp.html deleted file mode 100644 index abca830d97..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsMessageOpenPgp.html +++ /dev/null @@ -1,54 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsNewOpenPgpKey.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsNewOpenPgpKey.html deleted file mode 100644 index a492b392b3..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsNewOpenPgpKey.html +++ /dev/null @@ -1,58 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsTemplate.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsTemplate.html deleted file mode 100644 index fdfc11b003..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsTemplate.html +++ /dev/null @@ -1,44 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsTwoFactorConfiguration.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsTwoFactorConfiguration.html deleted file mode 100644 index 2d38d6bcec..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsTwoFactorConfiguration.html +++ /dev/null @@ -1,106 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsTwoFactorTest.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsTwoFactorTest.html deleted file mode 100644 index ae416930c0..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsTwoFactorTest.html +++ /dev/null @@ -1,34 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsViewOpenPgpKey.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsViewOpenPgpKey.html deleted file mode 100644 index c3673a82c6..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsViewOpenPgpKey.html +++ /dev/null @@ -1,31 +0,0 @@ -
    - -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/PopupsWindowSimpleMessage.html b/rainloop/v/0.0.0/app/templates/Views/User/PopupsWindowSimpleMessage.html deleted file mode 100644 index 6849d27c7a..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/PopupsWindowSimpleMessage.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsAccounts.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsAccounts.html deleted file mode 100644 index 5c33118d12..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsAccounts.html +++ /dev/null @@ -1,101 +0,0 @@ -
    -
    -
    -
    - -     - -
    -
    - - -    - - -
    -
    -
    -
    -
    - - - - - - - - - - - - - -
    -
    -
    -
    -
    -
    -
    - -
    -
    - - -    - - -
    -
    -
    -
    -
    - - - - - - - - - - - - - -
    - - -    - -    - - () - - - - - - - - - - -
    -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsChangePassword.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsChangePassword.html deleted file mode 100644 index f0d08fe228..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsChangePassword.html +++ /dev/null @@ -1,48 +0,0 @@ -
    -
    -
    - -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    - -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsContacts.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsContacts.html deleted file mode 100644 index fd2e3f9a82..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsContacts.html +++ /dev/null @@ -1,63 +0,0 @@ -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    - -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsCustom.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsCustom.html deleted file mode 100644 index 8d135e5c30..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsCustom.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - CUSTOM -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFilters.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFilters.html deleted file mode 100644 index eb3132d014..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFilters.html +++ /dev/null @@ -1,106 +0,0 @@ -
    -
    -
    - -     - -
    -
    -
    - -
    -
    -
    -
    -
    - -    - -
    -
    -
    -
    -
    -
    - -    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -						:
    -						
    -					
    - -
    -
    -
    -
    - - - - - - - - - - - - - - - - - -
    - - - - - - - -    - - - - - - - - - -
    -
    -
    -
    -
    - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionDiscard.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionDiscard.html deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionForward.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionForward.html deleted file mode 100644 index e9da576f60..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionForward.html +++ /dev/null @@ -1,24 +0,0 @@ -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionMoveToFolder.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionMoveToFolder.html deleted file mode 100644 index 0f32a1af77..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionMoveToFolder.html +++ /dev/null @@ -1,17 +0,0 @@ -
    -
    - -
    -
    -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionNone.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionNone.html deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionReject.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionReject.html deleted file mode 100644 index fbfaef0ef7..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionReject.html +++ /dev/null @@ -1,6 +0,0 @@ -
    -
    - -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionVacation.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionVacation.html deleted file mode 100644 index 0e12a70fc7..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersActionVacation.html +++ /dev/null @@ -1,45 +0,0 @@ -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersConditionDefault.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersConditionDefault.html deleted file mode 100644 index 7bf8c166d6..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersConditionDefault.html +++ /dev/null @@ -1,12 +0,0 @@ -
    - -   - -   - -   - - - -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersConditionMore.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersConditionMore.html deleted file mode 100644 index 61ac82ede3..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersConditionMore.html +++ /dev/null @@ -1,14 +0,0 @@ -
    - -   - -   - -   - -   - - - -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersConditionSize.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersConditionSize.html deleted file mode 100644 index 0830dc8003..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFiltersConditionSize.html +++ /dev/null @@ -1,12 +0,0 @@ -
    - -   - -   - -   - - - -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFolderItem.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFolderItem.html deleted file mode 100644 index 4c80e0f550..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFolderItem.html +++ /dev/null @@ -1,37 +0,0 @@ - - -   - -   - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFolders.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsFolders.html deleted file mode 100644 index 5b18f0d4c9..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsFolders.html +++ /dev/null @@ -1,45 +0,0 @@ -
    -
    -
    -
    - -     - -
    -
    - - -    - - -    - - -    - - -
    - -
    - -
    -
    - - -
    - - - - - - - - -
    -
    -
    -
    - -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsGeneral.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsGeneral.html deleted file mode 100644 index 28d917a590..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsGeneral.html +++ /dev/null @@ -1,158 +0,0 @@ -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - - - - -    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -   - - - -
    -
    -
    -    - - - -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsMenu.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsMenu.html deleted file mode 100644 index aa4f5b2c1a..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsMenu.html +++ /dev/null @@ -1,28 +0,0 @@ -
    -
    -
    - - - - - - - - - - -
    -
    -
    - -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsOpenPGP.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsOpenPGP.html deleted file mode 100644 index 9d6fb19574..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsOpenPGP.html +++ /dev/null @@ -1,99 +0,0 @@ -
    -
    -
    - -
    -
    - -    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - -
    - - - - - -
    - -
    - - - -
    - - - - - - - -
    - - - - - -
    - () -
    - -
    - - - -
    - - - - - - - -
    -
    diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsPane.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsPane.html deleted file mode 100644 index 3fdc7d7e07..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsPane.html +++ /dev/null @@ -1,28 +0,0 @@ -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsSecurity.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsSecurity.html deleted file mode 100644 index 51f8689a84..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsSecurity.html +++ /dev/null @@ -1,34 +0,0 @@ -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    - -
    - -   - -
    -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsSocial.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsSocial.html deleted file mode 100644 index 397cd25be9..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsSocial.html +++ /dev/null @@ -1,101 +0,0 @@ -
    -
    -
    - -
    -
    - -
    -
    -
    -

    -
    -
    -
    - - -    - - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -

    -
    -
    -
    - - -    - - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -

    -
    -
    -
    - - -    - - -
    -
    - -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsTemplates.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsTemplates.html deleted file mode 100644 index fdf385d41d..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsTemplates.html +++ /dev/null @@ -1,48 +0,0 @@ -
    -
    -
    - -     - -
    -
    - - -    - - -
    -
    -
    -
    -
    - - - - - - - - - - - - - -
    - - -    - - - - - - - - - -
    -
    -
    - \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SettingsThemes.html b/rainloop/v/0.0.0/app/templates/Views/User/SettingsThemes.html deleted file mode 100644 index cc86478ecf..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SettingsThemes.html +++ /dev/null @@ -1,44 +0,0 @@ -
    -
    -
    - -    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    - - - -     - -
    - -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/app/templates/Views/User/SystemDropDown.html b/rainloop/v/0.0.0/app/templates/Views/User/SystemDropDown.html deleted file mode 100644 index ec7e2d4195..0000000000 --- a/rainloop/v/0.0.0/app/templates/Views/User/SystemDropDown.html +++ /dev/null @@ -1,101 +0,0 @@ -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    - -
    - -
    - -
    -
    -
    \ No newline at end of file diff --git a/rainloop/v/0.0.0/check.php b/rainloop/v/0.0.0/check.php deleted file mode 100644 index 05aa947d9d..0000000000 --- a/rainloop/v/0.0.0/check.php +++ /dev/null @@ -1,43 +0,0 @@ - function_exists('curl_init'), - 'iconv' => function_exists('iconv'), - 'json' => function_exists('json_decode'), - 'DateTime' => class_exists('DateTime') && class_exists('DateTimeZone'), - 'libxml' => function_exists('libxml_use_internal_errors'), - 'dom' => class_exists('DOMDocument'), - 'Zlib' => function_exists('gzopen') || function_exists('gzopen64'), - 'PCRE' => function_exists('preg_replace'), - 'SPL' => function_exists('spl_autoload_register') - ); - - if (version_compare(PHP_VERSION, '5.4.0', '<')) - { - echo '

    '; - echo '[301] Your PHP version ('.PHP_VERSION.') is lower than the minimal required 5.4.0!'; - echo '

    '; - exit(301); - } - - if (in_array(false, $aRequirements)) - { - echo '

    '; - echo '[302] The following PHP extensions are not available in your PHP configuration!'; - echo '

    '; - - echo '
      '; - foreach ($aRequirements as $sKey => $bValue) - { - if (!$bValue) - { - echo '
    • '.$sKey.'
    • '; - } - } - echo '
    '; - - exit(302); - } - } diff --git a/rainloop/v/0.0.0/include.php b/rainloop/v/0.0.0/include.php deleted file mode 100644 index 605fd6e05f..0000000000 --- a/rainloop/v/0.0.0/include.php +++ /dev/null @@ -1,246 +0,0 @@ - 'localhost' !== $sClearedSiteName? 'imap.'.$sClearedSiteName : $sClearedSiteName, -// 'IMAP_PORT' => '993', -// 'SMTP_HOST' => 'localhost' !== $sClearedSiteName? 'smtp.'.$sClearedSiteName : $sClearedSiteName, -// 'SMTP_PORT' => '465' -// ))); -// } -// -// unset($sConfigTemplate); -// } - - unset($aFiles, $sFile, $sNewFileName, $sNewFile); - } - } - } - - unset($sSalt, $sData, $sInstalled, $sPrivateDataFolderInternalName); - } - - include APP_VERSION_ROOT_PATH.'app/handle.php'; - - if (defined('RAINLOOP_EXIT_ON_END') && RAINLOOP_EXIT_ON_END) - { - exit(0); - } - } diff --git a/rainloop/v/0.0.0/index.php.root b/rainloop/v/0.0.0/index.php.root deleted file mode 100644 index 7dd8734e73..0000000000 --- a/rainloop/v/0.0.0/index.php.root +++ /dev/null @@ -1,18 +0,0 @@ - + Require all denied + + + Deny from all + + + Options -Indexes + \ No newline at end of file diff --git a/snappymail/v/0.0.0/app/domains/default.json b/snappymail/v/0.0.0/app/domains/default.json new file mode 100644 index 0000000000..291589eeaa --- /dev/null +++ b/snappymail/v/0.0.0/app/domains/default.json @@ -0,0 +1,91 @@ +{ + "IMAP": { + "host": "localhost", + "port": 143, + "type": 0, + "timeout": 300, + "shortLogin": false, + "lowerLogin": true, + "sasl": [ + "SCRAM-SHA3-512", + "SCRAM-SHA-512", + "SCRAM-SHA-256", + "SCRAM-SHA-1", + "PLAIN", + "LOGIN" + ], + "ssl": { + "verify_peer": false, + "verify_peer_name": false, + "allow_self_signed": false, + "SNI_enabled": true, + "disable_compression": true, + "security_level": 1 + }, + "disabled_capabilities": [ + "METADATA", + "OBJECTID", + "PREVIEW", + "STATUS=SIZE" + ], + "use_expunge_all_on_delete": false, + "fast_simple_search": true, + "force_select": false, + "message_all_headers": false, + "message_list_limit": 10000, + "search_filter": "" + }, + "SMTP": { + "host": "localhost", + "port": 25, + "type": 0, + "timeout": 60, + "shortLogin": false, + "lowerLogin": true, + "sasl": [ + "SCRAM-SHA3-512", + "SCRAM-SHA-512", + "SCRAM-SHA-256", + "SCRAM-SHA-1", + "PLAIN", + "LOGIN" + ], + "ssl": { + "verify_peer": false, + "verify_peer_name": false, + "allow_self_signed": false, + "SNI_enabled": true, + "disable_compression": true, + "security_level": 1 + }, + "useAuth": false, + "setSender": false, + "usePhpMail": false + }, + "Sieve": { + "host": "localhost", + "port": 4190, + "type": 0, + "timeout": 10, + "shortLogin": false, + "lowerLogin": true, + "sasl": [ + "SCRAM-SHA3-512", + "SCRAM-SHA-512", + "SCRAM-SHA-256", + "SCRAM-SHA-1", + "PLAIN", + "LOGIN" + ], + "ssl": { + "verify_peer": false, + "verify_peer_name": false, + "allow_self_signed": false, + "SNI_enabled": true, + "disable_compression": true, + "security_level": 1 + }, + "enabled": false + }, + "whiteList": "" +} diff --git a/snappymail/v/0.0.0/app/domains/disabled b/snappymail/v/0.0.0/app/domains/disabled new file mode 100644 index 0000000000..c4d1d21437 --- /dev/null +++ b/snappymail/v/0.0.0/app/domains/disabled @@ -0,0 +1,5 @@ +outlook.com +qq.com +yahoo.com +gmail.com +hotmail.com diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Collection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Collection.php new file mode 100644 index 0000000000..e8aaec0ad4 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Collection.php @@ -0,0 +1,91 @@ +append($item); + } + } + + /** + * @param mixed $mItem + */ + public function append($mItem, bool $bToTop = false) : void + { + if ($bToTop) { + $array = $this->getArrayCopy(); + \array_unshift($array, $mItem); + $this->exchangeArray($array); + } else { + parent::append($mItem); + } + } + + public function keys() : array + { + return \array_keys($this->getArrayCopy()); + } + + public function Add($mItem, bool $bToTop = false) : self + { + $this->append($mItem, $bToTop); + return $this; + } + + public function Clear() : void + { + $this->exchangeArray([]); + } +/* + public function __call(string $name, array $arguments) + { + $callable = "array_{$name}"; + if (!\is_callable($callable)) { + throw new BadMethodCallException(__CLASS__.'->'.$name); + } + return $callable($this->getArrayCopy(), ...$arguments); + } +*/ + public function Slice(int $offset, ?int $length = null, bool $preserve_keys = false) + { + return new static( + \array_slice($this->getArrayCopy(), $offset, $length, $preserve_keys) + ); + } + + public function Crop(?int $length = null, int $offset = 0, bool $preserve_keys = false) + { + $this->exchangeArray( + \array_slice($this->getArrayCopy(), $offset, $length, $preserve_keys) + ); + return $this; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $aNames = \explode('\\', \get_class($this)); + return array( + '@Object' => 'Collection/' . \end($aNames), + '@Collection' => $this->getArrayCopy() + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/DateTimeHelper.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/DateTimeHelper.php new file mode 100644 index 0000000000..c4cb6c4bc7 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/DateTimeHelper.php @@ -0,0 +1,115 @@ + $oDateTime->getTimestamp()) { + \SnappyMail\Log::notice('', "Failed to parse RFC 2822 date '{$sDateTime}'"); + } + return $oDateTime ? $oDateTime->getTimestamp() : 0; + } + + /** + * Parse date string formated as "10-Jan-2012 01:58:17 -0800" + * IMAP INTERNALDATE Format + */ + public static function ParseInternalDateString(string $sDateTime) : int + { + $sDateTime = \trim($sDateTime); + if (empty($sDateTime)) { + return 0; + } + + // RFC2822 ~ "Thu, 10 Jun 2010 08:58:33 -0700 (PDT)" + if (\preg_match('/^[a-z]{2,4}, /i', $sDateTime)) { + return static::ParseRFC2822DateString($sDateTime); + } + + $oDateTime = \DateTime::createFromFormat('d-M-Y H:i:s O', $sDateTime, static::GetUtcTimeZoneObject()); + return $oDateTime ? $oDateTime->getTimestamp() : 0; + } + + /** + * Parse date string formated as "2011-06-14 23:59:59 +0400" + */ + public static function ParseDateStringType1(string $sDateTime) : int + { + $sDateTime = \trim($sDateTime); + if (empty($sDateTime)) { + return 0; + } + + $oDateTime = \DateTime::createFromFormat('Y-m-d H:i:s O', $sDateTime, static::GetUtcTimeZoneObject()); + return $oDateTime ? $oDateTime->getTimestamp() : 0; + } + + /** + * Parse date string formated as "2015-05-08T14:32:18.483-07:00" + */ + public static function TryToParseSpecEtagFormat(string $sDateTime) : int + { + $oDateTime = \DateTime::createFromFormat(\DateTime::RFC3339_EXTENDED, $sDateTime, static::GetUtcTimeZoneObject()); + if ($oDateTime) { + return $oDateTime->getTimestamp(); + } + + $sDateTime = \preg_replace('/ \([a-zA-Z0-9]+\)$/', '', \trim($sDateTime)); + $sDateTime = \preg_replace('/(:[\d]{2})\.[\d]{3}/', '$1', \trim($sDateTime)); + $sDateTime = \preg_replace('/(-[\d]{2})T([\d]{2}:)/', '$1 $2', \trim($sDateTime)); + $sDateTime = \preg_replace('/([\-+][\d]{2}):([\d]{2})$/', ' $1$2', \trim($sDateTime)); + + return static::ParseDateStringType1($sDateTime); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Enumerations/Charset.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Enumerations/Charset.php new file mode 100644 index 0000000000..069146900b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Enumerations/Charset.php @@ -0,0 +1,24 @@ +', '', $sHtml); + $sHtml = \str_replace('', '', $sHtml); + $sHtml = \str_replace('', '
    ', $sHtml); +/* + if (\function_exists('tidy_repair_string')) { + # http://tidy.sourceforge.net/docs/quickref.html + $tidyConfig = array( + 'bare' => 1, + 'join-classes' => 1, + 'newline' => 'LF', + 'numeric-entities' => 1, + 'quote-nbsp' => 0, + 'word-2000' => 1 + ); + $sHtml = \tidy_repair_string($sHtml, $tidyConfig, 'utf8'); + } +*/ + $sHtml = \preg_replace(array( + '/]*><\/p>/i', + '/]*>/i', + '/<\?xml [^>]*\?>/i' + ), '', $sHtml); + + $sHtmlAttrs = ''; + $aMatch = array(); + if (\preg_match('/]+)>/im', $sHtml, $aMatch) && !empty($aMatch[1])) { + $sHtmlAttrs = $aMatch[1]; + } + + $sBodyAttrs = ''; + $aMatch = array(); + if (\preg_match('/]+)>/im', $sHtml, $aMatch) && !empty($aMatch[1])) { + $sBodyAttrs = $aMatch[1]; + } + + $sHtml = \preg_replace('/<\/?(head|body|html)(\\s[^>]*)?>/si', '', $sHtml); + + $sHtmlAttrs = \preg_replace('/xmlns:[a-z]="[^"]*"/i', '', $sHtmlAttrs); + $sHtmlAttrs = \preg_replace('/xmlns:[a-z]=\'[^\']*\'/i', '', $sHtmlAttrs); + $sHtmlAttrs = \preg_replace('/xmlns="[^"]*"/i', '', $sHtmlAttrs); + $sHtmlAttrs = \preg_replace('/xmlns=\'[^\']*\'/i', '', $sHtmlAttrs); + + $sBodyAttrs = \preg_replace('/xmlns:[a-z]="[^"]*"/i', '', $sBodyAttrs); + $sBodyAttrs = \preg_replace('/xmlns:[a-z]=\'[^\']*\'/i', '', $sBodyAttrs); + + $oDoc = new \DOMDocument('1.0', 'UTF-8'); + $oDoc->encoding = 'UTF-8'; + $oDoc->strictErrorChecking = false; + $oDoc->formatOutput = false; + $oDoc->preserveWhiteSpace = false; + + @$oDoc->loadHTML(''. + ''. + ''. + ''.\MailSo\Base\Utils::Utf8Clear($sHtml).''); + + $oDoc->normalizeDocument(); + + if (\MailSo\Base\Utils::FunctionCallable('libxml_clear_errors')) { + \libxml_clear_errors(); + } + + \libxml_use_internal_errors($bState); + + $sHtml = ''; + $oBody = $oDoc->getElementsByTagName('body')->item(0); + + $aRemoveTags = array( + 'svg', 'link', 'base', 'meta', 'title', 'script', 'bgsound', 'keygen', 'source', + 'object', 'embed', 'applet', 'mocha', 'iframe', 'frame', 'frameset', 'video', 'audio', 'area', 'map', + 'head', 'style' + ); + foreach ($aRemoveTags as $name) { + $aNodes = $oBody->getElementsByTagName($name); + foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) { + if (isset($oElement->parentNode)) { + @$oElement->parentNode->removeChild($oElement); + } + } + } + + $xpath = new \DomXpath($oDoc); + + foreach ($xpath->query('//*[@data-x-src-broken]') as $oElement) { + if (isset($oElement->parentNode)) { + @$oElement->parentNode->removeChild($oElement); + } + } + + foreach ($xpath->query('//*[@data-x-src-hidden]') as $oElement) { + if (isset($oElement->parentNode)) { + @$oElement->parentNode->removeChild($oElement); + } + } + + foreach ($xpath->query('//*[@data-x-src-cid]') as $oElement) { + $sCid = $oElement->getAttribute('data-x-src-cid'); + $oElement->removeAttribute('data-x-src-cid'); + if (!empty($sCid)) { + $aFoundCids[] = $sCid; + $oElement->setAttribute('src', 'cid:'.$sCid); + } + } + + foreach ($xpath->query('//*[@data-x-src]') as $oElement) { + $oElement->setAttribute('src', $oElement->getAttribute('data-x-src')); + $oElement->removeAttribute('data-x-src'); + } + + // style attribute images + foreach ($xpath->query('//*[@data-x-style-url]') as $oElement) { + $aCid = \json_decode($oElement->getAttribute('data-x-style-url'), true); + $oElement->removeAttribute('data-x-style-url'); + if ($aCid) { + foreach ($aCid as $sCidName => $sCid) { + $sCidName = \strtolower(\preg_replace('/([A-Z])/', '-\1', $sCidName)); + if (\in_array($sCidName, array('background-image', 'list-style-image', 'content'))) { + $sStyles = $oElement->hasAttribute('style') + ? \trim(\trim($oElement->getAttribute('style')), ';') + : ''; + + $sBack = $sCidName.':url(cid:'.$sCid.')'; + $sStyles = \preg_replace('/'.\preg_quote($sCidName).'\\s*:\\s*[^;]+/i', $sBack, $sStyles); + if (false === \strpos($sStyles, $sBack)) { + $sStyles .= ";{$sBack}"; + } + + $oElement->setAttribute('style', \trim($sStyles, ';')); + $aFoundCids[] = $sCid; + } + } + } + } + + // Remove all remaining data-* attributes + foreach ($xpath->query('//*[@*[starts-with(name(), "data-")]]') as $oElement) { + $sTagNameLower = \strtolower($oElement->nodeName); + if ($oElement->hasAttributes()) { + foreach ($oElement->attributes as $oAttr) { + if ('data-' === \substr(\strtolower($oAttr->nodeName), 0, 5)) { + $oElement->removeAttribute($oAttr->nodeName); + } + } + } + } + + $sIdRight = \md5(\microtime()); + $aNodes = $oBody->getElementsByTagName('img'); + foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) { + $sSrc = $oElement->getAttribute('src'); + if ('data:image/' === \strtolower(\substr($sSrc, 0, 11))) { + $sHash = \md5($sSrc) . '@' . $sIdRight; + $aFoundDataURL[$sHash] = $sSrc; + $oElement->setAttribute('src', 'cid:'.$sHash); + } + } + + return '' + . $oDoc->saveHTML($oBody) . ''; + } + + public static function ConvertHtmlToPlain(string $sText) : string + { + $sText = \MailSo\Base\Utils::StripSpaces($sText); + + $sText = \preg_replace_callback('/]*>/', function($m) { + return "\n\n" . \str_repeat('#', $m[1]) . ' '; + }, $sText); + + $sText = \preg_replace(array( + "/\r/", + "/[\n\t]+/", + '/]*>.*?<\/script>|]*>.*?<\/style>|]*>.*?<\/title>/i', + '/<\/h[1-6]>/i', + '/]*>/i', + '/]*>/i', + '/]*>(.+?)<\/b>/i', + '/]*>(.+?)<\/i>/i', + '/]*>|<\/ul>|]*>|<\/ol>/i', + '/]*>/i', + '/]*href="([^"]+)"[^>]*>(.+?)<\/a>/i', + '/]*>/i', + '/(]*>|<\/table>)/i', + '/(]*>|<\/tr>)/i', + '/]*>(.+?)<\/td>/i', + '/]*>(.+?)<\/th>/i', + ), array( + '', + ' ', + '', + "\n\n", + "\n\n\t", + "\n", + '\\1', + '\\1', + "\n\n", + "\n\t* ", + '\\2 (\\1)', + "\n------------------------------------\n", + "\n", + "\n", + "\t\\1\n", + "\t\\1\n" + ), $sText); + + $sText = \str_ireplace('
    ',"\n
    ", $sText); + $sText = \strip_tags($sText, ''); + $sText = \preg_replace("/\n\\s+\n/", "\n", $sText); + $sText = \preg_replace("/[\n]{3,}/", "\n\n", $sText); + + $sText = \html_entity_decode($sText, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8'); + + return \trim($sText); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php new file mode 100644 index 0000000000..67bb61489d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php @@ -0,0 +1,276 @@ +GetMethod()); + } + + public function IsGet() : bool + { + return ('GET' === $this->GetMethod()); + } + + public function CheckLocalhost(string $sServer) : bool + { + return \in_array(\strtolower(\trim($sServer)), array( + 'localhost', '127.0.0.1', '::1' + )); + } + + public function IsLocalhost(string $sValueToCheck = '') : bool + { + if (empty($sValueToCheck)) { + $sValueToCheck = static::GetServer('REMOTE_ADDR', ''); + } + + return $this->CheckLocalhost($sValueToCheck); + } + + public function GetRawBody() : string + { + static $sRawBody = null; + if (null === $sRawBody) { + $sBody = \file_get_contents('php://input'); + $sRawBody = (false !== $sBody) ? $sBody : ''; + } + return $sRawBody; + } + + public static function GetHeader(string $sHeader) : string + { + $sServerKey = 'HTTP_'.\strtoupper(\str_replace('-', '_', $sHeader)); + $sResultHeader = static::GetServer($sServerKey, ''); + if (!\strlen($sResultHeader) && Utils::FunctionCallable('apache_request_headers')) { + $sHeaders = \apache_request_headers(); + if (isset($sHeaders[$sHeader])) { + $sResultHeader = $sHeaders[$sHeader]; + } + } + return $sResultHeader; + } + + public function GetScheme(bool $bCheckProxy = true) : string + { + return $this->IsSecure($bCheckProxy) ? 'https' : 'http'; + } + + public function IsSecure(bool $bCheckProxy = true) : bool + { + $sHttps = \strtolower(static::GetServer('HTTPS', '')); + return ('on' === $sHttps || ('' === $sHttps && '443' === (string) static::GetServer('SERVER_PORT', ''))) + || ($bCheckProxy && ( + ('https' === \strtolower(static::GetServer('HTTP_X_FORWARDED_PROTO', ''))) || + ('on' === \strtolower(static::GetServer('HTTP_X_FORWARDED_SSL', ''))) + )); + } + + public function GetHost(bool $bWithoutWWW = true, bool $bWithoutPort = false) : string + { + $sHost = static::GetServer('HTTP_HOST', ''); + if (!\strlen($sHost)) { + $sName = static::GetServer('SERVER_NAME', ''); + $iPort = (int) static::GetServer('SERVER_PORT', 80); + $sHost = (\in_array($iPort, array(80, 433))) ? $sName : $sName.':'.$iPort; + } + + if ($bWithoutWWW && \str_starts_with($sHost, 'www.')) { + $sHost = \substr($sHost, 4); + } + + return $bWithoutPort ? \preg_replace('/:\d+$/', '', $sHost) : $sHost; + } + + public function GetClientIp(bool $bCheckProxy = false) : string + { + if ($bCheckProxy && null !== static::GetServer('HTTP_CLIENT_IP', null)) { + return static::GetServer('HTTP_CLIENT_IP', ''); + } + if ($bCheckProxy && null !== static::GetServer('HTTP_X_FORWARDED_FOR', null)) { + return static::GetServer('HTTP_X_FORWARDED_FOR', ''); + } + return static::GetServer('REMOTE_ADDR', ''); + } + + public static function checkETag(string $ETag) : void + { + // $ETag . APP_VERSION + $sIfNoneMatch = static::GetHeader('If-None-Match'); + if ($sIfNoneMatch && false !== \strpos($sIfNoneMatch, $ETag)) { + static::StatusHeader(304); + exit; + } + $sIfMatch = static::GetHeader('If-Match'); + if ($sIfMatch && false === \strpos($sIfMatch, $ETag)) { + static::StatusHeader(412); + exit; + } + } + + public static function setETag(string $ETag) : void + { + // $ETag . APP_VERSION + static::checkETag($ETag); + \header("ETag: \"{$ETag}\""); + } + + public static function checkLastModified(int $mtime) : void + { + $sIfModifiedSince = static::GetHeader('If-Modified-Since'); + if ($sIfModifiedSince && $mtime <= \strtotime($sIfModifiedSince)) { + static::StatusHeader(304); + exit; + } + $sIfUnmodifiedSince = static::GetHeader('If-Unmodified-Since'); + if ($sIfUnmodifiedSince && $mtime > \strtotime($sIfUnmodifiedSince)) { + static::StatusHeader(412); + exit; + } + } + + public static function setLastModified(int $mtime) : void + { + static::checkLastModified($mtime); + \header('Last-Modified: '.\gmdate('D, d M Y H:i:s \G\M\T', $mtime)); # DATE_RFC1123 + } + + private static $bCache = false; + + public function ServerNoCache() + { + if (!static::$bCache) { + static::$bCache = true; + \header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); + \header('Last-Modified: '.\gmdate('D, d M Y H:i:s').' GMT'); + \header('Cache-Control: no-store'); + \header('Pragma: no-cache'); + } + } + + public static function ServerUseCache(string $sEtag = '', int $iLastModified = 0, int $iExpires = 0) : void + { + if (!static::$bCache) { + static::$bCache = true; + \header('Cache-Control: private'); + $iExpires && \header('Expires: '.\gmdate('D, j M Y H:i:s', \time() + $iExpires).' UTC'); + $sEtag && static::setETag($sEtag); + $iLastModified && static::setLastModified($iLastModified); + } + } + + public static function StatusHeader(int $iStatus, string $sCustomStatusText = '') : void + { + if (99 < $iStatus) { + $aStatus = array( + 200 => 'OK', + 206 => 'Partial Content', + 301 => 'Moved Permanently', + 302 => 'Found', + 304 => 'Not Modified', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 412 => 'Precondition Failed', + 416 => 'Requested range not satisfiable', + 500 => 'Internal Server Error' + ); + + $sHeaderText = (!\strlen($sCustomStatusText) && isset($aStatus[$iStatus]) ? $aStatus[$iStatus] : $sCustomStatusText); + + \http_response_code($iStatus); + if (isset($_SERVER['SERVER_PROTOCOL'])) { + \header("{$_SERVER['SERVER_PROTOCOL']} {$iStatus} {$sHeaderText}", true, $iStatus); + } + if (\ini_get('cgi.rfc2616_headers') && false !== \strpos(\strtolower(\php_sapi_name()), 'cgi')) { + \header("Status: {$iStatus} {$sHeaderText}"); + } + } + } + + public static function Location(string $sUrl, int $iStatus = 302): void + { + static::StatusHeader($iStatus); + \header('Location: ' . $sUrl); + } + + public static function setContentDisposition(string $type /* inline|attachment */, array $params) + { + $parms = array($type); + if (isset($params['filename'])) { + if (\preg_match('#^[\x01-\x7F]*$#D', $params['filename'])) { + $parms[] = "filename=\"{$params['filename']}\""; + } else { + // RFC 5987 +// $parms[] = Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileNameOut); + $parms[] = "filename*=utf-8''" . \rawurlencode($params['filename']); + } + } + if (isset($params['creation-date'])) $parms[] = 'creation-date=' . \date(DATE_RFC822, $params['creation-date']); + if (isset($params['modification-date'])) $parms[] = 'modification-date=' . \date(DATE_RFC822, $params['modification-date']); + if (isset($params['read-date'])) $parms[] = 'read-date=' . \date(DATE_RFC822, $params['read-date']); + if (isset($params['size'])) $parms[] = 'size=' . \intval($params['size']); + \header('Content-Disposition: ' . \implode('; ', $parms)); + } + + public function GetPath() : string + { + $sUrl = \ltrim(\substr(static::GetServer('SCRIPT_NAME', ''), 0, \strrpos(static::GetServer('SCRIPT_NAME', ''), '/')), '/'); + return '' === $sUrl ? '/' : '/'.$sUrl.'/'; + } + + public function GetUrl() : string + { + return $_SERVER['REQUEST_URI'] ?? ''; + } + + public function GetFullUrl() : string + { + return $this->GetScheme().'://'.$this->GetHost(false).$this->GetPath(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Locale.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Locale.php new file mode 100644 index 0000000000..5c5c153392 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Locale.php @@ -0,0 +1,99 @@ + 'utf-8', + '.20127' => 'iso-8859-1', + + '.1250' => 'windows-1250', + '.cp1250' => 'windows-1250', + '.cp-1250' => 'windows-1250', + '.1251' => 'windows-1251', + '.cp1251' => 'windows-1251', + '.cp-1251' => 'windows-1251', + '.1252' => 'windows-1252', + '.cp1252' => 'windows-1252', + '.cp-1252' => 'windows-1252', + '.1253' => 'windows-1253', + '.cp1253' => 'windows-1253', + '.cp-1253' => 'windows-1253', + '.1254' => 'windows-1254', + '.cp1254' => 'windows-1254', + '.cp-1254' => 'windows-1254', + '.1255' => 'windows-1255', + '.cp1255' => 'windows-1255', + '.cp-1255' => 'windows-1255', + '.1256' => 'windows-1256', + '.cp1256' => 'windows-1256', + '.cp-1256' => 'windows-1256', + '.1257' => 'windows-1257', + '.cp1257' => 'windows-1257', + '.cp-1257' => 'windows-1257', + '.1258' => 'windows-1258', + '.cp1258' => 'windows-1258', + '.cp-1258' => 'windows-1258', + + '.28591' => 'iso-8859-1', + '.28592' => 'iso-8859-2', + '.28593' => 'iso-8859-3', + '.28594' => 'iso-8859-4', + '.28595' => 'iso-8859-5', + '.28596' => 'iso-8859-6', + '.28597' => 'iso-8859-7', + '.28598' => 'iso-8859-8', + '.28599' => 'iso-8859-9', + '.28603' => 'iso-8859-13', + '.28605' => 'iso-8859-15', + + '.1125' => 'cp1125', + '.20866' => 'koi8-r', + '.21866' => 'koi8-u', + '.950' => 'big5', + '.936' => 'euc-cn', + '.20932' => 'euc-js', + '.949' => 'euc-kr', + ); + + public static function DetectSystemCharset() : string + { + $sLocale = \strtolower(\trim(\setlocale(LC_ALL, ''))); + foreach (static::$aLocaleMapping as $sKey => $sValue) { + if (\str_contains($sLocale, $sKey) || \str_contains($sLocale, '.'.$sValue)) { + return $sValue; + } + } + return ''; + } + + public static function ConvertSystemString(string $sSrt) : string + { + $sSrt = \trim($sSrt); + if (!empty($sSrt) && !Utils::IsUtf8($sSrt)) { + $sCharset = static::DetectSystemCharset(); + $sSrt = $sCharset + ? Utils::ConvertEncoding($sSrt, $sCharset, Enumerations\Charset::UTF_8) + : \mb_convert_encoding($sSrt, 'UTF-8', 'ISO-8859-1'); + } + return $sSrt; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/ResourceRegistry.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/ResourceRegistry.php new file mode 100644 index 0000000000..75d3c926d6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/ResourceRegistry.php @@ -0,0 +1,95 @@ +data = \preg_replace('/\R/s', "\r\n", \rtrim($bucket->data, "\r")); +// $bucket->data = \preg_replace('/\R/s', "\n", \rtrim($bucket->data, "\r")); + $consumed += $bucket->datalen; + \stream_bucket_append($out, $bucket); + } +/* + private $buffer = ''; + while ($bucket = \stream_bucket_make_writeable($in)) { + $this->buffer += $bucket->data; + $consumed += $bucket->datalen; + } + $this->buffer = \preg_replace('/\R/s', "\r\n", $this->buffer); + \stream_bucket_append($out, \stream_bucket_new($this->stream, $this->buffer)); + $this->buffer = ''; +*/ + return PSFS_PASS_ON; + } + +// public onClose(): void +// public onCreate(): bool + + public static function appendTo($fp) + { + \stream_filter_append($fp, 'crlf', STREAM_FILTER_ALL); + } +} + +\stream_filter_register('crlf', LineEndings::class); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Binary.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Binary.php new file mode 100644 index 0000000000..7302e953af --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Binary.php @@ -0,0 +1,278 @@ + 74, + 'line-break-chars' => "\r\n" + )); + return \is_resource($rFilter) ? $rStream : false; + } + + self::$aStreams[$sHashName] = + array($rStream, $sUtilsDecodeOrEncodeFunctionName, $sFromEncoding, $sToEncoding); + + return \fopen(self::STREAM_NAME.'://'.$sHashName, 'rb'); + } + + public function stream_cast(int $cast_as) /*: resource*/ + { + return false; // $this->rStream; + } + + public function stream_open(string $sPath) : bool + { + $this->iPos = 0; + $this->sBuffer = ''; + $this->sReadEndBuffer = ''; + $this->rStream = false; + $this->sFromEncoding = null; + $this->sToEncoding = null; + $this->sFunctionName = null; + + $aPath = \parse_url($sPath); + + if (isset($aPath['host']) && isset($aPath['scheme']) && + \strlen($aPath['host']) && \strlen($aPath['scheme']) && + self::STREAM_NAME === $aPath['scheme']) + { + $sHashName = $aPath['host']; + if (isset(self::$aStreams[$sHashName]) && + \is_array(self::$aStreams[$sHashName]) && + 4 === \count(self::$aStreams[$sHashName])) + { + $this->rStream = self::$aStreams[$sHashName][0]; + $this->sFunctionName = self::$aStreams[$sHashName][1]; + $this->sFromEncoding = self::$aStreams[$sHashName][2]; + $this->sToEncoding = self::$aStreams[$sHashName][3]; + } + + return \is_resource($this->rStream); + } + + return false; + } + + public function stream_read(int $iCount) : string + { + $sReturn = ''; + $sFunctionName = $this->sFunctionName; + + if ($iCount > 0) { + if ($iCount < \strlen($this->sBuffer)) { + $sReturn = \substr($this->sBuffer, 0, $iCount); + $this->sBuffer = \substr($this->sBuffer, $iCount); + } else { + $sReturn = $this->sBuffer; + while ($iCount > 0) { + if (\feof($this->rStream)) { + if (!\strlen($this->sBuffer.$sReturn)) { + return false; + } + + if (\strlen($this->sReadEndBuffer)) { + $sReturn .= self::$sFunctionName($this->sReadEndBuffer, + $this->sReadEndBuffer, $this->sFromEncoding, $this->sToEncoding); + + $iDecodeLen = \strlen($sReturn); + } + + $iCount = 0; + $this->sBuffer = ''; + } else { + $sReadResult = \fread($this->rStream, 8192); + if (false === $sReadResult) { + return false; + } + + $sReturn .= self::$sFunctionName($this->sReadEndBuffer.$sReadResult, + $this->sReadEndBuffer, $this->sFromEncoding, $this->sToEncoding); + + $iDecodeLen = \strlen($sReadResult); + if ($iCount < $iDecodeLen) { + $this->sBuffer = \substr($sReturn, $iCount); + $sReturn = \substr($sReturn, 0, $iCount); + $iCount = 0; + } else { + $iCount -= $iDecodeLen; + } + } + } + } + + $this->iPos += \strlen($sReturn); + return $sReturn; + } + + return false; + } + + public function stream_write() : int + { + return 0; + } + + public function stream_tell() : int + { + return $this->iPos; + } + + public function stream_eof() : bool + { + return !\strlen($this->sBuffer) && \feof($this->rStream); + } + + public function stream_stat() : array + { + return array( + 'dev' => 2, + 'ino' => 0, + 'mode' => 33206, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 2, + 'size' => 0, + 'atime' => 1061067181, + 'mtime' => 1056136526, + 'ctime' => 1056136526, + 'blksize' => -1, + 'blocks' => -1 + ); + } + + public function stream_seek() : bool + { + return false; + } +} + +\stream_wrapper_register(Binary::STREAM_NAME, Binary::class); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Literal.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Literal.php new file mode 100644 index 0000000000..f9ab9c645e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Literal.php @@ -0,0 +1,158 @@ +rStream; + } + + public function stream_open(string $sPath) : bool + { + $this->iPos = 0; + $this->iSize = 0; + $this->rStream = false; + + $aPath = \parse_url($sPath); + + if (isset($aPath['host']) && isset($aPath['scheme']) && + \strlen($aPath['host']) && \strlen($aPath['scheme']) && + self::STREAM_NAME === $aPath['scheme']) + { + $sHashName = $aPath['host']; + if (isset(self::$aStreams[$sHashName]) && + \is_array(self::$aStreams[$sHashName]) && + 2 === \count(self::$aStreams[$sHashName])) + { + $this->rStream = self::$aStreams[$sHashName][0]; + $this->iSize = self::$aStreams[$sHashName][1]; + } + + return \is_resource($this->rStream); + } + + return false; + } + + public function stream_read(int $iCount) : string + { + $sResult = false; + if ($this->iSize < $this->iPos + $iCount) { + $iCount = $this->iSize - $this->iPos; + } + + if ($iCount > 0) { + $sReadResult = ''; + $iRead = $iCount; + while (0 < $iRead) { + $sAddRead = \fread($this->rStream, $iRead); + if (false === $sAddRead) { + $sReadResult = false; + break; + } + + $sReadResult .= $sAddRead; + $iRead -= \strlen($sAddRead); + $this->iPos += \strlen($sAddRead); + } + + if (false !== $sReadResult) { + $sResult = $sReadResult; + } + } + + return $sResult; + } + + public function stream_write() : int + { + return 0; + } + + public function stream_tell() : int + { + return $this->iPos; + } + + public function stream_eof() : bool + { + return $this->iPos >= $this->iSize; + } + + public function stream_stat() : array + { + return array( + 'dev' => 2, + 'ino' => 0, + 'mode' => 33206, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 2, + 'size' => $this->iSize, + 'atime' => 1061067181, + 'mtime' => 1056136526, + 'ctime' => 1056136526, + 'blksize' => -1, + 'blocks' => -1 + ); + } + + public function stream_seek() : bool + { +// $this->iPos = $offset; +// \fseek($this->rStream, $offset, $whence); + return false; + } +} + +\stream_wrapper_register(Literal::STREAM_NAME, Literal::class); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/SubStreams.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/SubStreams.php new file mode 100644 index 0000000000..267a0240d3 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/SubStreams.php @@ -0,0 +1,179 @@ +aSubStreams = array(); + + $bResult = false; + $aPath = \parse_url($sPath); + + if (isset($aPath['host'], $aPath['scheme']) && + \strlen($aPath['host']) && \strlen($aPath['scheme']) && + self::STREAM_NAME === $aPath['scheme']) + { + $sHashName = $aPath['host']; + if (isset(self::$aStreams[$sHashName]) && + \is_array(self::$aStreams[$sHashName]) && + \count(self::$aStreams[$sHashName])) + { + $this->iIndex = 0; + $this->iPos = 0; + $this->bIsEnd = false; + $this->sBuffer = ''; + $this->aSubStreams = self::$aStreams[$sHashName]; + } + + $bResult = \count($this->aSubStreams); + } + + return $bResult; + } + + public function stream_read(int $iCount) : string + { + $sReturn = ''; + $mCurrentPart = null; + if ($iCount > 0) { + if ($iCount < \strlen($this->sBuffer)) { + $sReturn = \substr($this->sBuffer, 0, $iCount); + $this->sBuffer = \substr($this->sBuffer, $iCount); + } else { + $sReturn = $this->sBuffer; + while ($iCount > 0) { + $mCurrentPart = isset($this->aSubStreams[$this->iIndex]) + ? $this->aSubStreams[$this->iIndex] + : null; + if (null === $mCurrentPart) { + $this->bIsEnd = true; + $this->sBuffer = ''; + $iCount = 0; + break; + } + + if (\is_resource($mCurrentPart)) { + if (!\feof($mCurrentPart)) { + $sReadResult = \fread($mCurrentPart, 8192); + if (false === $sReadResult) { + return false; + } + $sReturn .= $sReadResult; + } else { + ++$this->iIndex; + } + } + + $iLen = \strlen($sReturn); + if ($iCount < $iLen) { + $this->sBuffer = \substr($sReturn, $iCount); + $sReturn = \substr($sReturn, 0, $iCount); + $iCount = 0; + } else { + $iCount -= $iLen; + } + } + } + + $this->iPos += \strlen($sReturn); + return $sReturn; + } + + return false; + } + + public function stream_write() : int + { + return 0; + } + + public function stream_tell() : int + { + return $this->iPos; + } + + public function stream_eof() : bool + { + return $this->bIsEnd; + } + + public function stream_stat() : array + { + return array( + 'dev' => 2, + 'ino' => 0, + 'mode' => 33206, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 2, + 'size' => 0, + 'atime' => 1061067181, + 'mtime' => 1056136526, + 'ctime' => 1056136526, + 'blksize' => -1, + 'blocks' => -1 + ); + } + + public function stream_seek() : bool + { +// $this->iPos = $offset; +// foreach ($this->aSubStreams as $rStream) \fseek($rStream, $offset, $whence); + return false; + } +} + +\stream_wrapper_register(SubStreams::STREAM_NAME, SubStreams::class); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php new file mode 100644 index 0000000000..a1e6e7e60e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php @@ -0,0 +1,118 @@ +rStream; + } + + public function stream_open(string $sPath) : bool + { + $bResult = false; + $aPath = \parse_url($sPath); + + if (isset($aPath['host']) && isset($aPath['scheme']) && + \strlen($aPath['host']) && \strlen($aPath['scheme']) && + self::STREAM_NAME === $aPath['scheme']) + { + $sHashName = $aPath['host']; + if (isset(self::$aStreams[$sHashName]) && \is_resource(self::$aStreams[$sHashName])) { + $this->rStream = self::$aStreams[$sHashName]; + \fseek($this->rStream, 0); + $bResult = true; + } else { + $this->rStream = \fopen('php://temp', 'r+b'); + self::$aStreams[$sHashName] = $this->rStream; + + $bResult = true; + } + } + + return $bResult; + } + + public function stream_close() : bool + { + return true; + } + + public function stream_flush() : bool + { + return \fflush($this->rStream); + } + + public function stream_read(int $iLen) : string + { + return \fread($this->rStream, $iLen); + } + + public function stream_write(string $sInputString) : int + { + return \fwrite($this->rStream, $sInputString); + } + + public function stream_tell() : int + { + return \ftell($this->rStream); + } + + public function stream_eof() : bool + { + return \feof($this->rStream); + } + + public function stream_stat() : array + { + return \fstat($this->rStream); + } + + public function stream_seek(int $iOffset, int $iWhence = SEEK_SET) : int + { + return \fseek($this->rStream, $iOffset, $iWhence); + } +} + +\stream_wrapper_register(TempFile::STREAM_NAME, TempFile::class); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php new file mode 100644 index 0000000000..5cf2f68ca1 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php @@ -0,0 +1,740 @@ + 'SJIS' + ]; + + public static function MbConvertEncoding(string $sInputString, ?string $sFromEncoding, string $sToEncoding) : string + { + $sToEncoding = \strtoupper($sToEncoding); + if ($sFromEncoding) { + $sFromEncoding = \strtoupper($sFromEncoding); + if (isset(static::$RenameEncoding[$sFromEncoding])) { + $sFromEncoding = static::$RenameEncoding[$sFromEncoding]; + } +/* + if ('UTF-8' === $sFromEncoding && $sToEncoding === 'UTF7-IMAP' && \is_callable('imap_utf8_to_mutf7')) { + $sResult = \imap_utf8_to_mutf7($sInputString); + if (false !== $sResult) { + return $sResult; + } + } + + if ('BASE64' === $sFromEncoding) { + static::Base64Decode($sFromEncoding); + $sFromEncoding = null; + } else if ('UUENCODE' === $sFromEncoding) { + \convert_uudecode($sFromEncoding); + $sFromEncoding = null; + } else if ('QUOTED-PRINTABLE' === $sFromEncoding) { + \quoted_printable_decode($sFromEncoding); + $sFromEncoding = null; + } else +*/ + if (!static::MbSupportedEncoding($sFromEncoding)) { + if (\function_exists('iconv')) { + $sResult = \iconv($sFromEncoding, "{$sToEncoding}//IGNORE", $sInputString); + return (false !== $sResult) ? $sResult : $sInputString; + } + \error_log("Unsupported encoding {$sFromEncoding}"); + $sFromEncoding = null; +// return $sInputString; + } + } + + \mb_substitute_character('none'); + $sResult = \mb_convert_encoding($sInputString, $sToEncoding, $sFromEncoding); + \mb_substitute_character(0xFFFD); + + return (false !== $sResult) ? $sResult : $sInputString; + } + + public static function ConvertEncoding(string $sInputString, string $sFromEncoding, string $sToEncoding) : string + { + $sFromEncoding = static::NormalizeCharset($sFromEncoding); + $sToEncoding = static::NormalizeCharset($sToEncoding); + + if ('' === \trim($sInputString) || ($sFromEncoding === $sToEncoding && Enumerations\Charset::UTF_8 !== $sFromEncoding)) + { + return $sInputString; + } + + return static::MbConvertEncoding($sInputString, $sFromEncoding, $sToEncoding); + } + + public static function IsAscii(string $sValue) : bool + { + return '' === \trim($sValue) + || !\preg_match('/[^\x09\x10\x13\x0A\x0D\x20-\x7E]/', $sValue); + } + + public static function StripSpaces(string $sValue) : string + { + return static::Trim( + \preg_replace('/[\s]+/u', ' ', $sValue)); + } + + public static function IsUtf8(string $sValue) : bool + { + return \mb_check_encoding($sValue, 'UTF-8'); + } + + public static function FormatFileSize(int $iSize, int $iRound = 0) : string + { + $aSizes = array('B', 'KB', 'MB'); + for ($iIndex = 0; $iSize > 1024 && isset($aSizes[$iIndex + 1]); ++$iIndex) { + $iSize /= 1024; + } + return \round($iSize, $iRound).$aSizes[$iIndex]; + } + + public static function DecodeEncodingValue(string $sEncodedValue, string $sEncodingType) : string + { + $sResult = $sEncodedValue; + switch (\strtolower($sEncodingType)) + { + case 'q': + case 'quoted_printable': + case 'quoted-printable': + $sResult = \quoted_printable_decode($sResult); + break; + case 'b': + case 'base64': + $sResult = static::Base64Decode($sResult); + break; + } + return $sResult; + } + + public static function DecodeFlowedFormat(string $sInputValue) : string + { + return \preg_replace('/ ([\r]?[\n])/m', ' ', $sInputValue); + } + + public static function DecodeHeaderValue(string $sEncodedValue, string $sIncomingCharset = '') : string + { + $sValue = $sEncodedValue; + if (\strlen($sIncomingCharset)) { + $sIncomingCharset = static::NormalizeCharsetByValue($sIncomingCharset, $sValue); + + $sValue = static::ConvertEncoding($sValue, $sIncomingCharset, Enumerations\Charset::UTF_8); + } + + $sValue = \preg_replace('/\?=[\n\r\t\s]{1,5}=\?/m', '?==?', $sValue); + $sValue = \preg_replace('/[\r\n\t]+/m', ' ', $sValue); + + $aEncodeArray = array(''); + $aMatch = array(); +// \preg_match_all('/=\?[^\?]+\?[q|b|Q|B]\?[^\?]*?\?=/', $sValue, $aMatch); + \preg_match_all('/=\?[^\?]+\?[q|b|Q|B]\?.*?\?=/', $sValue, $aMatch); + + if (isset($aMatch[0]) && \is_array($aMatch[0])) { + for ($iIndex = 0, $iLen = \count($aMatch[0]); $iIndex < $iLen; ++$iIndex) { + if (isset($aMatch[0][$iIndex])) { + $iPos = \strpos($aMatch[0][$iIndex], '*'); + if (false !== $iPos) { + $aMatch[0][$iIndex][0] = \substr($aMatch[0][$iIndex][0], 0, $iPos); + } + } + } + + $aEncodeArray = $aMatch[0]; + } + + $aParts = array(); + + $sMainCharset = ''; + $bOneCharset = true; + + for ($iIndex = 0, $iLen = \count($aEncodeArray); $iIndex < $iLen; ++$iIndex) { + $aTempArr = array('', $aEncodeArray[$iIndex]); + if ('=?' === \substr(\trim($aTempArr[1]), 0, 2)) { + $iPos = \strpos($aTempArr[1], '?', 2); + $aTempArr[0] = \substr($aTempArr[1], 2, $iPos - 2); + $sEncType = \strtoupper($aTempArr[1][$iPos + 1]); + switch ($sEncType) + { + case 'Q': + $sHeaderValuePart = \str_replace('_', ' ', $aTempArr[1]); + $aTempArr[1] = \quoted_printable_decode(\substr( + $sHeaderValuePart, $iPos + 3, \strlen($sHeaderValuePart) - $iPos - 5)); + break; + case 'B': + $sHeaderValuePart = $aTempArr[1]; + $aTempArr[1] = static::Base64Decode(\substr( + $sHeaderValuePart, $iPos + 3, \strlen($sHeaderValuePart) - $iPos - 5)); + break; + } + } + + if (\strlen($aTempArr[0])) { + $sCharset = static::NormalizeCharset($aTempArr[0], true); + + if ('' === $sMainCharset) { + $sMainCharset = $sCharset; + } else if ($sMainCharset !== $sCharset) { + $bOneCharset = false; + } + } + + $aParts[] = array( + $aEncodeArray[$iIndex], + $aTempArr[1], + $sCharset + ); + + unset($aTempArr); + } + + for ($iIndex = 0, $iLen = \count($aParts); $iIndex < $iLen; ++$iIndex) { + if ($bOneCharset) { + $sValue = \str_replace($aParts[$iIndex][0], $aParts[$iIndex][1], $sValue); + } else { + $aParts[$iIndex][2] = static::NormalizeCharsetByValue($aParts[$iIndex][2], $aParts[$iIndex][1]); + + $sValue = \str_replace($aParts[$iIndex][0], + static::ConvertEncoding($aParts[$iIndex][1], $aParts[$iIndex][2], Enumerations\Charset::UTF_8), + $sValue); + } + } + + if ($bOneCharset && \strlen($sMainCharset)) { + $sMainCharset = static::NormalizeCharsetByValue($sMainCharset, $sValue); + $sValue = static::ConvertEncoding($sValue, $sMainCharset, Enumerations\Charset::UTF_8); + } + + return $sValue; + } + + public static function RemoveHeaderFromHeaders(string $sIncHeaders, array $aHeadersToRemove = array()) : string + { + $sResultHeaders = $sIncHeaders; + + if ($aHeadersToRemove) { + $aHeadersToRemove = \array_map('strtolower', $aHeadersToRemove); + + $sIncHeaders = \preg_replace('/[\r\n]+/', "\n", $sIncHeaders); + $aHeaders = \explode("\n", $sIncHeaders); + + $bSkip = false; + $aResult = array(); + + foreach ($aHeaders as $sLine) { + if (\strlen($sLine)) { + $sFirst = \substr($sLine,0,1); + if (' ' === $sFirst || "\t" === $sFirst) { + if (!$bSkip) { + $aResult[] = $sLine; + } + } else { + $bSkip = false; + $aParts = \explode(':', $sLine, 2); + + if (!empty($aParts) && !empty($aParts[0])) { + if (\in_array(\strtolower(\trim($aParts[0])), $aHeadersToRemove)) { + $bSkip = true; + } else { + $aResult[] = $sLine; + } + } + } + } + } + + $sResultHeaders = \implode("\r\n", $aResult); + } + + return $sResultHeaders; + } + + /** + * https://datatracker.ietf.org/doc/html/rfc2047 + */ + public static function EncodeHeaderValue(string $sValue, string $sEncodeType = 'B') : string + { + $sValue = \trim($sValue); + if (\strlen($sValue) && !static::IsAscii($sValue)) { + switch (\strtoupper($sEncodeType)) + { + case 'B': + return '=?'.\strtolower(Enumerations\Charset::UTF_8). + '?B?'.\base64_encode($sValue).'?='; + + case 'Q': + return '=?'.\strtolower(Enumerations\Charset::UTF_8). + '?Q?'.\str_replace(array('?', ' ', '_'), array('=3F', '_', '=5F'), + \quoted_printable_encode($sValue)).'?='; + } + } + + return $sValue; + } + + public static function AttributeRfc2231Encode(string $sAttrName, string $sValue, string $sCharset = 'utf-8', string $sLang = '', int $iLen = 1000) : string + { + $sValue = \strtoupper($sCharset)."'{$sLang}'". + \preg_replace_callback('/[\x00-\x20*\'%()<>@,;:\\\\"\/[\]?=\x80-\xFF]/', function ($match) { + return \rawurlencode($match[0]); + }, $sValue); + + $iNlen = \strlen($sAttrName); + $iVlen = \strlen($sValue); + + if (\strlen($sAttrName) + $iVlen > $iLen - 3) { + $sections = array(); + $section = 0; + + for ($i = 0, $j = 0; $i < $iVlen; $i += $j) { + $j = $iLen - $iNlen - \strlen($section) - 4; + $sections[$section++] = \substr($sValue, $i, $j); + } + + for ($i = 0, $n = $section; $i < $n; ++$i) { + $sections[$i] = ' '.$sAttrName.'*'.$i.'*='.$sections[$i]; + } + + return \implode(";\r\n", $sections); + } + return $sAttrName.'*='.$sValue; + } + + public static function EncodeHeaderUtf8AttributeValue(string $sAttrName, string $sValue) : string + { + $sAttrName = \trim($sAttrName); + $sValue = \trim($sValue); + + if (\strlen($sValue) && !static::IsAscii($sValue)) { + $sValue = static::AttributeRfc2231Encode($sAttrName, $sValue); + } else { + $sValue = $sAttrName.'="'.\str_replace('"', '\\"', $sValue).'"'; + } + + return \trim($sValue); + } + + /** + * @deprecated use getEmailAddressLocalPart + */ + public static function GetAccountNameFromEmail(string $sEmail) : string + { + return static::getEmailAddressLocalPart($sEmail); + } + public static function getEmailAddressLocalPart(string $sEmail) : string + { + $iPos = \strrpos($sEmail, '@'); + return (false === $iPos) ? $sEmail : \substr($sEmail, 0, $iPos); + } + + /** + * @deprecated use getEmailAddressDomain + */ + public static function GetDomainFromEmail(string $sEmail) : string + { + return static::getEmailAddressDomain($sEmail); + } + public static function getEmailAddressDomain(string $sEmail) : string + { + $iPos = \strrpos($sEmail, '@'); + return (false === $iPos) ? '' : \substr($sEmail, $iPos + 1); + } + + public static function GetClearDomainName(string $sDomain) : string + { + $sResultDomain = \preg_replace( + '/^(webmail|email|mail|www|imap4|imap|demo|client|ssl|secure|test|cloud|box|m)\./i', + '', $sDomain); + + return false === \strpos($sResultDomain, '.') ? $sDomain : $sResultDomain; + } + + public static function GetFileExtension(string $sFileName) : string + { + $iLast = \strrpos($sFileName, '.'); + return false === $iLast ? '' : \strtolower(\substr($sFileName, $iLast + 1)); + } + + /** + * @staticvar bool $bValidateAction + */ + public static function ResetTimeLimit(int $iTimeToReset = 15, int $iTimeToAdd = 120) : bool + { + $iTime = \time(); + if ($iTime < $_SERVER['REQUEST_TIME_FLOAT'] + 5) { + // do nothing first 5s + return true; + } + + static $bValidateAction = null; + static $iResetTimer = null; + + if (null === $bValidateAction) { + $iResetTimer = 0; + + $bValidateAction = static::FunctionCallable('set_time_limit'); + } + + if ($bValidateAction && $iTimeToReset < $iTime - $iResetTimer) { + $iResetTimer = $iTime; + if (!\set_time_limit($iTimeToAdd)) { + $bValidateAction = false; + return false; + } + + return true; + } + + return false; + } + + /** + * Replace control characters, ampersand and reserved characters (based on Win95 VFAT) + * en.wikipedia.org/wiki/Filename#Reserved_characters_and_words + */ + public static function SecureFileName(string $sFileName) : string + { + return \preg_replace('#[|\\\\?*<":>+\\[\\]/&\\pC]#su', '-', $sFileName); + } + + public static function Trim(string $sValue) : string + { + return \trim(\preg_replace('/^[\x00-\x1F]+|[\x00-\x1F]+$/Du', '', \trim($sValue))); + } + + public static function RecRmDir(string $sDir) : bool + { + return static::RecTimeDirRemove($sDir, 0); + } + + public static function RecTimeDirRemove(string $sDir, int $iTime2Kill) : bool + { + \clearstatcache(); + if (\is_dir($sDir)) { + $iTime = \time() - $iTime2Kill; + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($sDir, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST); + foreach ($iterator as $path) { + if ($path->isFile() && (!$iTime2Kill || $path->getMTime() < $iTime)) { + \is_callable('opcache_invalidate') && \opcache_invalidate($path, true); + \unlink($path); + } else if ($path->isDir() && (!$iTime2Kill || !(new \FilesystemIterator($path))->valid())) { + \rmdir($path); + } + } + \clearstatcache(); +// \realpath_cache_size() && \clearstatcache(true); + return !(new \FilesystemIterator($sDir))->valid() && \rmdir($sDir); + } + + return false; + } + + public static function Utf8Clear(?string $sUtfString) : string + { + if (!\strlen($sUtfString)) { + return ''; + } + + $sSubstitute = ''; // '�' 0xFFFD +/* + $converter = new \UConverter('UTF-8', 'UTF-8'); + $converter->setSubstChars($sSubstitute); + $sNewUtfString = $converter->->convert($sUtfString); +// $sNewUtfString = \UConverter::transcode($str, 'UTF-8', 'UTF-8', [????]); +*/ + \mb_substitute_character($sSubstitute ?: 'none'); + $sNewUtfString = \mb_convert_encoding($sUtfString, 'UTF-8', 'UTF-8'); + \mb_substitute_character(0xFFFD); + + return (false !== $sNewUtfString) ? $sNewUtfString : $sUtfString; +// return (false !== $sNewUtfString) ? \preg_replace('/\\p{Cc}/u', '', $sNewUtfString) : $sUtfString; + } + + public static function Base64Decode(string $sString) : string + { + $sResultString = \base64_decode($sString, true); + if (false === $sResultString) { + $sString = \str_replace(array(' ', "\r", "\n", "\t"), '', $sString); + $sString = \preg_replace('/[^a-zA-Z0-9=+\/](.*)$/', '', $sString); + + if (false !== \strpos(\trim(\trim($sString), '='), '=')) { + $sString = \preg_replace('/=([^=])/', '= $1', $sString); + $aStrings = \explode(' ', $sString); + foreach ($aStrings as $iIndex => $sParts) { + $aStrings[$iIndex] = \base64_decode($sParts); + } + + $sResultString = \implode('', $aStrings); + } else { + $sResultString = \base64_decode($sString); + } + } + + return $sResultString; + } + + public static function UrlSafeBase64Encode(string $sValue) : string + { + return \rtrim(\strtr(\base64_encode($sValue), '+/', '-_'), '='); + } + + public static function UrlSafeBase64Decode(string $sValue) : string + { + return \base64_decode(\strtr($sValue, '-_', '+/'), true); + } + + /** + * @param resource $fResource + */ + public static function FpassthruWithTimeLimitReset($fResource, int $iBufferLen = 8192) : bool + { + $bResult = \is_resource($fResource); + if ($bResult) { + while (!\feof($fResource)) { + $sBuffer = \fread($fResource, $iBufferLen); + if (false === $sBuffer) { + break; + } + echo $sBuffer; + static::ResetTimeLimit(); + } + } + + return $bResult; + } + + /** + * @param resource $rRead + * @param resource $rWrite + */ + public static function WriteStream($rRead, $rWrite, int $iBufferLen = 8192, bool $bFixCrLf = false, bool $bRewindOnComplete = false) : int + { + if (!\is_resource($rRead) || !\is_resource($rWrite)) { + return -1; + } + + $iResult = 0; + + while (!\feof($rRead)) { + $sBuffer = \fread($rRead, $iBufferLen); + if (false === $sBuffer) { + return -1; + } + + if ('' === $sBuffer) { + break; + } + + if ($bFixCrLf) { + $sBuffer = \str_replace("\n", "\r\n", \str_replace("\r", '', $sBuffer)); + } + + $iResult += \strlen($sBuffer); + + if (false === \fwrite($rWrite, $sBuffer)) { + return -1; + } + + static::ResetTimeLimit(); + } + + if ($bRewindOnComplete) { + \rewind($rWrite); + } + + return $iResult; + } + + public static function Utf7ModifiedToUtf8(string $sStr) : string + { + // imap_mutf7_to_utf8() doesn't support U+10000 and up, + // thats why mb_convert_encoding is used + return static::MbConvertEncoding($sStr, 'UTF7-IMAP', 'UTF-8'); + } + + public static function Utf8ToUtf7Modified(string $sStr) : string + { + return static::MbConvertEncoding($sStr, 'UTF-8', 'UTF7-IMAP'); + } + + public static function FunctionsCallable(array $aFunctionNames) : bool + { + foreach ($aFunctionNames as $sFunctionName) { + if (!static::FunctionCallable($sFunctionName)) { + return false; + } + } + return true; + } + + private static $disabled_functions = null; + public static function FunctionCallable(string $sFunctionName) : bool + { + if (null === static::$disabled_functions) { + static::$disabled_functions = \array_map('trim', \explode(',', \ini_get('disable_functions'))); + } +/* + $disabled_classes = \explode(',', \ini_get('disable_classes')); + \in_array($function, $disabled_classes); +*/ + return \function_exists($sFunctionName) + && !\in_array($sFunctionName, static::$disabled_functions); +// && \is_callable($mFunctionNameOrNames); + } + + public static function Sha1Rand(string $sAdditionalSalt = '') : string + { + return \sha1($sAdditionalSalt . \random_bytes(16)); + } + + public static function ValidateIP(string $sIp) : bool + { + return !empty($sIp) && $sIp === \filter_var($sIp, FILTER_VALIDATE_IP); + } + + /** + * @deprecated + */ + public static function IdnToUtf8(string $sStr) : string + { + $trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + \SnappyMail\Log::warning('MailSo', "Deprecated function IdnToUtf8 called at {$trace['file']}#{$trace['line']}"); + if (\preg_match('/(^|\.|@)xn--/i', $sStr)) { + $sStr = \str_contains($sStr, '@') + ? \SnappyMail\IDN::emailToUtf8($string) + : \idn_to_utf8($string); + } + return $sStr; + } + + /** + * @deprecated + */ + public static function IdnToAscii(string $sStr, bool $bLowerCase = false) : string + { + $trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + \SnappyMail\Log::warning('MailSo', "Deprecated function IdnToAscii called at {$trace['file']}#{$trace['line']}"); + $aParts = \explode('@', $sStr); + $sDomain = \array_pop($aParts); + if (\preg_match('/[^\x20-\x7E]/', $sDomain)) { + $sDomain = \idn_to_ascii($string); + } + if ($bLowerCase) { + $sDomain = \strtolower($sDomain); + } + $aParts[] = $sDomain; + return \implode('@', $aParts); + } + + public static function mkdir(string $directory) : void + { + if (!\is_dir($directory)) { + if (!\mkdir($directory, 0700, true)) { + throw new \RuntimeException("Failed to create directory {$directory}"); + } + \clearstatcache(); + } +/* + if (!\is_writable($directory)) { + throw new \Exception("Failed to access directory {$directory}"); + } +*/ + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Xxtea.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Xxtea.php new file mode 100644 index 0000000000..cea141a1c9 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Xxtea.php @@ -0,0 +1,123 @@ +> 2 & 3; + for ($iPIndex = 0; $iPIndex < $iN; ++$iPIndex) { + $iY = $aV[$iPIndex + 1]; + $iMx = self::int32((($iZ >> 5 & 0x07ffffff) ^ $iY << 2) + + (($iY >> 3 & 0x1fffffff) ^ $iZ << 4)) ^ self::int32(($iSum ^ $iY) + ($aK[$iPIndex & 3 ^ $iE] ^ $iZ)); + $iZ = $aV[$iPIndex] = self::int32($aV[$iPIndex] + $iMx); + } + $iY = $aV[0]; + $iMx = self::int32((($iZ >> 5 & 0x07ffffff) ^ $iY << 2) + + (($iY >> 3 & 0x1fffffff) ^ $iZ << 4)) ^ self::int32(($iSum ^ $iY) + ($aK[$iPIndex & 3 ^ $iE] ^ $iZ)); + $iZ = $aV[$iN] = self::int32($aV[$iN] + $iMx); + } + + return self::long2str($aV, false); + } + + public static function Decrypt(string $sEncryptedString, string $sKey) : string + { + $aV = self::str2long($sEncryptedString, false); + $aK = self::str2long($sKey, false); + + if (\count($aK) < 4) { + for ($iIndex = \count($aK); $iIndex < 4; ++$iIndex) { + $aK[$iIndex] = 0; + } + } + + $iN = \count($aV) - 1; + + $iZ = $aV[$iN]; + $iY = $aV[0]; + $iDelta = 0x9E3779B9; + $iQ = \floor(6 + 52 / ($iN + 1)); + $iSum = self::int32($iQ * $iDelta); + while ($iSum != 0) { + $iE = $iSum >> 2 & 3; + for ($iPIndex = $iN; $iPIndex > 0; --$iPIndex) { + $iZ = $aV[$iPIndex - 1]; + $iMx = self::int32((($iZ >> 5 & 0x07ffffff) ^ $iY << 2) + + (($iY >> 3 & 0x1fffffff) ^ $iZ << 4)) ^ self::int32(($iSum ^ $iY) + ($aK[$iPIndex & 3 ^ $iE] ^ $iZ)); + $iY = $aV[$iPIndex] = self::int32($aV[$iPIndex] - $iMx); + } + $iZ = $aV[$iN]; + $iMx = self::int32((($iZ >> 5 & 0x07ffffff) ^ $iY << 2) + + (($iY >> 3 & 0x1fffffff) ^ $iZ << 4)) ^ self::int32(($iSum ^ $iY) + ($aK[$iPIndex & 3 ^ $iE] ^ $iZ)); + $iY = $aV[0] = self::int32($aV[0] - $iMx); + $iSum = self::int32($iSum - $iDelta); + } + + return self::long2str($aV, true); + } + + private static function long2str(array $aV, bool $aW) : string + { + $iLen = \count($aV); + $iN = ($iLen - 1) << 2; + if ($aW) { + $iM = $aV[$iLen - 1]; + if (($iM < $iN - 3) || ($iM > $iN)) { + return false; + } + $iN = $iM; + } + $aS = array(); + for ($iIndex = 0; $iIndex < $iLen; ++$iIndex) { + $aS[$iIndex] = \pack('V', $aV[$iIndex]); + } + return $aW ? \substr(\join('', $aS), 0, $iN) : \join('', $aS); + } + + private static function str2long(string $sS, string $sW) : array + { + $aV = \unpack('V*', $sS . \str_repeat("\0", (4 - \strlen($sS) % 4) & 3)); + $aV = \array_values($aV); + if ($sW) { + $aV[\count($aV)] = \strlen($sS); + } + return $aV; + } + + private static function int32($iN) : int + { + return $iN & 0xffffffff; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/CacheClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/CacheClient.php new file mode 100644 index 0000000000..d42b9f09b6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/CacheClient.php @@ -0,0 +1,126 @@ +oDriver ? $this->oDriver->Set($sKey.$this->sCacheIndex, $sValue) : false; + } + + public function SetTimer(string $sKey) : bool + { + return $this->Set($sKey.'/TIMER', time()); + } + + public function SetLock(string $sKey) : bool + { + return $this->Set($sKey.'/LOCK', '1'); + } + + public function RemoveLock(string $sKey) : bool + { + return $this->Set($sKey.'/LOCK', '0'); + } + + public function GetLock(string $sKey) : bool + { + return '1' === $this->Get($sKey.'/LOCK'); + } + + public function Exists(string $sKey) : bool + { + return $this->oDriver && $this->oDriver->Exists($sKey.$this->sCacheIndex); + } + + public function Get(string $sKey, bool $bClearAfterGet = false) : ?string + { + $sValue = null; + if ($this->oDriver) { + $sValue = $this->oDriver->Get($sKey.$this->sCacheIndex); + if ($bClearAfterGet) { + $this->Delete($sKey); + } + } + return $sValue; + } + + public function GetTimer(string $sKey) : int + { + $iTimer = 0; + $sValue = $this->Get($sKey.'/TIMER'); + if (\strlen($sValue) && \is_numeric($sValue)) { + $iTimer = (int) $sValue; + } + + return $iTimer; + } + + public function Delete(string $sKey) : self + { + if ($this->oDriver) { + $this->oDriver->Delete($sKey.$this->sCacheIndex); + } + + return $this; + } + + public function SetDriver(DriverInterface $oDriver) : self + { + $this->oDriver = $oDriver; + + return $this; + } + + public function GC(int $iTimeToClearInHours = 24) : bool + { + return $this->oDriver ? $this->oDriver->GC($iTimeToClearInHours) : false; + } + + public function IsInited() : bool + { + return !!$this->oDriver; + } + + public function SetCacheIndex(string $sCacheIndex) : self + { + $this->sCacheIndex = \strlen($sCacheIndex) ? "\x0".$sCacheIndex : ''; + + return $this; + } + + public function Verify(bool $bCache = false) : bool + { + if ($this->oDriver) { + $sCacheData = \gmdate('Y-m-d-H'); + if ($bCache && $sCacheData === $this->Get('__verify_key__')) { + return true; + } + + return $this->Set('__verify_key__', $sCacheData); + } + + return false; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/DriverInterface.php b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/DriverInterface.php new file mode 100644 index 0000000000..893a58ad17 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/DriverInterface.php @@ -0,0 +1,31 @@ +sCacheFolder = \rtrim(\trim($sCacheFolder), '\\/').'/'; + \MailSo\Base\Utils::mkdir($this->sCacheFolder); + + // http://www.brynosaurus.com/cachedir/ + $tag = $this->sCacheFolder . 'CACHEDIR.TAG'; + \is_file($tag) || \file_put_contents($tag, 'Signature: 8a477f597d28d172789f06886806bc55'); + } + + public function setPrefix(string $sKeyPrefix) : void + { + if (!empty($sKeyPrefix)) { + $sKeyPrefix = \str_pad(\preg_replace('/[^a-zA-Z0-9_]/', '_', + \rtrim(\trim($sKeyPrefix), '\\/')), 5, '_'); + $this->sKeyPrefix = '__/'. + \substr($sKeyPrefix, 0, 2).'/'.\substr($sKeyPrefix, 2, 2).'/'. + $sKeyPrefix.'/'; + } + } + + public function Set(string $sKey, string $sValue) : bool + { + $sPath = $this->generateCachedFileName($sKey, true); + return '' === $sPath ? false : false !== \file_put_contents($sPath, $sValue); + } + + public function Exists(string $sKey) : bool + { + $sPath = $this->generateCachedFileName($sKey); + return '' !== $sPath && \file_exists($sPath); + } + + public function Get(string $sKey) : ?string + { + $sValue = null; + $sPath = $this->generateCachedFileName($sKey); + if ('' !== $sPath && \file_exists($sPath)) { + $sValue = \file_get_contents($sPath); + } + return \is_string($sValue) ? $sValue : null; + } + + public function Delete(string $sKey) : void + { + $sPath = $this->generateCachedFileName($sKey); + if ('' !== $sPath && \file_exists($sPath)) { + \unlink($sPath); + } + } + + public function GC(int $iTimeToClearInHours = 24) : bool + { + \MailSo\Base\Utils::RecTimeDirRemove($this->sCacheFolder, 3600 * $iTimeToClearInHours); + return true; + } + + private function generateCachedFileName(string $sKey, bool $bMkDir = false) : string + { + $sFilePath = ''; + if (3 < \strlen($sKey)) { + $sKeyPath = \sha1($sKey); + $sFilePath = $this->sCacheFolder . $this->sKeyPrefix + . \substr($sKeyPath, 0, 2) . '/' . \substr($sKeyPath, 2, 2) . '/' . $sKeyPath; + if ($bMkDir) { + $dir = \dirname($sFilePath); + if (!\is_dir($dir) && !\mkdir($dir, 0700, true)) { + \error_log("mkdir({$dir}) failed"); + $sFilePath = ''; + } + } + } + return $sFilePath; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Config.php b/snappymail/v/0.0.0/app/libraries/MailSo/Config.php new file mode 100644 index 0000000000..c09480bb08 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Config.php @@ -0,0 +1,20 @@ +sCharset; + } + + public function ContentTransferEncoding() : string + { + return $this->sContentTransferEncoding; + } + + public function ContentType() : string + { + return $this->sContentType; + } + + public function PartID() : string + { + return $this->sPartID; + } + + public function FileName(bool $bCalculateOnEmpty = false) : string + { + $sFileName = \trim($this->sFileName); + if (\strlen($sFileName) || !$bCalculateOnEmpty) { + return $sFileName; + } + + $sIdx = '-' . $this->PartID(); + + $sMimeType = $this->sContentType; + if ('message/rfc822' === $sMimeType) { + return "message{$sIdx}.eml"; + } + if ('text/calendar' === $sMimeType) { + return "calendar{$sIdx}.ics"; + } + if ('text/plain' === $sMimeType) { + return "part{$sIdx}.txt"; + } + if (\preg_match('@text/(vcard|html|csv|xml|css|asp)@', $sMimeType, $aMatch) + || \preg_match('@image/(png|jpeg|gif|bmp|cgm|ief|tiff|webp)@', $sMimeType, $aMatch)) { + return "part{$sIdx}.{$aMatch[1]}"; + } + if (\strlen($sMimeType)) { + return \str_replace('/', $sIdx.'.', $sMimeType); + } + + return ($this->isInline() ? 'inline' : 'part' ) . $sIdx; + } + + public function EstimatedSize() : int + { + $fCoefficient = 1; + switch ($this->sContentTransferEncoding) + { + case 'base64': + $fCoefficient = 0.75; + break; + case 'quoted-printable': + $fCoefficient = 0.44; + break; + } + + return (int) ($this->iSize * $fCoefficient); + } + + public function SubParts() : array + { + return $this->aSubParts; + } + + public function isInline() : bool + { + return 'inline' === $this->sDisposition || \strlen($this->sContentID); + } + + public function isText() : bool + { + return 'text/html' === $this->sContentType + || 'text/plain' === $this->sContentType + // Also the useless AMP content + || 'text/x-amp-html' === $this->sContentType; + } + + // https://datatracker.ietf.org/doc/html/rfc3156#section-4 + public function isPgpEncrypted() : bool + { + return 'multipart/encrypted' === $this->sContentType + && !empty($this->aContentTypeParams['protocol']) + && 'application/pgp-encrypted' === \strtolower(\trim($this->aContentTypeParams['protocol'])) + // The multipart/encrypted body MUST consist of exactly two parts. + && 2 === \count($this->aSubParts) + && 'application/pgp-encrypted' === $this->aSubParts[0]->ContentType() + && 'application/octet-stream' === $this->aSubParts[1]->ContentType(); +// && 'Version: 1' === $this->aSubParts[0]->Body() + } + + // https://datatracker.ietf.org/doc/html/rfc3156#section-5 + public function isPgpSigned() : bool + { + return 'multipart/signed' === $this->sContentType + && !empty($this->aContentTypeParams['protocol']) + && 'application/pgp-signature' === \strtolower(\trim($this->aContentTypeParams['protocol'])) + // The multipart/signed body MUST consist of exactly two parts. + && 2 === \count($this->aSubParts) + && 'application/pgp-signature' === $this->aSubParts[1]->ContentType(); + } + + // https://datatracker.ietf.org/doc/html/rfc2633#section-3.3 + public function isSMimeEncrypted() : bool + { + $type = \strtolower(\trim($this->aContentTypeParams['smime-type'] ?? '')); + return ContentType::isPkcs7Mime($this->sContentType) + && !empty($this->aContentTypeParams['smime-type']) + && ('enveloped-data' === $type || 'authenveloped-data' === $type); + } + + // https://www.rfc-editor.org/rfc/rfc8551.html#section-3.5 + public function isSMimeSigned() : bool + { + return ('multipart/signed' === $this->sContentType + && !empty($this->aContentTypeParams['protocol']) + && ContentType::isPkcs7Signature(\strtolower(\trim($this->aContentTypeParams['protocol']))) + // The multipart/signed body MUST consist of exactly two parts. + && 2 === \count($this->aSubParts) + && ContentType::isPkcs7Signature($this->aSubParts[1]->ContentType()) + ) || (ContentType::isPkcs7Mime($this->sContentType) + && !empty($this->aContentTypeParams['smime-type']) + && 'signed-data' === \strtolower(\trim($this->aContentTypeParams['smime-type'])) + ); + } + + protected function IsAttachment(/*BodyStructure*/ $oParent) : bool + { +// return 'application/pgp-encrypted' !== $this->sContentType + return (!$oParent || !$oParent->isPgpEncrypted()) + && ( + 'attachment' === $this->sDisposition || ( + !\str_starts_with($this->sContentType, 'multipart/') + && !$this->isText() + ) + ); + } + + public function IsFlowedFormat() : bool + { + return !empty($this->aContentTypeParams['format']) + && 'flowed' === \strtolower(\trim($this->aContentTypeParams['format'])) + && !\in_array($this->sContentTransferEncoding, array('base64', 'quoted-printable')); + } + + public function GetHtmlAndPlainParts() : array + { + $aParts = []; + + $gParts = $this->SearchByCallback(function ($oItem, $oParent) { + return $oItem->isText() && !$oItem->IsAttachment($oParent); + }); + foreach ($gParts as $oPart) { + $aParts[] = $oPart; + } + + /** + * No text found, is it encrypted? + * If so, just return that. + * Only when \RainLoop\Api::Config()->Get('security', 'openpgp', true) + */ + if (!$aParts) { + $gEncryptedParts = $this->SearchByContentType('multipart/encrypted'); + foreach ($gEncryptedParts as $oPart) { + if ($oPart->isPgpEncrypted()) { + return array($oPart->SubParts()[1]); + } + } + } + + /** + * Still no text found? + * Look in attachments as it could be an X-Mms-Message + * https://github.com/the-djmaze/snappymail/issues/1294 + */ + if (!$aParts) { + $gParts = $this->SearchByCallback(fn($oItem) => $oItem->isText()); + foreach ($gParts as $oPart) { + $aParts[] = $oPart; + } + } + + return $aParts; + } + + public function SearchCharset() : string + { + $gParts = $this->SearchByCallback(function ($oPart, $oParent) { + return $oPart->Charset() && $oPart->isText() && !$oPart->IsAttachment($oParent); + }); + + if (!$gParts->valid()) { + $gParts = $this->SearchByCallback(function ($oPart, $oParent) { + return $oPart->Charset() && $oPart->IsAttachment($oParent); + }); + } + + return $gParts->valid() ? $gParts->current()->Charset() : ''; + } + + public function SearchByCallback(callable $fCallback, /*BodyStructure*/ $parent = null) : iterable + { + if ($fCallback($this, $parent)) { + yield $this; + } + foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart) { + yield from $oSubPart->SearchByCallback($fCallback, $this); + } + } + + public function SearchAttachmentsParts() : iterable + { + return $this->SearchByCallback(function ($oItem, $oParent) { +// return $oItem->IsAttachment(); + return $oItem->IsAttachment($oParent) && (!$oParent || !$oParent->isPgpEncrypted()); + }); + } + + public function SearchByContentType(string $sContentType) : iterable + { + $sContentType = \strtolower($sContentType); + return $this->SearchByCallback(function ($oItem) use ($sContentType) { + return $sContentType === $oItem->sContentType; + }); + } + + public function SearchByContentTypes(array $aContentTypes) : iterable + { + return $this->SearchByCallback(function ($oItem) use ($aContentTypes) { + return \in_array($oItem->sContentType, $aContentTypes); + }); + } + + public function GetPartByMimeIndex(string $sMimeIndex) : self + { + $oPart = null; + if (\strlen($sMimeIndex)) { + if ($sMimeIndex === $this->sPartID) { + $oPart = $this; + } + + if (null === $oPart) { + foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart) { + $oPart = $oSubPart->GetPartByMimeIndex($sMimeIndex); + if (null !== $oPart) { + break; + } + } + } + } + + return $oPart; + } + + private static function decodeAttrParameter(array $aParams, string $sParamName, string $sCharset) : string + { + $sResult = ''; + if (isset($aParams[$sParamName])) { + $sResult = \MailSo\Base\Utils::DecodeHeaderValue($aParams[$sParamName], $sCharset); + } else if (isset($aParams[$sParamName.'*'])) { + $aValueParts = \explode("''", $aParams[$sParamName.'*'], 2); + if (2 === \count($aValueParts)) { + $sCharset = isset($aValueParts[0]) ? $aValueParts[0] : \MailSo\Base\Enumerations\Charset::UTF_8; + $sResult = \MailSo\Base\Utils::ConvertEncoding( + \urldecode($aValueParts[1]), $sCharset, \MailSo\Base\Enumerations\Charset::UTF_8); + } else { + $sResult = \urldecode($aParams[$sParamName.'*']); + } + } else { + $sCharset = ''; + $sCharsetIndex = -1; + + $aFileNames = array(); + foreach ($aParams as $sName => $sValue) { + $aMatches = array(); + if (\preg_match('/^'.\preg_quote($sParamName, '/').'\*([0-9]+)\*$/i', $sName, $aMatches)) { + $iIndex = (int) $aMatches[1]; + if ($sCharsetIndex < $iIndex && false !== \strpos($sValue, "''")) { + $aValueParts = \explode("''", $sValue, 2); + if (2 === \count($aValueParts) && \strlen($aValueParts[0])) { + $sCharsetIndex = $iIndex; + $sCharset = $aValueParts[0]; + $sValue = $aValueParts[1]; + } + } + $aFileNames[$iIndex] = $sValue; + } + } + + if (\count($aFileNames)) { + \ksort($aFileNames, SORT_NUMERIC); + $sResult = \implode(\array_values($aFileNames)); + $sResult = \urldecode($sResult); + if (\strlen($sCharset)) { + $sResult = \MailSo\Base\Utils::ConvertEncoding($sResult, + $sCharset, \MailSo\Base\Enumerations\Charset::UTF_8); + } + } + } + + return $sResult; + } + + public static function NewInstance(array $aBodyStructure, string $sPartID = '') : ?self + { + if (2 > \count($aBodyStructure)) { + return null; + } + + $sContentTypeMain = ''; + $sContentTypeSub = ''; + $aSubParts = array(); + $aContentTypeParams = array(); + $sFileName = ''; + $sCharset = ''; // \MailSo\Base\Enumerations\Charset::UTF_8 ? + $sContentID = ''; + $sDescription = ''; + $sContentTransferEncoding = ''; + $iSize = 0; + $iExtraItemPos = 0; // list index of items which have no well-established position (such as 0, 1, 5, etc). + + if (\is_array($aBodyStructure[0])) { + // Process multipart body structure + $sContentTypeMain = 'multipart'; + $sContentTypeSub = 'mixed'; // primary default + $sSubPartIDPrefix = ''; + if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1]) { + // This multi-part is root part of message. + $sSubPartIDPrefix = $sPartID; + $sPartID .= 'TEXT'; + } else if (\strlen($sPartID)) { + // This multi-part is a part of another multi-part. + $sSubPartIDPrefix = $sPartID.'.'; + } + + $iIndex = 1; + + /** + * First process the subparts, like: + ("text" "plain" ("charset" "utf-8") …) + ("text" "html" …) + */ + while ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) { + $oPart = self::NewInstance($aBodyStructure[$iExtraItemPos], $sSubPartIDPrefix.$iIndex); + if (!$oPart) { + return null; + } + + // For multipart, we have no charset info in the part itself. Thus, + // obtain charset from nested parts. + if (!$sCharset) { + $sCharset = $oPart->Charset(); + } + + $aSubParts[] = $oPart; + ++$iExtraItemPos; + ++$iIndex; + } + + /** + * Now process the subparts containter like: + "alternative" ("boundary" "--boundary_id") … + */ + if ($iExtraItemPos < \count($aBodyStructure)) { + if (!\is_string($aBodyStructure[$iExtraItemPos])) { + return null; + } + $sContentTypeSub = \strtolower($aBodyStructure[$iExtraItemPos]); + + ++$iExtraItemPos; + if ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) { + $aContentTypeParams = self::getKeyValueListFromArrayList($aBodyStructure[$iExtraItemPos]); + } + } + } else if (\is_string($aBodyStructure[0])) { + // Process simple (singlepart) body structure + if (7 > \count($aBodyStructure) || !\is_string($aBodyStructure[1])) { + return null; + } + + $sContentTypeMain = \strtolower($aBodyStructure[0]); + $sContentTypeSub = \strtolower($aBodyStructure[1]); + + $aBodyParamList = $aBodyStructure[2]; + if (\is_array($aBodyParamList)) { + $aContentTypeParams = self::getKeyValueListFromArrayList($aBodyParamList); + if (isset($aContentTypeParams['charset'])) { + $sCharset = $aContentTypeParams['charset']; + } + $sFileName = self::decodeAttrParameter($aContentTypeParams, 'name', $sCharset); + if ($sFileName) { + $aContentTypeParams['name'] = $sFileName; + } + } + + if (null !== $aBodyStructure[3]) { + if (!\is_string($aBodyStructure[3])) { + return null; + } + $sContentID = $aBodyStructure[3]; + } + + if (null !== $aBodyStructure[4]) { + if (!\is_string($aBodyStructure[4])) { + return null; + } + $sDescription = $aBodyStructure[4]; + } + + if (null !== $aBodyStructure[5]) { + if (!\is_string($aBodyStructure[5])) { + return null; + } + $sContentTransferEncoding = $aBodyStructure[5]; + } + + $iSize = \is_numeric($aBodyStructure[6]) ? (int) $aBodyStructure[6] : 0; + + if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1]) { + // This is the only sub-part of the message (otherwise, it would be + // one of sub-parts of a multi-part, and partID would already be fully set up). + $sPartID .= '1'; + } + + $iExtraItemPos = 7; + if ('text' === $sContentTypeMain) { + /** + * A body type of type TEXT contains, immediately after the basic + * fields, the size of the body in text lines. + */ + ++$iExtraItemPos; + } else if ('message' === $sContentTypeMain && 'rfc822' === $sContentTypeSub) { + /** + * A body type of type MESSAGE and subtype RFC822 contains, + * immediately after the basic fields, the envelope structure, + * body structure, and size in text lines of the encapsulated message. + */ + $iExtraItemPos += 3; + } + } else { + return null; + } + + // Skip body MD5 because most mail servers leave it NIL anyway + ++$iExtraItemPos; + + $sDisposition = ''; + + if ($iExtraItemPos < \count($aBodyStructure)) { + $aDispList = $aBodyStructure[$iExtraItemPos]; + if (\is_array($aDispList) && 1 < \count($aDispList)) { + if (!\is_string($aDispList[0])) { + return null; + } + $sDisposition = $aDispList[0]; + if (\is_array($aDispList[1])) { + $aDispositionParams = self::getKeyValueListFromArrayList($aDispList[1]); + $sFileName = self::decodeAttrParameter($aDispositionParams, 'filename', $sCharset); + } + } + ++$iExtraItemPos; + } + + $oStructure = new self; + $oStructure->sContentType = \strtolower(\trim($sContentTypeMain.'/'.$sContentTypeSub)); + $oStructure->aContentTypeParams = $aContentTypeParams; + $oStructure->sCharset = $sCharset; + $oStructure->sContentID = \trim($sContentID); + $oStructure->sDescription = $sDescription; + $oStructure->sContentTransferEncoding = \strtolower($sContentTransferEncoding); + $oStructure->sDisposition = \strtolower($sDisposition); + $oStructure->sFileName = \MailSo\Base\Utils::Utf8Clear($sFileName); + $oStructure->iSize = $iSize; + $oStructure->sPartID = $sPartID; + $oStructure->aSubParts = $aSubParts; + + if ($iExtraItemPos < \count($aBodyStructure)) { + if (\is_array($aBodyStructure[$iExtraItemPos])) { + $oStructure->sLanguage = \implode(',', $aBodyStructure[$iExtraItemPos]); + } else if (\is_string($aBodyStructure[$iExtraItemPos])) { + $oStructure->sLanguage = $aBodyStructure[$iExtraItemPos]; + } + ++$iExtraItemPos; + + if ($iExtraItemPos < \count($aBodyStructure) && \is_string($aBodyStructure[$iExtraItemPos])) { + $oStructure->sLocation = $aBodyStructure[$iExtraItemPos]; + } + } + + return $oStructure; + } + + /** + * Returns dict with key="charset" and value="US-ASCII" for array ("CHARSET" "US-ASCII"). + * Keys are lowercased (StringDictionary itself does this), values are not altered. + */ + private static function getKeyValueListFromArrayList(array $aList) : array + { + $aDict = array(); + $iLen = \count($aList); + if (0 === ($iLen % 2)) { + for ($iIndex = 0; $iIndex < $iLen; $iIndex += 2) { + if (\is_string($aList[$iIndex]) && \is_string($aList[$iIndex + 1])) { + $aDict[\strtolower($aList[$iIndex])] = $aList[$iIndex + 1]; + } + } + } + + return $aDict; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( + 'mimeIndex' => $this->sPartID, + 'mimeType' => $this->sContentType, +// 'mimeTypeParams' => $this->aContentTypeParams, + 'fileName' => \MailSo\Base\Utils::SecureFileName($this->FileName(true)), + 'estimatedSize' => $this->EstimatedSize(), + 'cId' => $this->sContentID, + 'contentLocation' => $this->sLocation, + 'isInline' => $this->isInline() + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/ACL.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/ACL.php new file mode 100644 index 0000000000..103bf3b1e6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/ACL.php @@ -0,0 +1,194 @@ +ACLDisabled || !$this->hasCapability('ACL')) { + return false; + } + + // The "RIGHTS=" capability MUST NOT include any of the rights defined in RFC 2086: + // "l", "r", "s", "w", "i", "p", "a", "c", "d", and the digits ("0" .. "9") + // So it is: RIGHTS=texk +// $mainRights = \str_split($this->CapabilityValue('RIGHTS') ?: ''); + + if ('MYRIGHTS' === $command) { + // at least one of the "l", "r", "i", "k", "x", "a" rights is required + return true; + } + + if (\in_array($command, ['GETACL','SETACL','LISTRIGHTS','DELETEACL'])) { + return true; + } + + $rights = $this->FolderMyRights($sFolderName); + if ($rights) { + switch ($command) + { + case 'LIST': + case 'LSUB': + return $rights->hasRight('LOOKUP'); + case 'CREATE': + return true; // $parent->$rights->hasRight('k'); + case 'DELETE': + return $rights->hasRight('x'); + case 'RENAME': + return $rights->hasRight('k') && $rights->hasRight('x'); + case 'SELECT': + case 'EXAMINE': + case 'STATUS': + return $rights->hasRight('r'); + case 'APPEND': + case 'COPY': + return $rights->hasRight('i'); + case 'EXPUNGE': + return $rights->hasRight('e'); +/* + case 'SUBSCRIBE': + return $rights->hasRight('l') || true; + case 'UNSUBSCRIBE': + return true; + case 'CLOSE': + return $rights->hasRight('e') || true; + case 'FETCH': + return $rights->hasRight('s') || true; + case 'STORE': + return $rights->hasRight('s') || $rights->hasRight('w') || $rights->hasRight('t'); +*/ + case 'GETACL': + case 'SETACL': + case 'LISTRIGHTS': + case 'DELETEACL': + return $rights->hasRight('a'); + } + } + return true; + } + + private function FolderACLRequest(string $sFolderName, string $sCommand, array $aParams) : \MailSo\Imap\ResponseCollection + { + if ($this->ACLAllow($sFolderName, $sCommand)) try { +// \array_unshift($aParams, $this->EscapeFolderName($sFolderName)); + return $this->SendRequestGetResponse($sCommand, $aParams); + } catch (\Throwable $oException) { + // Error in IMAP command $sCommand: ACLs disabled + $this->ACLDisabled = true; + throw $oException; + } + } + + public function FolderSetACL(string $sFolderName, string $sIdentifier, string $sAccessRights) : void + { + $this->FolderACLRequest($sFolderName, 'SETACL', array( + $this->EscapeFolderName($sFolderName), + $this->EscapeString($sIdentifier), + $this->EscapeString($sAccessRights) + )); + } + + public function FolderDeleteACL(string $sFolderName, string $sIdentifier) : void + { + $this->FolderACLRequest($sFolderName, 'DELETEACL', array( + $this->EscapeFolderName($sFolderName), + $this->EscapeString($sIdentifier) + )); + } + + public function FolderGetACL(string $sFolderName) : array + { + $aResult = array(); + $response = $this->FolderMyRights($sFolderName); + $aResult[] = $response; + if ($response->hasRight('a')) { + // Else error: "NOPERM You lack administrator privileges on this mailbox." + $oResponses = $this->FolderACLRequest($sFolderName, 'GETACL', array($this->EscapeFolderName($sFolderName))); + foreach ($oResponses as $oResponse) { + // * ACL INBOX.shared demo@snappymail.eu akxeilprwtscd demo2@snappymail.eu lrwstipekxacd + if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType + && isset($oResponse->ResponseList[4]) + && 'ACL' === $oResponse->ResponseList[1] + && $sFolderName === $oResponse->ResponseList[2] + ) + { + $c = \count($oResponse->ResponseList); + for ($i = 3; $i < $c; $i += 2) { + $response = new ACLResponse( + $oResponse->ResponseList[$i], + $oResponse->ResponseList[$i+1] + ); + if ($response->identifier() != $this->Settings->username) { + $aResult[] = $response; + } + } + } + } + } + return $aResult; + } + + public function FolderListRights(string $sFolderName, string $sIdentifier) : ?ACLResponse + { + $oResponses = $this->FolderACLRequest($sFolderName, 'LISTRIGHTS', array( + $this->EscapeFolderName($sFolderName), + $this->EscapeString($sIdentifier) + )); + foreach ($oResponses as $oResponse) { + if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType + && isset($oResponse->ResponseList[4]) + && 'LISTRIGHTS' === $oResponse->ResponseList[1] + && $sFolderName === $oResponse->ResponseList[2] + && $sIdentifier === $oResponse->ResponseList[3] + ) + { + foreach (\array_slice($oResponse->ResponseList, 4) as $rule) { + $result = \array_merge($result, \str_split($rule)); + } + return new ACLResponse($sIdentifier, \implode('', \array_unique($result))); + } + } + return null; + } + + public function FolderMyRights(string $sFolderName) : ACLResponse + { + $oResponses = $this->FolderACLRequest($sFolderName, 'MYRIGHTS', array($this->EscapeFolderName($sFolderName))); + $rights = ''; + foreach ($oResponses as $oResponse) { + if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType + && isset($oResponse->ResponseList[3]) + && 'MYRIGHTS' === $oResponse->ResponseList[1] + && $sFolderName === $oResponse->ResponseList[2] + ) + { + $rights = $oResponse->ResponseList[3]; + break; + } + } + $response = new ACLResponse($this->Settings->username, $rights); + $response->mine = true; + return $response; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Folders.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Folders.php new file mode 100644 index 0000000000..7210e6add2 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Folders.php @@ -0,0 +1,617 @@ +FolderSelect($sFolderFullName)->MESSAGES) { + $this->MessageStoreFlag(new SequenceSet('1:*', false), + array(MessageFlag::DELETED), + StoreAction::ADD_FLAGS_SILENT + ); + $this->FolderExpunge(); + } + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderCreate(string $sFolderName, bool $bSubscribe = false) : void + { + $this->SendRequestGetResponse('CREATE', array( + $this->EscapeFolderName($sFolderName) +// , ['(USE (\Drafts \Sent))'] RFC 6154 + )); + $bSubscribe && $this->FolderSubscribe($sFolderName); + } + + /** + * @throws \ValueError + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderDelete(string $sFolderName) : void + { + if (!$sFolderName || 'INBOX' === $sFolderName) { + throw new \ValueError; + } + + $oInfo = $this->hasCapability('IMAP4rev2') + ? $this->FolderExamine($sFolderName) + : $this->FolderStatus($sFolderName); + if ($oInfo->MESSAGES) { + throw new \MailSo\Mail\Exceptions\NonEmptyFolder; + } + + $this->FolderUnsubscribe($sFolderName); + $this->FolderUnselect(); + + // Uncomment will work issue #124 ? +// $this->selectOrExamineFolder($sFolderName, true); + $this->SendRequestGetResponse('DELETE', [$this->EscapeFolderName($sFolderName)]); +// $this->FolderCheck(); + + // Will this workaround solve Dovecot issue? + // https://github.com/the-djmaze/snappymail/issues/124 + try { + $this->FolderRename($sFolderName, "{$sFolderName}-dummy"); + $this->FolderRename("{$sFolderName}-dummy", $sFolderName); + } catch (\Throwable $oException) { + $this->writeLogException($oException, \LOG_WARNING, false); + } + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderSubscribe(string $sFolderName) : void + { + $this->SendRequestGetResponse('SUBSCRIBE', [$this->EscapeFolderName($sFolderName)]); + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderUnsubscribe(string $sFolderName) : void + { + $this->SendRequestGetResponse('UNSUBSCRIBE', [$this->EscapeFolderName($sFolderName)]); + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderRename(string $sOldFolderName, string $sNewFolderName) : void + { + $this->SendRequestGetResponse('RENAME', [ + $this->EscapeFolderName($sOldFolderName), + $this->EscapeFolderName($sNewFolderName) + ]); + } + + private function FolderStatusItems() : array + { + $aStatusItems = array( + FolderStatus::MESSAGES, + FolderStatus::UNSEEN, + FolderStatus::UIDNEXT, + FolderStatus::UIDVALIDITY + ); + // RFC 4551 or RFC 5162 + if ($this->hasCapability('CONDSTORE') || $this->hasCapability('QRESYNC')) { + $aStatusItems[] = FolderStatus::HIGHESTMODSEQ; + } + // RFC 7889 + if ($this->hasCapability('APPENDLIMIT')) { + $aStatusItems[] = FolderStatus::APPENDLIMIT; + } + // RFC 8438 + if ($this->hasCapability('STATUS=SIZE')) { + $aStatusItems[] = FolderStatus::SIZE; + } + // RFC 8474 + if ($this->hasCapability('OBJECTID')) { + $aStatusItems[] = FolderStatus::MAILBOXID; +/* + } else if ($this->hasCapability('X-DOVECOT')) { + $aStatusItems[] = 'X-GUID'; +*/ + } + return $aStatusItems; + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + * + * https://datatracker.ietf.org/doc/html/rfc9051#section-6.3.11 + */ + public function FolderStatus(string $sFolderName, bool $bSelect = false) : FolderInformation + { + $oFolderInfo = $this->oCurrentFolderInfo; + $bReselect = false; + $bWritable = false; + if ($oFolderInfo && $sFolderName === $oFolderInfo->FullName) { + if ($oFolderInfo->hasStatus) { + return $oFolderInfo; + } + + /** + * There's a long standing IMAP CLIENTBUG where STATUS command is executed + * after SELECT/EXAMINE on same folder (it should not). + * So we must unselect the folder to be able to get the APPENDLIMIT and UNSEEN. + */ +/* + if ($this->hasCapability('ESEARCH') && !isset($oFolderInfo->UNSEEN)) { + $oFolderInfo->UNSEEN = $this->MessageESearch('UNSEEN', ['COUNT'])['COUNT']; + } + return $oFolderInfo; +*/ + $bWritable = $oFolderInfo->IsWritable; + $bReselect = true; + $this->FolderUnselect(); + } + + $oInfo = new FolderInformation($sFolderName, false); + $this->SendRequest('STATUS', array($this->EscapeFolderName($sFolderName), $this->FolderStatusItems())); + foreach ($this->yieldUntaggedResponses() as $oResponse) { + $oInfo->setStatusFromResponse($oResponse); + } + + if ($bReselect || $bSelect) { + // Don't use FolderExamine, else PERMANENTFLAGS is empty in Dovecot + $oFolderInfo = $this->selectOrExamineFolder($sFolderName, $bSelect || $bWritable, false); + $oFolderInfo->MESSAGES = \max(0, $oFolderInfo->MESSAGES, $oInfo->MESSAGES); + // SELECT or EXAMINE command then UNSEEN is the message sequence number of the first unseen message. + // And deprecated in IMAP4rev2, so we set it to the amount of unseen messages + $oFolderInfo->UNSEEN = \max(0, $oInfo->UNSEEN); + $oFolderInfo->UIDNEXT = \max(0, $oFolderInfo->UIDNEXT, $oInfo->UIDNEXT); + $oFolderInfo->UIDVALIDITY = \max(0, $oFolderInfo->UIDVALIDITY, $oInfo->UIDVALIDITY); + $oFolderInfo->HIGHESTMODSEQ = \max(0, $oInfo->HIGHESTMODSEQ); + $oFolderInfo->APPENDLIMIT = \max(0, $oFolderInfo->APPENDLIMIT, $oInfo->APPENDLIMIT); + $oFolderInfo->MAILBOXID = $oFolderInfo->MAILBOXID ?: $oInfo->MAILBOXID; +// $oFolderInfo->SIZE = \max($oFolderInfo->SIZE, $oInfo->SIZE); +// $oFolderInfo->RECENT = \max(0, $oFolderInfo->RECENT, $oInfo->RECENT); + $oFolderInfo->hasStatus = $oInfo->hasStatus; + $oInfo = $oFolderInfo; + } + + $oInfo->generateETag($this); + return $oInfo; + } + + public function FolderStatusAndSelect(string $sFolderName) : FolderInformation + { + return $this->FolderStatus($sFolderName, true); + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderCheck() : void + { + if ($this->IsSelected()) { + $this->SendRequestGetResponse('CHECK'); + } + } + + /** + * This also expunge the mailbox + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderClose() : int + { + if ($this->IsSelected()) { + $this->SendRequestGetResponse('CLOSE'); + $this->oCurrentFolderInfo = null; + // https://datatracker.ietf.org/doc/html/rfc5162#section-3.4 + // return HIGHESTMODSEQ ? + } + return 0; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderUnselect() : void + { + if ($this->IsSelected()) { + if ($this->hasCapability('UNSELECT')) { + $this->SendRequestGetResponse('UNSELECT'); + } else { + try { + $this->SendRequestGetResponse('SELECT', ['""']); + // * OK [CLOSED] Previous mailbox closed. + // 3 NO [CANNOT] Invalid mailbox name: Name is empty + } catch (\MailSo\Imap\Exceptions\NegativeResponseException $oException) { + } + } + } + $this->oCurrentFolderInfo = null; + } + + /** + * The EXPUNGE command permanently removes all messages that have the + * \Deleted flag set from the currently selected mailbox. + * + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderExpunge(?SequenceSet $oUidRange = null) : void + { + $sCmd = 'EXPUNGE'; + $aArguments = array(); + + if ($oUidRange && \count($oUidRange) && $oUidRange->UID && $this->hasCapability('UIDPLUS')) { + $sCmd = 'UID '.$sCmd; + $aArguments = array((string) $oUidRange); + } + + // https://datatracker.ietf.org/doc/html/rfc5162#section-3.5 + // Before returning an OK to the client, those messages that are removed + // are reported using a VANISHED response or EXPUNGE responses. + + $this->SendRequestGetResponse($sCmd, $aArguments); + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderHierarchyDelimiter(string $sFolderName = '') : ?string + { + $oResponse = $this->SendRequestGetResponse('LIST', ['""', $this->EscapeFolderName($sFolderName)]); + return ('LIST' === $oResponse[0]->ResponseList[1]) ? $oResponse[0]->ResponseList[3] : null; + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderSelect(string $sFolderName, bool $bForceReselect = false) : FolderInformation + { + return $this->selectOrExamineFolder($sFolderName, true, $bForceReselect); + } + + /** + * The EXAMINE command is identical to SELECT and returns the same output; + * however, the selected mailbox is identified as read-only. + * No changes to the permanent state of the mailbox, including per-user state, + * are permitted; in particular, EXAMINE MUST NOT cause messages to lose the \Recent flag. + * + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderExamine(string $sFolderName, bool $bForceReselect = false) : FolderInformation + { + return $this->selectOrExamineFolder($sFolderName, $this->Settings->force_select, $bForceReselect); + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + * + * REQUIRED IMAP4rev2 untagged responses: FLAGS, EXISTS, LIST + * REQUIRED IMAP4rev2 OK untagged responses: PERMANENTFLAGS, UIDNEXT, UIDVALIDITY + */ + protected function selectOrExamineFolder(string $sFolderName, bool $bIsWritable, bool $bForceReselect) : FolderInformation + { + if (!$bForceReselect + && $this->oCurrentFolderInfo + && $sFolderName === $this->oCurrentFolderInfo->FullName + && ($bIsWritable === $this->oCurrentFolderInfo->IsWritable || $this->oCurrentFolderInfo->IsWritable) + ) { + return $this->oCurrentFolderInfo; + } + + if (!\strlen(\trim($sFolderName))) { + throw new \ValueError; + } + + $aSelectParams = array(); + +/* + // RFC 5162 + if ($this->hasCapability('QRESYNC')) { + $this->Enable(['QRESYNC', 'CONDSTORE']); + - the last known UIDVALIDITY, + - the last known modification sequence, + - the optional set of known UIDs, + - and an optional parenthesized list of known sequence ranges and their corresponding UIDs. + QRESYNC (UIDVALIDITY HIGHESTMODSEQ 41,43:211,214:541) + QRESYNC (67890007 20050715194045000 41,43:211,214:541) + } + + // RFC 4551 + if ($this->hasCapability('CONDSTORE')) { + $aSelectParams[] = 'CONDSTORE'; + } + + // RFC 5738 + if ($this->UTF8) { + $aSelectParams[] = 'UTF8'; + } +*/ + + $aParams = array( + $this->EscapeFolderName($sFolderName) + ); + if ($aSelectParams) { + $aParams[] = $aSelectParams; + } + + $oResult = new FolderInformation($sFolderName, $bIsWritable); + + /** + * IMAP4rev2 SELECT/EXAMINE are now required to return an untagged LIST response. + */ + $this->SendRequest($bIsWritable ? 'SELECT' : 'EXAMINE', $aParams); + foreach ($this->yieldUntaggedResponses() as $oResponse) { + if (!$oResult->setStatusFromResponse($oResponse)) { + // OK untagged responses + if (\is_array($oResponse->OptionalResponse)) { + $key = $oResponse->OptionalResponse[0]; + if (\count($oResponse->OptionalResponse) > 1) { + if ('PERMANENTFLAGS' === $key && \is_array($oResponse->OptionalResponse[1])) { + $oResult->PermanentFlags = \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oResponse->OptionalResponse[1]); + } + } else if ('READ-ONLY' === $key) { + $oResult->IsWritable = false; + } else if ('READ-WRITE' === $key) { + $oResult->IsWritable = true; + } else if ('NOMODSEQ' === $key) { + // https://datatracker.ietf.org/doc/html/rfc4551#section-3.1.2 + } + } + + // untagged responses + else if (\count($oResponse->ResponseList) > 2 + && 'FLAGS' === $oResponse->ResponseList[1] + && \is_array($oResponse->ResponseList[2])) { + // These could be not permanent, so we don't use them +// $oResult->Flags = \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oResponse->ResponseList[2]); + } + } + } + + // SELECT or EXAMINE command then UNSEEN is the message sequence number of the first unseen message. + // IMAP4rev2 deprecated + $oResult->UNSEEN = null; +/* + if ($this->hasCapability('ESEARCH')) { + $oResult->UNSEEN = $this->MessageESearch('UNSEEN', ['COUNT'])['COUNT']; + } +*/ + $this->oCurrentFolderInfo = $oResult; + + return $oResult; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderList(string $sParentFolderName, string $sListPattern, bool $bIsSubscribeList = false, bool $bUseListStatus = false) : FolderCollection + { + $sCmd = 'LIST'; + + $aParameters = array(); + $aReturnParams = array(); + + if ($this->hasCapability('LIST-EXTENDED')) { + // RFC 5258 + $aReturnParams[] = 'SUBSCRIBED'; +// $aReturnParams[] = 'CHILDREN'; + if ($bIsSubscribeList) { + $aParameters[] = ['SUBSCRIBED'/*,'REMOTE','RECURSIVEMATCH'*/]; + } else { +// $aParameters[0] = '()'; + } + // RFC 6154 + if ($this->hasCapability('SPECIAL-USE')) { + $aReturnParams[] = 'SPECIAL-USE'; + } + } else if ($bIsSubscribeList) { + // IMAP4rev2 deprecated + $sCmd = 'LSUB'; + } + + $aParameters[] = $this->EscapeFolderName($sParentFolderName); + $aParameters[] = $this->EscapeString(\trim($sListPattern)); +// $aParameters[] = $this->EscapeString(\strlen(\trim($sListPattern)) ? $sListPattern : '*'); + + // RFC 5819 + if ($bUseListStatus && !$bIsSubscribeList && $this->hasCapability('LIST-STATUS')) { + $aReturnParams[] = 'STATUS'; + $aReturnParams[] = $this->FolderStatusItems(); + } +/* + // RFC 5738 + if ($this->UTF8) { + $aReturnParams[] = 'UTF8'; // 'UTF8ONLY'; + } +*/ + if ($aReturnParams) { + $aParameters[] = 'RETURN'; + $aParameters[] = $aReturnParams; + } + +/* + $bPassthru = false; + if ($bPassthru) { + $this->SendRequest($sCmd, $aParameters); + $this->streamResponse(); + return []; + } +*/ + + // RFC 5464 + $bMetadata = !$bIsSubscribeList && $this->hasCapability('METADATA'); + // Dovecot supports fetching all METADATA at once + $aMetadata = $bMetadata ? $this->getAllMetadata() : null; + + $this->SendRequest($sCmd, $aParameters); + $aFolders = array(); + $oFolderCollection = new FolderCollection; + $sDelimiter = ''; + $bInbox = false; + foreach ($this->yieldUntaggedResponses() as $oResponse) { + if ('STATUS' === $oResponse->StatusOrIndex && isset($oResponse->ResponseList[2])) { + $sFullName = $this->toUTF8($oResponse->ResponseList[2]); + if (!isset($oFolderCollection[$sFullName])) { + $oFolderCollection[$sFullName] = new Folder($sFullName); + } + $oFolderCollection[$sFullName]->setStatusFromResponse($oResponse); + $oFolderCollection[$sFullName]->generateETag($this); + } + else if ($sCmd === $oResponse->StatusOrIndex && 5 <= \count($oResponse->ResponseList)) { + try + { + $sFullName = $this->toUTF8($oResponse->ResponseList[4]); + + /** + * $oResponse->ResponseList[0] = * + * $oResponse->ResponseList[1] = LIST (all) | LSUB (subscribed) + * $oResponse->ResponseList[2] = Attribute flags + * $oResponse->ResponseList[3] = Delimiter + * $oResponse->ResponseList[4] = FullName + */ + if (isset($oFolderCollection[$sFullName])) { + $oFolder = $oFolderCollection[$sFullName]; + $oFolder->setDelimiter($oResponse->ResponseList[3]); + $oFolder->setAttributes($oResponse->ResponseList[2]); + } else { + $oFolder = new Folder($sFullName, $oResponse->ResponseList[3], $oResponse->ResponseList[2]); + $oFolderCollection[$sFullName] = $oFolder; + } + + $bInbox = $bInbox || $oFolder->IsInbox(); + + if (!$sDelimiter) { + $sDelimiter = $oFolder->Delimiter(); + } + + if (isset($aMetadata[$oResponse->ResponseList[4]])) { + $oFolder->SetAllMetadata($aMetadata[$oResponse->ResponseList[4]]); + } + } + catch (\Throwable $oException) + { + $this->writeLogException($oException, \LOG_WARNING, false); + } + } + } + +// $iOptimizationLimit = $this->Settings->folder_list_limit; +// $oFolderCollection->Optimized = 10 < $iOptimizationLimit && $oFolderCollection->count() > $iOptimizationLimit; + + // RFC 5464 + if ($bMetadata && !$aMetadata /*&& 50 < $oFolderCollection->count()*/) { + foreach ($oFolderCollection as $oFolder) { +// if (2 > \substr_count($oFolder->FullName, $oFolder->Delimiter())) + if ($oFolder->Selectable()) try { + $oFolder->SetAllMetadata( + $this->getMetadata($oFolder->FullName, ['/shared', '/private'], ['DEPTH'=>'infinity']) + ); + } catch (\Throwable $oException) { + // Ignore error + } + } + } +/* + // RFC 4314 + if ($this->hasCapability('ACL')) { + foreach ($oFolderCollection as $oFolder) { + if ($oFolder->Selectable()) try { + $oFolder->myRights = $this->FolderMyRights($oFolder->FullName); + } catch (\Throwable $oException) { + // BAD Error in IMAP command MYRIGHTS: ACLs disabled + break; + } + } + } +*/ + if (!$bInbox && !$sParentFolderName && !isset($oFolderCollection['INBOX'])) { + $oFolderCollection['INBOX'] = new Folder('INBOX', $sDelimiter); + } + + return $oFolderCollection; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderSubscribeList(string $sParentFolderName, string $sListPattern) : FolderCollection + { + return $this->FolderList($sParentFolderName, $sListPattern, true); + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderStatusList(string $sParentFolderName, string $sListPattern) : FolderCollection + { + return $this->FolderList($sParentFolderName, $sListPattern, false, true); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php new file mode 100644 index 0000000000..10003e83aa --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php @@ -0,0 +1,548 @@ +writeLogException(new \ValueError('$sIndexRange is empty'), \LOG_ERR); + } + + $aReturn = array(); + $this->aFetchCallbacks = array(); + try { + $aFetchItems = array( + FetchType::UID, + FetchType::RFC822_SIZE + ); + foreach ($aInputFetchItems as $mFetchKey) { + switch ($mFetchKey) + { + case FetchType::UID: + case FetchType::RFC822_SIZE: + // Already defined by default + break; + + // Macro's + case FetchType::FULL: + $aFetchItems[] = FetchType::BODY; + // Falls through + case FetchType::ALL: + $aFetchItems[] = FetchType::ENVELOPE; + // Falls through + case FetchType::FAST: + $aFetchItems[] = FetchType::FLAGS; + $aFetchItems[] = FetchType::INTERNALDATE; + break; + + default: + if (\is_string($mFetchKey)) { + $aFetchItems[] = $mFetchKey; + } else if (\is_array($mFetchKey) && 2 === \count($mFetchKey) + && \is_string($mFetchKey[0]) && \is_callable($mFetchKey[1])) + { + $aFetchItems[] = $mFetchKey[0]; + $this->aFetchCallbacks[$mFetchKey[0]] = $mFetchKey[1]; + } + break; + } + } + if ($this->hasCapability('OBJECTID')) { + $aFetchItems[] = FetchType::EMAILID; + $aFetchItems[] = FetchType::THREADID; + } else if ($this->hasCapability('X-GM-EXT-1')) { + // https://developers.google.com/gmail/imap/imap-extensions + $aFetchItems[] = 'X-GM-MSGID'; + $aFetchItems[] = 'X-GM-THRID'; +/* + } else if ($this->hasCapability('X-DOVECOT')) { + $aFetchItems[] = 'X-GUID'; +*/ + } +/* + if ($this->hasCapability('X-GM-EXT-1') && \in_array(FetchType::FLAGS, $aFetchItems)) { + $aFetchItems[] = 'X-GM-LABELS'; + } +*/ + + $aParams = array($sIndexRange, $aFetchItems); + + /** + * TODO: + * https://datatracker.ietf.org/doc/html/rfc4551#section-3.3.1 + * $aParams[1][] = FLAGS + * $aParams[] = (CHANGEDSINCE $modsequence) + * https://datatracker.ietf.org/doc/html/rfc4551#section-3.3.2 + * $aParams[1][] = MODSEQ + * https://datatracker.ietf.org/doc/html/rfc5162#section-3.2 + * $bIndexIsUid && $aParams[] = (CHANGEDSINCE $modsequence VANISHED) + */ + + $this->SendRequest($bIndexIsUid ? 'UID FETCH' : 'FETCH', $aParams); + foreach ($this->yieldUntaggedResponses() as $oResponse) { + if (FetchResponse::isValidImapResponse($oResponse)) { + if (FetchResponse::hasUidAndSize($oResponse)) { + yield new FetchResponse($oResponse); + } else { + $this->logWrite('Skipped Imap Response! ['.$oResponse.']', \LOG_NOTICE); + } + } + } + } finally { + $this->aFetchCallbacks = array(); + } + } + + public function Fetch(array $aInputFetchItems, string $sIndexRange, bool $bIndexIsUid) : array + { + $aReturn = array(); + foreach ($this->FetchIterate($aInputFetchItems, $sIndexRange, $bIndexIsUid) as $oFetchResponse) { + $aReturn[] = $oFetchResponse; + } + return $aReturn; + } + + public function FetchMessagePart(int $iUid, string $sPartId) : string + { + if ('TEXT' === $sPartId) { + $oFetchResponse = $this->Fetch([ + FetchType::BODY_PEEK.'['.$sPartId.']', + FetchType::BODY_HEADER_PEEK + ], $iUid, true)[0]; + $sHeader = $oFetchResponse->GetFetchValue(FetchType::BODY_HEADER); + } else { + $oFetchResponse = $this->Fetch([ + FetchType::BODY_PEEK.'['.$sPartId.']', + // An empty section specification refers to the entire message, including the header. + // But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME]. + FetchType::BODY_PEEK.'['.$sPartId.'.MIME]' + ], $iUid, true)[0]; + $sHeader = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.'.MIME]'); + } + return $sHeader . $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']'); + } + + /** + * Appends message to specified folder + * + * @param resource $rMessageStream + * + * @throws \InvalidArgumentException + * @throws \ValueError + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageAppendStream(string $sFolderName, $rMessageStream, int $iStreamSize, ?array $aFlagsList = null, int $iDateTime = 0) : ?int + { + if (!\is_resource($rMessageStream)) { + throw new \InvalidArgumentException('$rMessageStream must be a resource'); + } + if (!\strlen($sFolderName)) { + throw new \ValueError('$sFolderName is empty'); + } + if (1 > $iStreamSize) { + throw new \ValueError('$iStreamSize must be higher then 0'); + } + + $aParams = array( + $this->EscapeFolderName($sFolderName), + $aFlagsList + ); + if (0 < $iDateTime) { + $aParams[] = $this->EscapeString(\gmdate('d-M-Y H:i:s', $iDateTime).' +0000'); + } + +/* + // RFC 3516 || RFC 6855 section-4 + if ($this->hasCapability('BINARY') || $this->hasCapability('UTF8=ACCEPT')) { + $aParams[] = '~{'.$iStreamSize.'}'; + } +*/ + $aParams[] = '{'.$iStreamSize.'}'; + + $this->SendRequestGetResponse('APPEND', $aParams); + + return $this->writeMessageStream($rMessageStream); + } + + private function writeMessageStream($rMessageStream) : ?int + { + $this->writeLog('Write to connection stream', \LOG_INFO); + + \MailSo\Base\Utils::WriteStream($rMessageStream, $this->ConnectionResource()); + + $this->sendRaw(''); + $oResponses = $this->getResponse(); + /** + * Can be tagged + S: A003 OK [APPENDUID 1 2001] APPEND completed + * Or untagged + S: * OK [APPENDUID 1 2001] Replacement Message ready + */ + foreach ($oResponses as $oResponse) { + if (\is_array($oResponse->OptionalResponse) + && !empty($oResponse->OptionalResponse[2]) + && \is_numeric($oResponse->OptionalResponse[2]) + && 'APPENDUID' === \strtoupper($oResponse->OptionalResponse[0]) + ) { + return (int) $oResponse->OptionalResponse[2]; + } + } + + return null; + } + + /** + * RFC 3502 MULTIAPPEND + public function MessageAppendStreams(string $sFolderName, $rMessageAppendStream, int $iStreamSize, ?array $aFlagsList = null, ?int &$iUid = null, int $iDateTime = 0) : ?int + */ + + /** + * @throws \ValueError + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageCopy(string $sFromFolder, string $sToFolder, SequenceSet $oRange) : ResponseCollection + { + if (!$sFromFolder || !$sToFolder || !\count($oRange)) { + $this->writeLogException(new \ValueError, \LOG_ERR); + } + + $this->FolderSelect($sFromFolder); + + return $this->SendRequestGetResponse( + $oRange->UID ? 'UID COPY' : 'COPY', + array((string) $oRange, $this->EscapeFolderName($sToFolder)) + ); + } + + /** + * @throws \ValueError + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageMove(string $sFromFolder, string $sToFolder, SequenceSet $oRange) : void + { + if (!$sFromFolder || !$sToFolder || !\count($oRange)) { + $this->writeLogException(new \ValueError, \LOG_ERR); + } + + if ($this->hasCapability('MOVE')) { + $this->FolderSelect($sFromFolder); + $this->SendRequestGetResponse( + $oRange->UID ? 'UID MOVE' : 'MOVE', + array((string) $oRange, $this->EscapeFolderName($sToFolder)) + ); + } else { + $this->MessageCopy($sFromFolder, $sToFolder, $oRange); + $this->MessageDelete($sFromFolder, $oRange, true); + } + } + + /** + * @throws \ValueError + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageDelete(string $sFolder, SequenceSet $oRange, bool $bExpungeAll = false) : void + { + if (!$sFolder || !\count($oRange)) { + $this->writeLogException(new \ValueError, \LOG_ERR); + } + + $this->FolderSelect($sFolder); + + $this->MessageStoreFlag($oRange, + array(MessageFlag::DELETED), + StoreAction::ADD_FLAGS_SILENT + ); + + if ($bExpungeAll && $this->Settings->expunge_all_on_delete) { + $this->FolderExpunge(); + } else { + $this->FolderExpunge($oRange); + } + } + + /** + * RFC 8508 REPLACE + * Replaces message in specified folder + * When $iUid < 1 it only appends the message + * + * @param resource $rMessageStream + * + * @throws \InvalidArgumentException + * @throws \ValueError + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageReplaceStream(string $sFolderName, int $iUid, $rMessageStream, int $iStreamSize, ?array $aFlagsList = null, int $iDateTime = 0) : ?int + { + if (1 > $iUid || !$this->hasCapability('REPLACE')) { + $this->FolderSelect($sFolderName); + $iNewUid = $this->MessageAppendStream($sFolderName, $rMessageStream, $iStreamSize, $aFlagsList, $iDateTime); + if ($iUid) { + $oRange = new SequenceSet($iUid); + $this->MessageStoreFlag($oRange, + array(MessageFlag::DELETED), + StoreAction::ADD_FLAGS_SILENT + ); + $this->FolderExpunge($oRange); + } + return $iNewUid; + } + + $aParams = array( + $iUid, + $this->EscapeFolderName($sFolderName), + $aFlagsList + ); + if (0 < $iDateTime) { + $aParams[] = $this->EscapeString(\gmdate('d-M-Y H:i:s', $iDateTime).' +0000'); + } + +/* + // RFC 3516 || RFC 6855 section-4 + if ($this->hasCapability('BINARY') || $this->hasCapability('UTF8=ACCEPT')) { + $aParams[] = '~{'.$iStreamSize.'}'; + } +*/ + $aParams[] = '{'.$iStreamSize.'}'; + + $this->SendRequestGetResponse('UID REPLACE', $aParams); + + return $this->writeMessageStream($rMessageStream); + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + * $sStoreAction = \MailSo\Imap\Enumerations\StoreAction::ADD_FLAGS_SILENT + */ + public function MessageStoreFlag(SequenceSet $oRange, array $aInputStoreItems, string $sStoreAction) : ?ResponseCollection + { + if (!\count($oRange) || !\strlen(\trim($sStoreAction)) || !\count($aInputStoreItems)) { + return null; + } + + /** + * TODO: + * https://datatracker.ietf.org/doc/html/rfc4551#section-3.2 + * $sStoreAction[] = (UNCHANGEDSINCE $modsequence) + */ + + $aInputStoreItems = \array_map('\\MailSo\\Base\\Utils::Utf8ToUtf7Modified', $aInputStoreItems); + + return $this->SendRequestGetResponse( + $oRange->UID ? 'UID STORE' : 'STORE', + array((string) $oRange, $sStoreAction, $aInputStoreItems) + ); + } + + /** + * See https://tools.ietf.org/html/rfc5256 + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageSort(array $aSortTypes, string $sSearchCriterias, bool $bReturnUid = true) : array + { + $oSort = new \MailSo\Imap\Requests\SORT($this); + $oSort->sCriterias = $sSearchCriterias ?: 'ALL'; + $oSort->bUid = $bReturnUid; + $oSort->aSortTypes = $aSortTypes; + $oSort->SendRequest(); + $aReturn = array(); + foreach ($this->yieldUntaggedResponses() as $oResponse) { + $iOffset = ($bReturnUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && 'SORT' === $oResponse->ResponseList[2]) ? 1 : 0; + if (\is_array($oResponse->ResponseList) + && 2 < \count($oResponse->ResponseList) + && ('SORT' === $oResponse->StatusOrIndex || $iOffset)) + { + $iLen = \count($oResponse->ResponseList); + for ($iIndex = 2 + $iOffset; $iIndex < $iLen; ++$iIndex) { + $aReturn[] = (int) $oResponse->ResponseList[$iIndex]; + } + } + } + return $aReturn; + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageESearch(string $sSearchCriterias, ?array $aSearchReturn = null, bool $bReturnUid = true, string $sLimit = '') : array + { + $oESearch = new \MailSo\Imap\Requests\ESEARCH($this); + $oESearch->sCriterias = $sSearchCriterias ?: 'ALL'; + $oESearch->aReturn = $aSearchReturn; + $oESearch->bUid = $bReturnUid; + $oESearch->sLimit = $sLimit; +// if (!$this->UTF8 && !\mb_check_encoding($sSearchCriterias, 'UTF-8')) { + if (!$this->UTF8 && !\MailSo\Base\Utils::IsAscii($sSearchCriterias)) { + $oESearch->sCharset = 'UTF-8'; + } + $oESearch->SendRequest(); + return $this->getSimpleESearchOrESortResult($bReturnUid); + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageESort(array $aSortTypes, string $sSearchCriterias, array $aSearchReturn = ['ALL'], bool $bReturnUid = true, string $sLimit = '') : array + { + $oSort = new \MailSo\Imap\Requests\SORT($this); + $oSort->sCriterias = $sSearchCriterias ?: 'ALL'; + $oSort->bUid = $bReturnUid; + $oSort->aSortTypes = $aSortTypes; + $oSort->aReturn = $aSearchReturn ?: ['ALL']; + $oSort->sLimit = $sLimit; + $oSort->SendRequest(); + return $this->getSimpleESearchOrESortResult($bReturnUid); + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageSearch(string $sSearchCriterias, bool $bReturnUid = true) : array + { + $aRequest = array(); +// if (!$this->UTF8 && !\mb_check_encoding($sSearchCriterias, 'UTF-8')) { + if (!$this->UTF8 && !\MailSo\Base\Utils::IsAscii($sSearchCriterias)) { + $aRequest[] = 'CHARSET'; + $aRequest[] = 'UTF-8'; + } + + $aRequest[] = !\strlen($sSearchCriterias) || '*' === $sSearchCriterias ? 'ALL' : $sSearchCriterias; + + $sCont = $this->SendRequest($bReturnUid ? 'UID SEARCH' : 'SEARCH', $aRequest, true); + $oResult = $this->getResponse(); + if ('' !== $sCont) { + $oItem = $oResult->getLast(); + if ($oItem && ResponseType::CONTINUATION === $oItem->ResponseType) { + $aParts = \explode("\r\n", $sCont); + foreach ($aParts as $sLine) { + $this->sendRaw($sLine); + $oResult = $this->getResponse(); + $oItem = $oResult->getLast(); + if ($oItem && ResponseType::CONTINUATION === $oItem->ResponseType) { + continue; + } + } + } + } + + $aReturn = array(); + foreach ($oResult as $oResponse) { + $iOffset = ($bReturnUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && 'SEARCH' === $oResponse->ResponseList[2]) ? 1 : 0; + if (ResponseType::UNTAGGED === $oResponse->ResponseType + && ('SEARCH' === $oResponse->StatusOrIndex || $iOffset) + && \is_array($oResponse->ResponseList) + && 2 < \count($oResponse->ResponseList)) + { + $iLen = \count($oResponse->ResponseList); + for ($iIndex = 2 + $iOffset; $iIndex < $iLen; ++$iIndex) { + $aReturn[] = (int) $oResponse->ResponseList[$iIndex]; + } + } + } + return \array_reverse($aReturn); + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageThread(string $sSearchCriterias, string $sAlgorithm = '', $bReturnUid = true) : iterable + { + $oThread = new \MailSo\Imap\Requests\THREAD($this); + $oThread->sCriterias = $sSearchCriterias ?: 'ALL'; + $oThread->bUid = $bReturnUid; + try { + $sAlgorithm && $oThread->setAlgorithm($sAlgorithm); + } catch (\Throwable $e) { + // ignore + } + yield from $oThread->SendRequestIterateResponse(); + } + + private function getSimpleESearchOrESortResult(bool $bReturnUid) : array + { + $sRequestTag = $this->getCurrentTag(); + $aResult = array(); + foreach ($this->yieldUntaggedResponses() as $oResponse) { + if (\is_array($oResponse->ResponseList) + && isset($oResponse->ResponseList[2][1]) + && ('ESEARCH' === $oResponse->StatusOrIndex || 'SORT' === $oResponse->StatusOrIndex) + && 'TAG' === $oResponse->ResponseList[2][0] && $sRequestTag === $oResponse->ResponseList[2][1] + && (!$bReturnUid || (!empty($oResponse->ResponseList[3]) && 'UID' === $oResponse->ResponseList[3])) + ) + { + $i = \count($oResponse->ResponseList) - 1; + while (3 < --$i) { + $sItem = $oResponse->ResponseList[$i]; + switch ($sItem) + { + case 'ALL': + case 'MAX': + case 'MIN': + case 'COUNT': + $aResult[$sItem] = $oResponse->ResponseList[$i + 1]; + break; + } + } + } + } + return $aResult; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Metadata.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Metadata.php new file mode 100644 index 0000000000..43b147fef9 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Metadata.php @@ -0,0 +1,136 @@ +allMetadata) { + $this->allMetadata = array(); + try { + $arguments = [ + '(DEPTH infinity)', + $this->EscapeString('*') + ]; + $arguments[] = '(' . \implode(' ', \array_map([$this, 'EscapeString'], ['/shared', '/private'])) . ')'; + $this->SendRequest('GETMETADATA', $arguments); + foreach ($this->yieldUntaggedResponses() as $oResponse) { + if (isset($oResponse->ResponseList[3]) + && \is_array($oResponse->ResponseList[3]) + && 'METADATA' === $oResponse->ResponseList[1] + ) { + $aMetadata = array(); + $c = \count($oResponse->ResponseList[3]); + for ($i = 0; $i < $c; $i += 2) { + $aMetadata[$oResponse->ResponseList[3][$i]] = $oResponse->ResponseList[3][$i+1]; + } + $this->allMetadata[$this->toUTF8($oResponse->ResponseList[2])] = $aMetadata; + } + } + } catch (\Throwable $e) { + //\SnappyMail\Log::warning('IMAP', $e->getMessage()); + } + } + return $this->allMetadata; + } + + public function getMetadata(string $sFolderName, array $aEntries, array $aOptions = []) : array + { + $arguments = []; + + if ($aOptions) { + $options = []; + $aOptions = \array_intersect_key( + \array_change_key_case($aOptions, CASE_UPPER), + ['MAXSIZE' => 0, 'DEPTH' => 0] + ); + if (isset($aOptions['MAXSIZE']) && 0 < \intval($aOptions['MAXSIZE'])) { + $options[] = 'MAXSIZE ' . \intval($aOptions['MAXSIZE']); + } + if (isset($aOptions['DEPTH']) && (1 == $aOptions['DEPTH'] || 'infinity' === $aOptions['DEPTH'])) { + $options[] = "DEPTH {$aOptions['DEPTH']}"; + } + if ($options) { + $arguments[] = '(' . \implode(' ', $options) . ')'; + } + } + + $arguments[] = $this->EscapeFolderName($sFolderName); + + $arguments[] = '(' . \implode(' ', \array_map([$this, 'EscapeString'], $aEntries)) . ')'; + + $aReturn = array(); + $this->SendRequest('GETMETADATA', $arguments); + foreach ($this->yieldUntaggedResponses() as $oResponse) { + if (isset($oResponse->ResponseList[3]) + && \is_array($oResponse->ResponseList[3]) + && 'METADATA' === $oResponse->ResponseList[1] + ) { + $c = \count($oResponse->ResponseList[3]); + for ($i = 0; $i < $c; $i += 2) { + $aReturn[$oResponse->ResponseList[3][$i]] = $oResponse->ResponseList[3][$i+1]; + } + } + } + return $aReturn; + } + + public function ServerGetMetadata(array $aEntries, array $aOptions = []) : array + { + return $this->hasCapability('METADATA-SERVER') + ? $this->getMetadata('', $aEntries, $aOptions) + : []; + } + + public function FolderGetMetadata(string $sFolderName, array $aEntries, array $aOptions = []) : array + { + return $this->hasCapability('METADATA') + ? $this->getMetadata($sFolderName, $aEntries, $aOptions) + : []; + } + + public function FolderSetMetadata(string $sFolderName, array $aEntries) : void + { + if ($this->hasCapability('METADATA')) { + if (!$aEntries) { + throw new \ValueError('Wrong argument for SETMETADATA command'); + } + + $arguments = [$this->EscapeFolderName($sFolderName)]; + + \array_walk($aEntries, function(&$v, $k){ + $v = $this->EscapeString($k) . ' ' . $this->EscapeString($v); + }); + $arguments[] = '(' . \implode(' ', $aEntries) . ')'; + + $this->SendRequestGetResponse('SETMETADATA', $arguments); + } + } + + public function FolderRemoveMetadata($sFolderName, array $aEntries) : void + { + $this->FolderSetMetadata($sFolderName, \array_fill_keys(\array_keys($aEntries), null)); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Quota.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Quota.php new file mode 100644 index 0000000000..12cb374d5a --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Quota.php @@ -0,0 +1,87 @@ +getQuota(false, $sRootName); + } + + /** + * https://datatracker.ietf.org/doc/html/rfc2087#section-4.3 + * + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function QuotaRoot(string $sFolderName = 'INBOX') : ?array + { + return $this->getQuota(true, $sFolderName); + } + + // * QUOTA "User quota" (STORAGE 1284645 2097152)\r\n + private function getQuota(bool $root, string $sFolderName) : ?array + { + if (!$this->hasCapability('QUOTA')) { + return null; + } + $aReturn = array(0, 0, 0, 0); + $oResponseCollection = $this->SendRequest(($root?'GETQUOTAROOT':'GETQUOTA') . " {$this->EscapeFolderName($sFolderName)}"); + foreach ($this->yieldUntaggedResponses() as $oResponse) { + if ('QUOTA' === $oResponse->StatusOrIndex + && isset($oResponse->ResponseList[3]) + && \is_array($oResponse->ResponseList[3]) + && 2 < \count($oResponse->ResponseList[3]) + && 'STORAGE' === \strtoupper($oResponse->ResponseList[3][0]) + && \is_numeric($oResponse->ResponseList[3][1]) + && \is_numeric($oResponse->ResponseList[3][2]) + ) + { + $aReturn = array( + (int) $oResponse->ResponseList[3][1], + (int) $oResponse->ResponseList[3][2], + 0, + 0 + ); + + if (5 < \count($oResponse->ResponseList[3]) + && 'MESSAGE' === \strtoupper($oResponse->ResponseList[3][3]) + && \is_numeric($oResponse->ResponseList[3][4]) + && \is_numeric($oResponse->ResponseList[3][5]) + ) + { + $aReturn[2] = (int) $oResponse->ResponseList[3][4]; + $aReturn[3] = (int) $oResponse->ResponseList[3][5]; + } + } + } + + return $aReturn; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/UrlAuth.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/UrlAuth.php new file mode 100644 index 0000000000..77db79898f --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/UrlAuth.php @@ -0,0 +1,52 @@ +EscapeFolderName($sFolderName); + } + if ($sMechanism) { + $aParams[] = $this->EscapeString($sMechanism); + } + $oResponses = $this->SendRequestGetResponse('RESETKEY', $aParams); + return null; + } + + public function GENURLAUTH(string $sMechanisms) + { + $oResponses = $this->SendRequestGetResponse('GENURLAUTH', array( + $this->EscapeString($sMechanism) + )); + return null; + } + + public function URLFETCH(string $sURLs) + { + $oResponses = $this->SendRequestGetResponse('URLFETCH', array( + $this->EscapeString($sURLs) + )); + return null; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FetchType.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FetchType.php new file mode 100644 index 0000000000..20b30c6ab9 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FetchType.php @@ -0,0 +1,71 @@ +] that does not implicitly set the \Seen flag. + const BODY_PEEK = 'BODY.PEEK'; + // The text of a particular body section. + const BODY_HEADER = 'BODY[HEADER]'; + const BODY_HEADER_PEEK = 'BODY.PEEK[HEADER]'; + const BODYSTRUCTURE = 'BODYSTRUCTURE'; + const ENVELOPE = 'ENVELOPE'; + const FLAGS = 'FLAGS'; + const INTERNALDATE = 'INTERNALDATE'; +// const RFC822 = 'RFC822'; // Functionally equivalent to BODY[] +// const RFC822_HEADER = 'RFC822.HEADER'; // Functionally equivalent to BODY.PEEK[HEADER] + const RFC822_SIZE = 'RFC822.SIZE'; +// const RFC822_TEXT = 'RFC822.TEXT'; // Functionally equivalent to BODY[TEXT] + const UID = 'UID'; + // RFC 3516 + const BINARY = 'BINARY'; + const BINARY_PEEK = 'BINARY.PEEK'; + const BINARY_SIZE = 'BINARY.SIZE'; + // RFC 4551 + const MODSEQ = 'MODSEQ'; + // RFC 8474 + const EMAILID = 'EMAILID'; + const THREADID = 'THREADID'; + // RFC 8970 + const PREVIEW = 'PREVIEW'; + + public static function BuildBodyCustomHeaderRequest(array $aHeaders, bool $bPeek = true) : string + { + if (\count($aHeaders)) { + $aHeaders = \array_map(fn($sHeader) => \strtoupper(\trim($sHeader)), $aHeaders); + return ($bPeek ? self::BODY_PEEK : self::BODY) + . '[HEADER.FIELDS ('.\implode(' ', $aHeaders).')]'; + } + return ''; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FolderACL.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FolderACL.php new file mode 100644 index 0000000000..154b073935 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FolderACL.php @@ -0,0 +1,59 @@ +GetLastResponse(); + if ($oResponse && $oResponse->IsStatusResponse && !empty($oResponse->HumanReadable) && + 'ALERT' === ($oResponse->OptionalResponse[0] ?? '')) + { + $sResult = $oResponse->HumanReadable; + } + + return $sResult; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Exceptions/ResponseException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Exceptions/ResponseException.php new file mode 100644 index 0000000000..a063a8e2f4 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Exceptions/ResponseException.php @@ -0,0 +1,47 @@ +oResponses = $oResponses; + if (!$sMessage && $response = $this->GetLastResponse()) { + $sMessage = ($response->OptionalResponse[0] ?? '') . ' ' . $response->HumanReadable; + } + parent::__construct($sMessage, $iCode, $oPrevious); + } + + public function GetResponseStatus() : ?string + { + $oItem = $this->GetLastResponse(); + return $oItem && $oItem->IsStatusResponse ? $oItem->StatusOrIndex : null; + } + + public function GetResponses() : ?\MailSo\Imap\ResponseCollection + { + return $this->oResponses; + } + + public function GetLastResponse() : ?\MailSo\Imap\Response + { + return $this->oResponses ? $this->oResponses->getLast() : null; + } +} diff --git a/rainloop/v/0.0.0/app/libraries/MailSo/Imap/Exceptions/ResponseNotFoundException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Exceptions/ResponseNotFoundException.php similarity index 80% rename from rainloop/v/0.0.0/app/libraries/MailSo/Imap/Exceptions/ResponseNotFoundException.php rename to snappymail/v/0.0.0/app/libraries/MailSo/Imap/Exceptions/ResponseNotFoundException.php index 2d4d029646..e1419c1870 100644 --- a/rainloop/v/0.0.0/app/libraries/MailSo/Imap/Exceptions/ResponseNotFoundException.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Exceptions/ResponseNotFoundException.php @@ -16,4 +16,4 @@ * @package Imap * @subpackage Exceptions */ -class ResponseNotFoundException extends \MailSo\Imap\Exceptions\Exception {} +class ResponseNotFoundException extends \MailSo\RuntimeException {} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FetchResponse.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FetchResponse.php new file mode 100644 index 0000000000..c882df8f61 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FetchResponse.php @@ -0,0 +1,155 @@ +oImapResponse = $oImapResponse; + } + + public function GetEnvelope(bool $bForce = false) : ?array + { + if (null === $this->aEnvelopeCache || $bForce) { + $this->aEnvelopeCache = $this->GetFetchValue(Enumerations\FetchType::ENVELOPE); + } + return $this->aEnvelopeCache; + } + + /** + * @return mixed + */ + public function GetFetchEnvelopeValue(int $iIndex, ?string $mNullResult = null) + { + return self::findEnvelopeIndex($this->GetEnvelope(), $iIndex, $mNullResult); + } + + public function GetFetchEnvelopeEmailCollection(int $iIndex, string $sParentCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1) : ?\MailSo\Mime\EmailCollection + { + $oResult = null; + $aEmails = $this->GetFetchEnvelopeValue($iIndex); + if (\is_array($aEmails) && \count($aEmails)) { + $oResult = new \MailSo\Mime\EmailCollection; + foreach ($aEmails as $aEmailItem) { + if (\is_array($aEmailItem) && 4 === \count($aEmailItem)) { + $sDisplayName = \MailSo\Base\Utils::DecodeHeaderValue( + self::findEnvelopeIndex($aEmailItem, 0, ''), $sParentCharset); + +// $sRemark = \MailSo\Base\Utils::DecodeHeaderValue( +// self::findEnvelopeIndex($aEmailItem, 1, ''), $sParentCharset); + + $sLocalPart = self::findEnvelopeIndex($aEmailItem, 2, ''); + $sDomainPart = self::findEnvelopeIndex($aEmailItem, 3, ''); + + if (\strlen($sLocalPart) && \strlen($sDomainPart)) { + $oResult->append( + new \MailSo\Mime\Email($sLocalPart.'@'.$sDomainPart, $sDisplayName) + ); + } + } + } + } + + return $oResult; + } + + public function GetFetchBodyStructure() : ?BodyStructure + { + $aBodyStructureArray = $this->GetFetchValue(Enumerations\FetchType::BODYSTRUCTURE); + + return \is_array($aBodyStructureArray) + ? BodyStructure::NewInstance($aBodyStructureArray) + : null; + } + + /** + * Like: UID, RFC822.SIZE, MODSEQ, INTERNALDATE, FLAGS, BODYSTRUCTURE + * @return mixed + */ + public function GetFetchValue(string $sFetchItemName) + { + if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3])) { + $bNextIsValue = false; + foreach ($this->oImapResponse->ResponseList[3] as $mItem) { + if ($bNextIsValue) { + return $mItem; + } + + if ($sFetchItemName === $mItem) { + $bNextIsValue = true; + } + } + } + + return null; + } + + /** + * Like: BODY[HEADER.FIELDS (RETURN-PATH RECEIVED MIME-VERSION MESSAGE-ID CONTENT-TYPE FROM TO CC BCC SENDER REPLY-TO DELIVERED-TO IN-REPLY-TO REFERENCES DATE SUBJECT SENSITIVITY X-MSMAIL-PRIORITY IMPORTANCE X-PRIORITY X-DRAFT-INFO RETURN-RECEIPT-TO DISPOSITION-NOTIFICATION-TO X-CONFIRM-READING-TO AUTHENTICATION-RESULTS X-DKIM-AUTHENTICATION-RESULTS LIST-UNSUBSCRIBE X-SPAM-STATUS X-SPAMD-RESULT X-BOGOSITY X-VIRUS X-VIRUS-SCANNED X-VIRUS-STATUS)] + * @return mixed + */ + public function GetHeaderFieldsValue() : string + { + $bNextIsValue = false; + + if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3])) { + foreach ($this->oImapResponse->ResponseList[3] as $mItem) { + if ($bNextIsValue) { + return (string) $mItem; + } + + if (\is_string($mItem) && ( + $mItem === 'BODY[HEADER]' || + \str_starts_with($mItem, 'BODY[HEADER.FIELDS') || + $mItem === 'BODY[MIME]')) + { + $bNextIsValue = true; + } + } + } + + return ''; + } + + public static function isValidImapResponse(Response $oImapResponse) : bool + { + return + !$oImapResponse->IsStatusResponse + && Enumerations\ResponseType::UNTAGGED === $oImapResponse->ResponseType + && 3 < \count($oImapResponse->ResponseList) && 'FETCH' === $oImapResponse->ResponseList[2] + && \is_array($oImapResponse->ResponseList[3]); + } + + public static function hasUidAndSize(Response $oImapResponse) : bool + { + return \in_array(Enumerations\FetchType::UID, $oImapResponse->ResponseList[3]) + && \in_array(Enumerations\FetchType::RFC822_SIZE, $oImapResponse->ResponseList[3]); + } + + /** + * @return mixed + */ + private static function findEnvelopeIndex(array $aEnvelope, int $iIndex, ?string $mNullResult) + { + return (isset($aEnvelope[$iIndex]) && '' !== $aEnvelope[$iIndex]) + ? $aEnvelope[$iIndex] : $mNullResult; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Folder.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Folder.php new file mode 100644 index 0000000000..276eeb13f6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Folder.php @@ -0,0 +1,186 @@ +FullName = $sFullName; + $this->setDelimiter($sDelimiter); + $this->setAttributes($aAttributes); +/* + // RFC 5738 + if (\in_array('\\noutf8', $this->aAttributes)) { + } + if (\in_array('\\utf8only', $this->aAttributes)) { + } +*/ + } + + public function setAttributes(array $aAttributes) : void + { + $this->aAttributes = \array_map('mb_strtolower', $aAttributes); + } + + public function setSubscribed() : void + { + $this->aAttributes = \array_unique(\array_merge($this->aAttributes, ['\\subscribed'])); + } + + public function setDelimiter(?string $sDelimiter) : void + { + $this->sDelimiter = $sDelimiter; + } + + public function Name() : string + { + $sNameRaw = $this->FullName; + if ($this->sDelimiter) { + $aNames = \explode($this->sDelimiter, $sNameRaw); + return \end($aNames); + } + return $sNameRaw; + } + + public function Delimiter() : ?string + { + return $this->sDelimiter; + } + + public function Selectable() : bool + { + return !\in_array('\\noselect', $this->aAttributes) + && !\in_array('\\nonexistent', $this->aAttributes); + } + + public function IsSubscribed() : bool + { + return \in_array('\\subscribed', $this->aAttributes); + } + + public function IsInbox() : bool + { + return 'INBOX' === \strtoupper($this->FullName) || \in_array('\\inbox', $this->aAttributes); + } + + public function SetMetadata(string $sName, string $sData) : void + { + $this->aMetadata[$sName] = $sData; + } + + public function SetAllMetadata(array $aMetadata) : void + { + $this->aMetadata = $aMetadata; + } + + public function GetMetadata(string $sName) : ?string + { + return isset($this->aMetadata[$sName]) ? $this->aMetadata[$sName] : null; + } + + public function Metadata() : array + { + return $this->aMetadata; + } + + // JMAP RFC 8621 + public function Role() : ?string + { + $role = \strtolower($this->GetMetadata(MetadataKeys::SPECIALUSE) ?: ''); + if (!$role) { + $match = \array_intersect([ + '\\inbox', + '\\all', // '\\allmail' + '\\archive', + '\\drafts', + '\\flagged', // '\\starred' + '\\important', + '\\junk', // '\\spam' + '\\sent', // '\\sentmail' + '\\trash', // '\\bin' + ], $this->aAttributes); + if ($match) { + $role = \array_shift($match); + } + if (!$role && 'INBOX' === \strtoupper($this->FullName)) { + return 'inbox'; + } + } + return $role ? \ltrim($role, '\\') : null; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $selectable = $this->Selectable(); + $result = array( + '@Object' => 'Object/Folder', + 'name' => $this->Name(), + 'fullName' => $this->FullName, + 'delimiter' => (string) $this->sDelimiter, + 'attributes' => $this->aAttributes, + 'metadata' => $this->aMetadata, + 'uidNext' => $this->UIDNEXT, + // https://datatracker.ietf.org/doc/html/rfc8621#section-2 + 'totalEmails' => $this->MESSAGES, + 'unreadEmails' => $this->UNSEEN, + 'id' => $this->MAILBOXID, + 'size' => $this->SIZE, + 'role' => $this->Role(), +/* + 'rights' => $this->myRights, + 'myRights' => $this->myRights ?: [ + 'mayReadItems' => $selectable, + 'mayAddItems' => $selectable, + 'mayRemoveItems' => $selectable, + 'maySetSeen' => $selectable, + 'maySetKeywords' => $selectable, + 'mayCreateChild' => $selectable, + 'mayRename' => $selectable, + 'mayDelete' => $selectable, + 'maySubmit' => $selectable + ] +*/ + ); + if ($this->etag) { + $result['etag'] = $this->etag; + } + return $result; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderCollection.php new file mode 100644 index 0000000000..8e2d9d8c28 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderCollection.php @@ -0,0 +1,37 @@ + 'Collection/FolderCollection', + '@Collection' => \array_values($this->getArrayCopy()), +// 'optimized' => $this->Optimized + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderInformation.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderInformation.php new file mode 100644 index 0000000000..a26eab6969 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderInformation.php @@ -0,0 +1,85 @@ +FullName = $sFullName; + $this->IsWritable = $bIsWritable; + } + + public function IsFlagSupported(string $sFlag) : bool + { + return \in_array('\\*', $this->PermanentFlags) + || \in_array($sFlag, $this->PermanentFlags); +// || \in_array($sFlag, $this->Flags); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $result = array( + 'id' => $this->MAILBOXID, + 'name' => $this->FullName, + 'uidNext' => $this->UIDNEXT, + 'uidValidity' => $this->UIDVALIDITY + ); + if (isset($this->MESSAGES)) { + $result['totalEmails'] = $this->MESSAGES; + $result['unreadEmails'] = $this->UNSEEN; + } + if (isset($this->HIGHESTMODSEQ)) { + $result['highestModSeq'] = $this->HIGHESTMODSEQ; + } + if (isset($this->APPENDLIMIT)) { + $result['appendLimit'] = $this->APPENDLIMIT; + } + if (isset($this->SIZE)) { + $result['size'] = $this->SIZE; + } + if ($this->etag) { + $result['etag'] = $this->etag; + } +/* + if ($this->Flags) { + $result['flags'] = $this->Flags; + } +*/ + if ($this->PermanentFlags) { + $result['permanentFlags'] = $this->PermanentFlags; + } + return $result; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php new file mode 100644 index 0000000000..ace504b1cc --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php @@ -0,0 +1,675 @@ +Settings->username . '@' . + $this->Settings->host . ':' . + $this->Settings->port + ); + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ +// public function Connect(Settings $oSettings) : void + public function Connect(\MailSo\Net\ConnectSettings $oSettings) : void + { + $this->aTagTimeouts['*'] = \microtime(true); + + parent::Connect($oSettings); + + $this->setCapabilities($this->getResponse('*')); + + if (ConnectionSecurityType::STARTTLS === $this->Settings->type + || (ConnectionSecurityType::AUTO_DETECT === $this->Settings->type && $this->hasCapability('STARTTLS'))) { + $this->StartTLS(); + } + } + + private function StartTLS() : void + { + if ($this->hasCapability('STARTTLS')) { + $this->SendRequestGetResponse('STARTTLS'); + $this->EnableCrypto(); + $this->aCapa = null; + $this->aCapaRaw = null; + } else { + $this->writeLogException( + new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'), + \LOG_ERR); + } + } + + public function supportsAuthType(string $sasl_type) : bool + { + return $this->hasCapability("AUTH={$sasl_type}"); + } + + /** + * @throws \UnexpectedValueException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function Login(Settings $oSettings) : self + { + if ($this->bIsLoggined) { + return $this; + } + + $sLogin = $oSettings->username; + $sPassword = $oSettings->passphrase; + + if (!\strlen($sLogin) || !\strlen($sPassword)) { + $this->writeLogException(new \UnexpectedValueException, \LOG_ERR); + } + + $type = ''; + foreach ($oSettings->SASLMechanisms as $sasl_type) { + if ($this->hasCapability("AUTH={$sasl_type}") && \SnappyMail\SASL::isSupported($sasl_type)) { + $type = $sasl_type; + break; + } + } + // RFC3501 6.2.3 + if (!$type && \in_array('LOGIN', $oSettings->SASLMechanisms) && !$this->hasCapability('LOGINDISABLED')) { + $type = 'LOGIN'; + } + if (!$type) { + if (!$this->Encrypted() && $this->hasCapability('STARTTLS')) { + $this->StartTLS(); + return $this->Login($oSettings); + } + throw new \MailSo\RuntimeException('No supported SASL mechanism found, remote server wants: ' + . \implode(', ', \array_filter($this->Capability() ?: [], function($var){ + return \str_starts_with($var, 'AUTH='); + })) + ); + } + + $SASL = \SnappyMail\SASL::factory($type); + + try + { + if ('CRAM-MD5' === $type) + { + $oResponse = $this->SendRequestGetResponse('AUTHENTICATE', array($type)); + $sChallenge = $this->getResponseValue($oResponse, Enumerations\ResponseType::CONTINUATION); + $sAuth = $SASL->authenticate($sLogin, $sPassword, $sChallenge); + $this->logMask($sAuth); + $this->sendRaw($sAuth); + $oResponse = $this->getResponse(); + } + else if ('PLAIN' === $type || 'OAUTHBEARER' === $type || \str_starts_with($type, 'SCRAM-') /*|| 'PLAIN-CLIENTTOKEN' === $type*/) + { + $sAuth = $SASL->authenticate($sLogin, $sPassword); + $this->logMask($sAuth); + if ($this->hasCapability('SASL-IR')) { + $this->SendRequest('AUTHENTICATE', array($type, $sAuth)); + } else { + $this->SendRequestGetResponse('AUTHENTICATE', array($type)); + $this->sendRaw($sAuth); + } + $oResponse = $this->getResponse(); +// if ($oResponse->getLast()->ResponseType === Enumerations\ResponseType::CONTINUATION) + if ($SASL->hasChallenge()) { + $sChallenge = $SASL->challenge($this->getResponseValue($oResponse, Enumerations\ResponseType::CONTINUATION)); + $this->logMask($sChallenge); + $this->sendRaw($sChallenge); + $oResponse = $this->getResponse(); + $SASL->verify($this->getResponseValue($oResponse, Enumerations\ResponseType::CONTINUATION)); + $this->sendRaw(''); + $oResponse = $this->getResponse(); + } + } + else if ('XOAUTH2' === $type || 'OAUTHBEARER' === $type) + { + $sAuth = $SASL->authenticate($sLogin, $sPassword); + $this->logMask($sAuth); + $oResponse = $this->SendRequestGetResponse('AUTHENTICATE', array($type, $sAuth)); + $oR = $oResponse->getLast(); + if ($oR && Enumerations\ResponseType::CONTINUATION === $oR->ResponseType) { + if (!empty($oR->ResponseList[1]) && \preg_match('/^[a-zA-Z0-9=+\/]+$/', $oR->ResponseList[1])) { + $this->logWrite(\base64_decode($oR->ResponseList[1]), \LOG_WARNING); + } + $this->sendRaw(''); + $oResponse = $this->getResponse(); + } + } + else if ($this->hasCapability('LOGINDISABLED')) + { + $oResponse = $this->SendRequestGetResponse('AUTHENTICATE', array($type)); + $sB64 = $this->getResponseValue($oResponse, Enumerations\ResponseType::CONTINUATION); + $sAuth = $SASL->authenticate($sLogin, $sPassword, $sB64); + $this->logMask($sAuth); + $this->sendRaw($sAuth, true); + $this->getResponse(); + $sPass = $SASL->challenge(''/*UGFzc3dvcmQ6*/); + $this->logMask($sPass); + $this->sendRaw($sPass); + $oResponse = $this->getResponse(); + } + else + { + $sPassword = $this->EscapeString(\mb_convert_encoding($sPassword, 'ISO-8859-1', 'UTF-8')); + $this->logMask($sPassword); + $oResponse = $this->SendRequestGetResponse('LOGIN', + array( + $this->EscapeString($sLogin), + $sPassword + )); + } + + $this->setCapabilities($oResponse); + +/* + // TODO: RFC 9051 + if ($this->hasCapability('IMAP4rev2')) { + $this->Enable('IMAP4rev1'); + } +*/ + // RFC 6855 || RFC 5738 + $this->UTF8 = $this->hasCapability('UTF8=ONLY') || $this->hasCapability('UTF8=ACCEPT'); + if ($this->UTF8) { + $this->Enable('UTF8=ACCEPT'); + } + } + catch (Exceptions\NegativeResponseException $oException) + { + $this->writeLogException(new Exceptions\LoginBadCredentialsException($oException->GetResponses(), '', 0, $oException)); + } + + $this->bIsLoggined = true; + + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function Logout() : void + { + if ($this->bIsLoggined) { + $this->bIsLoggined = false; + $this->SendRequestGetResponse('LOGOUT'); + } + } + + public function IsLoggined() : bool + { + return $this->IsConnected() && $this->bIsLoggined; + } + + public function IsSelected() : bool + { + return $this->IsLoggined() && $this->oCurrentFolderInfo; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function Capability() : ?array + { + if (!$this->aCapaRaw) { + $this->setCapabilities($this->SendRequestGetResponse('CAPABILITY')); + } +/* + $this->aCapa[] = 'X-DOVECOT'; +*/ + return $this->aCapa; + } + + public function Capabilities() : ?array + { + if (!$this->aCapaRaw) { + $this->Capability(); + } + return $this->aCapaRaw; + } + + public function CapabilityValue(string $sExtentionName) : ?string + { + $sExtentionName = \trim($sExtentionName) . '='; + $aCapabilities = $this->Capability() ?: []; + foreach ($aCapabilities as $string) { + if (\str_starts_with($string, $sExtentionName)) { + return \substr($string, \strlen($sExtentionName)); + } + } + return null; + } + + private function setCapabilities(ResponseCollection $oResponseCollection) : void + { + $aList = $oResponseCollection->getCapabilityResult(); + $this->aCapaRaw = $aList; + if ($aList) { + // Strip unused capabilities + $aList = \array_diff($aList, ['PREVIEW=FUZZY', 'SNIPPET=FUZZY', 'SORT=DISPLAY']); + // Set raw response capabilities + $this->aCapaRaw = $aList; + // Set active capabilities + $aList = \array_diff($aList, $this->Settings->disabled_capabilities); + if (\in_array('THREAD', $this->Settings->disabled_capabilities)) { + $aList = \array_filter($aList, function ($item) { return !\str_starts_with($item, 'THREAD='); }); + } + } + $this->aCapa = $aList; + } + + /** + * Test support for things like: + * IMAP4rev1 IMAP4rev2 ID UIDPLUS QUOTA ENABLE IDLE + * SORT SORT=DISPLAY ESEARCH ESORT SEARCHRES WITHIN + * THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND + * URL-PARTIAL CATENATE UNSELECT NAMESPACE CONDSTORE + * CHILDREN LIST-EXTENDED LIST-STATUS STATUS=SIZE + * I18NLEVEL=1 QRESYNC CONTEXT=SEARCH + * BINARY MOVE SNIPPET=FUZZY PREVIEW=FUZZY LITERAL+ NOTIFY + * AUTH= LOGIN LOGINDISABLED LOGIN-REFERRALS SASL-IR STARTTLS + * METADATA METADATA-SERVER SPECIAL-USE + * + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function hasCapability(string $sExtentionName) : bool + { + $sExtentionName = \trim($sExtentionName); + return $sExtentionName && \in_array(\strtoupper($sExtentionName), $this->Capability() ?: []); + } + + /** + * RFC 5161 + */ + public function Enable(/*string|array*/ $mCapabilityNames) : void + { + if (\is_string($mCapabilityNames)) { + $mCapabilityNames = [$mCapabilityNames]; + } + if (\is_array($mCapabilityNames) /*&& $this->hasCapability('ENABLE')*/) { + $this->SendRequestGetResponse('ENABLE', $mCapabilityNames); + } + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function GetNamespaces() : ?NamespaceResult + { + if (!$this->hasCapability('NAMESPACE')) { + return null; + } + + try { + $oResponseCollection = $this->SendRequestGetResponse('NAMESPACE'); + foreach ($oResponseCollection as $oResponse) { + if (Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType + && 'NAMESPACE' === $oResponse->StatusOrIndex) + { + return new NamespaceResult($oResponse); + } + } + throw new Exceptions\ResponseException; + } catch (\Throwable $e) { + $this->writeLogException($e, \LOG_ERR); + } + } + + /** + * RFC 7889 + * APPENDLIMIT= indicates that the IMAP server has the same upload limit for all mailboxes. + * APPENDLIMIT without any value indicates that the IMAP server supports this extension, + * and that the client will need to discover upload limits for each mailbox, + * as they might differ from mailbox to mailbox. + */ + public function AppendLimit() : ?int + { + $string = $this->CapabilityValue('APPENDLIMIT'); + return \is_null($string) ? null : (int) $string; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function Noop() : self + { + $this->SendRequestGetResponse('NOOP'); + return $this; + } + + /** + * @throws \ValueError + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function SendRequest(string $sCommand, array $aParams = array(), bool $bBreakOnLiteral = false) : string + { + $sCommand = \trim($sCommand); + if (!\strlen($sCommand)) { + $this->writeLogException(new \ValueError, \LOG_ERR); + } + + $this->IsConnected(true); + + $sTag = $this->getNewTag(); + + $sRealCommand = $sTag.' '.$sCommand.$this->prepareParamLine($aParams); + + $this->aTagTimeouts[$sTag] = \microtime(true); + + if ($bBreakOnLiteral && !\preg_match('/\d\+\}\r\n/', $sRealCommand)) { + $iPos = \strpos($sRealCommand, "}\r\n"); + if (false !== $iPos) { + $this->sendRaw(\substr($sRealCommand, 0, $iPos + 1)); + return \substr($sRealCommand, $iPos + 3); + } + } + + $this->sendRaw($sRealCommand); + return ''; + } + + /** + * @throws \ValueError + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function SendRequestGetResponse(string $sCommand, array $aParams = array()) : ResponseCollection + { + $this->SendRequest($sCommand, $aParams); + return $this->getResponse(); + } + + protected function getResponseValue(ResponseCollection $oResponseCollection, int $type = 0) : string + { + $oResponse = $oResponseCollection->getLast(); + if ($oResponse && (!$type || $type === $oResponse->ResponseType)) { + $sResult = $oResponse->ResponseList[1] ?? null; + if ($sResult) { + return $sResult; + } + $this->writeLogException(new Exceptions\LoginException); + } + $this->writeLogException(new Exceptions\LoginException); + } + + /** + * TODO: passthru to parse response in JavaScript + * This will reduce CPU time on server and moves it to the client + * And can be used with the new JavaScript AbstractFetchRemote.streamPerLine(fCallback, sGetAdd) + * + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + protected function streamResponse(?string $sEndTag = null) : void + { + try { + if (\is_resource($this->ConnectionResource())) { + \SnappyMail\HTTP\Stream::start(); + $sEndTag = ($sEndTag ?: $this->getCurrentTag()) . ' '; + $sLine = \fgets($this->ConnectionResource()); + do { + if (\str_starts_with($sLine, $sEndTag)) { + echo 'T '.\substr($sLine, \strlen($sEndTag)); + break; + } + echo $sLine; + $sLine = \fgets($this->ConnectionResource()); + } while (\strlen($sLine)); + exit; + } + } catch (\Throwable $e) { + $this->writeLogException($e, \LOG_WARNING); + } + } + + protected function getResponse(?string $sEndTag = null) : ResponseCollection + { + try { + $oResult = new ResponseCollection; + + if (\is_resource($this->ConnectionResource())) { + $sEndTag = $sEndTag ?: $this->getCurrentTag(); + + while (true) { + $oResponse = $this->partialParseResponse(); + $oResult->append($oResponse); + + if ($oResponse->IsStatusResponse + && Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType + && Enumerations\ResponseStatus::PREAUTH === $oResponse->StatusOrIndex +// && (Enumerations\ResponseStatus::PREAUTH === $oResponse->StatusOrIndex || Enumerations\ResponseStatus::BYE === $oResponse->StatusOrIndex) + ) { + break; + } + + // RFC 5530 + if ($sEndTag === $oResponse->Tag && \is_array($oResponse->OptionalResponse) && 'CLIENTBUG' === $oResponse->OptionalResponse[0]) { + // The server has detected a client bug. +// \SnappyMail\Log::warning('IMAP', "{$oResponse->OptionalResponse[0]}: {$this->lastCommand}"); + } + + if ($sEndTag === $oResponse->Tag || Enumerations\ResponseType::CONTINUATION === $oResponse->ResponseType) { + if (isset($this->aTagTimeouts[$sEndTag])) { + $this->writeLog((\microtime(true) - $this->aTagTimeouts[$sEndTag]).' ('.$sEndTag.')', \LOG_DEBUG); + + unset($this->aTagTimeouts[$sEndTag]); + } + + break; + } + } + } + + $oResult->validate(); + + } catch (\Throwable $e) { + $this->writeLogException($e, \LOG_WARNING); + } + + return $oResult; + } + +// public function yieldUntaggedResponses() : \Generator + public function yieldUntaggedResponses() : iterable + { + try { + $oResult = new ResponseCollection; + + if (\is_resource($this->ConnectionResource())) { + $sEndTag = $this->getCurrentTag(); + + while (true) { + $oResponse = $this->partialParseResponse(); + if (Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType) { + yield $oResponse; + } else { + $oResult->append($oResponse); + } + + // RFC 5530 + if ($sEndTag === $oResponse->Tag && \is_array($oResponse->OptionalResponse) && 'CLIENTBUG' === $oResponse->OptionalResponse[0]) { + // The server has detected a client bug. +// \SnappyMail\Log::warning('IMAP', "{$oResponse->OptionalResponse[0]}: {$this->lastCommand}"); + } + + if ($sEndTag === $oResponse->Tag || Enumerations\ResponseType::CONTINUATION === $oResponse->ResponseType) { + if (isset($this->aTagTimeouts[$sEndTag])) { + $this->writeLog((\microtime(true) - $this->aTagTimeouts[$sEndTag]).' ('.$sEndTag.')', \LOG_DEBUG); + + unset($this->aTagTimeouts[$sEndTag]); + } + + break; + } + } + } + + $oResult->validate(); + + } catch (\Throwable $e) { + $this->writeLogException($e, \LOG_WARNING); + } + } + + protected function prepareParamLine(array $aParams = array()) : string + { + $sReturn = ''; + foreach ($aParams as $mParamItem) { + if (\is_array($mParamItem) && \count($mParamItem)) { + $sReturn .= ' ('.\trim($this->prepareParamLine($mParamItem)).')'; + } else if (\is_string($mParamItem)) { + $sReturn .= ' '.$mParamItem; + } + } + return $sReturn; + } + + protected function getNewTag() : string + { + ++$this->iTagCount; + return $this->getCurrentTag(); + } + + protected function getCurrentTag() : string + { + return $this->TAG_PREFIX.$this->iTagCount; + } + + public function EscapeString(?string $sStringForEscape) : string + { + if (null === $sStringForEscape) { + return 'NIL'; + } +/* + // literal-string + $this->hasCapability('LITERAL+') + if (\preg_match('/[\r\n\x00\x80-\xFF]/', $sStringForEscape)) { + return \sprintf("{%d}\r\n%s", \strlen($sStringForEscape), $sStringForEscape); + } +*/ + // quoted-string + return '"' . \addcslashes($sStringForEscape, '\\"') . '"'; + } + + public function getLogName() : string + { + return 'IMAP'; + } + + /** + * RFC 2971 + * Don't have to be logged in to call this command + */ + public function ServerID() : string + { + if ($this->hasCapability('ID')) { + foreach ($this->SendRequestGetResponse('ID', [null]) as $oResponse) { + if ('ID' === $oResponse->ResponseList[1] && \is_array($oResponse->ResponseList[2])) { + $c = \count($oResponse->ResponseList[2]); + $aResult = []; + for ($i = 0; $i < $c; $i += 2) { + $aResult[] = $oResponse->ResponseList[2][$i] . '=' . $oResponse->ResponseList[2][$i+1]; + } + return \implode(' ', $aResult); + } + } + } + return 'UNKNOWN'; + } + + /** + * RFC 4978 + * It is RECOMMENDED that the client uses TLS compression. + *//* + public function Compress() : bool + { + try { + if ($this->hasCapability('COMPRESS=DEFLATE')) { + $this->SendRequestGetResponse('COMPRESS', ['DEFLATE']); + \stream_filter_append($this->ConnectionResource(), 'zlib.inflate'); + \stream_filter_append($this->ConnectionResource(), 'zlib.deflate', STREAM_FILTER_WRITE, array( + 'level' => 6, 'window' => 15, 'memory' => 9 + )); + return true; + } + } catch (\Throwable $e) { + } + return false; + }*/ + + public function EscapeFolderName(string $sFolderName) : string + { + return $this->EscapeString($this->UTF8 ? $sFolderName : \MailSo\Base\Utils::Utf8ToUtf7Modified($sFolderName)); + } + + public function toUTF8(string $sText) : string + { + return $this->UTF8 ? $sText : \MailSo\Base\Utils::Utf7ModifiedToUtf8($sText); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/NamespaceResult.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/NamespaceResult.php new file mode 100644 index 0000000000..a0d2eb971d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/NamespaceResult.php @@ -0,0 +1,85 @@ +ResponseList[2])) { + foreach ($oImapResponse->ResponseList[2] as $entry) { + if (\is_array($entry) && 2 <= \count($entry)) { + $this->aPersonal[] = [ + 'prefix' => \array_shift($entry), + 'delimiter' => \array_shift($entry), + 'extension' => $entry + ]; + } + } + } + if (!empty($oImapResponse->ResponseList[3])) { + foreach ($oImapResponse->ResponseList[3] as $entry) { + if (\is_array($entry) && 2 <= \count($entry)) { + $this->aOtherUsers[] = [ + 'prefix' => \array_shift($entry), + 'delimiter' => \array_shift($entry), + 'extension' => $entry + ]; + } + } + } + if (!empty($oImapResponse->ResponseList[4])) { + foreach ($oImapResponse->ResponseList[4] as $entry) { + if (\is_array($entry) && 2 <= \count($entry)) { + $this->aShared[] = [ + 'prefix' => \array_shift($entry), + 'delimiter' => \array_shift($entry), + 'extension' => $entry + ]; + } + } + } + } + + public function GetPersonalPrefix() : string + { + $sPrefix = ''; + if (isset($this->aPersonal[0])) { + $sPrefix = $this->aPersonal[0]['prefix']; + $sDelimiter = $this->aPersonal[0]['delimiter']; + if ('INBOX'.$sDelimiter === \substr(\strtoupper($sPrefix), 0, 6)) { + $sPrefix = 'INBOX'.$sDelimiter.\substr($sPrefix, 6); + }; + } + return $sPrefix; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( + '@Object' => 'Object/Namespaces', + 'personal' => $this->aPersonal, + 'users' => $this->aOtherUsers, + 'shared' => $this->aShared, + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/ESEARCH.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/ESEARCH.php new file mode 100644 index 0000000000..9a51f8bd55 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/ESEARCH.php @@ -0,0 +1,129 @@ +hasCapability('ESEARCH')) { + $oImapClient->writeLogException(new \MailSo\RuntimeException('ESEARCH is not supported'), \LOG_ERR); + } + parent::__construct($oImapClient); + } + + public function SendRequest() : string + { + $sCmd = 'SEARCH'; + $aRequest = array(); + +/* // RFC 6203 + if (false !== \stripos($this->sCriterias, 'FUZZY') && !$this->oImapClient->hasCapability('SEARCH=FUZZY')) { + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('SEARCH=FUZZY is not supported'), \LOG_ERR); + } +*/ + + $aFolders = []; + if ($this->aMailboxes) { + $aFolders[] = 'mailboxes'; + $aFolders[] = $this->aMailboxes; + } + if ($this->aSubtrees) { + $aFolders[] = 'subtree'; + $aFolders[] = $this->aSubtrees; + } + if ($this->aSubtreesOne) { + $aFolders[] = 'subtree-one'; + $aFolders[] = $this->aSubtreesOne; + } + if ($aFolders) { + if (!$this->oImapClient->hasCapability('MULTISEARCH')) { + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('MULTISEARCH is not supported'), \LOG_ERR); + } + $sCmd = 'ESEARCH'; + $aReques[] = 'IN'; + $aReques[] = $aFolders; + } + + if (\strlen($this->sCharset)) { + $aRequest[] = 'CHARSET'; + $aRequest[] = \strtoupper($this->sCharset); + } + + $aRequest[] = 'RETURN'; + if ($this->aReturn) { + // RFC 5267 checks + if (!$this->oImapClient->hasCapability('CONTEXT=SEARCH')) { + foreach ($this->aReturn as $sReturn) { + if (\preg_match('/PARTIAL|UPDATE|CONTEXT/i', $sReturn)) { + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('CONTEXT=SEARCH is not supported'), \LOG_ERR); + } + } + } + $aRequest[] = $this->aReturn; + } else { + $aRequest[] = array('ALL'); + } + + $aRequest[] = (\strlen($this->sCriterias) && '*' !== $this->sCriterias) ? $this->sCriterias : 'ALL'; + + if (\strlen($this->sLimit)) { + $aRequest[] = $this->sLimit; + } + + return $this->oImapClient->SendRequest( + ($this->bUid ? 'UID ' : '') . $sCmd, + $aRequest + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/Request.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/Request.php new file mode 100644 index 0000000000..0f8ef9b7e0 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/Request.php @@ -0,0 +1,28 @@ +oImapClient = $oImapClient; + } + + final public function getName() + { + $name = \explode('\\', \get_class($this)); + return \end($name); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/SORT.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/SORT.php new file mode 100644 index 0000000000..3c34ed67fa --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/SORT.php @@ -0,0 +1,109 @@ +hasCapability('SORT')) { + $oImapClient->writeLogException(new \MailSo\RuntimeException('SORT is not supported'), \LOG_ERR); + } + parent::__construct($oImapClient); + } + + public function SendRequest() : string + { + if (!$this->aSortTypes) { + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('SortTypes are missing'), \LOG_ERR); + } + + $aRequest = array(); + + if ($this->aReturn) { + // RFC 5267 checks + if (!$this->oImapClient->hasCapability('ESORT')) { + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('ESORT is not supported'), \LOG_ERR); + } + if (!$this->oImapClient->hasCapability('CONTEXT=SORT')) { + foreach ($this->aReturn as $sReturn) { + if (\preg_match('/PARTIAL|UPDATE|CONTEXT/i', $sReturn)) { + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('CONTEXT=SORT is not supported'), \LOG_ERR); + } + } + } + $aRequest[] = 'RETURN'; + $aRequest[] = $this->aReturn; + } + + $aRequest[] = $this->aSortTypes; + + $aRequest[] = 'UTF-8'; // \strtoupper(\MailSo\Base\Enumerations\Charset::UTF_8) + + $aRequest[] = (\strlen($this->sCriterias) && '*' !== $this->sCriterias) ? $this->sCriterias : 'ALL'; + + if (\strlen($this->sLimit)) { + $aRequest[] = $this->sLimit; + } + + return $this->oImapClient->SendRequest( + ($this->bUid ? 'UID SORT' : 'SORT'), + $aRequest + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php new file mode 100644 index 0000000000..33665ed2e8 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php @@ -0,0 +1,114 @@ +hasCapability('THREAD=REFS')) { + $this->sAlgorithm = 'REFS'; + } else if ($oImapClient->hasCapability('THREAD=REFERENCES')) { + $this->sAlgorithm = 'REFERENCES'; + } else if ($oImapClient->hasCapability('THREAD=ORDEREDSUBJECT')) { + $this->sAlgorithm = 'ORDEREDSUBJECT'; + } else { + $oImapClient->writeLogException(new \MailSo\RuntimeException('THREAD is not supported'), \LOG_ERR); + } + parent::__construct($oImapClient); + } + + public function setAlgorithm(string $sAlgorithm) : void + { + $sAlgorithm = \strtoupper($sAlgorithm); + if (!$this->oImapClient->hasCapability("THREAD={$sAlgorithm}")) { + $this->oImapClient->writeLogException(new \MailSo\RuntimeException("THREAD={$sAlgorithm} is not supported"), \LOG_ERR); + } + $this->sAlgorithm = $sAlgorithm; + } + + public function SendRequestIterateResponse() : iterable + { + $this->oImapClient->SendRequest( + ($this->bUid ? 'UID THREAD' : 'THREAD'), + array( + $this->sAlgorithm, + 'UTF-8', // \strtoupper(\MailSo\Base\Enumerations\Charset::UTF_8) + (\strlen($this->sCriterias) && '*' !== $this->sCriterias) ? $this->sCriterias : 'ALL' + ) + ); + + foreach ($this->oImapClient->yieldUntaggedResponses() as $oResponse) { + $iOffset = ($this->bUid && 'UID' === $oResponse->StatusOrIndex && !empty($oResponse->ResponseList[2]) && 'THREAD' === $oResponse->ResponseList[2]) ? 1 : 0; + if (('THREAD' === $oResponse->StatusOrIndex || $iOffset) + && \is_array($oResponse->ResponseList) + && 2 < \count($oResponse->ResponseList)) + { + $iLen = \count($oResponse->ResponseList); + for ($iIndex = 2 + $iOffset; $iIndex < $iLen; ++$iIndex) { + $aNewValue = $this->validateThreadItem($oResponse->ResponseList[$iIndex]); + if (\is_array($aNewValue)) { + yield $aNewValue; + } + } + } + } + } + + /** + * @param mixed $mValue + * + * @return int | array | false + */ + private function validateThreadItem($mValue) + { + if (\is_numeric($mValue)) { + $mValue = (int) $mValue; + if (0 < $mValue) { + return $mValue; + } + } else if (\is_array($mValue)) { + if (1 === \count($mValue) && \is_numeric($mValue[0])) { + $mValue = (int) $mValue[0]; + if (0 < $mValue) { + return $mValue; + } + } else { + $aResult = array(); + foreach ($mValue as $mValueItem) { + $mValueItem = $this->validateThreadItem($mValueItem); + if ($mValueItem) { + $aResult[] = $mValueItem; + } + } + return $aResult; + } + } + + return false; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Response.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Response.php new file mode 100644 index 0000000000..1c76dfb2e9 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Response.php @@ -0,0 +1,68 @@ +recToLine($mItem).')' : (string) $mItem; + } + return \implode(' ', $aResult); + } + + public function setStatus(string $value) : void + { + $value = \strtoupper($value); + $this->StatusOrIndex = $value; + $this->IsStatusResponse = \defined("\\MailSo\\Imap\\Enumerations\\ResponseStatus::{$value}"); + } + + public function setTag(string $value) : void + { + $this->Tag = $value; + if ('+' === $value) { + $this->ResponseType = ResponseType::CONTINUATION; + } else if ('*' === $value) { + $this->ResponseType = ResponseType::UNTAGGED; + } else { + $this->ResponseType = ResponseType::UNKNOWN; + } + } + + public function __toString() + { + return $this->recToLine($this->ResponseList); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ResponseCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ResponseCollection.php new file mode 100644 index 0000000000..ba83a0247e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ResponseCollection.php @@ -0,0 +1,75 @@ +getLast(); + if (!$oItem) { + throw new Exceptions\ResponseNotFoundException; + } + + if ($oItem->ResponseType !== Enumerations\ResponseType::CONTINUATION) { + if (!$oItem->IsStatusResponse) { + throw new Exceptions\InvalidResponseException($this); + } + + if (Enumerations\ResponseStatus::OK !== $oItem->StatusOrIndex) { + throw new Exceptions\NegativeResponseException($this); + } + } + return $this; + } + + public function getCapabilityResult() : ?array + { + foreach ($this as $oResponse) { + $aList = null; + // ResponseList[2][0] => CAPABILITY + if (isset($oResponse->ResponseList[1]) && \is_string($oResponse->ResponseList[1]) && + 'CAPABILITY' === \strtoupper($oResponse->ResponseList[1])) + { + $aList = \array_slice($oResponse->ResponseList, 2); + } + else if (\is_array($oResponse->OptionalResponse) && + 1 < \count($oResponse->OptionalResponse) && \is_string($oResponse->OptionalResponse[0]) && + 'CAPABILITY' === \strtoupper($oResponse->OptionalResponse[0])) + { + $aList = \array_slice($oResponse->OptionalResponse, 1); + } + + if (\is_array($aList) && \count($aList)) { + return \array_map('strtoupper', $aList); + } + } + return null; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Responses/ACL.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Responses/ACL.php new file mode 100644 index 0000000000..bc5b6e8e56 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Responses/ACL.php @@ -0,0 +1,74 @@ +identifier = $identifier; + $this->rights = $rights; + } + + public function identifier() : string + { + return $this->identifier; + } + + /** PHP 8.1 + public function hasRight(string|\MailSo\Imap\Enumerations\FolderACL $right) + { + if ($right instanceof \BackedEnum) { + return \str_contains($this->rights, $right->value); + } + */ + public function hasRight(string $right) : bool + { + $const = '\\MailSo\\Imap\\Enumerations\\FolderACL::' . \strtoupper($right); + if (\defined($const)) { + $right = \constant($const); + } + return \str_contains($this->rights, $right); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + '@Object' => 'Object/FolderACLRights', + 'identifier' => $this->identifier, + 'rights' => $this->rights, + 'mine' => $this->mine, +/* + 'mayReadItems' => ($this->hasRight('l') && $this->hasRight('r')), + 'mayAddItems' => $this->hasRight('i'), + 'mayRemoveItems' => ($this->hasRight('t') && $this->hasRight('e')), + 'maySetSeen' => $this->hasRight('s'), + 'maySetKeywords' => $this->hasRight('w'), + 'mayCreateChild' => $this->hasRight('k'), + 'mayRename' => $this->hasRight('x'), + 'mayDelete' => $this->hasRight('x'), + 'maySubmit' => $this->hasRight('p') +*/ + ]; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SearchCriterias.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SearchCriterias.php new file mode 100644 index 0000000000..6f8e74db93 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SearchCriterias.php @@ -0,0 +1,548 @@ + + Messages that contain the specified string in the envelope + structure's BCC field. + + ✔ BEFORE + Messages whose internal date (disregarding time and timezone) + is earlier than the specified date. + + ✔ BODY + Messages that contain the specified string in the body of the + message. + + ✔ CC + Messages that contain the specified string in the envelope + structure's CC field. + + ✔ FROM + Messages that contain the specified string in the envelope + structure's FROM field. + + ✔ HEADER + Messages that have a header with the specified field-name (as + defined in [RFC-2822]) and that contains the specified string + in the text of the header (what comes after the colon). If the + string to search is zero-length, this matches all messages that + have a header line with the specified field-name regardless of + the contents. + + ✔ KEYWORD + Messages with the specified keyword flag set. + + ✔ LARGER + Messages with an [RFC-2822] size larger than the specified + number of octets. + + ☐ NOT + Messages that do not match the specified search key. + + ✔ ON + Messages whose internal date (disregarding time and timezone) + is within the specified date. + + ✔ OR + Messages that match either search key. + + ✔ SENTBEFORE + Messages whose [RFC-2822] Date: header (disregarding time and + timezone) is earlier than the specified date. + + ✔ SENTON + Messages whose [RFC-2822] Date: header (disregarding time and + timezone) is within the specified date. + + ✔ SENTSINCE + Messages whose [RFC-2822] Date: header (disregarding time and + timezone) is within or later than the specified date. + + ✔ SINCE + Messages whose internal date (disregarding time and timezone) + is within or later than the specified date. + + ✔ SMALLER + Messages with an [RFC-2822] size smaller than the specified + number of octets. + + ✔ SUBJECT + Messages that contain the specified string in the envelope + structure's SUBJECT field. + + ✔ TEXT + Messages that contain the specified string in the header or + body of the message. + + ✔ TO + Messages that contain the specified string in the envelope + structure's TO field. + + ☐ UID + Messages with unique identifiers corresponding to the specified + unique identifier set. Sequence set ranges are permitted. + + ☐ UNKEYWORD + Messages that do not have the specified keyword flag set. + + ✔ FLAGGED + ✔ UNFLAGGED + ✔ SEEN + ✔ UNSEEN + ✔ ANSWERED + ✔ UNANSWERED + ☐ DELETED + ☐ UNDELETED + ☐ DRAFT + ☐ UNDRAFT + X NEW + X OLD + X RECENT + */ + + private array $criterias = []; + public bool $fuzzy = false; + + function prepend(string $rule) + { + \array_unshift($this->criterias, $rule); + } + + function __toString() : string + { + if ($this->fuzzy) { + $keys = ['BCC','BODY','CC','FROM','SUBJECT','TEXT','TO']; + foreach ($this->criterias as $i => $key) { + if (\in_array($key, $keys)) { + $this->criterias[$i] = "FUZZY {$key}"; + } + } + } + $sCriteriasResult = \trim(\implode(' ', $this->criterias)); + return $sCriteriasResult ?: 'ALL'; + } + + public static function fromString(\MailSo\Imap\ImapClient $oImapClient, string $sFolderName, string $sSearch, bool $bHideDeleted, bool &$bUseCache = true) : self + { + $iTimeFilter = 0; + $aCriteriasResult = array(); + + $sSearch = \trim($sSearch); + if (\strlen($sSearch)) { + $aLines = \preg_match('/^('.static::RegEx.'):/i', $sSearch) + ? static::parseSearchString($sSearch) + : static::parseQueryString($sSearch); + + if (!$aLines) { + $sValue = static::escapeSearchString($oImapClient, $sSearch); + + if ($oImapClient->Settings->fast_simple_search) { + $aCriteriasResult[] = 'OR OR OR'; + $aCriteriasResult[] = 'FROM'; + $aCriteriasResult[] = $sValue; + $aCriteriasResult[] = 'TO'; + $aCriteriasResult[] = $sValue; + $aCriteriasResult[] = 'CC'; + $aCriteriasResult[] = $sValue; + $aCriteriasResult[] = 'SUBJECT'; + $aCriteriasResult[] = $sValue; + } else { + $aCriteriasResult[] = 'TEXT'; + $aCriteriasResult[] = $sValue; + } + } else { + if (isset($aLines['IN']) && $oImapClient->hasCapability('MULTISEARCH') && \in_array($aLines['IN'], ['subtree','subtree-one','mailboxes'])) { + $aCriteriasResult[] = "IN ({$aLines['IN']} \"{$sFolderName}\")"; + } + + if (isset($aLines['EMAIL'])) { + $sValue = static::escapeSearchString($oImapClient, $aLines['EMAIL']); + + $aCriteriasResult[] = 'OR OR'; + $aCriteriasResult[] = 'FROM'; + $aCriteriasResult[] = $sValue; + $aCriteriasResult[] = 'TO'; + $aCriteriasResult[] = $sValue; + $aCriteriasResult[] = 'CC'; + $aCriteriasResult[] = $sValue; + + unset($aLines['EMAIL']); + unset($aLines['FROM']); + unset($aLines['TO']); + } + + foreach ($aLines as $sName => $sRawValue) { + if (\is_string($sRawValue) && '' === \trim($sRawValue)) { + continue; + } + switch ($sName) { + case 'FROM': + case 'SUBJECT': + case 'BODY': // $sValue = \trim(\MailSo\Base\Utils::StripSpaces($sValue), '"'); + $aCriteriasResult[] = $sName; + $aCriteriasResult[] = static::escapeSearchString($oImapClient, $sRawValue); + break; + case 'KEYWORD': + $aCriteriasResult[] = $sName; + $aCriteriasResult[] = static::escapeSearchString( + $oImapClient, + \MailSo\Base\Utils::Utf8ToUtf7Modified($sRawValue) + ); + break; + + case 'TO': + $sValue = static::escapeSearchString($oImapClient, $sRawValue); + $aCriteriasResult[] = 'OR'; + $aCriteriasResult[] = 'TO'; + $aCriteriasResult[] = $sValue; + $aCriteriasResult[] = 'CC'; + $aCriteriasResult[] = $sValue; + break; + + case 'ATTACHMENT': + // Simple, is not detailed search (Sometimes doesn't work) + $aCriteriasResult[] = 'OR OR OR'; + $aCriteriasResult[] = 'HEADER Content-Type application/'; + $aCriteriasResult[] = 'HEADER Content-Type multipart/m'; + $aCriteriasResult[] = 'HEADER Content-Type multipart/signed'; + $aCriteriasResult[] = 'HEADER Content-Type multipart/report'; + break; + + case 'HEADER': + $aValue = \explode(' ', $sRawValue, 2); + $aCriteriasResult[] = 'HEADER ' + . static::escapeSearchString($oImapClient, $aValue[0]) + . ' ' + . static::escapeSearchString($oImapClient, $aValue[1]); + break; + + case 'FLAGGED': + case 'UNFLAGGED': + case 'SEEN': + case 'UNSEEN': + case 'ANSWERED': + case 'UNANSWERED': + case 'DELETED': + case 'UNDELETED': + $aCriteriasResult[] = $sName; + $bUseCache = false; + break; + + case 'LARGER': + case 'SMALLER': + $aCriteriasResult[] = $sName; + $aCriteriasResult[] = static::parseFriendlySize($sRawValue); + break; + + case 'SINCE': + $sValue = static::parseSearchDate($sRawValue); + if ($sValue) { + $iTimeFilter = \max($iTimeFilter, $sValue); + } + break; + case 'ON': + case 'SENTON': + case 'SENTSINCE': + case 'SENTBEFORE': + case 'BEFORE': + $sValue = static::parseSearchDate($sRawValue); + if ($sValue) { + $aCriteriasResult[] = $sName; + $aCriteriasResult[] = \gmdate('j-M-Y', $sValue); + } + break; + + case 'DATE': + $iDateStampFrom = $iDateStampTo = 0; + $aDate = \explode('/', $sRawValue); + if (2 === \count($aDate)) { + if (\strlen($aDate[0])) { + $iDateStampFrom = static::parseSearchDate($aDate[0]); + } + if (\strlen($aDate[1])) { + $iDateStampTo = static::parseSearchDate($aDate[1]); + $iDateStampTo += 60 * 60 * 24; + } + } else if (\strlen($sRawValue)) { + $iDateStampFrom = static::parseSearchDate($sRawValue); + $iDateStampTo = $iDateStampFrom + 60 * 60 * 24; + } + + if (0 < $iDateStampFrom) { + $iTimeFilter = \max($iTimeFilter, $iDateStampFrom); + } + + if (0 < $iDateStampTo) { + $aCriteriasResult[] = 'BEFORE'; + $aCriteriasResult[] = \gmdate('j-M-Y', $iDateStampTo); + } + break; + + // https://www.rfc-editor.org/rfc/rfc5032.html + case 'OLDER': + case 'YOUNGER': + // time interval in seconds + $sValue = \intval($sRawValue); + if (0 < $sValue && $oImapClient->hasCapability('WITHIN')) { + $aCriteriasResult[] = $sName; + $aCriteriasResult[] = $sValue; + } + break; + + // https://github.com/the-djmaze/snappymail/issues/625 + case 'READ': + case 'UNREAD': + $aCriteriasResult[] = \str_replace('READ', 'SEEN', $sName); + $bUseCache = false; + break; + case 'OLDER_THAN': + $oDate = (new \DateTime())->sub(new \DateInterval("P{$sRawValue}")); + if ($oImapClient->hasCapability('WITHIN')) { + $aCriteriasResult[] = 'OLDER'; + $aCriteriasResult[] = \time() - $oDate->getTimestamp(); + } else { + $aCriteriasResult[] = 'BEFORE'; + $aCriteriasResult[] = $oDate->format('j-M-Y'); + } + break; + case 'NEWER_THAN': + $oDate = (new \DateTime())->sub(new \DateInterval("P{$sRawValue}")); + if ($oImapClient->hasCapability('WITHIN')) { + $aCriteriasResult[] = 'YOUNGER'; + $aCriteriasResult[] = \time() - $oDate->getTimestamp(); + } else { + $iTimeFilter = \max( + $iTimeFilter, + (new \DateTime())->sub(new \DateInterval("P{$sRawValue}"))->getTimestamp() + ); + } + break; + } + } + } + } + + if (0 < $iTimeFilter) { + $aCriteriasResult[] = 'SINCE'; +// $aCriteriasResult[] = \gmdate('j-M-Y', $iTimeFilter); + $aCriteriasResult[] = \gmdate('j-M-Y', $iTimeFilter); + } + + if ($bHideDeleted && !\in_array('DELETED', $aCriteriasResult) && !\in_array('UNDELETED', $aCriteriasResult)) { + $aCriteriasResult[] = 'UNDELETED'; + } + + if ($oImapClient->Settings->search_filter) { + $aCriteriasResult[] = $oImapClient->Settings->search_filter; + } + + $search = new self; + $search->criterias = $aCriteriasResult; + return $search; + } + + public static function escapeSearchString(\MailSo\Imap\ImapClient $oImapClient, string $sSearch) : string + { + // https://github.com/the-djmaze/snappymail/issues/836 +// return $oImapClient->EscapeString($sSearch); +// return \MailSo\Base\Utils::IsAscii($sSearch) || $oImapClient->hasCapability('QQMail')) + return (\MailSo\Base\Utils::IsAscii($sSearch) || !$oImapClient->hasCapability('LITERAL+')) + ? $oImapClient->EscapeString($sSearch) + : '{'.\strlen($sSearch).'}'."\r\n{$sSearch}"; + } + + private static function parseSearchDate(string $sDate) : int + { + if (\strlen($sDate)) { + $oDateTime = \DateTime::createFromFormat('Y-m-d', $sDate, \MailSo\Base\DateTimeHelper::GetUtcTimeZoneObject()); + return $oDateTime ? $oDateTime->getTimestamp() : 0; + } + return 0; + } + + private static function parseFriendlySize(string $sSize) : int + { + $sSize = \preg_replace('/[^0-9KM]/', '', \strtoupper($sSize)); + $iResult = \intval($sSize); + switch (\substr($sSize, -1)) { + case 'M': + $iResult *= 1024; + case 'K': + $iResult *= 1024; + } + return $iResult; + } + + /** + * SnappyMail search like: 'from=foo&to=test&is[]=unseen&is[]=flagged' + */ + private static function parseQueryString(string $sSearch) : array + { + $aResult = array(); + $aParams = array(); + \parse_str($sSearch, $aParams); + foreach ($aParams as $sName => $mValue) { + if (\is_array($mValue)) { + $mValue = \implode(',', $mValue); + } + $sName = \strtoupper($sName); + if ('MAIL' === $sName) { + $sName = 'EMAIL'; + } else if ('TEXT' === $sName) { + $sName = 'BODY'; + } else if ('SIZE' === $sName || 'BIGGER' === $sName || 'MINSIZE' === $sName) { + $sName = 'LARGER'; + } else if ('MAXSIZE' === $sName) { + $sName = 'SMALLER'; + } + switch ($sName) { + case 'DATE': + $mValue = \rtrim($mValue,'/') . '/'; + case 'BODY': + case 'EMAIL': + case 'FROM': + case 'TO': + case 'SUBJECT': + case 'KEYWORD': + case 'IN': + case 'SMALLER': + case 'LARGER': + case 'SINCE': + case 'ON': + case 'SENTON': + case 'SENTSINCE': + case 'SENTBEFORE': + case 'BEFORE': + case 'OLDER': + case 'YOUNGER': + case 'HEADER': + if (\strlen($mValue)) { + $aResult[$sName] = $mValue; + } + break; + + case 'ATTACHMENT': + case 'FLAGGED': + case 'UNFLAGGED': + case 'SEEN': + case 'UNSEEN': + case 'ANSWERED': + case 'UNANSWERED': + case 'DELETED': + case 'UNDELETED': + $aResult[$sName] = true; + break; + } + } + return $aResult; + } + + /** + * RainLoop search like: 'from:"foo" to:"test" is:unseen,flagged' + */ + private static function parseSearchString(string $sSearch) : array + { + $aResult = array(); + + $aCache = array(); + + $sSearch = \MailSo\Base\Utils::StripSpaces($sSearch); + $sSearch = \trim(\preg_replace('/('.static::RegEx.'):\\s+/i', '\\1:', $sSearch)); + + $mMatch = array(); + if (\preg_match_all('/".*?(? $sName) { + if (isset($mMatch[2][$iIndex]) && \strlen($mMatch[2][$iIndex])) { + $sName = \strtoupper($sName); + if ('MAIL' === $sName) { + $sName = 'EMAIL'; + } else if ('TEXT' === $sName) { + $sName = 'BODY'; + } else if ('SIZE' === $sName || 'BIGGER' === $sName || 'MINSIZE' === $sName) { + $sName = 'LARGER'; + } else if ('MAXSIZE' === $sName) { + $sName = 'SMALLER'; + } + + if ('HAS' === $sName && \preg_match('/files?|attachments?/', $mMatch[2][$iIndex])) { + $aResult['ATTACHMENT'] = true; + } else if ('IS' === $sName) { + foreach (\explode(',', \strtoupper($mMatch[2][$iIndex])) as $sName) { + $aResult[\trim($sName)] = true; + } + } else if ('DATE' === $sName) { + $aResult[$sName] = \str_replace('.', '-', $mMatch[2][$iIndex]); + } else { + $aResult[$sName] = $mMatch[2][$iIndex]; + } + } + } + } + + foreach ($aResult as $sName => $sValue) { + if (isset($aCache[$sValue])) { + $aResult[$sName] = \trim($aCache[$sValue], '"\' '); + } + } + + return $aResult; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SequenceSet.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SequenceSet.php new file mode 100644 index 0000000000..e8f742980b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SequenceSet.php @@ -0,0 +1,99 @@ +data = \array_values($uid ? \array_filter(\array_map(function($id){ + return \preg_match('/^([0-9]+|\\*):([0-9]+|\\*)/', $id, $dummy) ? $id : \intval($id); + }, $mItems)) : $mItems); + } else if (\is_scalar($mItems)) { + $this->data[] = $mItems; + } + $this->UID = $uid; + } + + public function count(): int + { + return \count($this->data); + } + + public function contains($value): bool + { + return \in_array($value, $this->data); + } + + public function indexOf($value)/*: int|false*/ + { + return \array_search($value, $this->data); + } + + public function getArrayCopy(): array + { + return $this->data; + } + + public function __toString(): string + { + $aResult = array(); + $iStart = null; + $iPrev = null; + + foreach ($this->data as $mItem) { + if (false !== \strpos($mItem, ':')) { + $aResult[] = $mItem; + continue; + } + + if (null === $iStart || null === $iPrev) { + $iStart = $mItem; + $iPrev = $mItem; + continue; + } + + if ($iPrev === $mItem - 1) { + $iPrev = $mItem; + } else { + $aResult[] = $iStart === $iPrev ? $iStart : $iStart.':'.$iPrev; + $iStart = $mItem; + $iPrev = $mItem; + } + } + + if (null !== $iStart && null !== $iPrev) { + $aResult[] = $iStart === $iPrev ? $iStart : $iStart.':'.$iPrev; + } + + return \implode(',', $aResult); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Settings.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Settings.php new file mode 100644 index 0000000000..8993a051b0 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Settings.php @@ -0,0 +1,147 @@ +$option = !empty($aSettings[$option]); + } + } + + // Integer options + $options = [ +// 'body_text_limit', +// 'folder_list_limit', + 'message_list_limit', +// 'thread_limit', + ]; + foreach ($options as $option) { + if (isset($aSettings[$option])) { + $object->$option = \intval($aSettings[$option]); + } + } + + // String options + $options = [ + 'search_filter', + 'spam_headers', + 'virus_headers', + ]; + foreach ($options as $option) { + if (isset($aSettings[$option])) { + $object->$option = (string) $aSettings[$option]; + } + } + + if (!empty($aSettings['disabled_capabilities']) && \is_array($aSettings['disabled_capabilities'])) { + $object->disabled_capabilities = $aSettings['disabled_capabilities']; + } + // Convert old disable_* settings + if (!empty($aSettings['disable_list_status'])) { + $object->disabled_capabilities[] = 'list-status'; + } + if (!empty($aSettings['disable_metadata'])) { + // Issue #365: Many folders on Cyrus IMAP breaks login + $object->disabled_capabilities[] = 'METADATA'; + } + if (!empty($aSettings['disable_move'])) { + $object->disabled_capabilities[] = 'MOVE'; + } + if (!empty($aSettings['disable_sort'])) { + $object->disabled_capabilities[] = 'SORT'; + } + if (!empty($aSettings['disable_thread'])) { + $object->disabled_capabilities[] = 'THREAD'; + } + if (!empty($aSettings['disable_binary'])) { + $object->disabled_capabilities[] = 'BINARY'; + } + if (!empty($aSettings['disable_status_size'])) { + // STATUS SIZE can take a significant amount of time, therefore not active by default + $object->disabled_capabilities[] = 'STATUS=SIZE'; + } + if (!empty($aSettings['disable_preview'])) { + // RFC 8970 + $object->disabled_capabilities[] = 'PREVIEW'; + } + $object->disabled_capabilities = \array_values(\array_unique($object->disabled_capabilities)); + + return $object; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + if (\in_array('SORT', $this->disabled_capabilities)) { + $this->disabled_capabilities[] = 'ESORT'; + } + return \array_merge( + parent::jsonSerialize(), + [ +// '@Object' => 'Object/ImapSettings', + 'use_expunge_all_on_delete' => $this->expunge_all_on_delete, +// 'body_text_limit' => $this->body_text_limit, + 'fast_simple_search' => $this->fast_simple_search, +// 'folder_list_limit' => $this->folder_list_limit, + 'force_select' => $this->force_select, + 'message_all_headers' => $this->message_all_headers, + 'message_list_limit' => $this->message_list_limit, + 'search_filter' => $this->search_filter, + 'spam_headers' => $this->spam_headers, + 'virus_headers' => $this->virus_headers, +// 'thread_limit' => $this->thread_limit + 'disabled_capabilities' => \array_values(\array_unique($this->disabled_capabilities)) + ] + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/ResponseParser.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/ResponseParser.php new file mode 100644 index 0000000000..34a257717c --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/ResponseParser.php @@ -0,0 +1,429 @@ +iResponseBufParsedPos = 0; + $this->bNeedNext = true; + $oResponse = new Response; + $this->partialParseResponseBranch($oResponse, false, '', '', true); + if (ResponseType::UNKNOWN === $oResponse->ResponseType) { + throw new ResponseNotFoundException; + } + return $oResponse; + } + + /** + * A bug in the parser converts folder names that start with '[' into arrays. + * https://github.com/the-djmaze/snappymail/issues/1 + * https://github.com/the-djmaze/snappymail/issues/70 + * The fix RainLoop implemented isn't correct either. + * This one should as RFC 3501 only mentions: + * + * Status responses are OK, NO, BAD, PREAUTH and BYE. + * + * Status responses MAY include an OPTIONAL "response code". A response + * code consists of data inside square brackets in the form of an atom, + * possibly followed by a space and arguments. + * + * Like: + * * OK [HIGHESTMODSEQ 11102] + * * OK [PERMANENTFLAGS (\Answered $FORWARDED $SENT $SIGNED $TODO \*)] + * TAG1 OK [READ-WRITE] + */ + private static function skipSquareBracketParse(Response $oImapResponse) : bool + { + return !$oImapResponse->IsStatusResponse || 2 < \count($oImapResponse->ResponseList); + } + + private $sResponseBuffer = ''; + + /** + * @return array|string + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + private function partialParseResponseBranch(Response $oImapResponse, + bool $bTreatAsAtom, + ?string $sParentToken, + string $sOpenBracket, + bool $bRoot = false) + { + $iPos = $this->iResponseBufParsedPos; + + $sPreviousAtomUpperCase = null; + $iBufferEndIndex = 0; + + $bIsGotoDefault = false; + + $sAtomBuilder = $bTreatAsAtom ? '' : null; + $aResponseList = array(); + if ($bRoot) { + $aResponseList =& $oImapResponse->ResponseList; + } + + while (true) { + if ($this->bNeedNext) { + $iPos = 0; + $this->sResponseBuffer = $this->getNextBuffer(); + $this->iResponseBufParsedPos = $iPos; + $this->bNeedNext = false; + } + + $sChar = null; + if ($bIsGotoDefault) { + $bIsGotoDefault = false; + } else { + $iBufferEndIndex = \strlen($this->sResponseBuffer) - 3; + + if ($iPos > $iBufferEndIndex) { + break; + } + + $sChar = $this->sResponseBuffer[$iPos]; + } + + switch ($sChar) + { + case ']': + if ($bRoot && static::skipSquareBracketParse($oImapResponse)) { + $bIsGotoDefault = true; + break 2; + } + case ')': + ++$iPos; + $sPreviousAtomUpperCase = null; + break 2; + + case ' ': + if ($bTreatAsAtom) + { + $sAtomBuilder .= ' '; + } + ++$iPos; + break; + + case '[': + if ($bRoot && static::skipSquareBracketParse($oImapResponse)) { + $bIsGotoDefault = true; + break; + } + case '(': + $this->iResponseBufParsedPos = ++$iPos; + $mResult = $this->partialParseResponseBranch( + $oImapResponse, $bTreatAsAtom, $sPreviousAtomUpperCase, $sChar + ); + $sPreviousAtomUpperCase = null; + $iPos = $this->iResponseBufParsedPos; + if ($bTreatAsAtom) { + $sAtomBuilder .= $sChar . $mResult . ('[' === $sChar ? ']' : ')'); + } else { + $aResponseList[] = $mResult; + if ($bRoot && $oImapResponse->IsStatusResponse) { + $oImapResponse->OptionalResponse = $mResult; + $bIsGotoDefault = true; + } + } + unset($mResult); + continue 2; + + case '~': // literal8 + if ('{' !== $this->sResponseBuffer[++$iPos]) { + break; + } + case '{': + $iLength = \strspn($this->sResponseBuffer, '0123456789', $iPos + 1); + if ($iLength && "}\r\n" === \substr($this->sResponseBuffer, $iPos + 1 + $iLength, 3)) { + $iLiteralLen = (int) \substr($this->sResponseBuffer, $iPos + 1, $iLength); + $iPos += 4 + $iLength; + + if ($this->partialResponseLiteralCallbacks($sParentToken, $sPreviousAtomUpperCase, $iLiteralLen)) { + if (!$bTreatAsAtom) { + $aResponseList[] = ''; + } + } else { + $sLiteral = $this->partialResponseLiteral($iLiteralLen); + if (null !== $sLiteral) { + if (!$bTreatAsAtom) { + $aResponseList[] = $sLiteral; +// $this->writeLog('{'.$iLiteralLen.'} '.$sLiteral, \LOG_INFO); + } else { + \SnappyMail\Log::notice('IMAP', 'Literal treated as atom and skipped'); + } + unset($sLiteral); + } else { + $this->writeLog('Can\'t read imap stream', \LOG_WARNING); + } + } + + $sPreviousAtomUpperCase = null; + $this->bNeedNext = true; + + continue 2; + } else { + $iPos = $iBufferEndIndex; + $sPreviousAtomUpperCase = null; + } + break; + + /** + * A quoted string is a sequence of zero or more 7-bit characters, + * excluding CR and LF, with double quote (<">) characters at each end. + */ + case '"': + $iOffset = $iPos + 1; + while (true) { + if ($iOffset > $iBufferEndIndex) { + // need more data + $iPos = $iBufferEndIndex; + break; + } + $iLength = \strcspn($this->sResponseBuffer, "\r\n\\\"", $iOffset); + $sSpecial = $this->sResponseBuffer[$iOffset + $iLength]; + switch ($sSpecial) + { + case '\\': + // Is escaped character \ or "? + if (!\in_array($this->sResponseBuffer[$iOffset + $iLength + 1], ['\\','"'])) { + // No, not allowed in quoted string + break 2; + } + $iOffset += $iLength + 2; + break; + + case '"': + if ($bTreatAsAtom) { + $sAtomBuilder .= \stripslashes(\substr($this->sResponseBuffer, $iPos, $iOffset + $iLength - $iPos + 1)); + } else { + $aResponseList[] = \stripslashes(\substr($this->sResponseBuffer, $iPos + 1, $iOffset + $iLength - $iPos - 1)); + } + $iPos = $iOffset + $iLength + 1; + break 2; + + default: + case "\r": + case "\n": + \SnappyMail\Log::notice('IMAP', 'Invalid char in quoted string: "' . \substr($this->sResponseBuffer, $iPos, $iOffset + $iLength - $iPos) . '"'); + // Not allowed in quoted string + break 2; + } + } + $sPreviousAtomUpperCase = null; + break; + + default: + $iCharBlockStartPos = $iPos; + + if ($bRoot && $oImapResponse->IsStatusResponse) { + $iPos = $iBufferEndIndex; + if ($iPos > $iCharBlockStartPos) { + $iCharBlockStartPos += \strspn($this->sResponseBuffer, ' ', $iCharBlockStartPos, $iPos - $iCharBlockStartPos); + } + } + + while ($iPos <= $iBufferEndIndex) { + $sCharDef = $this->sResponseBuffer[$iPos]; + switch (true) + { + case $bRoot && ('[' === $sCharDef || ']' === $sCharDef) && static::skipSquareBracketParse($oImapResponse): + ++$iPos; + break; + case '[' === $sCharDef: + if (null === $sAtomBuilder) { + $sAtomBuilder = ''; + } + + $sAtomBuilder .= \substr($this->sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos + 1); + + $this->iResponseBufParsedPos = ++$iPos; + + $sListBlock = $this->partialParseResponseBranch( + $oImapResponse, true, $sPreviousAtomUpperCase, '[' + ); + + if (null !== $sListBlock) { + $sAtomBuilder .= $sListBlock.']'; + } + + $iPos = $this->iResponseBufParsedPos; + $iCharBlockStartPos = $iPos; + break; + case ' ' === $sCharDef: + case ')' === $sCharDef && '(' === $sOpenBracket: + case ']' === $sCharDef && '[' === $sOpenBracket: + break 2; + default: + ++$iPos; + break; + } + } + + if ($iPos > $iCharBlockStartPos || null !== $sAtomBuilder) { + $sLastCharBlock = \substr($this->sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos); + if (null === $sAtomBuilder) { + $aResponseList[] = 'NIL' === $sLastCharBlock ? null : $sLastCharBlock; + $sPreviousAtomUpperCase = \strtoupper($sLastCharBlock); + } else { + $sAtomBuilder .= $sLastCharBlock; + + if (!$bTreatAsAtom) { + $aResponseList[] = $sAtomBuilder; + $sPreviousAtomUpperCase = \strtoupper($sAtomBuilder); + $sAtomBuilder = null; + } + } + + if ($bRoot) { + if (!isset($oImapResponse->Tag) && 1 === \count($aResponseList)) { + $oImapResponse->setTag($aResponseList[0]); + if ($this->getCurrentTag() === $oImapResponse->Tag) { + $oImapResponse->ResponseType = ResponseType::TAGGED; + } + } + else if (!isset($oImapResponse->StatusOrIndex) && 2 === \count($aResponseList)) + { + $oImapResponse->setStatus($aResponseList[1]); + } + else if (ResponseType::CONTINUATION === $oImapResponse->ResponseType + || $oImapResponse->IsStatusResponse) + { + $oImapResponse->HumanReadable = $sLastCharBlock; + } + } + } + } + } + + $this->iResponseBufParsedPos = $iPos; + + return $bTreatAsAtom ? $sAtomBuilder : $aResponseList; + } + + private function partialResponseLiteral($iLiteralLen) : ?string + { + $sLiteral = ''; + $iRead = $iLiteralLen; + while (0 < $iRead) { + $sAddRead = \fread($this->ConnectionResource(), $iRead); + $iBLen = \strlen($sAddRead); + if (!$iBLen) { + $this->writeLog('Literal stream read warning "read '.\strlen($sLiteral).' of '. + $iLiteralLen.'" bytes', \LOG_WARNING); + return null; + } + $sLiteral .= $sAddRead; + $iRead -= $iBLen; + if ($iRead > 16384) { +// \set_time_limit(10); + \MailSo\Base\Utils::ResetTimeLimit(); + } + } + + $iLiteralSize = \strlen($sLiteral); + if ($iLiteralLen !== $iLiteralSize) { + $this->writeLog('Literal stream read warning "read '.$iLiteralSize.' of '.$iLiteralLen.'" bytes', \LOG_WARNING); + } + return $sLiteral; + } + + private function partialResponseLiteralCallbacks(?string $sParent, ?string $sLiteralAtomUpperCase, int $iLiteralLen) : bool + { + if (!$this->aFetchCallbacks || !$sLiteralAtomUpperCase || 'FETCH' !== $sParent) { + return false; + } + + $sLiteralAtomUpperCasePeek = ''; + if (\str_starts_with($sLiteralAtomUpperCase, 'BODY')) { + $sLiteralAtomUpperCasePeek = \str_replace('BODY', 'BODY.PEEK', $sLiteralAtomUpperCase); + } else if (\str_starts_with($sLiteralAtomUpperCase, 'BINARY')) { + $sLiteralAtomUpperCasePeek = \str_replace('BINARY', 'BINARY.PEEK', $sLiteralAtomUpperCase); + } + + $sFetchKey = $sLiteralAtomUpperCase; + if ($sLiteralAtomUpperCasePeek && isset($this->aFetchCallbacks[$sLiteralAtomUpperCasePeek])) { + $sFetchKey = $sLiteralAtomUpperCasePeek; + } + if (empty($this->aFetchCallbacks[$sFetchKey]) || !\is_callable($this->aFetchCallbacks[$sFetchKey])) { + return false; + } + + $rImapLiteralStream = \MailSo\Base\StreamWrappers\Literal::CreateStream($this->ConnectionResource(), $iLiteralLen); + + $this->writeLog('Start Callback for '.$sParent.' / '.$sLiteralAtomUpperCase. + ' - try to read '.$iLiteralLen.' bytes.', \LOG_INFO); + + $this->bRunningCallback = true; + + try + { + $this->aFetchCallbacks[$sFetchKey]($sParent, $sLiteralAtomUpperCase, $rImapLiteralStream, $iLiteralLen); + } + catch (\Throwable $oException) + { + $this->writeLogException($oException, \LOG_NOTICE, false); + } + + if (\is_resource($rImapLiteralStream)) { + $iNotReadLiteralLen = 0; + + $bFeof = \feof($rImapLiteralStream); + $this->writeLog('End Callback for '.$sParent.' / '.$sLiteralAtomUpperCase. + ' - feof = '.($bFeof ? 'good' : 'BAD'), $bFeof ? + \LOG_INFO : \LOG_WARNING); + + if (!$bFeof) { + while (!\feof($rImapLiteralStream)) { + $sBuf = \fread($rImapLiteralStream, 1024 * 1024); + if (!\strlen($sBuf)) { + break; + } + + \MailSo\Base\Utils::ResetTimeLimit(); + $iNotReadLiteralLen += \strlen($sBuf); + } + + if (!\feof($rImapLiteralStream)) { + \stream_get_contents($rImapLiteralStream); + } + } + + \fclose($rImapLiteralStream); + + if (0 < $iNotReadLiteralLen) { + $this->writeLog('Not read literal size is '.$iNotReadLiteralLen.' bytes.', \LOG_WARNING); + } + } else { + $this->writeLog('Literal stream is not resource after callback.', \LOG_WARNING); + } + + $this->bRunningCallback = false; + + return true; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/Status.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/Status.php new file mode 100644 index 0000000000..1fcba39972 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/Status.php @@ -0,0 +1,193 @@ +hasStatus) { + // UNSEEN undefined when only SELECT/EXAMINE is used + \error_log("{$this->FullName} STATUS missing " . \print_r(\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS),true)); + return; + } + if (!isset($this->MESSAGES, $this->UIDNEXT)) { + \error_log("{$this->FullName} MESSAGES or UIDNEXT missing " . \print_r(\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS),true)); + return; + } + $this->etag = \md5('FolderHash/'. \implode('-', [ + $this->FullName, + $this->MESSAGES, + $this->UIDNEXT, + $this->UIDVALIDITY, + $this->UNSEEN, + $this->HIGHESTMODSEQ, + $oImapClient->Hash() + ])); + } + + private function setStatusItem(string $name, $value) : bool + { + if ('EXISTS' === $name) { + $name = 'MESSAGES'; + } else if ('X-GUID' === $name) { + $name = 'MAILBOXID'; + } + if (\property_exists(__TRAIT__, $name)) { + if ('MAILBOXID' === $name) { + $value = \is_array($value) ? \reset($value) : $value; + if (\is_string($value)) { + $this->MAILBOXID = \base64_encode($value); + } else { + // Cyrus bug https://github.com/the-djmaze/snappymail/issues/1640 + \error_log("{$this->FullName} invalid MAILBOXID value. Disable the OBJECTID capability."); + } + } else { + $this->$name = (int) $value; + } + return true; + } + return false; + } + + /** + * SELECT https://datatracker.ietf.org/doc/html/rfc3501#section-6.3.1 + * EXAMINE https://datatracker.ietf.org/doc/html/rfc3501#section-6.3.2 + * STATUS https://datatracker.ietf.org/doc/html/rfc3501#section-6.3.10 + * + * selectOrExamineFolder + * ResponseList[2] => EXISTS | RECENT + * OptionalResponse[0] => UNSEEN + * FolderStatus + * OptionalResponse[0] => HIGHESTMODSEQ + * ResponseList[1] => STATUS + * getFoldersResult LIST-EXTENDED + * ResponseList[1] => STATUS + */ + public function setStatusFromResponse(\MailSo\Imap\Response $oResponse) : bool + { + $bResult = false; + + // OK untagged responses + if (\is_array($oResponse->OptionalResponse) && \count($oResponse->OptionalResponse) > 1) { + $bResult = $this->setStatusItem($oResponse->OptionalResponse[0], $oResponse->OptionalResponse[1]); + } + + // untagged responses + else if (\count($oResponse->ResponseList) > 2) { + // LIST or STATUS command + if ('STATUS' === $oResponse->ResponseList[1] + && isset($oResponse->ResponseList[3]) + && \is_array($oResponse->ResponseList[3]) + ) { + $c = \count($oResponse->ResponseList[3]) - 1; + for ($i = 0; $i < $c; $i += 2) { + if ($c > $i) { + $bResult |= $this->setStatusItem( + $oResponse->ResponseList[3][$i], + $oResponse->ResponseList[3][$i+1] + ); + } else { + // https://github.com/the-djmaze/snappymail/issues/1640 + \error_log("{$this->FullName} STATUS missing value for {$oResponse->ResponseList[3][$i]}"); + } + } + $this->hasStatus = $bResult; + } + // SELECT or EXAMINE command + else if (\is_numeric($oResponse->ResponseList[1]) && \is_string($oResponse->ResponseList[2])) { + // UNSEEN deprecated in IMAP4rev2 + if ('UNSEEN' !== $oResponse->ResponseList[2]) { + $bResult |= $this->setStatusItem($oResponse->ResponseList[2], $oResponse->ResponseList[1]); + } + } + } + + return $bResult; + } + +} diff --git a/rainloop/v/0.0.0/app/libraries/MailSo/LICENSE b/snappymail/v/0.0.0/app/libraries/MailSo/LICENSE similarity index 100% rename from rainloop/v/0.0.0/app/libraries/MailSo/LICENSE rename to snappymail/v/0.0.0/app/libraries/MailSo/LICENSE diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Driver.php b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Driver.php new file mode 100644 index 0000000000..33fb026e26 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Driver.php @@ -0,0 +1,112 @@ + '[EMERGENCY]', + \LOG_ALERT => '[ALERT]', + \LOG_CRIT => '[CRITICAL]', + \LOG_ERR => '[ERROR]', + \LOG_WARNING => '[WARNING]', + \LOG_NOTICE => '[NOTICE]', + \LOG_INFO => '[INFO]', + \LOG_DEBUG => '[DEBUG]' + ]; + + protected bool $bGuidPrefix = true; + + protected \DateTimeZone $oTimeZone; + + protected bool $bTimePrefix = true; + + protected bool $bTypedPrefix = true; + + function __construct() + { + $this->oTimeZone = new \DateTimeZone('UTC'); + } + + public function SetTimeZone(/*\DateTimeZone | string*/ $mTimeZone) : self + { + $this->oTimeZone = $mTimeZone instanceof \DateTimeZone + ? $mTimeZone + : new \DateTimeZone($mTimeZone); + return $this; + } + + public function DisableGuidPrefix() : self + { + $this->bGuidPrefix = false; + return $this; + } + + public function DisableTimePrefix() : self + { + $this->bTimePrefix = false; + return $this; + } + + public function DisableTypedPrefix() : self + { + $this->bTypedPrefix = false; + return $this; + } + + abstract protected function writeImplementation($mDesc) : bool; + + protected function clearImplementation() : bool + { + return true; + } + + protected function getTimeWithMicroSec() : string + { + return \substr((new \DateTime('now', $this->oTimeZone))->format('Y-m-d H:i:s.u'), 0, -3); + } + + protected function getTypedPrefix(int $iType, string $sName = '') : string + { + $sName = \strlen($sName) ? $sName : $this->sName; + return isset(self::PREFIXES[$iType]) ? $sName . self::PREFIXES[$iType].': ' : ''; + } + + final public function Write(string $sDesc, int $iType = \LOG_INFO, string $sName = '', bool $bDiplayCrLf = false) + { + $sDesc = \ltrim( + ($this->bTimePrefix ? '['.$this->getTimeWithMicroSec().']' : ''). + ($this->bGuidPrefix ? '['.Logger::Guid().']' : ''). + ($this->bTypedPrefix ? ' '.$this->getTypedPrefix($iType, $sName) : '') + ) . $sDesc; + if ($bDiplayCrLf) { + $sDesc = \rtrim(\strtr($sDesc, array("\r" => '\r', "\n" => '\n'."\n"))); + } + return $this->writeImplementation($sDesc); + } + + final public function Clear() : bool + { + return $this->clearImplementation(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/File.php b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/File.php new file mode 100644 index 0000000000..5f4ca9018a --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/File.php @@ -0,0 +1,46 @@ +sLoggerFileName = $sLoggerFileName; + } + + protected function writeImplementation($mDesc) : bool + { + if (\is_array($mDesc)) { + $mDesc = \implode("\n\t", $mDesc); + } + return \error_log($mDesc . "\n", 3, $this->sLoggerFileName); + } + + protected function clearImplementation() : bool + { + return \unlink($this->sLoggerFileName); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/StderrStream.php b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/StderrStream.php new file mode 100644 index 0000000000..89b56d8867 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/StderrStream.php @@ -0,0 +1,33 @@ +iLogLevel = \defined('LOG_INFO') ? LOG_INFO : 6; + } + } + + protected function writeImplementation($mDesc) : bool + { + $result = false; + if ($this->iLogLevel && \openlog('snappymail', LOG_ODELAY, LOG_USER)) { + $result = \syslog($this->iLogLevel, \is_array($mDesc) ? \implode(PHP_EOL, $mDesc) : $mDesc); + \closelog(); + } + return $result; + } + + protected function clearImplementation() : bool + { + return true; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Enumerations/Type.php b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Enumerations/Type.php new file mode 100644 index 0000000000..10bd5f0590 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Enumerations/Type.php @@ -0,0 +1,32 @@ +oLogger; + } + + public function SetLogger(?Logger $oLogger): void + { + $this->oLogger = $oLogger; + } + + public function logWrite(string $sDesc, int $iType = \LOG_INFO, string $sName = '', bool $bDiplayCrLf = false): bool + { + return $this->oLogger && $this->oLogger->Write($sDesc, $iType, $sName, $bDiplayCrLf); + } + + public function logException(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = ''): void + { + $this->oLogger && $this->oLogger->WriteException($oException, $iType, $sName); + } + + public function logMask( + #[\SensitiveParameter] + string $sWord + ): void + { + $this->oLogger && $this->oLogger->AddSecret($sWord); + } + +/* + public function logWriteDump($mValue, int $iType = \LOG_INFO, string $sName = '') : bool + public function logWriteExceptionShort(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void +*/ +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Logger.php b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Logger.php new file mode 100644 index 0000000000..4f48b340aa --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Logger.php @@ -0,0 +1,269 @@ +setSize(1); + $this[0] = $oDriver; + } + } + + public function IsEnabled() : bool + { + return 0 < $this->count(); + } + + public function AddSecret( + #[\SensitiveParameter] + string $sWord + ) : void + { +// $this->bShowSecrets && $this->Write("AddSecret '{$sWord}'", \LOG_INFO); + $sWord = \trim($sWord); + if (\strlen($sWord)) { + $this->aSecretWords[] = $sWord; + $this->aSecretWords = \array_unique($this->aSecretWords); + \usort($this->aSecretWords, fn($a,$b) => \strlen($b) - \strlen($a)); + } + } + + public function SetShowSecrets(bool $bShow) : self + { + $this->bShowSecrets = $bShow; + return $this; + } + + public function ShowSecrets() : bool + { + return $this->bShowSecrets; + } + + public function SetLevel(int $iLevel) : self + { + $this->iLevel = $iLevel; + return $this; + } + + const PHP_TYPES = array( + \E_ERROR => \LOG_ERR, + \E_WARNING => \LOG_WARNING, + \E_PARSE => \LOG_CRIT, + \E_NOTICE => \LOG_NOTICE, + \E_CORE_ERROR => \LOG_ERR, + \E_CORE_WARNING => \LOG_WARNING, + \E_COMPILE_ERROR => \LOG_ERR, + \E_COMPILE_WARNING => \LOG_WARNING, + \E_USER_ERROR => \LOG_ERR, + \E_USER_WARNING => \LOG_WARNING, + \E_USER_NOTICE => \LOG_NOTICE, +// \E_STRICT => \LOG_CRIT, + \E_RECOVERABLE_ERROR => \LOG_ERR, + \E_DEPRECATED => \LOG_INFO, + \E_USER_DEPRECATED => \LOG_INFO + ); + + const PHP_TYPE_POSTFIX = array( + \E_ERROR => '', + \E_WARNING => '', + \E_PARSE => '-PARSE', + \E_NOTICE => '', + \E_CORE_ERROR => '-CORE', + \E_CORE_WARNING => '-CORE', + \E_COMPILE_ERROR => '-COMPILE', + \E_COMPILE_WARNING => '-COMPILE', + \E_USER_ERROR => '-USER', + \E_USER_WARNING => '-USER', + \E_USER_NOTICE => '-USER', +// \E_STRICT => '-STRICT', + \E_RECOVERABLE_ERROR => '-RECOVERABLE', + \E_DEPRECATED => '-DEPRECATED', + \E_USER_DEPRECATED => '-USER_DEPRECATED' + ); + + public function __phpErrorHandler(int $iErrNo, string $sErrStr, string $sErrFile, int $iErrLine) : bool + { + if (\error_reporting() & $iErrNo) { + $this->Write( + "{$sErrStr} {$sErrFile} [line:{$iErrLine}, code:{$iErrNo}]", + static::PHP_TYPES[$iErrNo], + 'PHP' . static::PHP_TYPE_POSTFIX[$iErrNo] + ); + } + /* forward to standard PHP error handler */ + return false; + } + + /** + * Called by PHP when an Exception is uncaught + */ + public function __phpExceptionHandler(\Throwable $oException): void + { + $this->Write('Uncaught exception: ' . $oException, \LOG_CRIT); + \error_log('Uncaught exception: ' . $oException); + } + + public function __loggerShutDown() : void + { + if ($this->bUsed) { + $error = \error_get_last(); + $error && $this->Write('Last error: '.\json_encode($error)); + $this->Write('Memory peak usage: '.\MailSo\Base\Utils::FormatFileSize(\memory_get_peak_usage(true), 2)); + $this->Write('Time delta: '.(\microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'])); + } + } + + public function signalHandler($signo, /*?array*/$siginfo = null) + { + if (\SIGTERM == $signo) { + exit; + } + foreach (static::$SIGNALS as $SIGNAL) { + if (\defined($SIGNAL) && \constant($SIGNAL) == $signo) { + $this->Write("Caught {$SIGNAL} ".($siginfo ? \json_encode($siginfo) : ''), \LOG_CRIT, 'PHP'); + break; + } + } + } + + public function Write(string $sDesc, int $iType = \LOG_INFO, string $sName = '', bool $bDiplayCrLf = false) : bool + { + if ($this->iLevel < $iType) { + return true; + } + + $this->bUsed = true; + + if (!$this->bShowSecrets && $this->aSecretWords) { + $sDesc = \str_replace($this->aSecretWords, '*******', $sDesc); + } + + $iResult = 1; + + foreach ($this as /* @var $oLogger \MailSo\Log\Driver */ $oLogger) + { + $iResult = $oLogger->Write($sDesc, $iType, $sName, $bDiplayCrLf); + } + + return (bool) $iResult; + } + + /** + * @param mixed $mValue + */ + public function WriteDump($mValue, int $iType = \LOG_INFO, string $sName = '') : bool + { + return $this->Write(\print_r($mValue, true), $iType, $sName); + } + + private $aExceptions = []; + + public function WriteException(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void + { + if (!\in_array($oException, $this->aExceptions)) { + $this->Write((string) $oException, $iType, $sName); + $this->aExceptions[] = $oException; + } + } + + public function WriteExceptionShort(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void + { + if (!\in_array($oException, $this->aExceptions)) { + $this->Write($oException->getMessage(), $iType, $sName); + $this->aExceptions[] = $oException; + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Attachment.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Attachment.php new file mode 100644 index 0000000000..aa11a1c81d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Attachment.php @@ -0,0 +1,68 @@ +sFolder = $sFolder; + $this->iUid = $iUid; + $this->oBodyStructure = $oBodyStructure; + } + + public function Clear() : self + { + $this->sFolder = ''; + $this->iUid = 0; + $this->oBodyStructure = null; + + return $this; + } + + public function Folder() : string + { + return $this->sFolder; + } + + public function Uid() : int + { + return $this->iUid; + } + + public function __call(string $name, array $arguments) //: mixed + { + return $this->oBodyStructure->{$name}(...$arguments); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return \array_merge([ + '@Object' => 'Object/Attachment', + 'folder' => $this->sFolder, + 'uid' => $this->iUid + ], $this->oBodyStructure->jsonSerialize()); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/AttachmentCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/AttachmentCollection.php new file mode 100644 index 0000000000..0b1992af7c --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/AttachmentCollection.php @@ -0,0 +1,25 @@ +oImapClient = new \MailSo\Imap\ImapClient; + } + + public function ImapClient() : \MailSo\Imap\ImapClient + { + return $this->oImapClient; + } + + private function getEnvelopeOrHeadersRequestString() : string + { + if ($this->oImapClient->Settings->message_all_headers) { + return FetchType::BODY_HEADER_PEEK; + } + + $aHeaders = array( +// MimeHeader::RETURN_PATH, +// MimeHeader::RECEIVED, +// MimeHeader::MIME_VERSION, + MimeHeader::MESSAGE_ID, + MimeHeader::CONTENT_TYPE, + MimeHeader::FROM_, + MimeHeader::TO_, + MimeHeader::CC, + MimeHeader::BCC, + MimeHeader::SENDER, + MimeHeader::REPLY_TO, + MimeHeader::DELIVERED_TO, + MimeHeader::IN_REPLY_TO, + MimeHeader::REFERENCES, + MimeHeader::DATE, + MimeHeader::SUBJECT, + MimeHeader::X_MSMAIL_PRIORITY, + MimeHeader::IMPORTANCE, + MimeHeader::X_PRIORITY, + MimeHeader::X_DRAFT_INFO, +// MimeHeader::RETURN_RECEIPT_TO, + MimeHeader::DISPOSITION_NOTIFICATION_TO, + MimeHeader::X_CONFIRM_READING_TO, + MimeHeader::AUTHENTICATION_RESULTS, + MimeHeader::X_DKIM_AUTHENTICATION_RESULTS, + MimeHeader::LIST_UNSUBSCRIBE, + // https://autocrypt.org/level1.html#the-autocrypt-header + MimeHeader::AUTOCRYPT + ); + + // SPAM + $spam_headers = \explode(',', $this->oImapClient->Settings->spam_headers); + if (\in_array('rspamd', $spam_headers)) { + $aHeaders[] = MimeHeader::X_SPAMD_RESULT; + } + if (\in_array('spamassassin', $spam_headers)) { + $aHeaders[] = MimeHeader::X_SPAM_STATUS; + $aHeaders[] = MimeHeader::X_SPAM_FLAG; + $aHeaders[] = MimeHeader::X_SPAM_INFO; + } + if (\in_array('bogofilter', $spam_headers)) { + $aHeaders[] = MimeHeader::X_BOGOSITY; + } + + // Virus + $virus_headers = \explode(',', $this->oImapClient->Settings->virus_headers); + if (\in_array('rspamd', $virus_headers)) { + $aHeaders[] = MimeHeader::X_VIRUS; + } + if (\in_array('clamav', $virus_headers)) { + $aHeaders[] = MimeHeader::X_VIRUS_SCANNED; + $aHeaders[] = MimeHeader::X_VIRUS_STATUS; + } + + \RainLoop\Api::Actions()->Plugins()->RunHook('imap.message-headers', array(&$aHeaders)); + + return FetchType::BuildBodyCustomHeaderRequest($aHeaders, true); + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + * @throws \MailSo\Mail\Exceptions\* + */ + public function MessageSetFlag(string $sFolderName, SequenceSet $oRange, string $sMessageFlag, bool $bSetAction = true, bool $bSkipUnsupportedFlag = false) : void + { + if (\count($oRange)) { + if ($this->oImapClient->FolderSelect($sFolderName)->IsFlagSupported($sMessageFlag)) { + $sStoreAction = $bSetAction ? StoreAction::ADD_FLAGS_SILENT : StoreAction::REMOVE_FLAGS_SILENT; + $this->oImapClient->MessageStoreFlag($oRange, array($sMessageFlag), $sStoreAction); + } else if (!$bSkipUnsupportedFlag) { + throw new \MailSo\RuntimeException('Message flag "'.$sMessageFlag.'" is not supported.'); + } + } + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function Message(string $sFolderName, int $iIndex, bool $bIndexIsUid = true, ?\MailSo\Cache\CacheClient $oCacher = null) : ?Message + { + if (1 > $iIndex) { + throw new \ValueError; + } + + $this->oImapClient->FolderExamine($sFolderName); + + $oBodyStructure = null; + + $aFetchItems = array( + FetchType::UID, +// FetchType::FAST, + FetchType::RFC822_SIZE, + FetchType::INTERNALDATE, + FetchType::FLAGS, + $this->getEnvelopeOrHeadersRequestString() + ); + + $aFetchResponse = $this->oImapClient->Fetch(array(FetchType::BODYSTRUCTURE), $iIndex, $bIndexIsUid); + if (\count($aFetchResponse) && isset($aFetchResponse[0])) { + $oBodyStructure = $aFetchResponse[0]->GetFetchBodyStructure(); + if ($oBodyStructure) { + $iBodyTextLimit = $this->oImapClient->Settings->body_text_limit; + foreach ($oBodyStructure->GetHtmlAndPlainParts() as $oPart) { + $sLine = FetchType::BODY_PEEK.'['.$oPart->PartID().']'; + if (0 < $iBodyTextLimit && $iBodyTextLimit < $oPart->EstimatedSize()) { + $sLine .= "<0.{$iBodyTextLimit}>"; + } + $aFetchItems[] = $sLine; + } + } + } + + if (!$oBodyStructure) { + $aFetchItems[] = FetchType::BODYSTRUCTURE; + } + + $aFetchResponse = $this->oImapClient->Fetch($aFetchItems, $iIndex, $bIndexIsUid); + + return \count($aFetchResponse) + ? Message::fromFetchResponse($sFolderName, $aFetchResponse[0], $oBodyStructure) + : null; + } + + /** + * Streams mime part to $mCallback + * + * @param mixed $mCallback + * + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageMimeStream($mCallback, string $sFolderName, int $iIndex, string $sMimeIndex) : bool + { + if (!\is_callable($mCallback)) { + throw new \ValueError; + } + + $this->oImapClient->FolderExamine($sFolderName); + + $sFileName = ''; + $sContentType = ''; + $sMailEncoding = ''; + $sPeek = FetchType::BODY_PEEK; + + $sMimeIndex = \trim($sMimeIndex); + $aFetchResponse = $this->oImapClient->Fetch(array( + \strlen($sMimeIndex) + ? FetchType::BODY_PEEK.'['.$sMimeIndex.'.MIME]' + : FetchType::BODY_HEADER_PEEK), + $iIndex, true); + + if (\count($aFetchResponse)) { + $sMime = $aFetchResponse[0]->GetFetchValue( + \strlen($sMimeIndex) + ? FetchType::BODY.'['.$sMimeIndex.'.MIME]' + : FetchType::BODY_HEADER + ); + + if (\strlen($sMime)) { + $oHeaders = new \MailSo\Mime\HeaderCollection($sMime); + + if (\strlen($sMimeIndex)) { + $sFileName = $oHeaders->ParameterValue(MimeHeader::CONTENT_DISPOSITION, MimeParameter::FILENAME); + if (!\strlen($sFileName)) { + $sFileName = $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, MimeParameter::NAME); + } + + $sMailEncoding = \MailSo\Base\StreamWrappers\Binary::GetInlineDecodeOrEncodeFunctionName( + $oHeaders->ValueByName(MimeHeader::CONTENT_TRANSFER_ENCODING) + ); + + // RFC 3516 + // Should mailserver decode or PHP? + if ($sMailEncoding && $this->oImapClient->hasCapability('BINARY')) { + $sMailEncoding = ''; + $sPeek = FetchType::BINARY_PEEK; + } + + $sContentType = $oHeaders->ValueByName(MimeHeader::CONTENT_TYPE); + } else { + $sFileName = ($oHeaders->ValueByName(MimeHeader::SUBJECT) ?: $iIndex) . '.eml'; + + $sContentType = 'message/rfc822'; + } + } + } + + $callback = function ($sParent, $sLiteralAtomUpperCase, $rImapLiteralStream) + use ($mCallback, $sMimeIndex, $sMailEncoding, $sContentType, $sFileName) + { + if (\strlen($sLiteralAtomUpperCase) && \is_resource($rImapLiteralStream) && 'FETCH' === $sParent) { + $mCallback($sMailEncoding + ? \MailSo\Base\StreamWrappers\Binary::CreateStream($rImapLiteralStream, $sMailEncoding) + : $rImapLiteralStream, + $sContentType, $sFileName, $sMimeIndex); + } + }; + + try { + $aFetchResponse = $this->oImapClient->Fetch(array( +// FetchType::BINARY_SIZE.'['.$sMimeIndex.']', + // Push in the aFetchCallbacks array and then called by \MailSo\Imap\Traits\ResponseParser::partialResponseLiteralCallbackCallable + array( + $sPeek.'['.$sMimeIndex.']', + $callback + )), $iIndex, true); + } catch (\MailSo\Imap\Exceptions\NegativeResponseException $oException) { + if (FetchType::BINARY_PEEK === $sPeek && \preg_match('/UNKNOWN-CTE|PARSE/', $oException->getMessage())) { + $this->logException($oException, \LOG_WARNING); + $aFetchResponse = $this->oImapClient->Fetch(array( + array( + FetchType::BODY_PEEK . '[' . $sMimeIndex . ']', + $callback + )), $iIndex, true); + } else { + throw $e; + } + } + + return ($aFetchResponse && 1 === \count($aFetchResponse)); + } + + public function MessageAppendFile(string $sMessageFileName, string $sFolderToSave, ?array $aAppendFlags = null) : int + { + if (!\is_file($sMessageFileName) || !\is_readable($sMessageFileName)) { + throw new \ValueError; + } + + $iMessageStreamSize = \filesize($sMessageFileName); + $rMessageStream = \fopen($sMessageFileName, 'rb'); + + $iUid = $this->oImapClient->MessageAppendStream($sFolderToSave, $rMessageStream, $iMessageStreamSize, $aAppendFlags); + + \fclose($rMessageStream); + + return $iUid; + } + + /** + * Returns list of new messages since $iPrevUidNext + * Currently only for INBOX + */ + private function getFolderNextMessageInformation(string $sFolderName, int $iPrevUidNext, int $iCurrentUidNext) : array + { + $aNewMessages = array(); + + if ($this->oImapClient->Settings->fetch_new_messages && $iPrevUidNext && $iPrevUidNext != $iCurrentUidNext && 'INBOX' === $sFolderName) { + $this->oImapClient->FolderExamine($sFolderName); + + $aFetchResponse = $this->oImapClient->Fetch(array( + FetchType::UID, + FetchType::FLAGS, + FetchType::BuildBodyCustomHeaderRequest(array( + MimeHeader::FROM_, + MimeHeader::SUBJECT, + MimeHeader::CONTENT_TYPE + )) + ), $iPrevUidNext.':*', true); + + foreach ($aFetchResponse as $oFetchResponse) { + $aFlags = \array_map('strtolower', $oFetchResponse->GetFetchValue(FetchType::FLAGS)); + + if (!\in_array(\strtolower(MessageFlag::SEEN), $aFlags)) { + $iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); + + $oHeaders = new \MailSo\Mime\HeaderCollection($oFetchResponse->GetHeaderFieldsValue()); + + $sContentTypeCharset = $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, MimeParameter::CHARSET); + + if ($sContentTypeCharset) { + $oHeaders->SetParentCharset($sContentTypeCharset); + } + + $aNewMessages[] = array( + 'folder' => $sFolderName, + 'uid' => $iUid, + 'subject' => $oHeaders->ValueByName(MimeHeader::SUBJECT, !$sContentTypeCharset), + 'from' => $oHeaders->GetAsEmailCollection(MimeHeader::FROM_) + ); + } + } + } + + return $aNewMessages; + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, ?SequenceSet $oRange = null) : array + { + if ($oRange) { +// $aInfo = $this->oImapClient->FolderExamine($sFolderName)->jsonSerialize(); + $aInfo = $this->oImapClient->FolderStatusAndSelect($sFolderName)->jsonSerialize(); + $aInfo['messagesFlags'] = array(); + if (\count($oRange)) { + $aFetchResponse = $this->oImapClient->Fetch(array( + FetchType::UID, + FetchType::FLAGS + ), (string) $oRange, $oRange->UID); + foreach ($aFetchResponse as $oFetchResponse) { + $iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); + $aLowerFlags = \array_map('mb_strtolower', \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oFetchResponse->GetFetchValue(FetchType::FLAGS))); + $aInfo['messagesFlags'][] = array( + 'uid' => $iUid, + 'flags' => $aLowerFlags + ); + } + } + } else { + $aInfo = $this->oImapClient->FolderStatus($sFolderName)->jsonSerialize(); + } + + if ($iPrevUidNext) { + $aInfo['newMessages'] = $this->getFolderNextMessageInformation( + $sFolderName, + $iPrevUidNext, + \intval($aInfo['uidNext']) + ); + } + +// $aInfo['appendLimit'] = $aInfo['appendLimit'] ?: $this->oImapClient->AppendLimit(); + return $aInfo; + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function FolderHash(string $sFolderName) : string + { + try + { +// return $this->oImapClient->FolderStatusAndSelect($sFolderName)->etag; + return $this->oImapClient->FolderStatus($sFolderName)->etag; + } + catch (\Throwable $oException) + { + \SnappyMail\Log::warning('IMAP', "FolderHash({$sFolderName}) Exception: {$oException->getMessage()}"); + } + return ''; + } + + public function MessageThread(string $sFolderName, string $sMessageID) : MessageCollection + { + $this->oImapClient->FolderExamine($sFolderName); + + $sMessageID = \MailSo\Imap\SearchCriterias::escapeSearchString($this->oImapClient, $sMessageID); + $sSearch = "OR HEADER Message-ID {$sMessageID} HEADER References {$sMessageID}"; + $aResult = []; + try + { + foreach ($this->oImapClient->MessageThread($sSearch) as $mItem) { + // Flatten to single level + \array_walk_recursive($mItem, fn($a) => $aResult[] = $a); + } + } + catch (\MailSo\RuntimeException $oException) + { + \SnappyMail\Log::warning('MailClient', 'MessageThread ' . $oException->getMessage()); + unset($oException); + } +// $this->logWrite('MessageThreadList: '.\print_r($threads, 1)); + return $aResult; + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + protected function ThreadsMap(string $sAlgorithm, MessageCollection $oMessageCollection, ?\MailSo\Cache\CacheClient $oCacher, bool $bBackground = false) : array + { + $oFolderInfo = $oMessageCollection->FolderInfo; + $sFolderName = $oFolderInfo->FullName; + + $sSearch = 'ALL'; +// $sSearch = 'UNDELETED'; +/* + $iThreadLimit = $this->oImapClient->Settings->thread_limit; + if ($iThreadLimit && $iThreadLimit < $oFolderInfo->MESSAGES) { + $sSearch = ($oFolderInfo->MESSAGES - $iThreadLimit) . ':*'; + } +*/ +/* + $sAlgorithm = ''; + if ($this->oImapClient->hasCapability('THREAD=REFS')) { + $sAlgorithm = 'REFS'; + } else if ($this->oImapClient->hasCapability('THREAD=REFERENCES')) { + $sAlgorithm = 'REFERENCES'; + } else if ($this->oImapClient->hasCapability('THREAD=ORDEREDSUBJECT')) { + $sAlgorithm = 'ORDEREDSUBJECT'; + } +*/ + $sSerializedHashKey = null; + if ($oCacher && $oCacher->IsInited()) { + $sSerializedHashKey = "ThreadsMap/{$sAlgorithm}/{$sSearch}/{$oFolderInfo->etag}"; +// $sSerializedHashKey = "ThreadsMap/{$sAlgorithm}/{$sSearch}/{$iThreadLimit}/{$oFolderInfo->etag}"; + + $sSerializedUids = $oCacher->Get($sSerializedHashKey); + if (!empty($sSerializedUids)) { + $aSerializedUids = \json_decode($sSerializedUids, true); + if (isset($aSerializedUids['ThreadsUids']) && \is_array($aSerializedUids['ThreadsUids'])) { + $oMessageCollection->totalThreads = \count($aSerializedUids['ThreadsUids']); + $this->logWrite('Get Threads from cache ("'.$sFolderName.'" / '.$sSearch.') [count:'.\count($aSerializedUids['ThreadsUids']).']'); + return $aSerializedUids['ThreadsUids']; + } + } +/* + // Idea to fetch all UID's in background + else if (!$bBackground) { + $this->logWrite('Set ThreadsMap() as background task ("'.$sFolderName.'" / '.$sSearch.')'); + \SnappyMail\Shutdown::add(function($oMailClient, $oFolderInfo, $oCacher) { + $oFolderInfo->MESSAGES = 0; + $oMailClient->ThreadsMap($sAlgorithm, $oMessageCollection, $oCacher, true); + }, [$this, $oFolderInfo, $oCacher]); + return []; + } +*/ + } + + $this->oImapClient->FolderExamine($sFolderName); + + $aResult = array(); + try + { + foreach ($this->oImapClient->MessageThread($sSearch, $sAlgorithm) as $mItem) { + // Flatten to single level + $aMap = []; + \array_walk_recursive($mItem, function($a) use (&$aMap) { $aMap[] = $a; }); + $aResult[] = $aMap; + } + } + catch (\MailSo\RuntimeException $oException) + { + \SnappyMail\Log::warning('MailClient', 'ThreadsMap ' . $oException->getMessage()); + unset($oException); + } + + if ($sSerializedHashKey) { + $oCacher->Set($sSerializedHashKey, \json_encode(array('ThreadsUids' => $aResult))); + $this->logWrite('Save Threads to cache ("'.$sFolderName.'" / '.$sSearch.') [count:'.\count($aResult).']'); + } + + $oMessageCollection->totalThreads = \count($aResult); + return $aResult; + } + + // All threads UID's except the most recent UID of each thread + protected function ThreadsOldUids(array $aAllThreads, MessageCollection $oMessageCollection, ?\MailSo\Cache\CacheClient $oCacher, bool $bBackground = false) : array + { + $oFolderInfo = $oMessageCollection->FolderInfo; + + $bThreadSort = $this->bThreadSort && $this->oImapClient->hasCapability('SORT'); + + $sSerializedHashKey = null; + if ($oCacher && $oCacher->IsInited()) { + $sSerializedHashKey = "ThreadsOldUids/{$oFolderInfo->etag}/" . ($bThreadSort ? 'S' : 'N'); + $sSerializedUids = $oCacher->Get($sSerializedHashKey); + if (!empty($sSerializedUids)) { + $aSerializedUids = \json_decode($sSerializedUids, true); + if (isset($aSerializedUids['ThreadsUids']) && \is_array($aSerializedUids['ThreadsUids'])) { + $this->logWrite('Get old Threads UIDs from cache ("'.$oFolderInfo->FullName.'") [count:'.\count($aSerializedUids['ThreadsUids']).']'); + return $aSerializedUids['ThreadsUids']; + } + } + } + + $aUids = []; + + if ($bThreadSort) { + $oParams = new MessageListParams; + $oParams->sFolderName = $oFolderInfo->FullName; + $oParams->sSort = 'DATE'; + $oParams->bUseSort = true; + $oParams->bHideDeleted = false; + foreach ($aAllThreads as $aThreadUIDs) { + $oParams->oSequenceSet = new \MailSo\Imap\SequenceSet($aThreadUIDs); + $aThreadUIDs = $this->GetUids($oParams, $oFolderInfo); + if ($aThreadUIDs) { + // Remove the most recent UID + \array_pop($aThreadUIDs); + $aUids = \array_merge($aUids, $aThreadUIDs); + } + } +/* + // Idea to use one SORT for all threads instead of per thread + $aSortUids = \array_reduce($aAllThreads, 'array_merge', []); + $oParams->oSequenceSet = new \MailSo\Imap\SequenceSet($aSortUids); + $aSortUids = $this->GetUids($oParams, $oFolderInfo); + if ($aSortUids) { + foreach ($aAllThreads as $aThreadUIDs) { + $aThreadUIDs = \array_intersect($aSortUids, $aThreadUIDs); + // Remove the most recent UID + \array_pop($aThreadUIDs); + $aUids = \array_merge($aUids, $aThreadUIDs); + } + } +*/ + } else { + // Not the best solution to remove the most recent UID, + // as older messages could have a higher UID + foreach ($aAllThreads as $aThreadUIDs) { + unset($aThreadUIDs[\array_search(\max($aThreadUIDs), $aThreadUIDs)]); + $aUids = \array_merge($aUids, $aThreadUIDs); + } + } + + if ($sSerializedHashKey) { + $oCacher->Set($sSerializedHashKey, \json_encode(array('ThreadsUids' => $aUids))); + $this->logWrite('Save old Threads UIDs to cache ("'.$oFolderInfo->FullName.'") [count:'.\count($aUids).']'); + } + + return $aUids; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + protected function MessageListByRequestIndexOrUids(MessageCollection $oMessageCollection, SequenceSet $oRange, + array &$aAllThreads = [], array &$aUnseenUIDs = []) : void + { + if (\count($oRange)) { + $aFetchItems = array( + FetchType::UID, + FetchType::RFC822_SIZE, + FetchType::INTERNALDATE, + FetchType::FLAGS, + FetchType::BODYSTRUCTURE + ); + if ($this->oImapClient->hasCapability('PREVIEW')) { + $aFetchItems[] = FetchType::PREVIEW; // . ' (LAZY)'; + } + $aFetchItems[] = $this->getEnvelopeOrHeadersRequestString(); + $aFetchIterator = $this->oImapClient->FetchIterate($aFetchItems, (string) $oRange, $oRange->UID); + // FETCH does not respond in the id order of the SequenceSet, so we prefill $aCollection for the right sort order. + $aCollection = \array_fill_keys($oRange->getArrayCopy(), null); + foreach ($aFetchIterator as $oFetchResponse) { + $id = $oRange->UID + ? $oFetchResponse->GetFetchValue(FetchType::UID) + : $oFetchResponse->oImapResponse->ResponseList[1]; + $oMessage = Message::fromFetchResponse($oMessageCollection->FolderName, $oFetchResponse); + if ($oMessage) { + if ($aAllThreads) { + $iUid = $oMessage->Uid; + // Find thread and set it. + // Used by GUI to delete/move the whole thread or other features + foreach ($aAllThreads as $aMap) { + if (\in_array($iUid, $aMap)) { + $oMessage->SetThreads($aMap); + $oMessage->SetThreadUnseen(\array_values(\array_intersect($aUnseenUIDs, $aMap))); + break; + } + } + } + $aCollection[$id] = $oMessage; + } + } + $oMessageCollection->exchangeArray(\array_values(\array_filter($aCollection))); + } + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + private function GetUids(MessageListParams $oParams, FolderInformation $oInfo, bool $onlyCache = false) : ?array + { + $oCacher = $oParams->oCacher; + $sFolderName = $oParams->sFolderName; + + $bUseSort = $oParams->bUseSort && $this->oImapClient->hasCapability('SORT'); + $aSortTypes = []; + if ($bUseSort) { + if ($oParams->sSort) { + // TODO: $oParams->sortValid($this->oImapClient); + $aSortTypes[] = $oParams->sSort; + } + if (!\str_contains($oParams->sSort, 'DATE')) { + // Always also sort DATE descending when DATE is not defined + $aSortTypes[] = 'REVERSE DATE'; + } + } + $oParams->sSort = \implode(' ', $aSortTypes); + + $bUseCache = $oCacher && $oCacher->IsInited(); + $oSearchCriterias = \MailSo\Imap\SearchCriterias::fromString( + $this->oImapClient, + $sFolderName, + $oParams->sSearch, + $oParams->bHideDeleted, + $bUseCache + ); + // Disable? as there are many cases that change the result +// $bUseCache = false; + + $bReturnUid = true; + if ($oParams->oSequenceSet) { + $bReturnUid = $oParams->oSequenceSet->UID; + $oSearchCriterias->prepend(($bReturnUid ? 'UID ' : '') . $oParams->oSequenceSet); + } + +/* + $oSearchCriterias->fuzzy = $oParams->bSearchFuzzy && $this->oImapClient->hasCapability('SEARCH=FUZZY'); +*/ + $sSerializedHash = ''; + $sSerializedLog = ''; + if ($bUseCache && $oInfo->etag) { + $sSerializedHash = 'Get' + . ($bReturnUid ? 'UIDS/' : 'IDS/') + . "{$oParams->sSort}/{$this->oImapClient->Hash()}/{$sFolderName}/{$oSearchCriterias}"; + $sSerializedLog = "\"{$sFolderName}\" / {$oParams->sSort} / {$oSearchCriterias}"; + $sSerialized = $oCacher->Get($sSerializedHash); + if (!empty($sSerialized)) { + $aSerialized = \json_decode($sSerialized, true); + if (\is_array($aSerialized) + && isset($aSerialized['FolderHash'], $aSerialized['Uids']) + && $oInfo->etag === $aSerialized['FolderHash'] + && \is_array($aSerialized['Uids']) + ) { + $this->logWrite('Get Serialized '.($bReturnUid?'UIDS':'IDS').' from cache ('.$sSerializedLog.') [count:'.\count($aSerialized['Uids']).']'); + return $aSerialized['Uids']; + } + } + } + if ($onlyCache) { + return null; + } + + $this->oImapClient->FolderExamine($sFolderName); + + $aResultUids = []; + if ($bUseSort) { +// $this->oImapClient->hasCapability('ESORT') +// $aResultUids = $this->oImapClient->MessageESort($aSortTypes, $oSearchCriterias)['ALL']; + $aResultUids = $this->oImapClient->MessageSort($aSortTypes, $oSearchCriterias, $bReturnUid); + } else { +// $this->oImapClient->hasCapability('ESEARCH') +// $aResultUids = $this->oImapClient->MessageESearch($oSearchCriterias, null, $bReturnUid) + $aResultUids = $this->oImapClient->MessageSearch($oSearchCriterias, $bReturnUid); + } + + if ($bUseCache) { + $oCacher->Set($sSerializedHash, \json_encode(array( + 'FolderHash' => $oInfo->etag, + 'Uids' => $aResultUids + ))); + + $this->logWrite('Save Serialized '.($bReturnUid?'UIDS':'IDS').' to cache ('.$sSerializedLog.') [count:'.\count($aResultUids).']'); + } + +// $oSequenceSet = new SequenceSet($aResultUids, false); +// $oSequenceSet->UID = $bReturnUid; +// return $oSequenceSet; + + return $aResultUids; + } + + public function MessageListUnseen(MessageListParams $oParams, FolderInformation $oInfo) : array + { + $oUnseenParams = new MessageListParams; + $oUnseenParams->sFolderName = $oParams->sFolderName; + $oUnseenParams->sSearch = 'unseen'; +// $oUnseenParams->sSort = $oParams->sSort; + $oUnseenParams->oCacher = $oParams->oCacher; + $oUnseenParams->bUseSort = false; // $oParams->bUseSort + $oUnseenParams->bUseThreads = false; // $oParams->bUseThreads; + $oUnseenParams->bHideDeleted = $oParams->bHideDeleted; +// $oUnseenParams->iOffset = $oParams->iOffset; +// $oUnseenParams->iLimit = $oParams->iLimit; +// $oUnseenParams->iPrevUidNext = $oParams->iPrevUidNext; +// $oUnseenParams->iThreadUid = $oParams->iThreadUid; + return $this->GetUids($oUnseenParams, $oInfo); + } + + /** + * Runs SORT/SEARCH when $sSearch is provided + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageList(MessageListParams $oParams) : MessageCollection + { + if (0 > $oParams->iOffset || 0 > $oParams->iLimit) { + throw new \ValueError; + } + if (10 > $oParams->iLimit) { + $oParams->iLimit = 10; + } else if (999 < $oParams->iLimit) { + $oParams->iLimit = 50; + } + + $sSearch = \trim($oParams->sSearch); + + $oMessageCollection = new MessageCollection; + $oMessageCollection->FolderName = $oParams->sFolderName; + $oMessageCollection->Offset = $oParams->iOffset; + $oMessageCollection->Limit = $oParams->iLimit; + $oMessageCollection->Search = $sSearch; + $oMessageCollection->ThreadUid = $oParams->iThreadUid; +// $oMessageCollection->Filtered = '' !== $this->oImapClient->Settings->search_filter; + + $oInfo = $this->oImapClient->FolderStatusAndSelect($oParams->sFolderName); + $oMessageCollection->FolderInfo = $oInfo; + $oMessageCollection->totalEmails = $oInfo->MESSAGES; + + $oParams->bUseThreads = $oParams->bUseThreads && $this->oImapClient->CapabilityValue('THREAD'); +// && ($this->oImapClient->hasCapability('THREAD=REFS') || $this->oImapClient->hasCapability('THREAD=REFERENCES') || $this->oImapClient->hasCapability('THREAD=ORDEREDSUBJECT')); + if ($oParams->iThreadUid && !$oParams->bUseThreads) { + throw new \ValueError('THREAD not supported'); + } + + if (!$oInfo->MESSAGES || $oParams->iOffset > $oInfo->MESSAGES) { + return $oMessageCollection; + } + + if (!$oParams->iThreadUid) { + $oMessageCollection->NewMessages = $this->getFolderNextMessageInformation( + $oParams->sFolderName, $oParams->iPrevUidNext, $oInfo->UIDNEXT + ); + } + + $bUseSort = ($oParams->bUseSort || $oParams->sSort) && $this->oImapClient->hasCapability('SORT'); + $oParams->bUseSort = $bUseSort; + $oParams->sSearch = $sSearch; + + $aAllThreads = []; + $aUnseenUIDs = []; + $aUids = null; + + $message_list_limit = $this->oImapClient->Settings->message_list_limit; + if (100 > $message_list_limit || $message_list_limit > $oInfo->MESSAGES) { + $message_list_limit = 0; + } + + // Idea to fetch all UID's in background + $oAllParams = clone $oParams; + $oAllParams->sSearch = ''; + $oAllParams->oSequenceSet = null; + if ($message_list_limit && !$oParams->iThreadUid && $oParams->oCacher && $oParams->oCacher->IsInited()) { + $aUids = $this->GetUids($oAllParams, $oInfo, true); + if (null !== $aUids) { + $message_list_limit = 0; + $oMessageCollection->Sort = $oAllParams->sSort; + } else { + \SnappyMail\Shutdown::add(function($oMailClient, $oAllParams, $oInfo, $oMessageCollection) { + $oMailClient->GetUids($oAllParams, $oInfo); + if ($oAllParams->bUseThreads) { + $oMailClient->ThreadsMap($oAllParams->sThreadAlgorithm, $oMessageCollection, $oAllParams->oCacher, true); + } + }, [$this, $oAllParams, $oInfo, $oMessageCollection]); + } + } + + if ($message_list_limit && !$aUids) { +// if ($message_list_limit || (!$this->oImapClient->hasCapability('SORT') && !$this->oImapClient->CapabilityValue('THREAD'))) { + // Don't use THREAD for speed + $oMessageCollection->Limited = true; + $this->logWrite('List optimization (count: '.$oInfo->MESSAGES.', limit:'.$message_list_limit.')'); + if (\strlen($sSearch)) { + // Don't use SORT for speed + $oParams->bUseSort = false; + $aUids = $this->GetUids($oParams, $oInfo); + } else { + if ($bUseSort) { + // Attempt to sort REVERSE DATE with a bigger range then $oParams->iLimit + $end = \min($oInfo->MESSAGES, \max(1, $oInfo->MESSAGES - $oParams->iOffset + $oParams->iLimit)); + $start = \max(1, $end - ($oParams->iLimit * 3) + 1); + $oParams->oSequenceSet = new SequenceSet(\range($end, $start), false); + $aRequestIndexes = $this->GetUids($oParams, $oInfo); + // Attempt to get the correct $oParams->iLimit slice + $aRequestIndexes = \array_slice($aRequestIndexes, $oParams->iOffset ? $oParams->iLimit : 0, $oParams->iLimit); + } else { + // Fetch ID's from high to low + $end = \max(1, $oInfo->MESSAGES - $oParams->iOffset); + $start = \max(1, $end - $oParams->iLimit + 1); + $aRequestIndexes = \range($end, $start); + } + $this->MessageListByRequestIndexOrUids($oMessageCollection, new SequenceSet($aRequestIndexes, false)); + } + $oMessageCollection->Sort = $oParams->sSort; + } else { + if ($oParams->bUseThreads && $oParams->iThreadUid) { + $aUids = [$oParams->iThreadUid]; + } else if (!$aUids) { + $aUids = $this->GetUids($oAllParams, $oInfo); + $oMessageCollection->Sort = $oAllParams->sSort; + } + + if ($oParams->bUseThreads) { + $aAllThreads = $this->ThreadsMap($oParams->sThreadAlgorithm, $oMessageCollection, $oParams->oCacher); +// $iThreadLimit = $this->oImapClient->Settings->thread_limit; + if ($oParams->iThreadUid) { + // Only show the selected thread messages + foreach ($aAllThreads as $aMap) { + if (\in_array($oParams->iThreadUid, $aMap)) { + $aUids = $aMap; + break; + } + } + $aAllThreads = [$aUids]; + // This only speeds up the search when not cached +// $oParams->oSequenceSet = new SequenceSet($aUids); + } else { + // Remove all threaded UID's except the most recent of each thread + $aUids = \array_diff($aUids, $this->ThreadsOldUids($aAllThreads, $oMessageCollection, $oParams->oCacher)); + // Get all unseen + $aUnseenUIDs = $this->MessageListUnseen($oParams, $oInfo); + } + } + + if ($aUids && \strlen($sSearch)) { + $oParams->bUseSort = false; + $aSearchedUids = $this->GetUids($oParams, $oInfo); + if ($oParams->bUseThreads && !$oParams->iThreadUid) { + $matchingThreadUids = []; + foreach ($aAllThreads as $aMap) { + if (\array_intersect($aSearchedUids, $aMap)) { + $matchingThreadUids = \array_merge($matchingThreadUids, $aMap); + } + } + $aUids = \array_filter($aUids, function($iUid) use ($aSearchedUids, $matchingThreadUids) { + return \in_array($iUid, $aSearchedUids) || \in_array($iUid, $matchingThreadUids); + }); + } else { + $aUids = \array_filter($aUids, function($iUid) use ($aSearchedUids) { + return \in_array($iUid, $aSearchedUids); + }); + } + } + } + + if (\is_array($aUids)) { + $oMessageCollection->totalEmails = \count($aUids); + if ($oMessageCollection->totalEmails) { + $aUids = \array_slice($aUids, $oParams->iOffset, $oParams->iLimit); + $this->MessageListByRequestIndexOrUids($oMessageCollection, new SequenceSet($aUids), $aAllThreads, $aUnseenUIDs); + } + } + + return $oMessageCollection; + } + + public function FindMessageUidByMessageId(string $sFolderName, string $sMessageId) : ?int + { + if (!\strlen($sMessageId)) { + throw new \ValueError; + } + + $this->oImapClient->FolderExamine($sFolderName); + + $aUids = $this->oImapClient->MessageSearch('HEADER Message-ID '.$sMessageId); + + return 1 === \count($aUids) && \is_numeric($aUids[0]) ? (int) $aUids[0] : null; + } + + public function Folders(string $sParent, string $sListPattern, bool $bUseListSubscribeStatus) : ?FolderCollection + { + $oFolderCollection = $this->oImapClient->FolderStatusList($sParent, $sListPattern); + if (!$oFolderCollection->count()) { + return null; + } + + if ($bUseListSubscribeStatus && !$this->oImapClient->hasCapability('LIST-EXTENDED')) { +// $this->logWrite('RFC5258 not supported, using LSUB'); +// \SnappyMail\Log::warning('IMAP', 'RFC5258 not supported, using LSUB'); + try + { + $oSubscribedFolders = $this->oImapClient->FolderSubscribeList($sParent, $sListPattern); + foreach ($oSubscribedFolders as /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder) { + isset($oFolderCollection[$oImapFolder->FullName]) + && $oFolderCollection[$oImapFolder->FullName]->setSubscribed(); + } + } + catch (\Throwable $oException) + { + \SnappyMail\Log::error('IMAP', 'FolderSubscribeList: ' . $oException->getMessage()); + foreach ($oFolderCollection as /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder) { + $oImapFolder->setSubscribed(); + } + } + } + + return $oFolderCollection; + } + + /** + * @throws \ValueError + */ + public function FolderCreate(string $sFolderNameInUtf8, string $sFolderParentFullName = '', bool $bSubscribeOnCreation = true, string $sDelimiter = '') : ?\MailSo\Imap\Folder + { + $sFolderNameInUtf8 = \trim($sFolderNameInUtf8); + $sFolderParentFullName = \trim($sFolderParentFullName); + + if (!\strlen($sFolderNameInUtf8)) { + throw new \ValueError; + } + + if (!\strlen($sDelimiter) || \strlen($sFolderParentFullName)) { + $sDelimiter = $this->oImapClient->FolderHierarchyDelimiter($sFolderParentFullName); + if (null === $sDelimiter) { + // TODO: Translate + throw new \MailSo\RuntimeException( + \strlen($sFolderParentFullName) + ? 'Cannot create folder in non-existent parent folder.' + : 'Cannot get folder delimiter.'); + } + + if (\strlen($sDelimiter) && \strlen($sFolderParentFullName)) { + $sFolderParentFullName .= $sDelimiter; + } + } + +/* // Allow non existent parent folders + if (\strlen($sDelimiter) && false !== \strpos($sFolderNameInUtf8, $sDelimiter)) { + // TODO: Translate + throw new \MailSo\RuntimeException('New folder name contains delimiter.'); + } +*/ + $sFullNameToCreate = $sFolderParentFullName.$sFolderNameInUtf8; + + $this->oImapClient->FolderCreate($sFullNameToCreate, $bSubscribeOnCreation); + + $aFolders = $this->oImapClient->FolderStatusList($sFullNameToCreate, ''); + if (isset($aFolders[$sFullNameToCreate])) { + $oImapFolder = $aFolders[$sFullNameToCreate]; + $bSubscribeOnCreation && $oImapFolder->setSubscribed(); + return $oImapFolder; + } + + return null; + } + + /** + * @throws \InvalidArgumentException + */ + public function FolderRename(string $sPrevFolderFullName, string $sNewFolderFullName) : self + { + if (!\strlen($sPrevFolderFullName) || !\strlen($sNewFolderFullName)) { + throw new \ValueError; + } + + if (!$this->oImapClient->FolderHierarchyDelimiter($sPrevFolderFullName)) { + // TODO: Translate + throw new \MailSo\RuntimeException('Cannot rename non-existent folder.'); + } +/* + if (\strlen($sDelimiter) && false !== \strpos($sNewFolderFullName, $sDelimiter)) { + // TODO: Translate + throw new \MailSo\RuntimeException('New folder name contains delimiter.'); + } +*/ + + /** + * https://datatracker.ietf.org/doc/html/rfc3501#section-6.3.5 + * Does not mention subscriptions + * https://datatracker.ietf.org/doc/html/rfc9051#section-6.3.6 + * Mentions that a server doesn't automatically manage subscriptions + */ + $oSubscribedFolders = $this->oImapClient->FolderSubscribeList($sPrevFolderFullName, '*'); + + $this->oImapClient->FolderRename($sPrevFolderFullName, $sNewFolderFullName); + + foreach ($oSubscribedFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) { + $sFolderFullNameForResubscribe = $oFolder->FullName; + if (\str_starts_with($sFolderFullNameForResubscribe, $sPrevFolderFullName)) { + $this->oImapClient->FolderUnsubscribe($sFolderFullNameForResubscribe); + $this->oImapClient->FolderSubscribe( + $sNewFolderFullName . \substr($sFolderFullNameForResubscribe, \strlen($sPrevFolderFullName)) + ); + } + } + + return $this; + } + + /** + * @throws \InvalidArgumentException + */ + public function SetLogger(?\MailSo\Log\Logger $oLogger) : void + { + $this->oLogger = $oLogger; + $this->oImapClient->SetLogger($oLogger); + } + + public function __call(string $name, array $arguments) /*: mixed*/ + { + return $this->oImapClient->{$name}(...$arguments); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php new file mode 100644 index 0000000000..fa8c9cf2e6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php @@ -0,0 +1,584 @@ +$k : null; + } + + public function Subject() : string + { + return $this->sSubject; + } + + public function From() : ?\MailSo\Mime\EmailCollection + { + return $this->oFrom; + } + + public function Uid() : int + { + return $this->Uid; + } + + public function Attachments() : ?AttachmentCollection + { + return $this->Attachments; + } + + public function setPlain(string $value) : void + { + $this->sPlain = $value; + } + + public function setHtml(string $value) : void + { + $this->sHtml = $value; + } + + private function setSpamScore($value) : void + { + $this->SpamScore = \intval(\max(0, \min(100, $value))); + } + + public function SetThreads(array $aThreadUIDs) + { + $this->aThreadUIDs = $aThreadUIDs; + } + + public function SetThreadUnseen(array $aUnseenUIDs) + { + $this->aThreadUnseenUIDs = $aUnseenUIDs; + } + + public static function fromFetchResponse(string $sFolder, \MailSo\Imap\FetchResponse $oFetchResponse, ?\MailSo\Imap\BodyStructure $oBodyStructure = null) : self + { + $oMessage = new self; + + if (!$oBodyStructure) { + $oBodyStructure = $oFetchResponse->GetFetchBodyStructure(); + } + + $aFlags = $oFetchResponse->GetFetchValue(FetchType::FLAGS) ?: []; + + $oMessage->sFolder = $sFolder; + $oMessage->Uid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); + $oMessage->iSize = (int) $oFetchResponse->GetFetchValue(FetchType::RFC822_SIZE); +// $oMessage->aFlags = $aFlags; + $oMessage->aFlagsLowerCase = \array_map('mb_strtolower', \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $aFlags)); + $oMessage->iInternalTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseInternalDateString( + $oFetchResponse->GetFetchValue(FetchType::INTERNALDATE) + ); + + // https://www.rfc-editor.org/rfc/rfc8474 + $aEmailId = $oFetchResponse->GetFetchValue(FetchType::EMAILID); + $oMessage->sEmailId = $aEmailId ? $aEmailId[0] : $oFetchResponse->GetFetchValue('X-GM-MSGID'); +// $oMessage->sEmailId = $oMessage->sEmailId ?: $oFetchResponse->GetFetchValue('X-GUID'); + $aThreadId = $oFetchResponse->GetFetchValue(FetchType::THREADID); + $oMessage->sThreadId = $aThreadId ? $aThreadId[0] : $oFetchResponse->GetFetchValue('X-GM-THRID'); + $oMessage->sPreview = $oFetchResponse->GetFetchValue(FetchType::PREVIEW) ?: null; + $sCharset = $oBodyStructure ? Utils::NormalizeCharset($oBodyStructure->SearchCharset()) : ''; + + $sHeaders = $oFetchResponse->GetHeaderFieldsValue(); + $oHeaders = \strlen($sHeaders) ? new \MailSo\Mime\HeaderCollection($sHeaders, $sCharset) : null; + if ($oHeaders) { + $oMessage->Headers = $oHeaders; + + $sContentTypeCharset = $oHeaders->ParameterValue( + MimeHeader::CONTENT_TYPE, + \MailSo\Mime\Enumerations\Parameter::CHARSET + ); + if (\strlen($sContentTypeCharset)) { + $sCharset = Utils::NormalizeCharset($sContentTypeCharset); + } + if (\strlen($sCharset)) { + $oHeaders->SetParentCharset($sCharset); + } + + $bCharsetAutoDetect = !\strlen($sCharset); + + $oMessage->sSubject = $oHeaders->ValueByName(MimeHeader::SUBJECT, $bCharsetAutoDetect); + $oMessage->sMessageId = $oHeaders->ValueByName(MimeHeader::MESSAGE_ID); + $oMessage->sContentType = $oHeaders->ValueByName(MimeHeader::CONTENT_TYPE); + + $oMessage->oFrom = $oHeaders->GetAsEmailCollection(MimeHeader::FROM_); + $oMessage->oTo = $oHeaders->GetAsEmailCollection(MimeHeader::TO_); + $oMessage->oCc = $oHeaders->GetAsEmailCollection(MimeHeader::CC); + $oMessage->oBcc = $oHeaders->GetAsEmailCollection(MimeHeader::BCC); + + $oMessage->oSender = $oHeaders->GetAsEmailCollection(MimeHeader::SENDER); + $oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(MimeHeader::REPLY_TO); + $oMessage->oDeliveredTo = $oHeaders->GetAsEmailCollection(MimeHeader::DELIVERED_TO); + + $oMessage->InReplyTo = $oHeaders->ValueByName(MimeHeader::IN_REPLY_TO); + $oMessage->References = Utils::StripSpaces( + $oHeaders->ValueByName(MimeHeader::REFERENCES)); + + $oMessage->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString( + $oHeaders->ValueByName(MimeHeader::DATE) + ); + + // Delivery Receipt +// $oMessage->sDeliveryReceipt = \trim($oHeaders->ValueByName(MimeHeader::RETURN_RECEIPT_TO)); + + // Read Receipt + $sReadReceipt = \trim($oHeaders->ValueByName(MimeHeader::DISPOSITION_NOTIFICATION_TO)); + if (empty($sReadReceipt)) { + $sReadReceipt = \trim($oHeaders->ValueByName(MimeHeader::X_CONFIRM_READING_TO)); + } + if ($sReadReceipt) { + try + { + if (!\MailSo\Mime\Email::Parse($sReadReceipt)) { + $sReadReceipt = ''; + } + } + catch (\Throwable $oException) + { + $sReadReceipt = ''; + } + } + $oMessage->ReadReceipt = $sReadReceipt; + + if ($spam = $oHeaders->ValueByName(MimeHeader::X_SPAMD_RESULT)) { + if (\preg_match('/\\[([\\d\\.-]+)\\s*\\/\\s*([\\d\\.]+)\\];/', $spam, $match)) { + if ($threshold = \floatval($match[2])) { + $oMessage->setSpamScore(100 * \floatval($match[1]) / $threshold); + $oMessage->sSpamResult = "{$match[1]} / {$match[2]}"; + } + } + $oMessage->bIsSpam = false !== \stripos($oMessage->sSubject, '*** SPAM ***'); + } else if ($spam = $oHeaders->ValueByName(MimeHeader::X_BOGOSITY)) { + $oMessage->sSpamResult = $spam; + $oMessage->bIsSpam = !\str_contains($spam, 'Ham'); + if (\preg_match('/spamicity=([\\d\\.]+)/', $spam, $spamicity)) { + $oMessage->setSpamScore(100 * \floatval($spamicity[1])); + } + } else if ($spam = $oHeaders->ValueByName(MimeHeader::X_SPAM_STATUS)) { + $oMessage->sSpamResult = $spam; + if (\preg_match('/(?:hits|score)=([\\d\\.-]+)/', $spam, $value) + && \preg_match('/required=([\\d\\.-]+)/', $spam, $required)) { + if ($threshold = \floatval($required[1])) { + $oMessage->setSpamScore(100 * \floatval($value[1]) / $threshold); + $oMessage->sSpamResult = "{$value[1]} / {$required[1]}"; + } + } + // https://github.com/the-djmaze/snappymail/issues/1228 + else if (\preg_match('@([\\d\\.]+)/([\\d\\.]+)@', $spam, $value) + || \preg_match('@([\\d\\.]+)/([\\d\\.]+)@', $oHeaders->ValueByName(MimeHeader::X_SPAM_INFO), $value) + ) { + if ($threshold = \floatval($value[2])) { + $oMessage->setSpamScore(100 * \floatval($value[1]) / $threshold); + $oMessage->sSpamResult = "{$value[1]} / {$value[2]}"; + } + } + + $oMessage->bIsSpam = 'Yes' === \substr($spam, 0, 3) + || false !== \stripos($oHeaders->ValueByName(MimeHeader::X_SPAM_FLAG), 'YES'); + } + + $sDraftInfo = $oHeaders->ValueByName(MimeHeader::X_DRAFT_INFO); + if (\strlen($sDraftInfo)) { + $sType = ''; + $sFolder = ''; + $iUid = 0; + + $oParameters = new \MailSo\Mime\ParameterCollection($sDraftInfo); + foreach ($oParameters as $oParameter) { + switch (\strtolower($oParameter->Name())) + { + case 'type': + $sType = $oParameter->Value(); + break; + case 'uid': + $iUid = (int) $oParameter->Value(); + break; + case 'folder': + $sFolder = \base64_decode($oParameter->Value()); + break; + } + } + + if (\strlen($sType) && \strlen($sFolder) && $iUid) { + $oMessage->DraftInfo = array($sType, $iUid, $sFolder); + } + } + + $aAuth = $oHeaders->AuthStatuses(); + $oMessage->SPF = $aAuth['spf']; + $oMessage->DKIM = $aAuth['dkim']; + $oMessage->DMARC = $aAuth['dmarc']; + if ($aAuth['dkim'] && $oMessage->oFrom) { + foreach ($oMessage->oFrom as $oEmail) { + $sEmail = $oEmail->GetEmail(); + foreach ($aAuth['dkim'] as $aDkimData) { + if (\strpos($sEmail, $aDkimData[1])) { + $oEmail->SetDkimStatus($aDkimData[0]); + } + } + } + } + } + else if ($oFetchResponse->GetEnvelope()) + { + $sCharset = $sCharset ?: \MailSo\Base\Enumerations\Charset::ISO_8859_1; + + // date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to, message-id + $oMessage->sMessageId = $oFetchResponse->GetFetchEnvelopeValue(9, ''); + $oMessage->sSubject = Utils::DecodeHeaderValue($oFetchResponse->GetFetchEnvelopeValue(1, ''), $sCharset); + + $oMessage->oFrom = $oFetchResponse->GetFetchEnvelopeEmailCollection(2, $sCharset); + $oMessage->oSender = $oFetchResponse->GetFetchEnvelopeEmailCollection(3, $sCharset); + $oMessage->oReplyTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(4, $sCharset); + $oMessage->oTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(5, $sCharset); + $oMessage->oCc = $oFetchResponse->GetFetchEnvelopeEmailCollection(6, $sCharset); + $oMessage->oBcc = $oFetchResponse->GetFetchEnvelopeEmailCollection(7, $sCharset); + $oMessage->InReplyTo = $oFetchResponse->GetFetchEnvelopeValue(8, ''); + } + + if ($oBodyStructure) { + $gEncryptedParts = $oBodyStructure->SearchByContentType('multipart/encrypted'); + foreach ($gEncryptedParts as $oPart) { + if ($oPart->isPgpEncrypted()) { + $oMessage->pgpEncrypted = [ + 'partId' => $oPart->SubParts()[1]->PartID() + ]; + } + } + + $gEncryptedParts = $oBodyStructure->SearchByContentTypes(['application/pkcs7-mime','application/x-pkcs7-mime']); + foreach ($gEncryptedParts as $oPart) { + if ($oPart->isSMimeEncrypted()) { + $oMessage->smimeEncrypted = [ + 'partId' => $oPart->PartID() + ]; + } else if ($oPart->isSMimeSigned()) { + $oMessage->smimeSigned = [ + 'partId' => $oPart->PartID(), + 'micAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, 'micalg') : '', + 'detached' => false + ]; + } + } + + $gSignatureParts = $oBodyStructure->SearchByContentType('multipart/signed'); + foreach ($gSignatureParts as $oPart) { + if ($oPart->isPgpSigned()) { + $oMessage->pgpSigned = [ + // /?/Raw/&q[]=/0/Download/&q[]=/... + // /?/Raw/&q[]=/0/View/&q[]=/... + 'partId' => $oPart->SubParts()[0]->PartID(), + 'sigPartId' => $oPart->SubParts()[1]->PartID(), + 'micAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, 'micalg') : '' + ]; + } else if ($oPart->isSMimeSigned()) { + $oMessage->smimeSigned = [ + 'partId' => $oPart->PartID(), + 'sigPartId' => $oPart->SubParts()[1]->PartID(), + 'micAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, 'micalg') : '', + 'detached' => true + ]; + } +/* + // An empty section specification refers to the entire message, including the header. + // But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME]. + $sPgpText = \trim( + \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->pgpSigned['partId'].'.MIME]')) + . "\r\n\r\n" + . \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->pgpSigned['partId'].']')) + ); + if ($sPgpText) { + $oMessage->pgpSigned['body'] = $sPgpText; + } + $sPgpSignatureText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->pgpSigned['sigPartId'].']'); + if ($sPgpSignatureText && 0 < \strpos($sPgpSignatureText, 'BEGIN PGP SIGNATURE')) { + $oMessage->pgpSigned['signature'] = $oPart->SubParts()[0]->PartID(); + } +*/ + break; + } + + $aTextParts = $oBodyStructure->GetHtmlAndPlainParts(); + if ($aTextParts) { + $sCharset = $sCharset ?: \MailSo\Base\Enumerations\Charset::UTF_8; + + $aHtmlParts = array(); + $aPlainParts = array(); + + foreach ($aTextParts as $oPart) { + $sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']'); + if (null === $sText) { + // TextPartIsTrimmed ? + $sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']<0>'); + } + + if (\is_string($sText) && \strlen($sText)) { + $sText = Utils::DecodeEncodingValue($sText, $oPart->ContentTransferEncoding()); + $sText = Utils::ConvertEncoding($sText, + Utils::NormalizeCharset($oPart->Charset() ?: $sCharset, true), + \MailSo\Base\Enumerations\Charset::UTF_8 + ); + $sText = Utils::Utf8Clear($sText); + + // https://datatracker.ietf.org/doc/html/rfc4880#section-7 + // Cleartext Signature + if (!$oMessage->pgpSigned && \str_contains($sText, '-----BEGIN PGP SIGNED MESSAGE-----')) { + $oMessage->pgpSigned = [ + 'partId' => $oPart->PartID() + ]; + } + + if (\str_contains($sText, '-----BEGIN PGP MESSAGE-----')) { + $keyIds = []; + if (GPG::isSupported()) { + $GPG = new GPG(''); + $keyIds = $GPG->getEncryptedMessageKeys($sText); + } + $oMessage->pgpEncrypted = [ + 'partId' => $oPart->PartID(), + 'keyIds' => $keyIds + ]; + } + + if ('text/html' === $oPart->ContentType()) { + $aHtmlParts[] = $sText; + } else { + if ($oPart->IsFlowedFormat()) { + $sText = Utils::DecodeFlowedFormat($sText); + } + + $aPlainParts[] = $sText; + } + } + } + + $oMessage->sHtml = \implode('
    ', $aHtmlParts); + $oMessage->sPlain = \trim(\implode("\n", $aPlainParts)); + + unset($aHtmlParts, $aPlainParts); + } + + $gAttachmentsParts = $oBodyStructure->SearchAttachmentsParts(); + if ($gAttachmentsParts->valid()) { + $oMessage->Attachments = new AttachmentCollection; + foreach ($gAttachmentsParts as /* @var $oAttachmentItem \MailSo\Imap\BodyStructure */ $oAttachmentItem) { +// if ('application/pgp-keys' === $oAttachmentItem->ContentType()) import ??? + $oMessage->Attachments->append( + new Attachment($oMessage->sFolder, $oMessage->Uid, $oAttachmentItem) + ); + } + } + } + + if (\str_starts_with($oMessage->sSubject, '[Preview]')) { + $oMessage->sSubject = \mb_substr($oMessage->sSubject, 10); + } + + return $oMessage; + } + + public function ETag(string $sClientHash) : string + { + return \md5('MessageHash/' . \implode('/', [ + $this->sFolder, + $this->Uid, + \implode(',', $this->getFlags()), +// \implode(',', $this->aThreadUIDs), + $sClientHash + ])); + } + + // https://datatracker.ietf.org/doc/html/rfc5788#section-3.4.1 + // Thunderbird $label1 is same as $Important? + // Thunderbird $label4 is same as $todo? + protected function getFlags() : array + { + return \array_unique(\str_replace( + ['$readreceipt', '$replied', /* 'junk', 'nonjunk', '$queued', '$sent', 'sent'*/], + ['$mdnsent', '\\answered',/* '$junk', '$notjunk', '$submitpending', '$submitted', '$submitted'*/], + $this->aFlagsLowerCase + )); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { +/* + // JMAP-only RFC8621 keywords (RFC5788) + $keywords = \array_fill_keys(\str_replace( + ['\\draft', '\\seen', '\\flagged', '\\answered'], + [ '$draft', '$seen', '$flagged', '$answered'], + $this->aFlagsLowerCase + ), true); +*/ + $result = array( + '@Object' => 'Object/Message', + 'folder' => $this->sFolder, + 'uid' => $this->Uid, + 'hash' => \md5($this->sFolder . $this->Uid), + 'subject' => \trim(Utils::Utf8Clear($this->sSubject)), + 'encrypted' => 'multipart/encrypted' == $this->sContentType || $this->pgpEncrypted || $this->smimeEncrypted, + 'messageId' => $this->sMessageId, + 'spamScore' => $this->bIsSpam ? 100 : $this->SpamScore, + 'spamResult' => $this->sSpamResult, + 'isSpam' => $this->bIsSpam, + // RainLoop had the date_from_headers option + 'dateTimestamp' => $this->iHeaderTimeStampInUTC ?: $this->iInternalTimeStampInUTC, + 'dateTimestampSource' => $this->iHeaderTimeStampInUTC ? 'header' : 'internal', + + // \MailSo\Mime\EmailCollection + 'from' => $this->oFrom, + 'replyTo' => $this->oReplyTo, + 'to' => $this->oTo, + 'cc' => $this->oCc, + 'bcc' => $this->oBcc, + 'sender' => $this->oSender, + 'deliveredTo' => $this->oDeliveredTo, + + 'readReceipt' => $this->ReadReceipt, + + 'attachments' => $this->Attachments, + + 'spf' => $this->SPF, + 'dkim' => $this->DKIM, + 'dmarc' => $this->DMARC, + + 'flags' => $this->getFlags(), + + 'inReplyTo' => $this->InReplyTo, + + // https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.1 + 'id' => $this->sEmailId, +// 'blobId' => $this->sEmailIdBlob, +// 'threadId' => $this->sThreadId, +// 'mailboxIds' => ['mailboxid'=>true], +// 'keywords' => $keywords, + 'size' => $this->iSize, + + 'preview' => $this->sPreview, + + 'headers' => $this->Headers + ); + + if ($this->DraftInfo) { + $result['draftInfo'] = $this->DraftInfo; + } + if ($this->References) { + $result['references'] = $this->References; +// $result['references'] = \explode(' ', $this->References); + } + if ($this->sHtml || $this->sPlain) { + $result['html'] = $this->sHtml; + $result['plain'] = $this->sPlain; + } +// $this->GetCapa(Capa::OPENPGP) || $this->GetCapa(Capa::GNUPG) + if ($this->pgpSigned) { + $result['pgpSigned'] = $this->pgpSigned; + } + if ($this->pgpEncrypted) { + $result['pgpEncrypted'] = $this->pgpEncrypted; + } + + if ($this->smimeSigned) { + $result['smimeSigned'] = $this->smimeSigned; + } + if ($this->smimeEncrypted) { + $result['smimeEncrypted'] = $this->smimeEncrypted; + } + + if ($this->aThreadUIDs) { + $result['threads'] = $this->aThreadUIDs; + } + if ($this->aThreadUnseenUIDs) { + $result['threadUnseen'] = $this->aThreadUnseenUIDs; + } + + return $result; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageCollection.php new file mode 100644 index 0000000000..c4feac4caa --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageCollection.php @@ -0,0 +1,76 @@ + $this->totalEmails, + 'totalThreads' => $this->totalThreads, + 'threadUid' => $this->ThreadUid, + 'newMessages' => $this->NewMessages, +// 'filtered' => $this->Filtered, + 'offset' => $this->Offset, + 'limit' => $this->Limit, + 'search' => $this->Search, + 'sort' => $this->Sort, + 'limited' => $this->Limited, + 'folder' => $this->FolderInfo + )); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageListParams.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageListParams.php new file mode 100644 index 0000000000..a249ef7986 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageListParams.php @@ -0,0 +1,122 @@ +$k : null; + } + + public function __set($k, $v) + { + if ('i' === $k[0]) { + $this->$k = \max(0, (int) $v); + } +// 0 > $oParams->iOffset +// 0 > $oParams->iLimit +// 999 < $oParams->iLimit + } + + public function hash() : string + { + return \md5(\implode('-', [ + $this->sFolderName, + $this->iOffset, + $this->iLimit, + $this->bHideDeleted ? '1' : '0', + $this->sSearch, + $this->bSearchFuzzy ? '1' : '0', + $this->bUseSort ? $this->sSort : '0', + $this->bUseThreads ? $this->iThreadUid : '', + $this->bUseThreads ? $this->sThreadAlgorithm : '', +// $this->oSequenceSet ? $this->oSequenceSet : '', + $this->iPrevUidNext + ])); + } + +/* + public function sortValid($oImapClient) : bool + { + if (!$this->sSort) { + return true; + } + /(REVERSE\s+)?(ARRIVAL|CC|DATE|FROM|SIZE|SUBJECT|TO|DISPLAYFROM|DISPLAYTO)/ + ARRIVAL + Internal date and time of the message. This differs from the + ON criteria in SEARCH, which uses just the internal date. + + CC + [IMAP] addr-mailbox of the first "cc" address. + + DATE + Sent date and time, as described in section 2.2. + + FROM + [IMAP] addr-mailbox of the first "From" address. + + REVERSE + Followed by another sort criterion, has the effect of that + criterion but in reverse (descending) order. + Note: REVERSE only reverses a single criterion, and does not + affect the implicit "sequence number" sort criterion if all + other criteria are identical. Consequently, a sort of + REVERSE SUBJECT is not the same as a reverse ordering of a + SUBJECT sort. This can be avoided by use of additional + criteria, e.g., SUBJECT DATE vs. REVERSE SUBJECT REVERSE + DATE. In general, however, it's better (and faster, if the + client has a "reverse current ordering" command) to reverse + the results in the client instead of issuing a new SORT. + + SIZE + Size of the message in octets. + + SUBJECT + Base subject text. + + TO + [IMAP] addr-mailbox of the first "To" address. + + RFC 5957: + $oImapClient->hasCapability('SORT=DISPLAY') + DISPLAYFROM, DISPLAYTO + } +*/ + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Attachment.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Attachment.php new file mode 100644 index 0000000000..1fb0690967 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Attachment.php @@ -0,0 +1,164 @@ +rResource = $rResource; + $this->sFileName = $sFileName; +// $this->iFileSize = $iFileSize; + $this->bIsInline = $bIsInline; + $this->bIsLinked = $bIsLinked; + $this->sContentID = $sContentID; + $this->aCustomContentTypeParams = $aCustomContentTypeParams; + $this->sContentLocation = $sContentLocation; + $this->sContentType = $sContentType + ?: \SnappyMail\File\MimeType::fromStream($rResource, $sFileName) + ?: \SnappyMail\File\MimeType::fromFilename($sFileName) + ?: 'application/octet-stream'; + } + + /** + * @return resource + */ + public function Resource() + { + return $this->rResource; + } + + public function ContentType() : string + { + return $this->sContentType; + } + + public function CustomContentTypeParams() : array + { + return $this->aCustomContentTypeParams; + } + + public function FileName() : string + { + return $this->sFileName; + } + + public function isInline() : bool + { + return $this->bIsInline; + } + + public function isLinked() : bool + { + return $this->bIsLinked && \strlen($this->sContentID); + } + + public function ToPart() : Part + { + $oAttachmentPart = new Part; + + $sFileName = \trim($this->sFileName); + $sContentID = $this->sContentID; + $sContentLocation = $this->sContentLocation; + + $oContentTypeParameters = null; + $oContentDispositionParameters = null; + + if (\strlen($sFileName)) { + $oContentTypeParameters = + (new ParameterCollection)->Add(new Parameter( + Enumerations\Parameter::NAME, $sFileName)); + + $oContentDispositionParameters = + (new ParameterCollection)->Add(new Parameter( + Enumerations\Parameter::FILENAME, $sFileName)); + } + + $oAttachmentPart->Headers->append( + new Header(Enumerations\Header::CONTENT_TYPE, + $this->ContentType(). + ($oContentTypeParameters ? '; '.$oContentTypeParameters : '') + ) + ); + + $oAttachmentPart->Headers->append( + new Header(Enumerations\Header::CONTENT_DISPOSITION, + ($this->isInline() ? 'inline' : 'attachment'). + ($oContentDispositionParameters ? '; '.$oContentDispositionParameters : '') + ) + ); + + if (\strlen($sContentID)) { + $oAttachmentPart->Headers->append( + new Header(Enumerations\Header::CONTENT_ID, $sContentID) + ); + } + + if (\strlen($sContentLocation)) { + $oAttachmentPart->Headers->append( + new Header(Enumerations\Header::CONTENT_LOCATION, $sContentLocation) + ); + } + + $oAttachmentPart->Body = $this->Resource(); + + if ('message/rfc822' !== \strtolower($this->ContentType())) { + $oAttachmentPart->Headers->append( + new Header( + Enumerations\Header::CONTENT_TRANSFER_ENCODING, + \MailSo\Base\Enumerations\Encoding::BASE64_LOWER + ) + ); + + if (\is_resource($oAttachmentPart->Body) && !\MailSo\Base\StreamWrappers\Binary::IsStreamRemembed($oAttachmentPart->Body)) { + $oAttachmentPart->Body = + \MailSo\Base\StreamWrappers\Binary::CreateStream($oAttachmentPart->Body, + \MailSo\Base\StreamWrappers\Binary::GetInlineDecodeOrEncodeFunctionName( + \MailSo\Base\Enumerations\Encoding::BASE64_LOWER, false)); + + \MailSo\Base\StreamWrappers\Binary::RememberStream($oAttachmentPart->Body); + } + } + + return $oAttachmentPart; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/AttachmentCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/AttachmentCollection.php new file mode 100644 index 0000000000..48a591d358 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/AttachmentCollection.php @@ -0,0 +1,25 @@ +sEmail = \SnappyMail\IDN::emailToAscii(Utils::Trim($sEmail)); + + $this->sDisplayName = Utils::Trim($sDisplayName); + } + + /** + * @throws \ValueError + */ + public static function Parse(string $sEmailAddress) : self + { + $sEmailAddress = Utils::Trim(Utils::DecodeHeaderValue($sEmailAddress)); + if (!\strlen(\trim($sEmailAddress))) { + throw new \ValueError; + } + + $sName = ''; + $sEmail = ''; + $sComment = ''; + + $bInName = false; + $bInAddress = false; + $bInComment = false; + + $iStartIndex = 0; + $iCurrentIndex = 0; + + while ($iCurrentIndex < \strlen($sEmailAddress)) { + switch ($sEmailAddress[$iCurrentIndex]) + { +// case '\'': + case '"': +// $sQuoteChar = $sEmailAddress[$iCurrentIndex]; + if (!$bInName && !$bInAddress && !$bInComment) { + $bInName = true; + $iStartIndex = $iCurrentIndex; + } else if (!$bInAddress && !$bInComment) { + $sName = \substr($sEmailAddress, $iStartIndex + 1, $iCurrentIndex - $iStartIndex - 1); + $sEmailAddress = \substr_replace($sEmailAddress, '', $iStartIndex, $iCurrentIndex - $iStartIndex + 1); + $iCurrentIndex = 0; + $iStartIndex = 0; + $bInName = false; + } + break; + case '<': + if (!$bInName && !$bInAddress && !$bInComment) { + if ($iCurrentIndex > 0 && !\strlen($sName)) { + $sName = \substr($sEmailAddress, 0, $iCurrentIndex); + } + + $bInAddress = true; + $iStartIndex = $iCurrentIndex; + } + break; + case '>': + if ($bInAddress) { + $sEmail = \substr($sEmailAddress, $iStartIndex + 1, $iCurrentIndex - $iStartIndex - 1); + $sEmailAddress = \substr_replace($sEmailAddress, '', $iStartIndex, $iCurrentIndex - $iStartIndex + 1); + $iCurrentIndex = 0; + $iStartIndex = 0; + $bInAddress = false; + } + break; + case '(': + if (!$bInName && !$bInAddress && !$bInComment) { + $bInComment = true; + $iStartIndex = $iCurrentIndex; + } + break; + case ')': + if ($bInComment) { + $sComment = \substr($sEmailAddress, $iStartIndex + 1, $iCurrentIndex - $iStartIndex - 1); + $sEmailAddress = \substr_replace($sEmailAddress, '', $iStartIndex, $iCurrentIndex - $iStartIndex + 1); + $iCurrentIndex = 0; + $iStartIndex = 0; + $bInComment = false; + } + break; + case '\\': + ++$iCurrentIndex; + break; + } + + ++$iCurrentIndex; + } + + if (!\strlen($sEmail)) { + $aRegs = array(''); + if (\preg_match('/[^@\s]+@\S+/i', $sEmailAddress, $aRegs) && isset($aRegs[0])) { + $sEmail = $aRegs[0]; + } else { + $sName = $sEmailAddress; + } + } + + if (\strlen($sEmail) && !\strlen($sName) && !\strlen($sComment)) { + $sName = \str_replace($sEmail, '', $sEmailAddress); + } + + $sEmail = \trim(\trim($sEmail), '<>'); + $sEmail = \rtrim(\trim($sEmail), '.'); + $sEmail = \trim($sEmail); + + $sName = \trim(\trim($sName), '"'); + $sName = \trim($sName, '\''); + $sComment = \trim(\trim($sComment), '()'); + + // Remove backslash + $sName = \preg_replace('/\\\\(.)/s', '$1', $sName); + $sComment = \preg_replace('/\\\\(.)/s', '$1', $sComment); + + return new self($sEmail, $sName); + } + + public function GetEmail(bool $bUtf8 = false) : string + { + return $bUtf8 ? \SnappyMail\IDN::emailToUtf8($this->sEmail) : $this->sEmail; + } + + public function GetDisplayName() : string + { + return $this->sDisplayName; + } + + public function getLocalPart() : string + { + return Utils::getEmailAddressLocalPart($this->sEmail); + } + + public function GetDomain(bool $bIdn = false) : string + { + return Utils::getEmailAddressDomain($this->GetEmail($bIdn)); + } + + public function SetDkimStatus(string $sDkimStatus) + { + $this->sDkimStatus = Enumerations\DkimStatus::normalizeValue($sDkimStatus); + } + + public function ToString(bool $bConvertSpecialsName = false, bool $bUtf8 = false) : string + { + $sReturn = ''; + if (\strlen($this->sEmail)) { + $sReturn = $this->GetEmail($bUtf8); + $sDisplayName = $this->sDisplayName; + if (\strlen($sDisplayName)) { + $sDisplayName = \str_replace('"', '\"', $sDisplayName); + if ($bConvertSpecialsName) { + $sDisplayName = Utils::EncodeHeaderValue($sDisplayName); + } + $sReturn = '"'.$sDisplayName.'" <'.$sReturn.'>'; + } + } + return $sReturn; + } + + public function __toString() : string + { + return $this->ToString(); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( + '@Object' => 'Object/Email', + 'name' => Utils::Utf8Clear($this->sDisplayName), + 'email' => Utils::Utf8Clear($this->GetEmail(true)), + 'dkimStatus' => $this->sDkimStatus + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/EmailCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/EmailCollection.php new file mode 100644 index 0000000000..0d56809af5 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/EmailCollection.php @@ -0,0 +1,165 @@ +parseEmailAddresses($mEmailAddresses); + } else { + parent::__construct($mEmailAddresses); + } + } + + public function append($oEmail, bool $bToTop = false) : void + { + assert($oEmail instanceof Email); + parent::append($oEmail, $bToTop); + } + + public function Unique() : self + { + $aReturn = array(); + + foreach ($this as $oEmail) { + $sEmail = $oEmail->GetEmail(); + if (!isset($aReturn[$sEmail])) { + $aReturn[$sEmail] = $oEmail; + } + } + + return new static($aReturn); + } + + public function ToString(bool $bConvertSpecialsName = false, bool $bIdn = false) : string + { + $aReturn = array(); + foreach ($this as $oEmail) { + $aReturn[] = $oEmail->ToString($bConvertSpecialsName, $bIdn); + } + + return \implode(', ', $aReturn); + } + + public function __toString() : string + { + return $this->ToString(); + } + + private function parseEmailAddresses(string $sRawEmails) : void + { +// $sRawEmails = \MailSo\Base\Utils::Trim($sRawEmails); + $sRawEmails = \trim($sRawEmails); + + $sWorkingRecipientsLen = \strlen($sRawEmails); + if (!$sWorkingRecipientsLen) { + return; + } + + $iEmailStartPos = 0; + $iEmailEndPos = 0; + + $bIsInQuotes = false; + $sChQuote = '"'; + $bIsInAngleBrackets = false; + $bIsInBrackets = false; + + $iCurrentPos = 0; + + while ($iCurrentPos < $sWorkingRecipientsLen) { + switch ($sRawEmails[$iCurrentPos]) + { + case '\'': + case '"': + if (!$bIsInQuotes) { + $sChQuote = $sRawEmails[$iCurrentPos]; + $bIsInQuotes = true; + } else if ($sChQuote == $sRawEmails[$iCurrentPos]) { + $bIsInQuotes = false; + } + break; + + case '<': + if (!$bIsInAngleBrackets) { + $bIsInAngleBrackets = true; + if ($bIsInQuotes) { + $bIsInQuotes = false; + } + } + break; + + case '>': + if ($bIsInAngleBrackets) { + $bIsInAngleBrackets = false; + } + break; + + case '(': + if (!$bIsInBrackets) { + $bIsInBrackets = true; + } + break; + + case ')': + if ($bIsInBrackets) { + $bIsInBrackets = false; + } + break; + + case ',': + case ';': + if (!$bIsInAngleBrackets && !$bIsInBrackets && !$bIsInQuotes) { + $iEmailEndPos = $iCurrentPos; + + try + { + $this->append( + Email::Parse(\substr($sRawEmails, $iEmailStartPos, $iEmailEndPos - $iEmailStartPos)) + ); + + $iEmailStartPos = $iCurrentPos + 1; + } + catch (\Throwable $oException) + { + } + } + break; + } + + ++$iCurrentPos; + } + + if ($iEmailStartPos < $iCurrentPos) { + try + { + $this->append( + Email::Parse(\substr($sRawEmails, $iEmailStartPos, $iCurrentPos - $iEmailStartPos)) + ); + } + catch (\Throwable $oException) {} + } + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return \array_slice($this->getArrayCopy(), 0, 100); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/ContentType.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/ContentType.php new file mode 100644 index 0000000000..f6761dd459 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/ContentType.php @@ -0,0 +1,41 @@ +sParentCharset = $sParentCharset; + $this->initInputData($sName, $sValue, $sEncodedValueForReparse); + } + + private function initInputData(string $sName, string $sValue, string $sEncodedValueForReparse) : void + { + $this->sName = \trim($sName); + $this->sFullValue = \trim($sValue); + $this->sEncodedValue = ''; + + if (\strlen($sEncodedValueForReparse) && ($this->IsEmail() || $this->IsSubject() || $this->IsParameterized())) { + $this->sEncodedValue = \trim($sEncodedValueForReparse); + } + + if (\strlen($this->sFullValue) && $this->IsParameterized()) { + $aRawExplode = \explode(';', $this->sFullValue, 2); + if (2 === \count($aRawExplode)) { + $this->sValue = $aRawExplode[0]; + $this->oParameters = new ParameterCollection($aRawExplode[1]); + } else { + $this->sValue = $this->sFullValue; + } + } else { + $this->sValue = $this->sFullValue; + } + + if (!$this->oParameters) { + $this->oParameters = new ParameterCollection(); + } + } + + public static function NewInstanceFromEncodedString(string $sEncodedLines, string $sIncomingCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1) : Header + { + if (empty($sIncomingCharset)) { + $sIncomingCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1; + } + + $aParts = \explode(':', \str_replace("\r", '', $sEncodedLines), 2); + if (isset($aParts[0]) && isset($aParts[1]) && \strlen($aParts[0]) && \strlen($aParts[1])) { + return new self( + \trim($aParts[0]), + \trim(\MailSo\Base\Utils::DecodeHeaderValue(\trim($aParts[1]), $sIncomingCharset)), + \trim($aParts[1]), + $sIncomingCharset + ); + } + + return false; + } + + public function Name() : string + { + return $this->sName; + } + + public function Value() : string + { + return $this->sValue; + } + + public function FullValue() : string + { + return $this->sFullValue; + } + + public function EncodedValue() : string + { + return $this->sEncodedValue ?: $this->sFullValue; + } + + public function SetParentCharset(string $sParentCharset) : Header + { + if ($this->sParentCharset !== $sParentCharset && \strlen($this->sEncodedValue)) { + $this->initInputData( + $this->sName, + \trim(\MailSo\Base\Utils::DecodeHeaderValue($this->sEncodedValue, $sParentCharset)), + $this->sEncodedValue + ); + } + + $this->sParentCharset = $sParentCharset; + + return $this; + } + + public function Parameters() : ?ParameterCollection + { + return $this->oParameters; + } + + public function setParameter(string $sName, string $sValue) : void + { + $this->oParameters->setParameter($sName, $sValue); + } + + public function __toString() : string + { + $sResult = $this->sFullValue; + + if ($this->IsSubject()) { + if (!\MailSo\Base\Utils::IsAscii($sResult) && \function_exists('iconv_mime_encode')) { + return \iconv_mime_encode($this->Name(), $sResult, array( +// 'scheme' => \MailSo\Base\Enumerations\Encoding::QUOTED_PRINTABLE_SHORT, + 'scheme' => \MailSo\Base\Enumerations\Encoding::BASE64_SHORT, + 'input-charset' => \MailSo\Base\Enumerations\Charset::UTF_8, + 'output-charset' => \MailSo\Base\Enumerations\Charset::UTF_8, + 'line-length' => 74, + 'line-break-chars' => "\r\n" + )); + } + } + else if ($this->IsParameterized() && $this->oParameters->count()) + { + $sResult = $this->sValue.'; '.$this->oParameters->ToString(true); + } + else if ($this->IsEmail()) + { + $oEmailCollection = new EmailCollection($this->sFullValue); + if ($oEmailCollection && $oEmailCollection->count()) { + $sResult = $oEmailCollection->ToString(true); + } + } + + // https://www.rfc-editor.org/rfc/rfc2822#section-2.1.1, avoid folding immediately after the header name + $sName = $this->sName . ': '; + return $sName . \wordwrap($sResult, 78 - \strlen($sName) - 1, "\r\n "); + } + + private function IsSubject() : bool + { + return \strtolower(Enumerations\Header::SUBJECT) === \strtolower($this->Name()); + } + + private function IsParameterized() : bool + { + return \in_array(\strtolower($this->sName), array( + \strtolower(Enumerations\Header::CONTENT_TYPE), + \strtolower(Enumerations\Header::CONTENT_DISPOSITION) +// ,\strtolower(Enumerations\Header::AUTOCRYPT) + )); + } + + private function IsEmail() : bool + { + return \in_array(\strtolower($this->sName), array( + \strtolower(Enumerations\Header::FROM_), + \strtolower(Enumerations\Header::TO_), + \strtolower(Enumerations\Header::CC), + \strtolower(Enumerations\Header::BCC), + \strtolower(Enumerations\Header::REPLY_TO), +// \strtolower(Enumerations\Header::RETURN_PATH), + \strtolower(Enumerations\Header::SENDER) + )); + } + + public function ValueWithCharsetAutoDetect() : string + { + if (!\MailSo\Base\Utils::IsAscii($this->Value()) + && \strlen($this->sEncodedValue) + && !\MailSo\Base\Utils::IsAscii($this->sEncodedValue) + && ($mEncoding = \mb_detect_encoding($this->sEncodedValue, 'auto', true)) + ) { + $this->SetParentCharset($mEncoding); + } + return $this->Value(); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $aResult = array( + '@Object' => 'Object/MimeHeader', + 'name' => $this->sName, + 'value' => $this->sValue // $this->EncodedValue() + ); + if ($this->oParameters->count()) { + $aResult['parameters'] = $this->oParameters; + } + return $aResult; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php new file mode 100644 index 0000000000..5b3674dbeb --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php @@ -0,0 +1,240 @@ +Parse($sRawHeaders, $sParentCharset); + } + } + + public function append($oHeader, bool $bToTop = false) : void + { + assert($oHeader instanceof Header); + parent::append($oHeader, $bToTop); + } + + public function AddByName(string $sName, string $sValue, bool $bToTop = false) : self + { + $this->append(new Header($sName, $sValue), $bToTop); + return $this; + } + + public function SetByName(string $sName, string $sValue, bool $bToTop = false) : self + { + return $this->RemoveByName($sName)->Add(new Header($sName, $sValue), $bToTop); + } + + public function ValueByName(string $sHeaderName, bool $bCharsetAutoDetect = false) : string + { + $oHeader = $this->GetByName($sHeaderName); + return $oHeader ? ($bCharsetAutoDetect ? $oHeader->ValueWithCharsetAutoDetect() : $oHeader->Value()) : ''; + } + + public function ValuesByName(string $sHeaderName) : array + { + $aResult = array(); + $sHeaderNameLower = \strtolower($sHeaderName); + foreach ($this as $oHeader) { + if ($sHeaderNameLower === \strtolower($oHeader->Name())) { + $aResult[] = $oHeader->Value(); + } + } + return $aResult; + } + + public function RemoveByName(string $sHeaderName) : self + { + $sHeaderName = \strtolower($sHeaderName); + $this->exchangeArray(array_filter($this->getArrayCopy(), function ($oHeader) use ($sHeaderName) { + return $oHeader && \strtolower($oHeader->Name()) !== $sHeaderName; + })); + return $this; + } + + public function GetAsEmailCollection(string $sHeaderName) : ?EmailCollection + { + if ($oHeader = $this->GetByName($sHeaderName)) { + return new EmailCollection($oHeader->EncodedValue()); + } + return new EmailCollection(); + } + + public function ParameterValue(string $sHeaderName, string $sParamName) : string + { + $oHeader = $this->GetByName($sHeaderName); + $oParameters = $oHeader ? $oHeader->Parameters() : null; + return (null !== $oParameters) ? $oParameters->ParameterValueByName($sParamName) : ''; + } + + public function GetByName(string $sHeaderName) : ?Header + { + $sHeaderNameLower = \strtolower($sHeaderName); + foreach ($this as $oHeader) { + if ($sHeaderNameLower === \strtolower($oHeader->Name())) { + return $oHeader; + } + } + return null; + } + + public function SetParentCharset(string $sParentCharset) : self + { + if (\strlen($sParentCharset) && $this->sParentCharset !== $sParentCharset) { + foreach ($this as $oHeader) { + $oHeader->SetParentCharset($sParentCharset); + } + $this->sParentCharset = $sParentCharset; + } + return $this; + } + + public function Parse(string $sRawHeaders, string $sParentCharset = '') : self + { + $this->Clear(); + + if (\strlen($this->sParentCharset)) { + $this->sParentCharset = $sParentCharset; + } + + $aHeaders = \explode("\n", \str_replace("\r", '', $sRawHeaders)); + + $sName = null; + $sValue = null; + foreach ($aHeaders as $sHeadersValue) { + if (!\strlen($sHeadersValue)) { + continue; + } + + $sFirstChar = \substr($sHeadersValue, 0, 1); + if ($sFirstChar !== ' ' && $sFirstChar !== "\t" && false === \strpos($sHeadersValue, ':')) { + continue; + } + if (null !== $sName && ($sFirstChar === ' ' || $sFirstChar === "\t")) { + $sValue = \is_null($sValue) ? '' : $sValue; + + if ('?=' === \substr(\rtrim($sHeadersValue), -2)) { + $sHeadersValue = \rtrim($sHeadersValue); + } + + if ('=?' === \substr(\ltrim($sHeadersValue), 0, 2)) { + $sHeadersValue = \ltrim($sHeadersValue); + } + + if ('=?' === \substr($sHeadersValue, 0, 2)) { + $sValue .= $sHeadersValue; + } else { + $sValue .= "\n".$sHeadersValue; + } + } else { + if (null !== $sName) { + $oHeader = Header::NewInstanceFromEncodedString($sName.': '.$sValue, $this->sParentCharset); + if ($oHeader) { + $this->append($oHeader); + } + + $sName = null; + $sValue = null; + } + + $aHeaderParts = \explode(':', $sHeadersValue, 2); + $sName = $aHeaderParts[0]; + $sValue = isset($aHeaderParts[1]) ? $aHeaderParts[1] : ''; + + if ('?=' === \substr(\rtrim($sValue), -2)) { + $sValue = \rtrim($sValue); + } + } + } + + if (null !== $sName) { + $oHeader = Header::NewInstanceFromEncodedString($sName.': '.$sValue, $this->sParentCharset); + if ($oHeader) { + $this->append($oHeader); + } + } + + return $this; + } + + /** + * https://www.rfc-editor.org/rfc/rfc8601 + * dkim=pass header.d=domain.tld header.s=s1 header.b=F2SfoZWw; + * spf=pass (ORIGINATING: domain of "snappymail@domain.tld" designates 0.0.0.0 as permitted sender) smtp.mailfrom="snappymail@domain.tld"; + * dmarc=fail reason="SPF not aligned (relaxed), DKIM not aligned (relaxed)" header.from=domain.tld (policy=none) + */ + public function AuthStatuses() : array + { + $aResult = [ + 'dkim' => [], + 'dmarc' => [], + 'spf' => [] + ]; + $aHeaders = $this->ValuesByName(Enumerations\Header::AUTHENTICATION_RESULTS); + if (\count($aHeaders)) { + $aHeaders = \implode(';', $aHeaders); + $aHeaders = \preg_replace('/[\\r\\n\\t\\s]+/', ' ', $aHeaders); + $aHeaders = \str_replace('-bit key;', '-bit key,', $aHeaders); + $aHeaders = \explode(';', $aHeaders); + foreach ($aHeaders as $sLine) { + $aStatus = array(); + $aHeader = array(); + if (\preg_match("/(dkim|dmarc|spf)=([a-z0-9]+).*?(;|$)/Di", $sLine, $aStatus) + && \preg_match('/(?:header\\.(?:d|i|from)|smtp.mailfrom)="?([^\\s;"]+)/i', $sLine, $aHeader) + ) { + $sType = \strtolower($aStatus[1]); + $aResult[$sType][] = array(\strtolower($aStatus[2]), $aHeader[1], \trim($sLine)); + } + } + } + if (!\count($aResult['dkim'])) { + // X-DKIM-Authentication-Results: signer="hostinger.com" status="pass" + $aHeaders = $this->ValuesByName(Enumerations\Header::X_DKIM_AUTHENTICATION_RESULTS); + foreach ($aHeaders as $sHeaderValue) { + $aStatus = array(); + $aHeader = array(); + $sHeaderValue = \preg_replace('/[\\r\\n\\t\\s]+/', ' ', $sHeaderValue); + if (\preg_match('/status[\\s]?=[\\s]?"([a-zA-Z0-9]+)"/i', $sHeaderValue, $aStatus) && !empty($aStatus[1]) + && \preg_match('/signer[\\s]?=[\\s]?"([^";]+)"/i', $sHeaderValue, $aHeader) && !empty($aHeader[1]) + ) { + $aResult['dkim'][] = array($aStatus[1], \trim($aHeader[1]), $sHeaderValue); + } + } + } + + return $aResult; + } + + public function __toString() : string + { + return \implode("\r\n", $this->getArrayCopy()); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( + '@Object' => 'Collection/MimeHeaderCollection', + '@Collection' => $this->getArrayCopy() + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Message.php new file mode 100644 index 0000000000..48253b33de --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Message.php @@ -0,0 +1,443 @@ + '', + Enumerations\Header::CC => '', + Enumerations\Header::DATE => '', + Enumerations\Header::DISPOSITION_NOTIFICATION_TO => '', + Enumerations\Header::FROM_ => '', + Enumerations\Header::IN_REPLY_TO => '', + Enumerations\Header::MESSAGE_ID => '', + Enumerations\Header::MIME_VERSION => '', + Enumerations\Header::REFERENCES => '', + Enumerations\Header::REPLY_TO => '', + Enumerations\Header::SENDER => '', + Enumerations\Header::SUBJECT => '', + Enumerations\Header::TO_ => '', + Enumerations\Header::X_CONFIRM_READING_TO => '', + Enumerations\Header::X_DRAFT_INFO => '', + Enumerations\Header::X_MAILER => '', + Enumerations\Header::X_PRIORITY => '', +*/ + ); + + private AttachmentCollection $oAttachmentCollection; + + private bool $bAddEmptyTextPart = true; + + private bool $bAddDefaultXMailer = true; + + function __construct() + { + parent::__construct(); + $this->oAttachmentCollection = new AttachmentCollection; + } + + private function getHeaderValue(string $name) + { + return isset($this->aHeadersValue[$name]) + ? $this->aHeadersValue[$name] + : null; + } + + public function DoesNotAddDefaultXMailer() : void + { + $this->bAddDefaultXMailer = false; + } + + public function MessageId() : string + { + return $this->getHeaderValue(Enumerations\Header::MESSAGE_ID) ?: ''; + } + + public function SetMessageId(string $sMessageId) : void + { + $this->aHeadersValue[Enumerations\Header::MESSAGE_ID] = $sMessageId; + } + + public function RegenerateMessageId(string $sHostName = '') : void + { + $this->SetMessageId($this->generateNewMessageId($sHostName)); + } + + public function Attachments() : AttachmentCollection + { + return $this->oAttachmentCollection; + } + + public function GetSubject() : string + { + return $this->getHeaderValue(Enumerations\Header::SUBJECT) ?: ''; + } + + public function GetFrom() : ?Email + { + $value = $this->getHeaderValue(Enumerations\Header::FROM_); + return ($value instanceof Email) ? $value : null; + } + + public function GetTo() : ?EmailCollection + { + $value = $this->getHeaderValue(Enumerations\Header::TO_); + return ($value instanceof EmailCollection) ? $value->Unique() : null; + } + + public function GetCc() : ?EmailCollection + { + $value = $this->getHeaderValue(Enumerations\Header::CC); + return ($value instanceof EmailCollection) ? $value->Unique() : null; + } + + public function GetBcc() : ?EmailCollection + { + $value = $this->getHeaderValue(Enumerations\Header::BCC); + return ($value instanceof EmailCollection) ? $value->Unique() : null; + } + + public function GetRcpt() : EmailCollection + { + $oResult = new EmailCollection; + + $headers = array(Enumerations\Header::TO_, Enumerations\Header::CC, Enumerations\Header::BCC); + foreach ($headers as $header) { + $value = $this->getHeaderValue($header); + if ($value instanceof EmailCollection) { + foreach ($value as $oEmail) { + $oResult->append($oEmail); + } + } + } + +/* + $aReturn = array(); + $headers = array(Enumerations\Header::TO_, Enumerations\Header::CC, Enumerations\Header::BCC); + foreach ($headers as $header) { + $value = $this->getHeaderValue($header); + if ($value instanceof EmailCollection) { + foreach ($value as $oEmail) { + $oResult->append($oEmail); + $sEmail = $oEmail->GetEmail(); + if (!isset($aReturn[$sEmail])) { + $aReturn[$sEmail] = $oEmail; + } + } + } + } + return new EmailCollection($aReturn); +*/ + + return $oResult->Unique(); + } + + public function SetCustomHeader(string $sHeaderName, string $sValue) : self + { + $sHeaderName = \trim($sHeaderName); + if (\strlen($sHeaderName)) { + $this->aHeadersValue[$sHeaderName] = $sValue; + } + + return $this; + } + + public function SetAutocrypt(array $aValue) : self + { + $this->aHeadersValue['Autocrypt'] = $aValue; + return $this; + } + + public function SetSubject(string $sSubject) : self + { + $this->aHeadersValue[Enumerations\Header::SUBJECT] = $sSubject; + + return $this; + } + + public function SetInReplyTo(string $sInReplyTo) : self + { + $sInReplyTo = \trim($sInReplyTo); + if (\strlen($sInReplyTo)) { + $this->aHeadersValue[Enumerations\Header::IN_REPLY_TO] = $sInReplyTo; + } + return $this; + } + + public function SetReferences(string $sReferences) : self + { + $sReferences = \MailSo\Base\Utils::StripSpaces($sReferences); + if (\strlen($sReferences)) { + $this->aHeadersValue[Enumerations\Header::REFERENCES] = $sReferences; + } + return $this; + } + + public function SetReadReceipt(string $sEmail) : self + { + $this->aHeadersValue[Enumerations\Header::DISPOSITION_NOTIFICATION_TO] = $sEmail; + $this->aHeadersValue[Enumerations\Header::X_CONFIRM_READING_TO] = $sEmail; + + return $this; + } + + public function SetPriority(int $iValue) : self + { + $sResult = ''; + switch ($iValue) + { + case Enumerations\MessagePriority::HIGH: + $sResult = Enumerations\MessagePriority::HIGH.' (Highest)'; + break; + case Enumerations\MessagePriority::NORMAL: + $sResult = Enumerations\MessagePriority::NORMAL.' (Normal)'; + break; + case Enumerations\MessagePriority::LOW: + $sResult = Enumerations\MessagePriority::LOW.' (Lowest)'; + break; + } + + if (\strlen($sResult)) { + $this->aHeadersValue[Enumerations\Header::X_PRIORITY] = $sResult; + } + + return $this; + } + + public function SetXMailer(string $sXMailer) : self + { + $this->aHeadersValue[Enumerations\Header::X_MAILER] = $sXMailer; + + return $this; + } + + public function SetFrom(Email $oEmail) : self + { + $this->aHeadersValue[Enumerations\Header::FROM_] = $oEmail; + + return $this; + } + + public function SetTo(EmailCollection $oEmails) : self + { + if ($oEmails->count()) { + $this->aHeadersValue[Enumerations\Header::TO_] = $oEmails; + } + return $this; + } + + public function SetDate(int $iDateTime) : self + { + $this->aHeadersValue[Enumerations\Header::DATE] = \gmdate('r', $iDateTime); + + return $this; + } + + public function SetReplyTo(EmailCollection $oEmails) : self + { + if ($oEmails->count()) { + $this->aHeadersValue[Enumerations\Header::REPLY_TO] = $oEmails; + } + return $this; + } + + public function SetCc(EmailCollection $oEmails) : self + { + if ($oEmails->count()) { + $this->aHeadersValue[Enumerations\Header::CC] = $oEmails; + } + return $this; + } + + public function SetBcc(EmailCollection $oEmails) : self + { + if ($oEmails->count()) { + $this->aHeadersValue[Enumerations\Header::BCC] = $oEmails; + } + return $this; + } + + public function SetSender(Email $oEmail) : self + { + $this->aHeadersValue[Enumerations\Header::SENDER] = $oEmail; + + return $this; + } + + public function SetDraftInfo(string $sType, int $iUid, string $sFolder) : self + { + $this->aHeadersValue[Enumerations\Header::X_DRAFT_INFO] = (new ParameterCollection) + ->Add(new Parameter('type', $sType)) + ->Add(new Parameter('uid', $iUid)) + ->Add(new Parameter('folder', \base64_encode($sFolder))) + ; + + return $this; + } + + private function generateNewMessageId(string $sHostName = '') : string + { + if (!\strlen($sHostName)) { + $sHostName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''; + } + + if (empty($sHostName) && \MailSo\Base\Utils::FunctionCallable('php_uname')) { + $sHostName = \php_uname('n'); + } + + if (empty($sHostName)) { + $sHostName = 'localhost'; + } + + return '<'. + \MailSo\Base\Utils::Sha1Rand($sHostName. + (\MailSo\Base\Utils::FunctionCallable('getmypid') ? \getmypid() : '')).'@'.$sHostName.'>'; + } + + public function GetRootPart() : Part + { + if (!\count($this->SubParts)) { + if ($this->bAddEmptyTextPart) { + $oPart = new Part; + $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'text/plain; charset="utf-8"'); + $oPart->Body = ''; + $this->SubParts->append($oPart); + } else { + $aAttachments = $this->oAttachmentCollection->getArrayCopy(); + if (1 === \count($aAttachments) && isset($aAttachments[0])) { + $this->oAttachmentCollection->Clear(); + + $oPart = new Part; + $oParameters = new ParameterCollection; + $oParameters->append( + new Parameter( + Enumerations\Parameter::CHARSET, + \MailSo\Base\Enumerations\Charset::UTF_8) + ); + $params = $aAttachments[0]->CustomContentTypeParams(); + if ($params && \is_array($params)) { + foreach ($params as $sName => $sValue) { + $oParameters->append(new Parameter($sName, $sValue)); + } + } + $oPart->Headers->append( + new Header(Enumerations\Header::CONTENT_TYPE, + $aAttachments[0]->ContentType().'; '.$oParameters) + ); + + if ($resource = $aAttachments[0]->Resource()) { + if (\is_resource($resource)) { + $oPart->Body = $resource; + } else if (\is_string($resource) && \strlen($resource)) { + $oPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString($resource); + } + } + if (!\is_resource($oPart->Body)) { + $oPart->Body = ''; + } + + $this->SubParts->append($oPart); + } + } + } + + $oRootPart = $oRelatedPart = null; + if (1 == \count($this->SubParts)) { + $oRootPart = $this->SubParts[0]; + foreach ($this->oAttachmentCollection as $oAttachment) { + if ($oAttachment->isLinked()) { + $oRelatedPart = new Part; + $oRelatedPart->Headers->append( + new Header(Enumerations\Header::CONTENT_TYPE, 'multipart/related') + ); + $oRelatedPart->SubParts->append($oRootPart); + $oRootPart = $oRelatedPart; + break; + } + } + } else { + $oRootPart = new Part; + $oRootPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'multipart/mixed'); + $oRootPart->SubParts = $this->SubParts; + } + + $oMixedPart = null; + foreach ($this->oAttachmentCollection as $oAttachment) { + if ($oRelatedPart && $oAttachment->isLinked()) { + $oRelatedPart->SubParts->append($oAttachment->ToPart()); + } else { + if (!$oMixedPart) { + $oMixedPart = new Part; + $oMixedPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'multipart/mixed'); + $oMixedPart->SubParts->append($oRootPart); + $oRootPart = $oMixedPart; + } + $oMixedPart->SubParts->append($oAttachment->ToPart()); + } + } + + return $oRootPart; + } + + /** + * @return resource|bool + */ + public function ToStream(bool $bWithoutBcc = false) + { + $oRootPart = $this->GetRootPart(); + + /** + * setDefaultHeaders + */ + if (!isset($this->aHeadersValue[Enumerations\Header::DATE])) { + $oRootPart->Headers->SetByName(Enumerations\Header::DATE, \gmdate('r'), true); + } + + if (!isset($this->aHeadersValue[Enumerations\Header::MESSAGE_ID])) { + $oRootPart->Headers->SetByName(Enumerations\Header::MESSAGE_ID, $this->generateNewMessageId(), true); + } + + if ($this->bAddDefaultXMailer && !isset($this->aHeadersValue[Enumerations\Header::X_MAILER])) { + $oRootPart->Headers->SetByName(Enumerations\Header::X_MAILER, 'SnappyMail/'.APP_VERSION, true); + } + + if (!isset($this->aHeadersValue[Enumerations\Header::MIME_VERSION])) { + $oRootPart->Headers->SetByName(Enumerations\Header::MIME_VERSION, '1.0', true); + } + + foreach ($this->aHeadersValue as $sName => $mValue) { + if ('autocrypt' === \strtolower($sName)) { + foreach ($mValue as $key) { + $oRootPart->Headers->AddByName($sName, $key); + } + } else if (!($bWithoutBcc && \strtolower(Enumerations\Header::BCC) === \strtolower($sName))) { + $oRootPart->Headers->SetByName($sName, (string) $mValue); + } + } + + $resource = $oRootPart->ToStream(); + \MailSo\Base\StreamFilters\LineEndings::appendTo($resource); + return $resource; + } +/* + public function ToString(bool $bWithoutBcc = false) : string + { + return \stream_get_contents($this->ToStream($bWithoutBcc)); + } +*/ +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php new file mode 100644 index 0000000000..e73ce1df58 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php @@ -0,0 +1,88 @@ +sName = $sName; + $this->sValue = $sValue; + } + + public static function FromString(string $sRawParam) : self + { + $oParameter = new self('', ''); + + $aParts = \explode('=', $sRawParam, 2); + $oParameter->sName = \trim(\trim($aParts[0]), '"\''); + if (2 === \count($aParts)) { + $oParameter->sValue = \trim(\trim($aParts[1]), '"\''); + } + + return $oParameter; + } + + public function Name() : string + { + return $this->sName; + } + + public function Value() : string + { + return $this->sValue; + } + + public function setValue(string $sValue) : void + { + $this->sValue = $sValue; + } + + public function ToString(bool $bConvertSpecialsName = false) : string + { + if (!\strlen($this->sName)) { + return ''; + } + + if ($bConvertSpecialsName && \in_array(\strtolower($this->sName), array( + \strtolower(Enumerations\Parameter::NAME), + \strtolower(Enumerations\Parameter::FILENAME) + ))) + { + return $this->sName . '="' . \MailSo\Base\Utils::EncodeHeaderValue($this->sValue) . '"'; + } + + return $this->sName . '="' . $this->sValue . '"'; + } + + public function __toString() : string + { + return $this->ToString(); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( + 'name' => $this->sName, + 'value' => $this->sValue + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/ParameterCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/ParameterCollection.php new file mode 100644 index 0000000000..688fa48eaf --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/ParameterCollection.php @@ -0,0 +1,169 @@ +Parse($sRawParams); + } + + public function append($oParameter, bool $bToTop = false) : void + { + assert($oParameter instanceof Parameter); + parent::offsetSet(\strtolower($oParameter->Name()), $oParameter); + } + + public function ParameterValueByName(string $sName) : string + { + $oParam = $this->getParameter($sName); + return $oParam ? $oParam->Value() : ''; + } + + public function getParameter(string $sName) : ?Parameter + { + $sName = \strtolower(\trim($sName)); + return parent::offsetExists($sName) ? parent::offsetGet($sName) : null; + } + + public function setParameter(string $sName, string $sValue) : void + { + $oParam = $this->getParameter($sName); + if ($oParam) { + $oParam->setValue($sValue); + } else { + $this->append(new Parameter(\trim($sName), $sValue)); + } + } + + public function Parse(string $sRawParams) : self + { + $this->Clear(); + + $aDataToParse = \explode(';', $sRawParams); + + foreach ($aDataToParse as $sParam) { + $this->append(Parameter::FromString($sParam)); + } + + $this->reParseParameters(); + + return $this; + } + + public function ToString(bool $bConvertSpecialsName = false) : string + { + $aResult = array(); + foreach ($this as $oParam) { + $sLine = $oParam->ToString($bConvertSpecialsName); + if (\strlen($sLine)) { + $aResult[] = $sLine; + } + } + return \count($aResult) ? \implode('; ', $aResult) : ''; + } + + public function __toString() : string + { + return $this->ToString(); + } + +/* + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $aResult = array(); + foreach ($this as $oParam) { + $aResult[$oParam->Name()] = $oParam->Value(); + } + return array( + '@Object' => 'Collection/ParameterCollection', + '@Collection' => $aResult + ); + } +*/ + + private function reParseParameters() : void + { + $aDataToReParse = $this->getArrayCopy(); + $sCharset = \MailSo\Base\Enumerations\Charset::UTF_8; + + $this->Clear(); + + $aPreParams = array(); + foreach ($aDataToReParse as $oParam) { + $aMatch = array(); + $sParamName = $oParam->Name(); + + if (\preg_match('/([^\*]+)\*([\d]{1,2})\*/', $sParamName, $aMatch) && isset($aMatch[1], $aMatch[2]) + && \strlen($aMatch[1]) && \is_numeric($aMatch[2])) + { + if (!isset($aPreParams[$aMatch[1]])) { + $aPreParams[$aMatch[1]] = array(); + } + + $sValue = $oParam->Value(); + + if (false !== \strpos($sValue, "''")) { + $aValueParts = \explode("''", $sValue, 2); + if (\is_array($aValueParts) && 2 === \count($aValueParts) && \strlen($aValueParts[1])) { + $sCharset = $aValueParts[0]; + $sValue = $aValueParts[1]; + } + } + + $aPreParams[$aMatch[1]][(int) $aMatch[2]] = $sValue; + } + else if (\preg_match('/([^\*]+)\*/', $sParamName, $aMatch) && isset($aMatch[1])) + { + if (!isset($aPreParams[$aMatch[1]])) { + $aPreParams[$aMatch[1]] = array(); + } + + $sValue = $oParam->Value(); + if (false !== \strpos($sValue, "''")) { + $aValueParts = \explode("''", $sValue, 2); + if (\is_array($aValueParts) && 2 === \count($aValueParts) && \strlen($aValueParts[1])) { + $sCharset = $aValueParts[0]; + $sValue = $aValueParts[1]; + } + } + + $aPreParams[$aMatch[1]][0] = $sValue; + } + else + { + $this->append($oParam); + } + } + + foreach ($aPreParams as $sName => $aValues) { + \ksort($aValues); + $sResult = \implode(\array_values($aValues)); + $sResult = \urldecode($sResult); + + if (\strlen($sCharset)) { + $sResult = \MailSo\Base\Utils::ConvertEncoding($sResult, + $sCharset, \MailSo\Base\Enumerations\Charset::UTF_8); + } + + $this->append(new Parameter($sName, $sResult)); + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parser.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parser.php new file mode 100644 index 0000000000..b5c77755f1 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parser.php @@ -0,0 +1,264 @@ +Body) { + $oPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResource(); + } + if (\is_resource($oPart->Body)) { + \fwrite($oPart->Body, $sBuffer); + } + } + + /** + * @param resource $rStreamHandle + */ + public static function parseStream($rStreamHandle) : ?Part + { + if (!\is_resource($rStreamHandle)) { + return null; + } + + $oPart = new Part; + + $bIsOef = false; + $iOffset = 0; + $sBuffer = ''; + $sPrevBuffer = ''; + $aBoundaryStack = array(); + + static::$LineParts = [$oPart]; + + static::parseStreamRecursive($oPart, $rStreamHandle, $iOffset, + $sPrevBuffer, $sBuffer, $aBoundaryStack, $bIsOef); + + $oMimePart = null; + $sFirstNotNullCharset = null; + foreach (static::$LineParts as /* @var $oMimePart Part */ $oMimePart) { + $sCharset = $oMimePart->HeaderCharset(); + if (\strlen($sCharset)) { + $sFirstNotNullCharset = $sCharset; + break; + } + } + + $sFirstNotNullCharset = (null !== $sFirstNotNullCharset) + ? $sFirstNotNullCharset : \MailSo\Base\Enumerations\Charset::ISO_8859_1; + + foreach (static::$LineParts as /* @var $oMimePart Part */ $oMimePart) { + $sHeaderCharset = $oMimePart->HeaderCharset(); + $oMimePart->Headers->SetParentCharset($sHeaderCharset); + } + + static::$LineParts = []; + return $oPart; + } + + /** + * @param resource $rStreamHandle + */ + public static function parseStreamRecursive(Part $oPart, $rStreamHandle, int &$iOffset, + string &$sPrevBuffer, string &$sBuffer, array &$aBoundaryStack, bool &$bIsOef, bool $bNotFirstRead = false) : void + { + $iPos = 0; + $iParsePosition = self::POS_HEADERS; + $sCurrentBoundary = ''; + $bIsBoundaryCheck = false; + $aHeadersLines = array(); + while (true) { + if (!$bNotFirstRead) { + $sPrevBuffer = $sBuffer; + $sBuffer = ''; + } + + if (!$bIsOef && !\feof($rStreamHandle)) { + if (!$bNotFirstRead) { + $sBuffer = \fread($rStreamHandle, 8192); + if (false === $sBuffer) { + break; + } + } else { + $bNotFirstRead = false; + } + } else if ($bIsOef && !\strlen($sBuffer)) { + break; + } else { + $bIsOef = true; + } + + while (true) { + $sCurrentLine = $sPrevBuffer.$sBuffer; + if (self::POS_HEADERS === $iParsePosition) { + $iEndLen = 4; + $iPos = \strpos($sCurrentLine, "\r\n\r\n", $iOffset); + if (false === $iPos) { + $iEndLen = 2; + $iPos = \strpos($sCurrentLine, "\n\n", $iOffset); + } + + if (false !== $iPos) { + $aHeadersLines[] = \substr($sCurrentLine, $iOffset, $iPos + $iEndLen - $iOffset); + + $oPart->Headers->Parse(\implode($aHeadersLines))->SetParentCharset($oPart->HeaderCharset()); + $aHeadersLines = array(); + + $sBoundary = $oPart->HeaderBoundary(); + if (\strlen($sBoundary)) + { + $sBoundary = '--'.$sBoundary; + $sCurrentBoundary = $sBoundary; + \array_unshift($aBoundaryStack, $sBoundary); + } + + $iOffset = $iPos + $iEndLen; + $iParsePosition = self::POS_BODY; + continue; + } else { + $iBufferLen = \strlen($sPrevBuffer); + if ($iBufferLen > $iOffset) { + $aHeadersLines[] = \substr($sPrevBuffer, $iOffset); + $iOffset = 0; + } else { + $iOffset -= $iBufferLen; + } + break; + } + } else if (self::POS_BODY === $iParsePosition) { + $iPos = false; + $sBoundaryLen = 0; + $bIsBoundaryEnd = false; + $bCurrentPartBody = false; + $bIsBoundaryCheck = \count($aBoundaryStack); + + foreach ($aBoundaryStack as $sKey => $sBoundary) { + if (false !== ($iPos = \strpos($sCurrentLine, $sBoundary, $iOffset))) { + if ($sCurrentBoundary === $sBoundary) { + $bCurrentPartBody = true; + } + + $sBoundaryLen = \strlen($sBoundary); + if ('--' === \substr($sCurrentLine, $iPos + $sBoundaryLen, 2)) { + $sBoundaryLen += 2; + $bIsBoundaryEnd = true; + unset($aBoundaryStack[$sKey]); + $sCurrentBoundary = (isset($aBoundaryStack[$sKey + 1])) + ? $aBoundaryStack[$sKey + 1] : ''; + } + + break; + } + } + + if (false !== $iPos) { + static::writeBody($oPart, \substr($sCurrentLine, $iOffset, $iPos - $iOffset)); + $iOffset = $iPos; + + if ($bCurrentPartBody) { + $iParsePosition = self::POS_SUBPARTS; + continue; + } + + return; + } else { + $iBufferLen = \strlen($sPrevBuffer); + if ($iBufferLen > $iOffset) { + static::writeBody($oPart, \substr($sPrevBuffer, $iOffset)); + $iOffset = 0; + } else { + $iOffset -= $iBufferLen; + } + break; + } + } else if (self::POS_SUBPARTS === $iParsePosition) { + $iPos = false; + $iBoundaryLen = 0; + $bIsBoundaryEnd = false; + $bCurrentPartBody = false; + $bIsBoundaryCheck = \count($aBoundaryStack); + + foreach ($aBoundaryStack as $sKey => $sBoundary) { + $iPos = \strpos($sCurrentLine, $sBoundary, $iOffset); + if (false !== $iPos) { + if ($sCurrentBoundary === $sBoundary) { + $bCurrentPartBody = true; + } + + $iBoundaryLen = \strlen($sBoundary); + if ('--' === \substr($sCurrentLine, $iPos + $iBoundaryLen, 2)) { + $iBoundaryLen += 2; + $bIsBoundaryEnd = true; + unset($aBoundaryStack[$sKey]); + $sCurrentBoundary = (isset($aBoundaryStack[$sKey + 1])) + ? $aBoundaryStack[$sKey + 1] : ''; + } + break; + } + } + + if (false !== $iPos && $bCurrentPartBody) { + $iOffset = $iPos + $iBoundaryLen; + + $oSubPart = new Part; + + static::parseStreamRecursive($oSubPart, $rStreamHandle, + $iOffset, $sPrevBuffer, $sBuffer, $aBoundaryStack, $bIsOef, true); + + $oPart->SubParts->append($oSubPart); + static::$LineParts[] = $oSubPart; + //$iParsePosition = self::POS_HEADERS; + unset($oSubPart); + } else { + return; + } + } + } + } + + if (\strlen($sPrevBuffer)) { + if (self::POS_HEADERS === $iParsePosition) { + $aHeadersLines[] = ($iOffset < \strlen($sPrevBuffer)) + ? \substr($sPrevBuffer, $iOffset) + : $sPrevBuffer; + + $oPart->Headers->Parse(\implode($aHeadersLines))->SetParentCharset($oPart->HeaderCharset()); + $aHeadersLines = array(); + } else if (!$bIsBoundaryCheck && self::POS_BODY === $iParsePosition) { + static::writeBody($oPart, ($iOffset < \strlen($sPrevBuffer)) + ? \substr($sPrevBuffer, $iOffset) : $sPrevBuffer); + } + } else { + if (self::POS_HEADERS === $iParsePosition && \count($aHeadersLines)) { + $oPart->Headers->Parse(\implode($aHeadersLines))->SetParentCharset($oPart->HeaderCharset()); + $aHeadersLines = array(); + } + } + + return; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php new file mode 100644 index 0000000000..79b8e07ea4 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php @@ -0,0 +1,230 @@ +Headers = new HeaderCollection; + $this->SubParts = new PartCollection; + } + + public function HeaderCharset() : string + { + return \trim(\strtolower($this->Headers->ParameterValue(Enumerations\Header::CONTENT_TYPE, Enumerations\Parameter::CHARSET))); + } + + public function HeaderBoundary() : string + { + return \trim($this->Headers->ParameterValue(Enumerations\Header::CONTENT_TYPE, Enumerations\Parameter::BOUNDARY)); + } + + public function ContentType() : string + { + return \trim(\strtolower($this->Headers->ValueByName(Enumerations\Header::CONTENT_TYPE))); + } + + public function IsFlowedFormat() : bool + { + $bResult = 'flowed' === \trim(\strtolower($this->Headers->ParameterValue( + Enumerations\Header::CONTENT_TYPE, + Enumerations\Parameter::FORMAT))); + + if ($bResult && \in_array($this->ContentTransferEncoding(), array('base64', 'quoted-printable'))) { + $bResult = false; + } + + return $bResult; + } + + public function FileName() : string + { + $sResult = \trim($this->Headers->ParameterValue( + Enumerations\Header::CONTENT_DISPOSITION, + Enumerations\Parameter::FILENAME)); + + if (!\strlen($sResult)) { + $sResult = \trim($this->Headers->ParameterValue( + Enumerations\Header::CONTENT_TYPE, + Enumerations\Parameter::NAME)); + } + + return $sResult; + } + + // https://datatracker.ietf.org/doc/html/rfc3156#section-5 + public function isPgpSigned() : bool + { + $header = $this->Headers->GetByName(Enumerations\Header::CONTENT_TYPE); + return $header + && \preg_match('#multipart/signed.+protocol=["\']?application/pgp-signature#si', $header->FullValue()) + // The multipart/signed body MUST consist of exactly two parts. + && 2 === \count($this->SubParts) + && 'application/pgp-signature' === $this->SubParts[1]->ContentType(); + } + + // https://www.rfc-editor.org/rfc/rfc8551.html#section-3.5 + public function isSMimeSigned() : bool + { + $header = $this->Headers->GetByName(Enumerations\Header::CONTENT_TYPE); + return ($header + && \preg_match('#multipart/signed.+protocol=["\']?application/(x-)?pkcs7-signature#si', $header->FullValue()) + // The multipart/signed body MUST consist of exactly two parts. + && 2 === \count($this->SubParts) + && ContentType::isPkcs7Signature($this->SubParts[1]->ContentType()) + ) || ($header + && \preg_match('#application/(x-)?pkcs7-mime.+smime-type=["\']?signed-data#si', $header->FullValue()) + ); + } + + public static function FromFile(string $sFileName) : ?self + { + $rStreamHandle = \file_exists($sFileName) ? \fopen($sFileName, 'rb') : false; + if ($rStreamHandle) { + try { + return Parser::parseStream($rStreamHandle); + } finally { + \fclose($rStreamHandle); + } + } + return null; + } + + public static function FromString(string $sRawMessage) : ?self + { + $rStreamHandle = \strlen($sRawMessage) ? + \MailSo\Base\ResourceRegistry::CreateMemoryResource() : false; + if ($rStreamHandle) { + \fwrite($rStreamHandle, $sRawMessage); + unset($sRawMessage); + \fseek($rStreamHandle, 0); + + try { + return Parser::parseStream($rStreamHandle); + } finally { + \MailSo\Base\ResourceRegistry::CloseMemoryResource($rStreamHandle); + } + } + return null; + } + + /** + * @param resource $rStreamHandle + */ + public static function FromStream($rStreamHandle) : ?Part + { + return Parser::parseStream($rStreamHandle); + } + + /** + * @return resource|bool + */ + public function ToStream() + { + if ($this->Raw) { + $aSubStreams = array( + $this->Raw + ); + } else { + if ($this->SubParts->count()) { + $sBoundary = $this->HeaderBoundary(); + if (!\strlen($sBoundary)) { + $this->Headers->GetByName(Enumerations\Header::CONTENT_TYPE)->setParameter( + Enumerations\Parameter::BOUNDARY, + $this->SubParts->Boundary() + ); + } else { + $this->SubParts->SetBoundary($sBoundary); + } + } + + $aSubStreams = array( + $this->Headers . "\r\n" + ); + + if ($this->Body) { + $aSubStreams[0] .= "\r\n"; + if (\is_resource($this->Body)) { + $aMeta = \stream_get_meta_data($this->Body); + if (!empty($aMeta['seekable'])) { + \rewind($this->Body); + } + } + $aSubStreams[] = $this->Body; + } + + if ($this->SubParts->count()) { + $rSubPartsStream = $this->SubParts->ToStream(); + if (\is_resource($rSubPartsStream)) { + $aSubStreams[] = $rSubPartsStream; + } + } + } + + return \MailSo\Base\StreamWrappers\SubStreams::CreateStream($aSubStreams); + } + + public function addPgpEncrypted(string $sEncrypted) + { + $oPart = new self; + $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'multipart/encrypted; protocol="application/pgp-encrypted"'); + $this->SubParts->append($oPart); + + $oSubPart = new self; + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'application/pgp-encrypted'); + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_DISPOSITION, 'attachment'); + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oSubPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString('Version: 1'); + $oPart->SubParts->append($oSubPart); + + $oSubPart = new self; + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'application/octet-stream'); + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_DISPOSITION, 'inline; filename="msg.asc"'); + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oSubPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString($sEncrypted); + $oPart->SubParts->append($oSubPart); + } + + public function addPlain(string $sPlain) + { + $oPart = new self; + $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'text/plain; charset=utf-8'); + $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TRANSFER_ENCODING, 'quoted-printable'); + $oPart->Body = \MailSo\Base\StreamWrappers\Binary::CreateStream( + \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sPlain))), + 'convert.quoted-printable-encode' + ); + $this->SubParts->append($oPart); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/PartCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/PartCollection.php new file mode 100644 index 0000000000..bd7b8fa7cf --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/PartCollection.php @@ -0,0 +1,62 @@ +sBoundary) { + $this->sBoundary = + \MailSo\Config::$BoundaryPrefix + . \SnappyMail\UUID::generate() + . '-' . ++static::$increment; + + } + return $this->sBoundary; + } + + public function SetBoundary(string $sBoundary) : void + { + $this->sBoundary = $sBoundary; + } + + /** + * @return resource|bool|null + */ + public function ToStream() + { + if ($this->count() && $this->sBoundary) { + $aResult = array(); + foreach ($this as $oPart) { + $aResult[] = "\r\n--{$this->sBoundary}\r\n"; + $aResult[] = $oPart->ToStream(); + } + $aResult[] = "\r\n--{$this->sBoundary}--\r\n"; + return \MailSo\Base\StreamWrappers\SubStreams::CreateStream($aResult); + } + return null; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Net/ConnectSettings.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/ConnectSettings.php new file mode 100644 index 0000000000..3da9575929 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/ConnectSettings.php @@ -0,0 +1,149 @@ +ssl = new SSLContext; + } + + public function __get(string $name) + { + $name = \strtolower($name); + if ('passphrase' === $name || 'password' === $name) { + return $this->passphrase ? $this->passphrase->getValue() : ''; + } + if ('username' === $name || 'login' === $name) { + return $this->username; + } + } + + public function __set(string $name, + #[\SensitiveParameter] + $value + ) { + $name = \strtolower($name); + if ('passphrase' === $name || 'password' === $name) { + $this->passphrase = \is_string($value) ? new SensitiveString($value) : $value; + } + if ('username' === $name || 'login' === $name) { + $this->username = $value; + } + } + + public function fixUsername(string $value, bool $allowShorten = true) : string + { + $value = \SnappyMail\IDN::emailToAscii($value); +// $value = \SnappyMail\IDN::emailToAscii(\MailSo\Base\Utils::Trim($value)); + // Strip the domain part + if ($this->shortLogin && $allowShorten) { + $value = \MailSo\Base\Utils::getEmailAddressLocalPart($value); + } + // Convert to lowercase + if ($this->lowerLogin) { + $value = \mb_strtolower($value); + } + // Strip certain characters + if ($this->stripLogin) { + $value = \explode('@', $value); + $value[0] = \str_replace(\str_split($this->stripLogin), '', $value[0]); + $value = \implode('@', $value); + } + return $value; + } + + public static function fromArray(array $aSettings) : self + { + $object = new static; + $object->host = $aSettings['host']; + $object->port = $aSettings['port']; + $object->type = isset($aSettings['type']) ? $aSettings['type'] : $aSettings['secure']; + if (isset($aSettings['timeout'])) { + $object->timeout = $aSettings['timeout']; + } + $object->shortLogin = !empty($aSettings['shortLogin']); + if (isset($aSettings['lowerLogin'])) { + $object->lowerLogin = !empty($aSettings['lowerLogin']); + } + if (isset($aSettings['stripLogin'])) { + $object->stripLogin = $aSettings['stripLogin']; + } + $object->ssl = SSLContext::fromArray($aSettings['ssl'] ?? []); + if (!empty($aSettings['sasl']) && \is_array($aSettings['sasl'])) { + $object->SASLMechanisms = $aSettings['sasl']; + } +// $object->tls_weak = !empty($aSettings['tls_weak']); + return $object; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( +// '@Object' => 'Object/ConnectSettings', + 'host' => $this->host, + 'port' => $this->port, + 'type' => $this->type, + 'timeout' => $this->timeout, + 'shortLogin' => $this->shortLogin, + 'lowerLogin' => $this->lowerLogin, + 'stripLogin' => $this->stripLogin, + 'sasl' => $this->SASLMechanisms, + 'ssl' => $this->ssl +// 'tls_weak' => $this->tls_weak + ); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Net/Enumerations/ConnectionSecurityType.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Enumerations/ConnectionSecurityType.php new file mode 100644 index 0000000000..4db1fd99f4 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Enumerations/ConnectionSecurityType.php @@ -0,0 +1,48 @@ +sSocketMessage = $sSocketMessage; + $this->iSocketCode = $iSocketCode; + } + + public function getSocketMessage() : string + { + return $this->sSocketMessage; + } + + public function getSocketCode() : int + { + return $this->iSocketCode; + } +} diff --git a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketConnectionDoesNotAvailableException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketConnectionDoesNotAvailableException.php similarity index 75% rename from rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketConnectionDoesNotAvailableException.php rename to snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketConnectionDoesNotAvailableException.php index b406b3d2a2..8c663804ba 100644 --- a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketConnectionDoesNotAvailableException.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketConnectionDoesNotAvailableException.php @@ -16,4 +16,4 @@ * @package Net * @subpackage Exceptions */ -class SocketConnectionDoesNotAvailableException extends \MailSo\Net\Exceptions\ConnectionException {} +class SocketConnectionDoesNotAvailableException extends ConnectionException {} diff --git a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadException.php similarity index 81% rename from rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadException.php rename to snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadException.php index bc5bf33f53..48361b2aed 100644 --- a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadException.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadException.php @@ -16,4 +16,4 @@ * @package Net * @subpackage Exceptions */ -class SocketReadException extends \MailSo\Net\Exceptions\Exception {} +class SocketReadException extends \MailSo\RuntimeException {} diff --git a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadTimeoutException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadTimeoutException.php similarity index 80% rename from rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadTimeoutException.php rename to snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadTimeoutException.php index 96e3dc171b..cd55b874f7 100644 --- a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadTimeoutException.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketReadTimeoutException.php @@ -16,4 +16,4 @@ * @package Net * @subpackage Exceptions */ -class SocketReadTimeoutException extends \MailSo\Net\Exceptions\Exception {} +class SocketReadTimeoutException extends \MailSo\RuntimeException {} diff --git a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnreadBufferException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnreadBufferException.php similarity index 79% rename from rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnreadBufferException.php rename to snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnreadBufferException.php index 5d376379df..95350963e4 100644 --- a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnreadBufferException.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnreadBufferException.php @@ -16,4 +16,4 @@ * @package Net * @subpackage Exceptions */ -class SocketUnreadBufferException extends \MailSo\Net\Exceptions\Exception {} +class SocketUnreadBufferException extends \MailSo\RuntimeException {} diff --git a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnsuppoterdSecureConnectionException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnsuppoterdSecureConnectionException.php similarity index 75% rename from rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnsuppoterdSecureConnectionException.php rename to snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnsuppoterdSecureConnectionException.php index 0314e3a258..7bf3e2e4ce 100644 --- a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnsuppoterdSecureConnectionException.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketUnsuppoterdSecureConnectionException.php @@ -16,4 +16,4 @@ * @package Net * @subpackage Exceptions */ -class SocketUnsuppoterdSecureConnectionException extends \MailSo\Net\Exceptions\ConnectionException {} +class SocketUnsuppoterdSecureConnectionException extends ConnectionException {} diff --git a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketWriteException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketWriteException.php similarity index 81% rename from rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketWriteException.php rename to snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketWriteException.php index b18766b0d7..e8f9372a8d 100644 --- a/rainloop/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketWriteException.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketWriteException.php @@ -16,4 +16,4 @@ * @package Net * @subpackage Exceptions */ -class SocketWriteException extends \MailSo\Net\Exceptions\Exception {} +class SocketWriteException extends \MailSo\RuntimeException {} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Net/NetClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/NetClient.php new file mode 100644 index 0000000000..9196ac534b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/NetClient.php @@ -0,0 +1,314 @@ +Disconnect(); + } + catch (\Throwable $oException) {} + } + + public function GetConnectedHost() : string + { + return $this->sConnectedHost; + } + + public function SetTimeOuts(int $iConnectTimeOut = 10) : void + { + $this->iConnectTimeOut = \max(5, $iConnectTimeOut); + } + + /** + * @return resource|null + */ + public function ConnectionResource() + { + return $this->rConnect; + } + + public function capturePhpErrorWithException(int $iErrNo, string $sErrStr, string $sErrFile, int $iErrLine) : bool + { + throw new \MailSo\RuntimeException($sErrStr, $iErrNo); + } + + /** + * @throws \ValueError + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\SocketAlreadyConnectedException + * @throws \MailSo\Net\Exceptions\SocketCanNotConnectToHostException + */ + public function Connect(ConnectSettings $oSettings) : void + { + $oSettings->host = \trim($oSettings->host); + if (!\strlen($oSettings->host) || 0 > $oSettings->port || 65535 < $oSettings->port) { + $this->writeLogException(new \ValueError, \LOG_ERR); + } + + if ($this->IsConnected()) { + $this->writeLogException(new Exceptions\SocketAlreadyConnectedException, \LOG_ERR, false); +// $this->Disconnect(); + return; + } + + $this->Settings = $oSettings; + + $sErrorStr = ''; + $iErrorNo = 0; + + $this->sConnectedHost = $oSettings->host; + + $this->ssl = \MailSo\Net\Enumerations\ConnectionSecurityType::UseSSL($this->Settings->port, $this->Settings->type); + + if (!\preg_match('/^[a-z0-9._]{2,8}:\/\//i', $this->sConnectedHost)) { + $this->sConnectedHost = ($this->ssl ? 'ssl://' : 'tcp://') . $this->sConnectedHost; +// $this->sConnectedHost = ($this->ssl ? 'ssl://' : '') . $this->sConnectedHost; + } + + if (!$this->ssl && \MailSo\Net\Enumerations\ConnectionSecurityType::SSL === $this->Settings->type) { + $this->writeLogException( + new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('SSL isn\'t supported: ('.\implode(', ', \stream_get_transports()).')'), + \LOG_ERR); + } + + $this->iStartConnectTime = \microtime(true); + $this->writeLog('Start connection to "'.$this->sConnectedHost.':'.$this->Settings->port.'"'); + + $rStreamContext = \stream_context_create(array( + 'ssl' => $oSettings->ssl->jsonSerialize() + )); + + \set_error_handler(array($this, 'capturePhpErrorWithException')); + + try + { + $this->rConnect = \stream_socket_client($this->sConnectedHost.':'.$this->Settings->port, + $iErrorNo, $sErrorStr, $this->iConnectTimeOut, STREAM_CLIENT_CONNECT, $rStreamContext); + } + catch (\Throwable $oExc) + { + $sErrorStr = $oExc->getMessage(); + $iErrorNo = $oExc->getCode(); + } + + \restore_error_handler(); + + $this->writeLog('Connect ('.($this->rConnect ? 'success' : 'failed').')'); + + if (!$this->rConnect) { + $this->writeLogException( + new Exceptions\SocketCanNotConnectToHostException( + \MailSo\Base\Locale::ConvertSystemString($sErrorStr), (int) $iErrorNo, + 'Can\'t connect to host "'.$this->sConnectedHost.':'.$this->Settings->port.'"' + ) + ); + } + + $this->writeLog((\microtime(true) - $this->iStartConnectTime).' (raw connection)', \LOG_DEBUG); + + if ($this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_set_timeout')) { + \stream_set_timeout($this->rConnect, \max(5, $oSettings->timeout)); + } + } + + public function Encrypted() : bool + { + return $this->rConnect && !empty(\stream_get_meta_data($this->rConnect)['crypto']); + } + + public function EnableCrypto() : void + { + $bSuccess = false; + if ($this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_socket_enable_crypto')) { + $crypto_method = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + if (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && OPENSSL_VERSION_NUMBER >= 0x10101000) { + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT; + } +// if ($this->Settings->tls_weak) { +// $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; +// } + $bSuccess = \stream_socket_enable_crypto($this->rConnect, true, $crypto_method); + } + + $bSuccess || $this->writeLogException(new \MailSo\RuntimeException('Cannot enable STARTTLS.'), \LOG_ERR); + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + */ + public function Disconnect() : void + { + if ($this->rConnect) { + if (!$this->bUnreadBuffer && !$this->bRunningCallback) { + $this->Logout(); + } + + $bResult = \fclose($this->rConnect); + + $this->writeLog('Disconnected from "'.$this->sConnectedHost.':'.$this->Settings->port.'" ('. + (($bResult) ? 'success' : 'unsuccess').')'); + + if ($this->iStartConnectTime) { + $this->writeLog((\microtime(true) - $this->iStartConnectTime).' (net session)', \LOG_DEBUG); + $this->iStartConnectTime = 0; + } + $this->rConnect = null; + } + } + + abstract public function supportsAuthType(string $sasl_type) : bool; +// abstract public function Login(ConnectSettings $oSettings) : self; + abstract public function Logout() : void; + + public function IsConnected(bool $bThrowExceptionOnFalse = false) : bool + { + if ($this->rConnect) { + return true; + } + if ($bThrowExceptionOnFalse) { + $this->writeLogException(new Exceptions\SocketConnectionDoesNotAvailableException, \LOG_ERR); + } + return false; + } + + public function StreamContextParams() : array + { + return $this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_context_get_options') + ? \stream_context_get_params($this->rConnect) : false; + } + + /** + * @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException + * @throws \MailSo\Net\Exceptions\SocketWriteException + */ + protected function sendRaw(string $sRaw, bool $bWriteToLog = true, string $sFakeRaw = '') : void + { + if ($this->bUnreadBuffer) { + $this->writeLogException(new Exceptions\SocketUnreadBufferException, \LOG_ERR); + } + + $sRaw .= "\r\n"; + + $bFake = \strlen($sFakeRaw) && $this->oLogger && !$this->oLogger->ShowSecrets(); + if ($bFake) { + $sFakeRaw .= "\r\n"; + } + + $mResult = \fwrite($this->rConnect, $sRaw); + if (false === $mResult) { + $this->IsConnected(true); + $this->writeLogException(new Exceptions\SocketWriteException, \LOG_ERR); + } else if ($bWriteToLog) { + $this->writeLogWithCrlf('> '.($bFake ? $sFakeRaw : $sRaw)); + } + } + + /** + * @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException + * @throws \MailSo\Net\Exceptions\SocketReadException + */ + protected function getNextBuffer(?int $iReadLen = null) : ?string + { + if (null === $iReadLen) { + $sResponseBuffer = \fgets($this->rConnect); + } else { + $sResponseBuffer = ''; + $iRead = $iReadLen; + while (0 < $iRead) { + $sAddRead = \fread($this->rConnect, $iRead); + if (false === $sAddRead) { + $sResponseBuffer = false; + break; + } + $sResponseBuffer .= $sAddRead; + $iRead -= \strlen($sAddRead); + } + } + + if (false === $sResponseBuffer) { + $this->IsConnected(true); + $this->bUnreadBuffer = true; + $aSocketStatus = \stream_get_meta_data($this->rConnect); + if (isset($aSocketStatus['timed_out']) && $aSocketStatus['timed_out']) { + $this->writeLogException(new Exceptions\SocketReadTimeoutException, \LOG_ERR); + } else { + $this->writeLog('Stream Meta: '.\print_r($aSocketStatus, true), \LOG_ERR); + $this->writeLogException(new Exceptions\SocketReadException, \LOG_ERR); + } + return null; + } + + $iReadBytes = \strlen($sResponseBuffer); +// $iReadLen && $this->writeLog('Received '.$iReadBytes.'/'.$iReadLen.' bytes.'); + $iLimit = 5000; // 5KB + if ($iLimit < $iReadBytes) { + $this->writeLogWithCrlf('[cutted:'.$iReadBytes.'] < '.\substr($sResponseBuffer, 0, $iLimit).'...'); + } else { + $this->writeLogWithCrlf('< '.$sResponseBuffer); + } + + return $sResponseBuffer; + } + + abstract function getLogName() : string; + + protected function writeLog(string $sDesc, int $iDescType = \LOG_INFO) : void + { + $this->logWrite($sDesc, $iDescType, $this->getLogName()); + } + + protected function writeLogWithCrlf(string $sDesc) : void + { + $this->logWrite($sDesc, \LOG_INFO, $this->getLogName(), true, true); + } + + protected function writeLogException(\Throwable $oException, int $iDescType = \LOG_NOTICE, bool $bThrowException = true) : void + { + if ($oException instanceof Exceptions\SocketCanNotConnectToHostException) { + $this->logWrite('Socket: ['.$oException->getSocketCode().'] '.$oException->getSocketMessage(), $iDescType, $this->getLogName()); + } + $this->logException($oException, $iDescType, $this->getLogName()); + if ($bThrowException) { + throw $oException; + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Net/SSLContext.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/SSLContext.php new file mode 100644 index 0000000000..e349720612 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/SSLContext.php @@ -0,0 +1,73 @@ +verify_peer = !!$oConfig->Get('ssl', 'verify_certificate', true); + $this->verify_peer_name = !!$oConfig->Get('ssl', 'verify_certificate', true); + $this->allow_self_signed = !!$oConfig->Get('ssl', 'allow_self_signed', false); + $this->cafile = \trim($oConfig->Get('ssl', 'cafile', '')); + $this->capath = \trim($oConfig->Get('ssl', 'capath', '')); + $this->disable_compression = !!$oConfig->Get('ssl', 'disable_compression', true); + $this->security_level = (int) $oConfig->Get('ssl', 'security_level', 1); + $this->local_cert = \trim($oConfig->Get('ssl', 'local_cert', '')); + } + + public static function fromArray(array $settings) : self + { + $object = new static; + foreach ($settings as $key => $value) { + $object->$key = $value; + } + return $object; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $aResult = \get_object_vars($this); +// $aResult['@Object'] = 'Object/SSLContext'; + return \array_filter( + $aResult, + function($var){return !\is_string($var) || \strlen($var);} + ); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/RuntimeException.php b/snappymail/v/0.0.0/app/libraries/MailSo/RuntimeException.php new file mode 100644 index 0000000000..004764f934 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/RuntimeException.php @@ -0,0 +1,28 @@ +getFile()).'#'.$this->getLine().')'; + + parent::__construct($sMessage, $iCode, $oPrevious); + } +*/ +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Exceptions/LoginBadCredentialsException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Exceptions/LoginBadCredentialsException.php new file mode 100644 index 0000000000..0e7a3dd610 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Exceptions/LoginBadCredentialsException.php @@ -0,0 +1,19 @@ +aResponses = $aResponses; + } + + public function GetResponses() : array + { + return $this->aResponses; + } + + public function GetLastResponse() : ?\MailSo\Sieve\Response + { + $iCnt = \count($this->aResponses); + return $iCnt ? $this->aResponses[$iCnt - 1] : null; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Settings.php b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Settings.php new file mode 100644 index 0000000000..7bd7692728 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Settings.php @@ -0,0 +1,47 @@ +enabled = !empty($aSettings['enabled']); + $object->authLiteral = !isset($aSettings['authLiteral']) || !empty($aSettings['authLiteral']); + return $object; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return \array_merge( + parent::jsonSerialize(), + [ +// '@Object' => 'Object/SmtpSettings', + 'enabled' => $this->enabled, + 'authLiteral' => $this->authLiteral + ] + ); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/SieveClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/SieveClient.php new file mode 100644 index 0000000000..a0a1cd5951 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/SieveClient.php @@ -0,0 +1,450 @@ +aCapa[\strtoupper($sCapa)]); + } + + public function Modules() : array + { + return $this->aModules; + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function Connect(\MailSo\Net\ConnectSettings $oSettings) : void + { + parent::Connect($oSettings); + + $aResponse = $this->parseResponse(); + $this->parseStartupResponse($aResponse); + + if (ConnectionSecurityType::STARTTLS === $this->Settings->type + || (ConnectionSecurityType::AUTO_DETECT === $this->Settings->type && $this->hasCapability('STARTTLS'))) { + $this->StartTLS(); + } + } + + private function StartTLS() : void + { + if ($this->hasCapability('STARTTLS')) { + $this->sendRequestWithCheck('STARTTLS'); + $this->EnableCrypto(); + $aResponse = $this->parseResponse(); + $this->parseStartupResponse($aResponse); + } else { + $this->writeLogException( + new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'), + \LOG_ERR); + } + } + + public function supportsAuthType(string $sasl_type) : bool + { + return \in_array(\strtoupper($sasl_type), $this->aAuth); + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function Login(Settings $oSettings) : self + { + if ($this->bIsLoggined) { + return $this; + } + + $sLogin = $oSettings->username; + $sPassword = $oSettings->passphrase; + $sLoginAuthKey = ''; + if (!\strlen($sLogin) || !\strlen($sPassword)) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); + } + + $type = ''; + if ($this->hasCapability('SASL')) { + foreach ($oSettings->SASLMechanisms as $sasl_type) { + if (\in_array(\strtoupper($sasl_type), $this->aAuth) && \SnappyMail\SASL::isSupported($sasl_type)) { + $type = $sasl_type; + break; + } + } + } + if (!$type) { + if (!$this->Encrypted() && $this->hasCapability('STARTTLS')) { + $this->StartTLS(); + return $this->Login($oSettings); + } +// $this->writeLogException(new \UnexpectedValueException('No supported SASL mechanism found'), \LOG_ERR); + $this->writeLogException(new \MailSo\Sieve\Exceptions\LoginException, \LOG_ERR); + } + + $SASL = \SnappyMail\SASL::factory($type); + + $bAuth = false; + try + { + if (\str_starts_with($type, 'SCRAM-')) + { +/* + $sAuthzid = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), \MailSo\Imap\Enumerations\ResponseType::CONTINUATION); + $this->sendRaw($SASL->authenticate($sLogin, $sPassword/*, $sAuthzid* /), true); + $sChallenge = $SASL->challenge($this->getResponseValue($this->getResponse(), \MailSo\Imap\Enumerations\ResponseType::CONTINUATION)); + $this->logMask($sChallenge); + $this->sendRaw($sChallenge); + $oResponse = $this->getResponse(); + $SASL->verify($this->getResponseValue($oResponse)); +*/ + } + else if ('PLAIN' === $type || 'OAUTHBEARER' === $type || 'XOAUTH2' === $type) + { + $sAuth = $SASL->authenticate($sLogin, $sPassword, $sLoginAuthKey); + $this->logMask($sAuth); + + if ($oSettings->authLiteral) { + $this->sendRaw("AUTHENTICATE \"{$type}\" {".\strlen($sAuth).'+}'); + $this->sendRaw($sAuth); + } else { + $this->sendRaw("AUTHENTICATE \"{$type}\" \"{$sAuth}\""); + } + + $aResponse = $this->parseResponse(); + $this->parseStartupResponse($aResponse); + $bAuth = true; + } + else if ('LOGIN' === $type) + { + $sLogin = $SASL->authenticate($sLogin, $sPassword); + $sPassword = $SASL->challenge(''); + $this->logMask($sPassword); + + $this->sendRaw('AUTHENTICATE "LOGIN"'); + $this->sendRaw('{'.\strlen($sLogin).'+}'); + $this->sendRaw($sLogin); + $this->sendRaw('{'.\strlen($sPassword).'+}'); + $this->sendRaw($sPassword); + + $aResponse = $this->parseResponse(); + $this->parseStartupResponse($aResponse); + $bAuth = true; + } + } + catch (\MailSo\Sieve\Exceptions\NegativeResponseException $oException) + { + $this->writeLogException( + new \MailSo\Sieve\Exceptions\LoginBadCredentialsException($oException->GetResponses(), '', 0, $oException), + \LOG_ERR); + } + + $bAuth || $this->writeLogException(new \MailSo\Sieve\Exceptions\LoginBadMethodException, \LOG_ERR); + + $this->bIsLoggined = true; + + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function Logout() : void + { + if ($this->bIsLoggined) { + try { + $this->sendRequestWithCheck('LOGOUT'); + } catch (\Throwable $e) { + // https://github.com/the-djmaze/snappymail/issues/1455 + $this->writeLogException($e, \LOG_WARNING, false); + } + $this->bIsLoggined = false; + } + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function ListScripts() : array + { + $this->sendRequest('LISTSCRIPTS'); + $aResponse = $this->parseResponse(); + $aResult = array(); + foreach ($aResponse as $sLine) { + $aTokens = $this->parseLine($sLine); + if ($aTokens) { + $aResult[$aTokens[0]] = 'ACTIVE' === \substr($sLine, -6); + } + } + + return $aResult; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function Capability(bool $force = false) : array + { + if (!$this->aCapa || $force) { + $this->sendRequest('CAPABILITY'); + $aResponse = $this->parseResponse(); + $this->parseStartupResponse($aResponse); + } + return $this->aCapa; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function Noop() : self + { + $this->sendRequestWithCheck('NOOP'); + + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function GetScript(string $sScriptName) : string + { + $sScriptName = \addcslashes($sScriptName, '"\\'); + $this->sendRequest('GETSCRIPT "'.$sScriptName.'"'); + $aResponse = $this->parseResponse(); + + $sScript = ''; + if (\count($aResponse)) { + if ('{' === $aResponse[0][0]) { + \array_shift($aResponse); + } + + if (\in_array(\substr($aResponse[\count($aResponse) - 1], 0, 2), array('OK', 'NO'))) { + \array_pop($aResponse); + } + + $sScript = \implode("\r\n", $aResponse); + } + + return $sScript; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function PutScript(string $sScriptName, string $sScriptSource) : self + { + $sScriptName = \addcslashes($sScriptName, '"\\'); + $sScriptSource = \preg_replace('/\r?\n/', "\r\n", $sScriptSource); + $this->sendRequest('PUTSCRIPT "'.$sScriptName.'" {'.\strlen($sScriptSource).'+}'); + $this->sendRequestWithCheck($sScriptSource); + + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function CheckScript(string $sScriptSource) : self + { + $sScriptSource = \preg_replace('/\r?\n/', "\r\n", $sScriptSource); + $this->sendRequest('CHECKSCRIPT {'.\strlen($sScriptSource).'+}'); + $this->sendRequestWithCheck($sScriptSource); + + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function SetActiveScript(string $sScriptName) : self + { + $sScriptName = \addcslashes($sScriptName, '"\\'); + $this->sendRequestWithCheck('SETACTIVE "'.$sScriptName.'"'); + + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function DeleteScript(string $sScriptName) : self + { + $sScriptName = \addcslashes($sScriptName, '"\\'); + $this->sendRequestWithCheck('DELETESCRIPT "'.$sScriptName.'"'); + + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + public function RenameScript(string $sOldName, string $sNewName) : self + { + $sOldName = \addcslashes($sOldName, '"\\'); + $sNewName = \addcslashes($sNewName, '"\\'); + $this->sendRequestWithCheck('RENAMESCRIPT "'.$sOldName.'" "'.$sNewName.'"'); + + return $this; + } + + private function parseLine(string $sLine) : ?array + { + if (!\in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) { + $aResult = array(); + if (\preg_match_all('/(?:(?:"((?:\\\\"|[^"])*)"))/', $sLine, $aResult)) { + return \array_map('stripcslashes', $aResult[1]); + } + } + return null; + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + */ + private function parseStartupResponse(array $aResponse) : void + { + $this->aCapa = []; + foreach ($aResponse as $sLine) { + $aTokens = $this->parseLine($sLine); + if (empty($aTokens[0]) || \in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) { + continue; + } + + $sToken = \strtoupper($aTokens[0]); + $this->aCapa[$sToken] = isset($aTokens[1]) ? $aTokens[1] : ''; + + if (isset($aTokens[1])) { + switch ($sToken) { + case 'SASL': + $this->aAuth = \explode(' ', \strtoupper($aTokens[1])); + break; + case 'SIEVE': + $this->aModules = \explode(' ', \strtolower($aTokens[1])); + break; + } + } + } + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + */ + private function sendRequest(string $sRequest) : void + { + if (!\strlen(\trim($sRequest))) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); + } + + $this->IsConnected(true); + + $this->sendRaw($sRequest); + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Sieve\Exceptions\* + */ + private function sendRequestWithCheck(string $sRequest) : void + { + $this->sendRequest($sRequest); + $this->parseResponse(); + } + + private function parseResponse() : array + { + $aResult = array(); + while (true) { + $sResponseBuffer = $this->getNextBuffer(); + if (null === $sResponseBuffer) { + break; + } + // \MailSo\Imap\Enumerations\ResponseStatus + $bEnd = \in_array(\substr($sResponseBuffer, 0, 2), array('OK', 'NO')); + // convertEndOfLine + $sLine = \trim($sResponseBuffer); + if ('}' === \substr($sLine, -1)) { + $iPos = \strrpos($sLine, '{'); + if (false !== $iPos) { + $iLen = \intval(\substr($sLine, $iPos + 1, -1)); + if (0 < $iLen) { + $sResponseBuffer = $this->getNextBuffer($iLen); + if (\strlen($sResponseBuffer) === $iLen) { + $sLine = \trim(\substr_replace($sLine, $sResponseBuffer, $iPos)); + } + } + } + } + $aResult[] = $sLine; + if ($bEnd) { + break; + } + } + + if (!$aResult || 'OK' !== \substr($aResult[\array_key_last($aResult)], 0, 2)) { + $this->writeLogException(new \MailSo\Sieve\Exceptions\NegativeResponseException($aResult), \LOG_WARNING); + } + + return $aResult; + } + + public function getLogName() : string + { + return 'SIEVE'; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Exceptions/LoginBadCredentialsException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Exceptions/LoginBadCredentialsException.php new file mode 100644 index 0000000000..087afaba4c --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Exceptions/LoginBadCredentialsException.php @@ -0,0 +1,19 @@ +aResponses = $aResponses; + } + + public function GetResponses() : array + { + return $this->aResponses; + } + + public function GetLastResponse() : ?\MailSo\Smtp\Response + { + $iCnt = \count($this->aResponses); + return $iCnt ? $this->aResponses[$iCnt - 1] : null; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Settings.php b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Settings.php new file mode 100644 index 0000000000..de25b99cd5 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Settings.php @@ -0,0 +1,67 @@ +viewErrors = !!$oConfig->Get('labs', 'smtp_show_server_errors', false); + } + + public static function fromArray(array $aSettings) : self + { + $object = parent::fromArray($aSettings); + $object->useAuth = !empty($aSettings['useAuth']); + $object->setSender = !empty($aSettings['setSender']); + $object->usePhpMail = !empty($aSettings['usePhpMail']); + $object->authPlainLine = !empty($aSettings['authPlainLine']); +// $object->viewErrors = !empty($aSettings['viewErrors']); + return $object; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return \array_merge( + parent::jsonSerialize(), + [ +// '@Object' => 'Object/SmtpSettings', + 'useAuth' => $this->useAuth, + 'setSender' => $this->setSender, + 'usePhpMail' => $this->usePhpMail, + 'authPlainLine' => $this->authPlainLine +// 'viewErrors' => $this->viewErrors + ] + ); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/SmtpClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/SmtpClient.php new file mode 100644 index 0000000000..4ca6cf8e26 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/SmtpClient.php @@ -0,0 +1,622 @@ +aCapa; + } + + public function hasCapability(string $sCapa) : bool + { + return \in_array(\strtoupper($sCapa), $this->aCapa); + } + + public function maxSize() : int + { + return $this->iSizeCapaValue; + } + + public static function EhloHelper() : string + { + $sEhloHost = empty($_SERVER['SERVER_NAME']) ? '' : \trim($_SERVER['SERVER_NAME']); + if (empty($sEhloHost)) { + $sEhloHost = empty($_SERVER['HTTP_HOST']) ? '' : \trim($_SERVER['HTTP_HOST']); + } + + if (empty($sEhloHost)) { + $sEhloHost = \function_exists('gethostname') ? \gethostname() : 'localhost'; + } + + $sEhloHost = \trim(\preg_replace('/:\d+$/', '', \trim($sEhloHost))); + + if (\preg_match('/^\d+\.\d+\.\d+\.\d+$/', $sEhloHost)) { + $sEhloHost = '['.$sEhloHost.']'; + } + + return empty($sEhloHost) ? 'localhost' : $sEhloHost; + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + public function Connect(\MailSo\Net\ConnectSettings $oSettings) : void + { + parent::Connect($oSettings); + + $this->validateResponse(220); + + $this->ehloOrHelo($oSettings->Ehlo); + $this->sEhlo = $oSettings->Ehlo; + + if (ConnectionSecurityType::STARTTLS === $this->Settings->type + || (ConnectionSecurityType::AUTO_DETECT === $this->Settings->type && $this->hasCapability('STARTTLS'))) { + $this->StartTLS(); + } + } + + private function StartTLS() : void + { + if ($this->hasCapability('STARTTLS')) { + $this->sendRequestWithCheck('STARTTLS', 220); + $this->EnableCrypto(); + $this->ehloOrHelo($this->sEhlo); + } else { + $this->writeLogException( + new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'), + \LOG_ERR); + } + } + + public function supportsAuthType(string $sasl_type) : bool + { + return \in_array(\strtoupper($sasl_type), $this->aAuthTypes); + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + public function Login(Settings $oSettings) : self + { + if ($this->bIsLoggined) { + return $this; + } + + $sLogin = $oSettings->username; + $sPassword = $oSettings->passphrase; + + $type = ''; + foreach ($oSettings->SASLMechanisms as $sasl_type) { + if (\in_array(\strtoupper($sasl_type), $this->aAuthTypes) && \SnappyMail\SASL::isSupported($sasl_type)) { + $type = $sasl_type; + break; + } + } + if (!$type) { + if (!$this->Encrypted() && $this->hasCapability('STARTTLS')) { + $this->StartTLS(); + return $this->Login($oSettings); + } + \trigger_error("SMTP {$this->GetConnectedHost()} no supported AUTH options. Disable login"); + $this->writeLogException(new \MailSo\Smtp\Exceptions\LoginBadMethodException); + } + + $SASL = \SnappyMail\SASL::factory($type); + + if ($this->Settings->authPlainLine && $SASL instanceof \SnappyMail\SASL\Plain) { + // https://github.com/the-djmaze/snappymail/issues/1038 + try + { + $sRequest = $SASL->authenticate($sLogin, $sPassword); + $this->logMask($sRequest); + $sResult = $this->sendRequestWithCheck('AUTH PLAIN ' . $sRequest, 235); + } + catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) + { + $this->writeLogException( + new \MailSo\Smtp\Exceptions\LoginBadCredentialsException($oException->GetResponses(), $oException->getMessage(), 0, $oException) + ); + } + } else { + // Start authentication + try + { + $sResult = $this->sendRequestWithCheck("AUTH {$type}", 334); + } + catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) + { + $this->writeLogException( + new \MailSo\Smtp\Exceptions\LoginBadMethodException($oException->GetResponses(), $oException->getMessage(), 0, $oException) + ); + } + + try + { + $sRequest = ''; + if (\str_starts_with($type, 'SCRAM-')) { + // RFC 5802 send "client-first-message" and receive "server-first-message" + $sRequest = $SASL->authenticate($sLogin, $sPassword, $sResult); + $this->logMask($sRequest); + $sResult = $this->sendRequestWithCheck($sRequest, 334); + // RFC 5802 send "client-final-message" and receive "server-final-message" + $sRequest = $SASL->challenge($sResult); + $this->logMask($sRequest); + $sResult = $this->sendRequestWithCheck($sRequest, 334); + $SASL->verify($sResult); + // Now end the authentication + $sRequest = ''; + $this->sendRaw($sRequest, true, ''); + $this->validateResponse(235, ''); + } else switch ($type) { + // RFC 4616 + case 'PLAIN': + case 'XOAUTH2': + case 'OAUTHBEARER': + $sRequest = $SASL->authenticate($sLogin, $sPassword); + break; + + case 'LOGIN': + $sRequest = $SASL->authenticate($sLogin, $sPassword, $sResult); + $this->logMask($sRequest); + $sResult = $this->sendRequestWithCheck($sRequest, 334); + $sRequest = $SASL->challenge($sResult); + break; + + // RFC 2195 + case 'CRAM-MD5': + $sRequest = $SASL->authenticate($sLogin, $sPassword, $sResult); + break; + } + if ($sRequest) { + $this->logMask($sRequest); + $SASL->verify($this->sendRequestWithCheck($sRequest, 235)); + } + } + catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) + { + $this->writeLogException( + new \MailSo\Smtp\Exceptions\LoginBadCredentialsException($oException->GetResponses(), $oException->getMessage(), 0, $oException) + ); + } + } + + $this->bIsLoggined = true; + + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + public function MailFrom(string $sFrom, int $iSizeIfSupported = 0, bool $bDsn = false, bool $bRequireTLS = false) : self + { +// $sFrom = IDN::emailToAscii($sFrom); + $sCmd = "FROM:<{$sFrom}>"; + // RFC 6531 + if ($this->hasCapability('SMTPUTF8')) { +// $sFrom = IDN::emailToUtf8($sFrom); +// $sCmd = "FROM:<{$sFrom}> SMTPUTF8"; + } + + if (0 < $iSizeIfSupported && $this->hasCapability('SIZE')) { + $sCmd .= ' SIZE='.$iSizeIfSupported; + } + + // RFC 3461 + if ($bDsn && $this->hasCapability('DSN')) { + $sCmd .= ' RET=HDRS'; + } + + // RFC 6152 + if ($this->hasCapability('8BITMIME')) { +// $sCmd .= ' BODY=8BITMIME'; + } + // RFC 3030 + else if ($this->hasCapability('BINARYMIME')) { +// $sCmd .= ' BODY=BINARYMIME'; + } + + // RFC 8689 + if ($bRequireTLS && $this->hasCapability('REQUIRETLS')) { + $sCmd .= ' REQUIRETLS'; + } + + $this->sendRequestWithCheck("MAIL {$sCmd}", 250); + + $this->bMail = true; + $this->bRcpt = false; + + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + public function Rcpt(string $sTo, bool $bDsn = false) : self + { + if (!$this->bMail) { + $this->writeLogException(new \MailSo\RuntimeException('No sender reverse path has been supplied'), \LOG_ERR); + } + +// $sTo = IDN::emailToAscii($sTo); + + $sCmd = 'TO:<'.$sTo.'>'; + + if ($bDsn && $this->hasCapability('DSN')) { + $sCmd .= ' NOTIFY=SUCCESS,FAILURE'; + } + + $this->sendRequestWithCheck("RCPT {$sCmd}", [250, 251], "Failed to add recipient '{$sTo}'"); + + $this->bRcpt = true; + + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + public function MailTo(string $sTo) : self + { + return $this->Rcpt($sTo); + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + public function Data(string $sData) : self + { + if (!\strlen(\trim($sData))) { + throw new \InvalidArgumentException; + } + + $rDataStream = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString($sData); + unset($sData); + $this->DataWithStream($rDataStream); + \MailSo\Base\ResourceRegistry::CloseMemoryResource($rDataStream); + + return $this; + } + + /** + * @param resource $rDataStream + * + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + public function DataWithStream($rDataStream) : self + { + if (!\is_resource($rDataStream)) { + throw new \InvalidArgumentException; + } + + if (!$this->bRcpt) { + $this->writeLogException(new \MailSo\RuntimeException('No recipient forward path has been supplied'), \LOG_ERR); + } + +/* + // RFC 3030 + if ($this->hasCapability('CHUNKING')) { + $this->bRunningCallback = true; + while (!\feof($rDataStream)) { + $sBuffer = \fgets($rDataStream); + if (false === $sBuffer) { + if (!\feof($rDataStream)) { + $this->writeLogException(new \MailSo\RuntimeException('Cannot read input resource'), \LOG_ERR); + } + break; + } + $this->sendRequestWithCheck("BDAT " . \strlen($sBuffer) . "\r\n{$sBuffer}", 250); + \MailSo\Base\Utils::ResetTimeLimit(); + } + $this->sendRequestWithCheck("BDAT 0 LAST\r\n", 250); + } + else { +*/ + $this->sendRequestWithCheck('DATA', 354); + + $this->writeLog('Message data.'); + + $this->bRunningCallback = true; + + while (!\feof($rDataStream)) { + $sBuffer = \fgets($rDataStream); + if (false === $sBuffer) { + if (!\feof($rDataStream)) { + $this->writeLogException(new \MailSo\RuntimeException('Cannot read input resource'), \LOG_ERR); + } + break; + } + if (\str_starts_with($sBuffer, '.')) { + $sBuffer = '.' . $sBuffer; + } + $this->sendRaw(\rtrim($sBuffer, "\r\n"), false); + \MailSo\Base\Utils::ResetTimeLimit(); + } + + $this->sendRequestWithCheck('.', 250); + + $this->bRunningCallback = false; + + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + public function Rset() : self + { + $this->sendRequestWithCheck('RSET', [250, 220]); + + $this->bMail = false; + $this->bRcpt = false; + + return $this; + } + + /** + * VERIFY + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + public function Vrfy(string $sUser) : self + { + $sUser = \MailSo\Base\Utils::Trim($sUser); +/* + // RFC 6531 + if ($this->hasCapability('SMTPUTF8')) { + $this->sendRequestWithCheck('VRFY ' . IDN::emailToUtf8($sUser) . ' SMTPUTF8', [250, 251, 252]); + } else { +*/ + $this->sendRequestWithCheck('VRFY ' . IDN::emailToAscii($sUser), [250, 251, 252]); + return $this; + } + + /** + * EXPAND command, the string identifies a mailing list, and the + * successful (i.e., 250) multiline response MAY include the full name + * of the users and MUST give the mailboxes on the mailing list. + * + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ +/* + public function Expn(string $sUser) : self + { + $sUser = \MailSo\Base\Utils::Trim($sUser); + // RFC 6531 + if ($this->hasCapability('SMTPUTF8')) { + $this->sendRequestWithCheck('EXPN ' . IDN::emailToUtf8($sUser) . ' SMTPUTF8', [250, 251, 252]); + } else { + $this->sendRequestWithCheck('EXPN ' . IDN::emailToAscii($sUser), [250, 251, 252]); + } + return $this; + } +*/ + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + public function Noop() : self + { + $this->sendRequestWithCheck('NOOP', 250); + return $this; + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + public function Logout() : void + { + if ($this->IsConnected()) { + $this->sendRequestWithCheck('QUIT', 221); + } + $this->bMail = false; + $this->bRcpt = false; + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + private function sendRequestWithCheck(string $sCommand, $mExpectCode, string $sErrorPrefix = '') : string + { + if (!\strlen(\trim($sCommand))) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); + } + $this->IsConnected(true); + $this->sendRaw($sCommand, true, ''); + $this->validateResponse($mExpectCode, $sErrorPrefix); + return empty($this->aResults[0]) ? '' : \trim(\substr($this->aResults[0], 4)); + } + + private function ehloOrHelo(string $sHost) : void + { + try + { + $this->ehlo($sHost); + } + catch (\Throwable $oException) + { + try + { + $this->helo($sHost); + } + catch (\Throwable $oException) + { + throw $oException; + } + } + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + private function ehlo(string $sHost) : void + { + $this->sendRequestWithCheck("EHLO {$sHost}", 250); + /* + 250-PIPELINING\r\n + 250-SIZE 256000000\r\n + 250-ETRN\r\n + 250-ENHANCEDSTATUSCODES\r\n + */ + $this->aCapa = []; + foreach ($this->aResults as $sLine) { + $aMatch = array(); + if (\preg_match('/[\d]+[ \-](.+)$/', $sLine, $aMatch) && isset($aMatch[1]) && \strlen($aMatch[1])) { + $aLine = \preg_split('/[ =]/', \trim($aMatch[1]), 2); + if (!empty($aLine[0])) { + $sCapa = \strtoupper($aLine[0]); + if (!empty($aLine[1]) && ('AUTH' === $sCapa || 'SIZE' === $sCapa)) { + $sSubLine = \trim(\strtoupper($aLine[1])); + if (\strlen($sSubLine)) { + if ('AUTH' === $sCapa) { + $this->aAuthTypes = \explode(' ', $sSubLine); + } else if ('SIZE' === $sCapa && \is_numeric($sSubLine)) { + $this->iSizeCapaValue = (int) $sSubLine; + } + } + } + $this->aCapa[] = $sCapa; + } + } + } + } + + /** + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ + private function helo(string $sHost) : void + { + $this->sendRequestWithCheck("HELO {$sHost}", 250); + $this->aAuthTypes = array(); + $this->iSizeCapaValue = 0; + $this->aCapa = []; + } + + /** + * @throws \MailSo\Smtp\Exceptions\ResponseException + */ + private function validateResponse($mExpectCode, string $sErrorPrefix = '') : void + { + $mExpectCode = \is_array($mExpectCode) + ? \array_map('intval', $mExpectCode) + : array((int) $mExpectCode); + + $aParts = array('', '', ''); + $this->aResults = array(); + do + { + $sResponse = $this->getNextBuffer(); + $aParts = \preg_split('/([\s\-]+)/', $sResponse, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (3 === \count($aParts) && \is_numeric($aParts[0])) { + if ('-' !== \substr($aParts[1], 0, 1) && !\in_array((int) $aParts[0], $mExpectCode)) { + $this->writeLogException( + new Exceptions\NegativeResponseException($this->aResults, + ('' === $sErrorPrefix ? '' : $sErrorPrefix.': ').\trim( + (\count($this->aResults) ? \implode("\r\n", $this->aResults)."\r\n" : ''). + $sResponse)), \LOG_ERR); + } + } else { + $this->writeLogException( + new Exceptions\ResponseException($this->aResults, + ('' === $sErrorPrefix ? '' : $sErrorPrefix.': ').\trim( + (\count($this->aResults) ? \implode("\r\n", $this->aResults)."\r\n" : ''). + $sResponse)), \LOG_ERR); + } + + $this->aResults[] = $sResponse; + } + while ('-' === \substr($aParts[1], 0, 1)); + } + + public function getLogName() : string + { + return 'SMTP'; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/OAuth2/Client.php b/snappymail/v/0.0.0/app/libraries/OAuth2/Client.php new file mode 100644 index 0000000000..5d001130c7 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/OAuth2/Client.php @@ -0,0 +1,526 @@ + + * @author Anis Berejeb + * @version 1.3.1 + */ +namespace OAuth2; + +class Client +{ + /** + * Different AUTH method + */ + const AUTH_TYPE_URI = 0; + const AUTH_TYPE_AUTHORIZATION_BASIC = 1; + const AUTH_TYPE_FORM = 2; + + /** + * Different Access token type + */ + const ACCESS_TOKEN_URI = 0; + const ACCESS_TOKEN_BEARER = 1; + const ACCESS_TOKEN_OAUTH = 2; + const ACCESS_TOKEN_MAC = 3; + + /** + * Different Grant types + */ + const GRANT_TYPE_AUTH_CODE = 'authorization_code'; + const GRANT_TYPE_PASSWORD = 'password'; + const GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials'; + const GRANT_TYPE_REFRESH_TOKEN = 'refresh_token'; + + /** + * HTTP Methods + */ + const HTTP_METHOD_GET = 'GET'; + const HTTP_METHOD_POST = 'POST'; + const HTTP_METHOD_PUT = 'PUT'; + const HTTP_METHOD_DELETE = 'DELETE'; + const HTTP_METHOD_HEAD = 'HEAD'; + const HTTP_METHOD_PATCH = 'PATCH'; + + /** + * HTTP Form content types + */ + const HTTP_FORM_CONTENT_TYPE_APPLICATION = 0; + const HTTP_FORM_CONTENT_TYPE_MULTIPART = 1; + + /** + * Client ID + * + * @var string + */ + protected $client_id = null; + + /** + * Client Secret + * + * @var string + */ + protected $client_secret = null; + + /** + * Client Authentication method + * + * @var int + */ + protected $client_auth = self::AUTH_TYPE_URI; + + /** + * Access Token + * + * @var string + */ + protected $access_token = null; + + /** + * Access Token Type + * + * @var int + */ + protected $access_token_type = self::ACCESS_TOKEN_URI; + + /** + * Access Token Secret + * + * @var string + */ + protected $access_token_secret = null; + + /** + * Access Token crypt algorithm + * + * @var string + */ + protected $access_token_algorithm = null; + + /** + * Access Token Parameter name + * + * @var string + */ + protected $access_token_param_name = 'access_token'; + + /** + * The path to the certificate file to use for https connections + * + * @var string Defaults to . + */ + protected $certificate_file = null; + + /** + * cURL options + * + * @var array + */ + protected $curl_options = array(); + + /** + * Construct + * + * @param string $client_id Client ID + * @param string $client_secret Client Secret + * @param int $client_auth (AUTH_TYPE_URI, AUTH_TYPE_AUTHORIZATION_BASIC, AUTH_TYPE_FORM) + * @param string $certificate_file Indicates if we want to use a certificate file to trust the server. Optional, defaults to null. + * @return void + */ + public function __construct($client_id, $client_secret, $client_auth = self::AUTH_TYPE_URI, $certificate_file = null) + { + if (!extension_loaded('curl')) { + throw new Exception('The PHP exention curl must be installed to use this library.', Exception::CURL_NOT_FOUND); + } + + $this->client_id = $client_id; + $this->client_secret = $client_secret; + $this->client_auth = $client_auth; + $this->certificate_file = $certificate_file; + if (!empty($this->certificate_file) && !is_file($this->certificate_file)) { + throw new InvalidArgumentException('The certificate file was not found', InvalidArgumentException::CERTIFICATE_NOT_FOUND); + } + } + + /** + * Get the client Id + * + * @return string Client ID + */ + public function getClientId() + { + return $this->client_id; + } + + /** + * Get the client Secret + * + * @return string Client Secret + */ + public function getClientSecret() + { + return $this->client_secret; + } + + /** + * getAuthenticationUrl + * + * @param string $auth_endpoint Url of the authentication endpoint + * @param string $redirect_uri Redirection URI + * @param array $extra_parameters Array of extra parameters like scope or state (Ex: array('scope' => null, 'state' => '')) + * @return string URL used for authentication + */ + public function getAuthenticationUrl($auth_endpoint, $redirect_uri, array $extra_parameters = array()) + { + $parameters = array_merge(array( + 'response_type' => 'code', + 'client_id' => $this->client_id, + 'redirect_uri' => $redirect_uri + ), $extra_parameters); + return $auth_endpoint . '?' . http_build_query($parameters, '', '&'); + } + + /** + * getAccessToken + * + * @param string $token_endpoint Url of the token endpoint + * @param string $grant_type Grant Type ('authorization_code', 'password', 'client_credentials', 'refresh_token', or a custom code (@see GrantType Classes) + * @param array $parameters Array sent to the server (depend on which grant type you're using) + * @param array $extra_headers Array of extra headers + * @return array Array of parameters required by the grant_type (CF SPEC) + */ + public function getAccessToken($token_endpoint, $grant_type, array $parameters, array $extra_headers = array()) + { + if (!$grant_type) { + throw new InvalidArgumentException('The grant_type is mandatory.', InvalidArgumentException::INVALID_GRANT_TYPE); + } + $grantTypeClassName = $this->convertToCamelCase($grant_type); + $grantTypeClass = __NAMESPACE__ . '\\GrantType\\' . $grantTypeClassName; + if (!class_exists($grantTypeClass)) { + throw new InvalidArgumentException('Unknown grant type \'' . $grant_type . '\'', InvalidArgumentException::INVALID_GRANT_TYPE); + } + $grantTypeObject = new $grantTypeClass(); + $grantTypeObject->validateParameters($parameters); + if (!defined($grantTypeClass . '::GRANT_TYPE')) { + throw new Exception('Unknown constant GRANT_TYPE for class ' . $grantTypeClassName, Exception::GRANT_TYPE_ERROR); + } + $parameters['grant_type'] = $grantTypeClass::GRANT_TYPE; + $http_headers = $extra_headers; + switch ($this->client_auth) { + case self::AUTH_TYPE_URI: + case self::AUTH_TYPE_FORM: + $parameters['client_id'] = $this->client_id; + $parameters['client_secret'] = $this->client_secret; + break; + case self::AUTH_TYPE_AUTHORIZATION_BASIC: + $parameters['client_id'] = $this->client_id; + $http_headers['Authorization'] = 'Basic ' . base64_encode($this->client_id . ':' . $this->client_secret); + break; + default: + throw new Exception('Unknown client auth type.', Exception::INVALID_CLIENT_AUTHENTICATION_TYPE); + break; + } + + return $this->executeRequest($token_endpoint, $parameters, self::HTTP_METHOD_POST, $http_headers, self::HTTP_FORM_CONTENT_TYPE_APPLICATION); + } + + /** + * setToken + * + * @param string $token Set the access token + * @return void + */ + public function setAccessToken($token) + { + $this->access_token = $token; + } + + /** + * Check if there is an access token present + * + * @return bool Whether the access token is present + */ + public function hasAccessToken() + { + return !!$this->access_token; + } + + /** + * Set the client authentication type + * + * @param string $client_auth (AUTH_TYPE_URI, AUTH_TYPE_AUTHORIZATION_BASIC, AUTH_TYPE_FORM) + * @return void + */ + public function setClientAuthType($client_auth) + { + $this->client_auth = $client_auth; + } + + /** + * Set an option for the curl transfer + * + * @param int $option The CURLOPT_XXX option to set + * @param mixed $value The value to be set on option + * @return void + */ + public function setCurlOption($option, $value) + { + $this->curl_options[$option] = $value; + } + + /** + * Set multiple options for a cURL transfer + * + * @param array $options An array specifying which options to set and their values + * @return void + */ + public function setCurlOptions($options) + { + $this->curl_options = array_merge($this->curl_options, $options); + } + + /** + * Set the access token type + * + * @param int $type Access token type (ACCESS_TOKEN_BEARER, ACCESS_TOKEN_MAC, ACCESS_TOKEN_URI) + * @param string $secret The secret key used to encrypt the MAC header + * @param string $algorithm Algorithm used to encrypt the signature + * @return void + */ + public function setAccessTokenType($type, $secret = null, $algorithm = null) + { + $this->access_token_type = $type; + $this->access_token_secret = $secret; + $this->access_token_algorithm = $algorithm; + } + + /** + * Fetch a protected ressource + * + * @param string $protected_ressource_url Protected resource URL + * @param array $parameters Array of parameters + * @param string $http_method HTTP Method to use (POST, PUT, GET, HEAD, DELETE) + * @param array $http_headers HTTP headers + * @param int $form_content_type HTTP form content type to use + * @return array + */ + public function fetch($protected_resource_url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, array $http_headers = array(), $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART) + { + if ($this->access_token) { + switch ($this->access_token_type) { + case self::ACCESS_TOKEN_URI: + if (is_array($parameters)) { + $parameters[$this->access_token_param_name] = $this->access_token; + } else { + throw new InvalidArgumentException( + 'You need to give parameters as array if you want to give the token within the URI.', + InvalidArgumentException::REQUIRE_PARAMS_AS_ARRAY + ); + } + break; + case self::ACCESS_TOKEN_BEARER: + $http_headers['Authorization'] = 'Bearer ' . $this->access_token; + break; + case self::ACCESS_TOKEN_OAUTH: + $http_headers['Authorization'] = 'OAuth ' . $this->access_token; + break; + case self::ACCESS_TOKEN_MAC: + $http_headers['Authorization'] = 'MAC ' . $this->generateMACSignature($protected_resource_url, $parameters, $http_method); + break; + default: + throw new Exception('Unknown access token type.', Exception::INVALID_ACCESS_TOKEN_TYPE); + break; + } + } + return $this->executeRequest($protected_resource_url, $parameters, $http_method, $http_headers, $form_content_type); + } + + /** + * Generate the MAC signature + * + * @param string $url Called URL + * @param array $parameters Parameters + * @param string $http_method Http Method + * @return string + */ + private function generateMACSignature($url, $parameters, $http_method) + { + $timestamp = time(); + $nonce = uniqid(); + $parsed_url = parse_url($url); + if (!isset($parsed_url['port'])) + { + $parsed_url['port'] = ($parsed_url['scheme'] == 'https') ? 443 : 80; + } + if ($http_method == self::HTTP_METHOD_GET) { + if (is_array($parameters)) { + $parsed_url['path'] .= '?' . http_build_query($parameters, '', '&'); + } elseif ($parameters) { + $parsed_url['path'] .= '?' . $parameters; + } + } + + $signature = base64_encode(hash_hmac($this->access_token_algorithm, + $timestamp . "\n" + . $nonce . "\n" + . $http_method . "\n" + . $parsed_url['path'] . "\n" + . $parsed_url['host'] . "\n" + . $parsed_url['port'] . "\n\n" + , $this->access_token_secret, true)); + + return 'id="' . $this->access_token . '", ts="' . $timestamp . '", nonce="' . $nonce . '", mac="' . $signature . '"'; + } + + /** + * Execute a request (with curl) + * + * @param string $url URL + * @param mixed $parameters Array of parameters + * @param string $http_method HTTP Method + * @param array $http_headers HTTP Headers + * @param int $form_content_type HTTP form content type to use + * @return array + */ + private function executeRequest($url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, ?array $http_headers = null, $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART) + { + $curl_options = array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_CUSTOMREQUEST => $http_method + ); + + switch($http_method) { + case self::HTTP_METHOD_POST: + $curl_options[CURLOPT_POST] = true; + /* No break */ + case self::HTTP_METHOD_PUT: + case self::HTTP_METHOD_PATCH: + + /** + * Passing an array to CURLOPT_POSTFIELDS will encode the data as multipart/form-data, + * while passing a URL-encoded string will encode the data as application/x-www-form-urlencoded. + * http://php.net/manual/en/function.curl-setopt.php + */ + if(is_array($parameters) && self::HTTP_FORM_CONTENT_TYPE_APPLICATION === $form_content_type) { + $parameters = http_build_query($parameters, '', '&'); + } + $curl_options[CURLOPT_POSTFIELDS] = $parameters; + break; + case self::HTTP_METHOD_HEAD: + $curl_options[CURLOPT_NOBODY] = true; + /* No break */ + case self::HTTP_METHOD_DELETE: + case self::HTTP_METHOD_GET: + if (is_array($parameters) && count($parameters) > 0) { + $url .= '?' . http_build_query($parameters, '', '&'); + } elseif ($parameters) { + $url .= '?' . $parameters; + } + break; + default: + break; + } + + $curl_options[CURLOPT_URL] = $url; + + if (is_array($http_headers)) { + $header = array(); + foreach($http_headers as $key => $parsed_urlvalue) { + $header[] = "$key: $parsed_urlvalue"; + } + $curl_options[CURLOPT_HTTPHEADER] = $header; + } + + $ch = curl_init(); + curl_setopt_array($ch, $curl_options); + // https handling + if (!empty($this->certificate_file)) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_CAINFO, $this->certificate_file); + } else { + // bypass ssl verification + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + } + if (!empty($this->curl_options)) { + curl_setopt_array($ch, $this->curl_options); + } + $result = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + if ($curl_error = curl_error($ch)) { + throw new Exception($curl_error, Exception::CURL_ERROR); + } else { + $json_decode = json_decode($result, true); + } + curl_close($ch); + + return array( + 'result' => (null === $json_decode) ? $result : $json_decode, + 'code' => $http_code, + 'content_type' => $content_type + ); + } + + /** + * Set the name of the parameter that carry the access token + * + * @param string $name Token parameter name + * @return void + */ + public function setAccessTokenParamName($name) + { + $this->access_token_param_name = $name; + } + + /** + * Converts the class name to camel case + * + * @param mixed $grant_type the grant type + * @return string + */ + private function convertToCamelCase($grant_type) + { + $parts = explode('_', $grant_type); + array_walk($parts, function(&$item) { $item = ucfirst($item);}); + return implode('', $parts); + } +} + +class Exception extends \Exception +{ + const CURL_NOT_FOUND = 0x01; + const CURL_ERROR = 0x02; + const GRANT_TYPE_ERROR = 0x03; + const INVALID_CLIENT_AUTHENTICATION_TYPE = 0x04; + const INVALID_ACCESS_TOKEN_TYPE = 0x05; +} + +class InvalidArgumentException extends \InvalidArgumentException +{ + const INVALID_GRANT_TYPE = 0x01; + const CERTIFICATE_NOT_FOUND = 0x02; + const REQUIRE_PARAMS_AS_ARRAY = 0x03; + const MISSING_PARAMETER = 0x04; +} diff --git a/snappymail/v/0.0.0/app/libraries/OAuth2/GrantType/AuthorizationCode.php b/snappymail/v/0.0.0/app/libraries/OAuth2/GrantType/AuthorizationCode.php new file mode 100644 index 0000000000..f3436e4c54 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/OAuth2/GrantType/AuthorizationCode.php @@ -0,0 +1,41 @@ + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/snappymail/v/0.0.0/app/libraries/OAuth2/README.md b/snappymail/v/0.0.0/app/libraries/OAuth2/README.md new file mode 100644 index 0000000000..9de3570e04 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/OAuth2/README.md @@ -0,0 +1,105 @@ +# Light PHP wrapper for the OAuth 2.0 + +[![Latest Stable Version](https://poser.pugx.org/adoy/fastcgi-client/v/stable)](https://packagist.org/packages/adoy/fastcgi-client) +[![GitHub](https://img.shields.io/github/license/adoy/PHP-OAuth2)](LICENSE) +[![Total Downloads](https://poser.pugx.org/adoy/fastcgi-client/downloads)](https://packagist.org/packages/adoy/fastcgi-client) + + +## How can I use it ? + +```php +getAuthenticationUrl(AUTHORIZATION_ENDPOINT, REDIRECT_URI); + header('Location: ' . $auth_url); + die('Redirect'); +} +else +{ + $params = array('code' => $_GET['code'], 'redirect_uri' => REDIRECT_URI); + $response = $client->getAccessToken(TOKEN_ENDPOINT, 'authorization_code', $params); + parse_str($response['result'], $info); + $client->setAccessToken($info['access_token']); + $response = $client->fetch('https://graph.facebook.com/me'); + var_dump($response, $response['result']); +} +``` + +## How can I add a new Grant Type ? + +Simply write a new class in the namespace OAuth2\GrantType. You can place the class file under GrantType. +Here is an example : + +```php +getAccessToken(TOKEN_ENDPOINT, 'my_custom_grant_type', $params); +``` + +## LICENSE + +This Code is released under the GNU LGPL + +Please do not change the header of the file(s). + +This library is free software; you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This library is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU Lesser General Public License for more details. diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php new file mode 100644 index 0000000000..8b9f032a63 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -0,0 +1,986 @@ +oConfig = API::Config(); + + $this->oLogger = API::Logger(); + if ($this->oConfig->Get('logs', 'enable', false) || $this->oConfig->Get('debug', 'enable', false)) { + + $oDriver = null; + $sLogFileName = $this->oConfig->Get('logs', 'filename', ''); + if ('syslog' === $sLogFileName) { + $oDriver = new \MailSo\Log\Drivers\Syslog(); + } else if ('stderr' === $sLogFileName) { + $oDriver = new \MailSo\Log\Drivers\StderrStream(); + } else { + $sLogFileFullPath = \trim($this->oConfig->Get('logs', 'path', '')) ?: \APP_PRIVATE_DATA . 'logs'; + \is_dir($sLogFileFullPath) || \mkdir($sLogFileFullPath, 0700, true); + $oDriver = new \MailSo\Log\Drivers\File($sLogFileFullPath . '/' . $this->compileLogFileName($sLogFileName)); + } + $this->oLogger->append($oDriver + ->SetTimeZone($this->oConfig->Get('logs', 'time_zone', 'UTC')) + ); + + $oHttp = $this->Http(); + + $this->logWrite( + '[SM:' . APP_VERSION . '][IP:' + . $oHttp->GetClientIp($this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)) + . '][PID:' . (\MailSo\Base\Utils::FunctionCallable('getmypid') ? \getmypid() : 'unknown') + . '][' . \MailSo\Base\Http::GetServer('SERVER_SOFTWARE', '~') + . '][' . \PHP_SAPI + . '][Streams:' . \implode(',', \stream_get_transports()) + . '][' . $oHttp->GetMethod() . ' ' . $oHttp->GetScheme() . '://' . $oHttp->GetHost(false) . \MailSo\Base\Http::GetServer('REQUEST_URI', '') . ']' + ); + } + + $this->oPlugins = new Plugins\Manager($this); + $this->oPlugins->RunHook('filter.application-config', array($this->oConfig)); + } + + public function SetIsJson(bool $bIsJson): self + { + $this->bIsJson = $bIsJson; + + return $this; + } + + public function GetIsJson(): bool + { + return $this->bIsJson; + } + + public function Config(): Config\Application + { + return $this->oConfig; + } + + /** + * @return mixed + */ + protected function fabrica(string $sName, ?Model\Account $oAccount = null) + { + $mResult = null; + $this->oPlugins->RunHook('main.fabrica', array($sName, &$mResult), false); + + if (null === $mResult) { + switch ($sName) { + case 'files': + // RainLoop\Providers\Files\IFiles + $mResult = new Providers\Files\FileStorage(APP_PRIVATE_DATA . 'storage'); + break; + case 'storage': + case 'storage-local': + // RainLoop\Providers\Storage\IStorage + $mResult = new Providers\Storage\FileStorage( + APP_PRIVATE_DATA . 'storage', 'storage-local' === $sName); + break; + case 'settings': + // RainLoop\Providers\Settings\ISettings + $mResult = new Providers\Settings\DefaultSettings($this->StorageProvider()); + break; + case 'settings-local': + // RainLoop\Providers\Settings\ISettings + $mResult = new Providers\Settings\DefaultSettings($this->LocalStorageProvider()); + break; + case 'domain': + // Providers\Domain\DomainInterface + $mResult = new Providers\Domain\DefaultDomain(APP_PRIVATE_DATA . 'domains', $this->Cacher()); + break; + case 'filters': + // Providers\Filters\FiltersInterface + $mResult = new Providers\Filters\SieveStorage( + $this->oPlugins, $this->oConfig + ); + break; + case 'address-book': + // Providers\AddressBook\AddressBookInterface + $mResult = new Providers\AddressBook\PdoAddressBook(); + break; + case 'identities': + case 'suggestions': + $mResult = []; + break; +/* // See function Cacher + case 'cache': + $mResult = new \MailSo\Cache\Drivers\File( + \trim($this->oConfig->Get('cache', 'path', '')) ?: APP_PRIVATE_DATA . 'cache' + ); + break; +*/ + } + } + + // Always give the file provider as last for identities, it is the override + if ('identities' === $sName) { + $mResult[] = new Providers\Identities\FileIdentities($this->LocalStorageProvider()); + } + + foreach (\is_array($mResult) ? $mResult : array($mResult) as $oItem) { + if ($oItem && \method_exists($oItem, 'SetLogger')) { + $oItem->SetLogger($this->oLogger); + } + } + + $this->oPlugins->RunHook('filter.fabrica', array($sName, &$mResult, $oAccount), false); + + return $mResult; + } + + public function BootEnd(): void + { +/* + try { + if (!\SnappyMail\Shutdown::count() && $this->ImapClient()->IsLoggined()) { + $this->ImapClient()->Disconnect(); + } + } catch (\Throwable $oException) { + unset($oException); + } +*/ + } + + protected function compileLogParams(string $sLine, ?Model\Account $oAccount = null, array $aAdditionalParams = array()): string + { + $aClear = array(); + + if (false !== \strpos($sLine, '{date:')) { + $oConfig = $this->oConfig; + $sLine = \preg_replace_callback('/\{date:([^}]+)\}/', function ($aMatch) use ($oConfig) { + return (new \DateTime('now', new \DateTimeZone($oConfig->Get('logs', 'time_zone', 'UTC'))))->format($aMatch[1]); + }, $sLine); + + $aClear['/\{date:([^}]*)\}/'] = 'date'; + } + + if (false !== \strpos($sLine, '{imap:') || false !== \strpos($sLine, '{smtp:')) { + if (!$oAccount) { + $oAccount = $this->getAccountFromToken(false); + } + + if ($oAccount) { + $sLine = \str_replace('{imap:login}', $oAccount->ImapUser(), $sLine); + $sLine = \str_replace('{smtp:login}', $oAccount->SmtpUser(), $sLine); + $oDomain = $oAccount->Domain(); + if ($oDomain) { + $sLine = \str_replace('{imap:host}', $oDomain->ImapSettings()->host, $sLine); + $sLine = \str_replace('{imap:port}', $oDomain->ImapSettings()->port, $sLine); + $sLine = \str_replace('{smtp:host}', $oDomain->SmtpSettings()->host, $sLine); + $sLine = \str_replace('{smtp:port}', $oDomain->SmtpSettings()->port, $sLine); + } + } + + $aClear['/\{imap:([^}]*)\}/i'] = 'imap'; + $aClear['/\{smtp:([^}]*)\}/i'] = 'smtp'; + } + + if (false !== \strpos($sLine, '{request:')) { + if (false !== \strpos($sLine, '{request:ip}')) { + $sLine = \str_replace('{request:ip}', + $this->Http()->GetClientIp($this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)), + $sLine); + } + + if (false !== \strpos($sLine, '{request:domain}')) { + $sLine = \str_replace('{request:domain}', $this->Http()->GetHost(true, true), $sLine); + } + + if (false !== \strpos($sLine, '{request:domain-clear}')) { + $sLine = \str_replace('{request:domain-clear}', + \MailSo\Base\Utils::GetClearDomainName($this->Http()->GetHost(true, true)), + $sLine); + } + + $aClear['/\{request:([^}]*)\}/i'] = 'request'; + } + + if (false !== \strpos($sLine, '{user:')) { + if (false !== \strpos($sLine, '{user:uid}')) { + $sLine = \str_replace('{user:uid}', + \base_convert(\sprintf('%u', \crc32(Utils::GetConnectionToken())), 10, 32), + $sLine + ); + } + + if (false !== \strpos($sLine, '{user:ip}')) { + $sLine = \str_replace('{user:ip}', + $this->Http()->GetClientIp($this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)), + $sLine); + } + + if (\preg_match('/\{user:(email|login|domain)\}/i', $sLine)) { + if (!$oAccount) { + $oAccount = $this->getAccountFromToken(false); + } + + if ($oAccount) { + $sEmail = $oAccount->Email(); + + $sLine = \str_replace('{user:email}', $sEmail, $sLine); + $sLine = \str_replace('{user:login}', \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail), $sLine); + $sLine = \str_replace('{user:domain}', \MailSo\Base\Utils::getEmailAddressDomain($sEmail), $sLine); + $sLine = \str_replace('{user:domain-clear}', + \MailSo\Base\Utils::GetClearDomainName(\MailSo\Base\Utils::getEmailAddressDomain($sEmail)), + $sLine); + } + } + + $aClear['/\{user:([^}]*)\}/i'] = 'unknown'; + } + + if (false !== \strpos($sLine, '{labs:')) { + $sLine = \preg_replace_callback('/\{labs:rand:([1-9])\}/', function ($aMatch) { + return \rand(\pow(10, $aMatch[1] - 1), \pow(10, $aMatch[1]) - 1); + }, $sLine); + + $aClear['/\{labs:([^}]*)\}/'] = 'labs'; + } + + foreach ($aAdditionalParams as $sKey => $sValue) { + $sLine = \str_replace($sKey, $sValue, $sLine); + } + + foreach ($aClear as $sKey => $sValue) { + $sLine = \preg_replace($sKey, $sValue, $sLine); + } + + return $sLine; + } + + protected function compileLogFileName(string $sFileName): string + { + $sFileName = \trim($sFileName); + + if (\strlen($sFileName)) { + $sFileName = $this->compileLogParams($sFileName); + + $sFileName = \preg_replace('/[\/]+/', '/', \preg_replace('/[.]+/', '.', $sFileName)); + $sFileName = \preg_replace('/[^a-zA-Z0-9@_+=\-\.\/!()\[\]]/', '', $sFileName); + } + + if (!\strlen($sFileName)) { + $sFileName = 'snappymail-log.txt'; + } + + return $sFileName; + } + + /** + * @throws \RainLoop\Exceptions\ClientException + */ + public function GetAccount(bool $bThrowExceptionOnFalse = false): ?Model\Account + { + return $this->getAccountFromToken($bThrowExceptionOnFalse); + } + + public function Http(): \MailSo\Base\Http + { + if (null === $this->oHttp) { + $this->oHttp = \MailSo\Base\Http::SingletonInstance(); + } + + return $this->oHttp; + } + + public function MailClient(): \MailSo\Mail\MailClient + { + if (null === $this->oMailClient) { + $this->oMailClient = new \MailSo\Mail\MailClient(); + $this->oMailClient->SetLogger($this->oLogger); + } + + return $this->oMailClient; + } + + public function ImapClient(): \MailSo\Imap\ImapClient + { +// $this->initMailClientConnection(); + return $this->MailClient()->ImapClient(); + } + + // Stores data in AdditionalAccount else MainAccount + public function LocalStorageProvider(): Providers\Storage + { + if (!$this->oLocalStorageProvider) { + $this->oLocalStorageProvider = new Providers\Storage($this->fabrica('storage-local')); + } + return $this->oLocalStorageProvider; + } + + // Stores data in MainAccount + public function StorageProvider(): Providers\Storage + { + if (!$this->oStorageProvider) { + $this->oStorageProvider = new Providers\Storage($this->fabrica('storage')); + } + return $this->oStorageProvider; + } + + public function SettingsProvider(bool $bLocal = false): Providers\Settings + { + if ($bLocal) { + if (null === $this->oLocalSettingsProvider) { + $this->oLocalSettingsProvider = new Providers\Settings( + $this->fabrica('settings-local')); + } + + return $this->oLocalSettingsProvider; + } else { + if (null === $this->oSettingsProvider) { + $this->oSettingsProvider = new Providers\Settings( + $this->fabrica('settings')); + } + + return $this->oSettingsProvider; + } + } + + public function FilesProvider(): Providers\Files + { + if (null === $this->oFilesProvider) { + $this->oFilesProvider = new Providers\Files( + $this->fabrica('files')); + } + + return $this->oFilesProvider; + } + + public function DomainProvider(): Providers\Domain + { + if (null === $this->oDomainProvider) { + $this->oDomainProvider = new Providers\Domain( + $this->fabrica('domain'), $this->oPlugins); + } + + return $this->oDomainProvider; + } + + /** + * bForceFile is only used by admin session token + */ + public function Cacher(?Model\Account $oAccount = null, bool $bForceFile = false): \MailSo\Cache\CacheClient + { + $sKey = ''; + if ($oAccount) { + $sKey = $this->GetMainEmail($oAccount); + } + + $sIndexKey = empty($sKey) ? '_default_' : $sKey; + if ($bForceFile) { + $sIndexKey .= '/_files_'; + } + + if (!isset($this->aCachers[$sIndexKey])) { + $this->aCachers[$sIndexKey] = new \MailSo\Cache\CacheClient(); + + $oDriver = $bForceFile ? null : $this->fabrica('cache'); + if (!($oDriver instanceof \MailSo\Cache\DriverInterface)) { + $oDriver = new \MailSo\Cache\Drivers\File( + \trim($this->oConfig->Get('cache', 'path', '')) ?: APP_PRIVATE_DATA . 'cache' + ); + } + $oDriver->setPrefix($sKey); + + $this->aCachers[$sIndexKey]->SetDriver($oDriver); + $this->aCachers[$sIndexKey]->SetCacheIndex($this->oConfig->Get('cache', 'fast_cache_index', '')); + } + + return $this->aCachers[$sIndexKey]; + } + + public function Plugins(): Plugins\Manager + { + return $this->oPlugins; + } + + public function LoggerAuthHelper(?Model\Account $oAccount, string $sLogin = '', bool $admin = false): void + { + if ($sLogin) { + $sHost = $admin ? $this->Http()->GetHost(true, true) : \MailSo\Base\Utils::getEmailAddressDomain($sLogin); + $aAdditionalParams = array( + '{imap:login}' => $sLogin, + '{imap:host}' => $sHost, + '{smtp:login}' => $sLogin, + '{smtp:host}' => $sHost, + '{user:email}' => $sLogin, + '{user:login}' => $admin ? $sLogin : \MailSo\Base\Utils::getEmailAddressLocalPart($sLogin), + '{user:domain}' => $sHost, + ); + } else { + $aAdditionalParams = array(); + } + $sLine = $this->oConfig->Get('logs', 'auth_logging_format', ''); + if (!empty($sLine)) { + if (!$this->oLoggerAuth) { + $this->oLoggerAuth = new \MailSo\Log\Logger(false); + if ($this->oConfig->Get('logs', 'auth_logging', false)) { +// $this->oLoggerAuth->SetLevel(\LOG_WARNING); + + $sAuthLogFileFullPath = (\trim($this->oConfig->Get('logs', 'path', '') ?: \APP_PRIVATE_DATA . 'logs')) + . '/' . $this->compileLogFileName($this->oConfig->Get('logs', 'auth_logging_filename', '')); + $sLogFileDir = \dirname($sAuthLogFileFullPath); + \is_dir($sLogFileDir) || \mkdir($sLogFileDir, 0755, true); + $this->oLoggerAuth->append( + (new \MailSo\Log\Drivers\File($sAuthLogFileFullPath)) + ->DisableTimePrefix() + ->DisableGuidPrefix() + ->DisableTypedPrefix() + ); + } + } + $this->oLoggerAuth->Write($this->compileLogParams($sLine, $oAccount, $aAdditionalParams), \LOG_WARNING); + } + if (($this->oConfig->Get('logs', 'auth_logging', false) || $this->oConfig->Get('logs', 'auth_syslog', false)) + && \openlog('snappymail', 0, \LOG_AUTHPRIV)) { + \syslog(\LOG_ERR, $this->compileLogParams( + $admin ? 'Admin Auth failed: ip={request:ip} user={user:login}' : 'Auth failed: ip={request:ip} user={imap:login}', + $oAccount, $aAdditionalParams + )); + \closelog(); + } + } + + public function AppData(bool $bAdmin): array + { + $oAccount = null; + $oConfig = $this->oConfig; + + $aResult = array( + 'Auth' => false, + 'title' => $oConfig->Get('webmail', 'title', 'SnappyMail Webmail'), + 'loadingDescription' => $oConfig->Get('webmail', 'loading_description', 'SnappyMail'), + 'Plugins' => array(), + 'System' => array( + 'version' => APP_VERSION, + 'token' => Utils::GetCsrfToken(), + 'languages' => \SnappyMail\L10n::getLanguages(false), + 'webPath' => \RainLoop\Utils::WebPath(), + 'webVersionPath' => \RainLoop\Utils::WebVersionPath() + ), + 'allowLanguagesOnLogin' => (bool) $oConfig->Get('login', 'allow_languages_on_login', true) + ); + + if ($bAdmin) { + ActionsAdmin::AdminAppData($this, $aResult); + } else { + $oAccount = $this->getAccountFromToken(false); + if ($oAccount) { + $aResult = \array_merge( + $aResult, + [ + 'Auth' => true, + 'accountSignMe' => isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY]), + 'allowSpellcheck' => $oConfig->Get('defaults', 'allow_spellcheck', false), + 'ViewHTML' => (bool) $oConfig->Get('defaults', 'view_html', true), + 'ViewImages' => $oConfig->Get('defaults', 'view_images', 'ask'), + 'ViewImagesWhitelist' => '', + 'RemoveColors' => (bool) $oConfig->Get('defaults', 'remove_colors', false), + 'AllowStyles' => false, + 'ListInlineAttachments' => false, + 'CollapseBlockquotes' => $oConfig->Get('defaults', 'collapse_blockquotes', true), + 'MaxBlockquotesLevel' => 0, + 'simpleAttachmentsList' => false, + 'listGrouped' => $oConfig->Get('defaults', 'mail_list_grouped', false), + 'MessagesPerPage' => \max(10, \intval($oConfig->Get('webmail', 'messages_per_page', 25)) ?: 25), + 'messageNewWindow' => false, + 'markdown' => false, + 'messageReadAuto' => true, // (bool) $oConfig->Get('webmail', 'message_read_auto', true), + 'MessageReadDelay' => (int) $oConfig->Get('webmail', 'message_read_delay', 5), + 'MsgDefaultAction' => (int) $oConfig->Get('defaults', 'msg_default_action', 1), + 'SoundNotification' => true, + 'NotificationSound' => 'new-mail', + 'DesktopNotifications' => true, + 'Layout' => (int) $oConfig->Get('defaults', 'view_layout', Enumerations\Layout::SIDE_PREVIEW), + 'EditorDefaultType' => \str_replace('Forced', '', $oConfig->Get('defaults', 'view_editor_type', '')), + 'editorWysiwyg' => 'Squire', + 'UseCheckboxesInList' => (bool) $oConfig->Get('defaults', 'view_use_checkboxes', true), + 'showNextMessage' => (bool) $oConfig->Get('defaults', 'view_show_next_message', false), + 'AutoLogout' => (int) $oConfig->Get('defaults', 'autologout', 30), + 'keyPassForget' => 15, + 'AllowDraftAutosave' => (bool) $oConfig->Get('defaults', 'allow_draft_autosave', true), + 'ContactsAutosave' => (bool) $oConfig->Get('defaults', 'contacts_autosave', true) + ], + // MainAccount or AdditionalAccount + $this->getAccountData($oAccount) + ); + + $aAttachmentsActions = array(); + if ($this->GetCapa(Capa::ATTACHMENTS_ACTIONS)) { + if (\class_exists('PharData') || \class_exists('ZipArchive')) { + $aAttachmentsActions[] = 'zip'; + } + } + $aResult['System'] = \array_merge( + $aResult['System'], array( + 'allowAppendMessage' => (bool)$oConfig->Get('labs', 'allow_message_append', false), + 'folderSpecLimit' => (int)$oConfig->Get('labs', 'folders_spec_limit', 50), + 'listPermanentFiltered' => '' !== \trim($oConfig->Get('imap', 'message_list_permanent_filter', '')), + 'attachmentsActions' => $aAttachmentsActions, + 'customLogoutLink' => $oConfig->Get('labs', 'custom_logout_link', ''), + ) + ); + + if ($aResult['contactsAllowed'] && $oConfig->Get('contacts', 'allow_sync', false)) { + $aData = $this->getContactsSyncData($oAccount) ?: [ + 'Mode' => 0, + 'Url' => '', + 'User' => '' + ]; + $aData['Password'] = empty($aData['Password']) ? '' : static::APP_DUMMY; + $aData['Interval'] = \max(20, \min(320, (int) $oConfig->Get('contacts', 'sync_interval', 20))); + unset($aData['PasswordHMAC']); + $aResult['ContactsSync'] = $aData; + } + + $sToken = \SnappyMail\Cookies::get(self::AUTH_MAILTO_TOKEN_KEY); + if (null !== $sToken) { + \SnappyMail\Cookies::clear(self::AUTH_MAILTO_TOKEN_KEY); + + $mMailToData = Utils::DecodeKeyValuesQ($sToken); + if (!empty($mMailToData['MailTo']) && 'MailTo' === $mMailToData['MailTo'] && !empty($mMailToData['To'])) { + $aResult['mailToEmail'] = \SnappyMail\IDN::emailToUtf8($mMailToData['To']); + } + } + + // MainAccount + $oSettings = $this->SettingsProvider()->Load($oAccount); + if ($oSettings instanceof Settings) { +/* + foreach ($oSettings->toArray() as $key => $value) { + $aResult[\lcfirst($key)] = $value; + } +*/ + $aResult['hourCycle'] = $oSettings->GetConf('hourCycle', ''); + + if (!$oSettings->GetConf('MessagesPerPage')) { + $oSettings->SetConf('MessagesPerPage', $oSettings->GetConf('MPP', $aResult['MessagesPerPage'])); + } + + $aResult['EditorDefaultType'] = \str_replace('Forced', '', $oSettings->GetConf('EditorDefaultType', $aResult['EditorDefaultType'])); + $aResult['editorWysiwyg'] = $oSettings->GetConf('editorWysiwyg', $aResult['editorWysiwyg']); + $aResult['requestReadReceipt'] = (bool) $oSettings->GetConf('requestReadReceipt', false); + $aResult['requestDsn'] = (bool) $oSettings->GetConf('requestDsn', false); + $aResult['requireTLS'] = (bool) $oSettings->GetConf('requireTLS', false); + $aResult['pgpSign'] = (bool) $oSettings->GetConf('pgpSign', false); + $aResult['pgpEncrypt'] = (bool) $oSettings->GetConf('pgpEncrypt', false); + $aResult['allowSpellcheck'] = (bool) $oSettings->GetConf('allowSpellcheck', $aResult['allowSpellcheck']); +// $aResult['allowCtrlEnterOnCompose'] = (bool) $oSettings->GetConf('allowCtrlEnterOnCompose', true); + + $aResult['ViewHTML'] = (bool)$oSettings->GetConf('ViewHTML', $aResult['ViewHTML']); + $show_images = (bool) $oSettings->GetConf('ShowImages', false); + $aResult['ViewImages'] = $oSettings->GetConf('ViewImages', $show_images ? 'always' : $aResult['ViewImages']); + $aResult['ViewImagesWhitelist'] = $oSettings->GetConf('ViewImagesWhitelist', ''); + $aResult['RemoveColors'] = (bool)$oSettings->GetConf('RemoveColors', $aResult['RemoveColors']); + $aResult['AllowStyles'] = (bool)$oSettings->GetConf('AllowStyles', $aResult['AllowStyles']); + $aResult['ListInlineAttachments'] = (bool)$oSettings->GetConf('ListInlineAttachments', $aResult['ListInlineAttachments']); + $aResult['CollapseBlockquotes'] = (bool)$oSettings->GetConf('CollapseBlockquotes', $aResult['CollapseBlockquotes']); + $aResult['MaxBlockquotesLevel'] = (int)$oSettings->GetConf('MaxBlockquotesLevel', $aResult['MaxBlockquotesLevel']); + $aResult['simpleAttachmentsList'] = (bool)$oSettings->GetConf('simpleAttachmentsList', $aResult['simpleAttachmentsList']); + $aResult['listGrouped'] = (bool)$oSettings->GetConf('listGrouped', $aResult['listGrouped']); + $aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']); + $aResult['MessagesPerPage'] = \max(10, \intval($oSettings->GetConf('MessagesPerPage', $aResult['MessagesPerPage']) ?: $aResult['MessagesPerPage'])); + $aResult['messageNewWindow'] = (bool)$oSettings->GetConf('messageNewWindow', $aResult['messageNewWindow']); + $aResult['markdown'] = (bool)$oSettings->GetConf('markdown', $aResult['markdown']); + $aResult['messageReadAuto'] = (int)$oSettings->GetConf('messageReadAuto', $aResult['messageReadAuto']); + $aResult['MessageReadDelay'] = (int)$oSettings->GetConf('MessageReadDelay', $aResult['MessageReadDelay']); + $aResult['MsgDefaultAction'] = (int)$oSettings->GetConf('MsgDefaultAction', $aResult['MsgDefaultAction']); + $aResult['SoundNotification'] = (bool)$oSettings->GetConf('SoundNotification', $aResult['SoundNotification']); + $aResult['NotificationSound'] = (string)$oSettings->GetConf('NotificationSound', $aResult['NotificationSound']); + $aResult['DesktopNotifications'] = (bool)$oSettings->GetConf('DesktopNotifications', $aResult['DesktopNotifications']); + $aResult['UseCheckboxesInList'] = (bool)$oSettings->GetConf('UseCheckboxesInList', $aResult['UseCheckboxesInList']); + $aResult['showNextMessage'] = (bool)$oSettings->GetConf('showNextMessage', $aResult['showNextMessage']); + $aResult['AllowDraftAutosave'] = (bool)$oSettings->GetConf('AllowDraftAutosave', $aResult['AllowDraftAutosave']); + $aResult['AutoLogout'] = (int)$oSettings->GetConf('AutoLogout', $aResult['AutoLogout']); + $aResult['keyPassForget'] = (int)$oSettings->GetConf('keyPassForget', $aResult['keyPassForget']); + $aResult['Layout'] = (int)$oSettings->GetConf('Layout', $aResult['Layout']); + $aResult['Resizer4Width'] = (int)$oSettings->GetConf('Resizer4Width', 0); + $aResult['Resizer5Width'] = (int)$oSettings->GetConf('Resizer5Width', 0); + $aResult['Resizer5Height'] = (int)$oSettings->GetConf('Resizer5Height', 0); + + $aResult['fontSansSerif'] = $oSettings->GetConf('fontSansSerif', ''); + $aResult['fontSerif'] = $oSettings->GetConf('fontSerif', ''); + $aResult['fontMono'] = $oSettings->GetConf('fontMono', ''); + + if ($this->GetCapa(Capa::USER_BACKGROUND)) { + $aResult['userBackgroundName'] = (string)$oSettings->GetConf('UserBackgroundName', ''); + $aResult['userBackgroundHash'] = (string)$oSettings->GetConf('UserBackgroundHash', ''); + } + } + + $aResult['newMailSounds'] = []; + foreach (\glob(APP_VERSION_ROOT_PATH.'static/sounds/*.mp3') as $file) { + $aResult['newMailSounds'][] = \basename($file, '.mp3'); + } +// foreach (\glob(APP_INDEX_ROOT_PATH.'notifications/*.mp3') as $file) { +// $aResult['newMailSounds'][] = 'custom@'.\basename($file, '.mp3'); +// } + } + else { + if (SNAPPYMAIL_DEV) { + $aResult['DevEmail'] = $oConfig->Get('labs', 'dev_email', ''); + $aResult['DevPassword'] = $oConfig->Get('labs', 'dev_password', ''); + } else { + $aResult['DevEmail'] = ''; + $aResult['DevPassword'] = ''; + } + + $aResult['signMe'] = [ + SignMeType::DefaultOff => 0, + SignMeType::DefaultOn => 1, + SignMeType::Unused => 2 + ][(string) $oConfig->Get('login', 'sign_me_auto', SignMeType::DefaultOff)]; + } + } + + if ($aResult['Auth']) { + $aResult['proxyExternalImages'] = (bool)$oConfig->Get('labs', 'use_local_proxy_for_external_images', false); + $aResult['autoVerifySignatures'] = (bool)$oConfig->Get('security', 'auto_verify_signatures', false); + $aResult['allowLanguagesOnSettings'] = (bool) $oConfig->Get('webmail', 'allow_languages_on_settings', true); + $aResult['minRefreshInterval'] = (int) $oConfig->Get('webmail', 'min_refresh_interval', 5); + $aResult['Capa'] = $this->Capa($bAdmin, $oAccount); + $value = \ini_get('upload_max_filesize'); + $upload_max_filesize = \intval($value); + switch (\strtoupper(\substr($value, -1))) { + case 'G': $upload_max_filesize *= 1024; + case 'M': $upload_max_filesize *= 1024; + case 'K': $upload_max_filesize *= 1024; + } + $aResult['attachmentLimit'] = \min($upload_max_filesize, ((int) $oConfig->Get('webmail', 'attachment_size_limit', 10)) * 1024 * 1024); + $aResult['phpUploadSizes'] = array( + 'upload_max_filesize' => $value, + 'post_max_size' => \ini_get('post_max_size') + ); + $aResult['System']['themes'] = $this->GetThemes(); + } + + $aResult['Theme'] = $this->GetTheme($bAdmin); + + $aResult['language'] = $this->GetLanguage(); + $aResult['clientLanguage'] = $this->ValidateLanguage($this->detectClientLanguage($bAdmin), '', false, true); + + $aResult['PluginsLink'] = $this->oPlugins->HaveJs($bAdmin) + ? 'Plugins/0/' . ($bAdmin ? 'Admin' : 'User') . '/' . $this->etag($this->oPlugins->Hash()) . '/' + : ''; + + $bAppJsDebug = $this->oConfig->Get('debug', 'javascript', false) + || $this->oConfig->Get('debug', 'enable', false); + + $aResult['StaticLibsJs'] = Utils::WebStaticPath('js/' . ($bAppJsDebug ? '' : 'min/') . + 'libs' . ($bAppJsDebug ? '' : '.min') . '.js'); + + $this->oPlugins->InitAppData($bAdmin, $aResult, $oAccount); + + return $aResult; + } + + protected function loginErrorDelay(): void + { + $iDelay = (int) $this->oConfig->Get('login', 'fault_delay', 0); + if (0 < $iDelay) { + $seconds = $iDelay - (\microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']); + if (0 < $seconds) { + \usleep(\intval($seconds * 1000000)); + } + } + } + + public function DoPing(): array + { + return $this->DefaultResponse('Pong'); + } + + public function DoVersion(): array + { + return $this->DefaultResponse(APP_VERSION === (string)$this->GetActionParam('version', '')); + } + + public function Upload(?array $aFile, int $iError): array + { + $oAccount = $this->getAccountFromToken(); + + $aResponse = array(); + + if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) { + $sSavedName = 'upload-post-' . \md5($aFile['name'] . $aFile['tmp_name']); + + // Detect content-type + $type = \SnappyMail\File\MimeType::fromFile($aFile['tmp_name'], $aFile['name']) + ?: \SnappyMail\File\MimeType::fromFilename($aFile['name']); + if ($type) { + $aFile['type'] = $type; + $sSavedName .= \SnappyMail\File\MimeType::toExtension($type); + } + + if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) { + $iError = Enumerations\UploadError::ON_SAVING; + } else { + $aResponse['Attachment'] = array( + 'name' => $aFile['name'], + 'tempName' => $sSavedName, + 'mimeType' => $aFile['type'], + 'size' => (int) $aFile['size'] + ); + } + } + + if (UPLOAD_ERR_OK !== $iError) { + $iClientError = 0; + $sError = Enumerations\UploadError::getUserMessage($iError, $iClientError); + + if (!empty($sError)) { + $aResponse['code'] = $iClientError; + $aResponse['Error'] = $sError; + } + } + + return $this->DefaultResponse($aResponse); + } + + public function Capa(bool $bAdmin, ?Model\Account $oAccount = null): array + { + static $aResult; + if (!$aResult) { + $oConfig = $this->oConfig; + $aResult = array( + Capa::ADDITIONAL_ACCOUNTS => (bool) $oConfig->Get('webmail', 'allow_additional_accounts', false), + Capa::ATTACHMENT_THUMBNAILS => (bool) $oConfig->Get('interface', 'show_attachment_thumbnail', true) + && ($bAdmin + || \extension_loaded('gd') + || \extension_loaded('gmagick') + || \extension_loaded('imagick') + ), + Capa::ATTACHMENTS_ACTIONS => (bool) $oConfig->Get('capa', 'attachments_actions', false), + Capa::CONTACTS => (bool) $oConfig->Get('contacts', 'enable', false), + Capa::DANGEROUS_ACTIONS => (bool) $oConfig->Get('capa', 'dangerous_actions', true), + Capa::GNUPG => (bool) $oConfig->Get('security', 'gnupg', true) && \SnappyMail\PGP\GnuPG::isSupported(), + Capa::IDENTITIES => (bool) $oConfig->Get('webmail', 'allow_additional_identities', false), + Capa::OPENPGP => (bool) $oConfig->Get('security', 'openpgp', true), + Capa::SIEVE => false, + Capa::THEMES => (bool) $oConfig->Get('webmail', 'allow_themes', false), + Capa::USER_BACKGROUND => (bool) $oConfig->Get('webmail', 'allow_user_background', false), + 'Kolab' => false, // See Kolab plugin + ); + } + $aResult[Capa::SIEVE] = $bAdmin || ($oAccount && $oAccount->Domain()->SieveSettings()->enabled); + return $aResult; + } + + public function GetCapa(string $sName, ?Model\Account $oAccount = null): bool + { + return !empty($this->Capa(false, $oAccount)[$sName]); + } + + public function etag(string $sKey): string + { +// if ($sKey && $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)) { + return \md5($sKey . $this->oConfig->Get('cache', 'index', '') . APP_VERSION); + } + + public function cacheByKey(string $sKey): bool + { + if ($sKey && $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)) { + \MailSo\Base\Http::ServerUseCache( + $this->etag($sKey), + 0, // issue with messages + $this->oConfig->Get('cache', 'http_expires', 3600) + ); + return true; + } + $this->Http()->ServerNoCache(); + return false; + } + + public function verifyCacheByKey(string $sKey): void + { + if ($sKey && $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)) { + \MailSo\Base\Http::checkETag($this->etag($sKey)); +// \MailSo\Base\Http::checkLastModified(0); + } + } + + /** + * @throws \RainLoop\Exceptions\ClientException + */ + protected function initMailClientConnection(): ?Model\Account + { + $oAccount = $this->getAccountFromToken(); + + if ($oAccount && !$this->ImapClient()->IsLoggined()) { + try { + $oAccount->ImapConnectAndLogin($this->oPlugins, $this->ImapClient(), $this->oConfig); + } catch (\MailSo\Net\Exceptions\ConnectionException $oException) { + throw new Exceptions\ClientException(Notifications::ConnectionError, $oException); + } catch (\Throwable $oException) { + throw new Exceptions\ClientException(Notifications::AuthError, $oException); + } + } + + return $oAccount; + } + + public function encodeRawKey(array $aValues): string + { + $aValues['accountHash'] = $this->getAccountFromToken()->Hash(); + return \MailSo\Base\Utils::UrlSafeBase64Encode(\json_encode($aValues)); + } + + public function decodeRawKey(string $sRawKey): array + { + return empty($sRawKey) ? [] + : (\json_decode(\MailSo\Base\Utils::UrlSafeBase64Decode($sRawKey), true) ?: []); +/* + if (empty($aValues['accountHash']) || $aValues['accountHash'] !== $oAccount->Hash()) { + return []; + } +*/ + } + + public function SetActionParams(array $aCurrentActionParams, string $sMethodName = ''): self + { + $this->oPlugins->RunHook('filter.action-params', array($sMethodName, &$aCurrentActionParams)); + + $this->aCurrentActionParams = $aCurrentActionParams; + + return $this; + } + + /** + * @param mixed $mDefault = null + * + * @return mixed + */ + public function GetActionParam(string $sKey, $mDefault = null) + { + return isset($this->aCurrentActionParams[$sKey]) ? + $this->aCurrentActionParams[$sKey] : $mDefault; + } + + public function GetActionParams(): array + { + return $this->aCurrentActionParams; + } + + public function HasActionParam(string $sKey): bool + { + return isset($this->aCurrentActionParams[$sKey]); + } + + public function Location(string $sUrl, int $iStatus = 302): void + { + $this->logWrite("{$iStatus} Location: {$sUrl}"); + \MailSo\Base\Http::Location($sUrl, $iStatus); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php new file mode 100644 index 0000000000..4e78236e98 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php @@ -0,0 +1,410 @@ +getMainAccountFromToken() : $oAccount)->Email(); + } + + public function IdentitiesProvider(): Identities + { + if (null === $this->oIdentitiesProvider) { + $this->oIdentitiesProvider = new Identities($this->fabrica('identities')); + } + + return $this->oIdentitiesProvider; + } + + public function GetAccounts(MainAccount $oAccount): array + { + if ($this->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) { + $sAccounts = $this->StorageProvider()->Get($oAccount, + StorageType::CONFIG, + 'additionalaccounts' + ); + $aAccounts = $sAccounts ? \json_decode($sAccounts, true) + : \SnappyMail\Upgrade::ConvertInsecureAccounts($this, $oAccount); + if ($aAccounts && \is_array($aAccounts)) { + return $aAccounts; + } + } + + return array(); + } + + public function SetAccounts(MainAccount $oAccount, array $aAccounts = array()): void + { + $sParentEmail = $oAccount->Email(); + if ($aAccounts) { + $this->StorageProvider()->Put( + $oAccount, + StorageType::CONFIG, + 'additionalaccounts', + \json_encode($aAccounts) + ); + } else { + $this->StorageProvider()->Clear( + $oAccount, + StorageType::CONFIG, + 'additionalaccounts' + ); + } + } + + /** + * Add/Edit additional account + * @throws \MailSo\RuntimeException + */ + public function DoAccountSetup(): array + { + if (!$this->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) { + return $this->FalseResponse(); + } + + $oMainAccount = $this->getMainAccountFromToken(); + $aAccounts = $this->GetAccounts($oMainAccount); + + $sEmail = \trim($this->GetActionParam('email', '')); + $oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('password', '')); + $bNew = !empty($this->GetActionParam('new', 1)); + + if ($bNew || \strlen($oPassword)) { + $oNewAccount = $this->LoginProcess($sEmail, $oPassword, false); + $sEmail = $oNewAccount->Email(); + $aAccount = $oNewAccount->asTokenArray($oMainAccount); + } else { + $aAccount = \RainLoop\Model\AdditionalAccount::convertArray($aAccounts[$sEmail]); + } + + if ($bNew) { + if ($oMainAccount->Email() === $sEmail || isset($aAccounts[$sEmail])) { + throw new ClientException(Notifications::AccountAlreadyExists); + } + } else if (!isset($aAccounts[$sEmail])) { + throw new ClientException(Notifications::AccountDoesNotExist); + } + + $aAccounts[$sEmail] = $aAccount; + + if ($aAccounts[$sEmail]) { + $aAccounts[$sEmail]['name'] = \trim($this->GetActionParam('name', '')); + $this->SetAccounts($oMainAccount, $aAccounts); + } + + return $this->TrueResponse(); + } + + protected function loadAdditionalAccountImapClient(string $sEmail): \MailSo\Imap\ImapClient + { + $sEmail = IDN::emailToAscii($sEmail); + if (!\strlen($sEmail)) { + throw new ClientException(Notifications::AccountDoesNotExist); + } + + $oMainAccount = $this->getMainAccountFromToken(); + $aAccounts = $this->GetAccounts($oMainAccount); + if (!isset($aAccounts[$sEmail])) { + throw new ClientException(Notifications::AccountDoesNotExist); + } + $oAccount = AdditionalAccount::NewInstanceFromTokenArray($this, $aAccounts[$sEmail]); + if (!$oAccount) { + throw new ClientException(Notifications::AccountDoesNotExist); + } + + $oImapClient = new \MailSo\Imap\ImapClient; + $oImapClient->SetLogger($this->Logger()); + $this->imapConnect($oAccount, false, $oImapClient); + return $oImapClient; + } + + public function DoAccountUnread(): array + { + $oImapClient = $this->loadAdditionalAccountImapClient($this->GetActionParam('email', '')); + $oInfo = $oImapClient->FolderStatus('INBOX'); + return $this->DefaultResponse([ + 'unreadEmails' => \max(0, $oInfo->UNSEEN) + ]); + } + + /** + * Imports all mail from AdditionalAccount into MainAccount + */ + public function DoAccountImport(): array + { + $sEmail = $this->GetActionParam('email', ''); + $oImapSource = $this->loadAdditionalAccountImapClient($sEmail); + + $oMainAccount = $this->getMainAccountFromToken(); + $oImapTarget = new \MailSo\Imap\ImapClient; + $oImapTarget->SetLogger($this->Logger()); + $this->imapConnect($oMainAccount, false, $oImapTarget); + + $oSync = new \SnappyMail\Imap\Sync; + $oSync->oImapSource = $oImapSource; + $oSync->oImapTarget = $oImapTarget; + + $rootfolder = $this->GetActionParam('rootfolder', '') ?: $sEmail; + $oSync->import($rootfolder); + exit; + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoAccountDelete(): array + { + $oMainAccount = $this->getMainAccountFromToken(); + + if (!$this->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) { + return $this->FalseResponse(); + } + + $sEmailToDelete = \trim($this->GetActionParam('emailToDelete', '')); + $sEmailToDelete = IDN::emailToAscii($sEmailToDelete); + + $aAccounts = $this->GetAccounts($oMainAccount); + + if (\strlen($sEmailToDelete) && isset($aAccounts[$sEmailToDelete])) { + $bReload = false; + $oAccount = $this->getAccountFromToken(); + if ($oAccount instanceof AdditionalAccount && $oAccount->Email() === $sEmailToDelete) { +// $this->SetAdditionalAuthToken(null); + \SnappyMail\Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY); + $bReload = true; + } + + unset($aAccounts[$sEmailToDelete]); + $this->SetAccounts($oMainAccount, $aAccounts); + + return $this->TrueResponse(array('Reload' => $bReload)); + } + + return $this->FalseResponse(); + } + + public function getAccountData(Account $oAccount): array + { + $oConfig = $this->Config(); + $minRefreshInterval = (int) $oConfig->Get('webmail', 'min_refresh_interval', 5); + $aResult = [ +// 'Email' => IDN::emailToUtf8($oAccount->Email()), + 'Email' => $oAccount->Email(), + 'accountHash' => $oAccount->Hash(), + 'mainEmail' => \RainLoop\Api::Actions()->getMainAccountFromToken()->Email(), + 'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(), + 'HideUnsubscribed' => false, + 'defaultSort' => '', + 'useThreads' => (bool) $oConfig->Get('defaults', 'mail_use_threads', false), + 'threadAlgorithm' => '', + 'ReplySameFolder' => (bool) $oConfig->Get('defaults', 'mail_reply_same_folder', false), + 'HideDeleted' => true, + 'ShowUnreadCount' => false, + 'UnhideKolabFolders' => false, + 'CheckMailInterval' => \max(15, $minRefreshInterval) + ]; + $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); + if ($oSettingsLocal instanceof \RainLoop\Settings) { + $aResult['SentFolder'] = (string) $oSettingsLocal->GetConf('SentFolder', ''); + $aResult['DraftsFolder'] = (string) $oSettingsLocal->GetConf('DraftsFolder', ''); + $aResult['JunkFolder'] = (string) $oSettingsLocal->GetConf('JunkFolder', ''); + $aResult['TrashFolder'] = (string) $oSettingsLocal->GetConf('TrashFolder', ''); + $aResult['ArchiveFolder'] = (string) $oSettingsLocal->GetConf('ArchiveFolder', ''); + $aResult['HideUnsubscribed'] = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $aResult['HideUnsubscribed']); + $aResult['defaultSort'] = (string) $oSettingsLocal->GetConf('defaultSort', $aResult['defaultSort']); + $aResult['useThreads'] = (bool) $oSettingsLocal->GetConf('UseThreads', $aResult['useThreads']); + $aResult['threadAlgorithm'] = (string) $oSettingsLocal->GetConf('threadAlgorithm', $aResult['threadAlgorithm']); + $aResult['ReplySameFolder'] = (bool) $oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']); + $aResult['HideDeleted'] = (bool)$oSettingsLocal->GetConf('HideDeleted', $aResult['HideDeleted']); + $aResult['ShowUnreadCount'] = (bool)$oSettingsLocal->GetConf('ShowUnreadCount', $aResult['ShowUnreadCount']); + $aResult['UnhideKolabFolders'] = (bool)$oSettingsLocal->GetConf('UnhideKolabFolders', $aResult['UnhideKolabFolders']); + $aResult['CheckMailInterval'] = \max((int) $oSettingsLocal->GetConf('CheckMailInterval', $aResult['CheckMailInterval']), $minRefreshInterval); +/* + foreach ($oSettingsLocal->toArray() as $key => $value) { + $aResult[\lcfirst($key)] = $value; + } + $aResult['junkFolder'] = $aResult['spamFolder']; + unset($aResult['checkableFolder']); + unset($aResult['theme']); +*/ + } + return $aResult; + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoAccountSwitch(): array + { + if ($this->switchAccount(\trim($this->GetActionParam('Email', '')))) { + $oAccount = $this->getAccountFromToken(); + $aResult = $this->getAccountData($oAccount); +// $this->Plugins()->InitAppData($bAdmin, $aResult, $oAccount); + return $this->DefaultResponse($aResult); + } + return $this->FalseResponse(); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoIdentityUpdate(): array + { + $oAccount = $this->getAccountFromToken(); + + $oIdentity = new Identity(); + if (!$oIdentity->FromJSON($this->GetActionParams(), true)) { + throw new ClientException(Notifications::InvalidInputArgument); + } +/* // TODO: verify private key for certificate? + if ($oIdentity->smimeCertificate && $oIdentity->smimeKey) { + new \SnappyMail\SMime\Certificate($oIdentity->smimeCertificate, $oIdentity->smimeKey); + } +*/ + $this->IdentitiesProvider()->UpdateIdentity($oAccount, $oIdentity); + return $this->TrueResponse(); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoIdentityDelete(): array + { + $oAccount = $this->getAccountFromToken(); + + if (!$this->GetCapa(Capa::IDENTITIES)) { + return $this->FalseResponse(); + } + + $sId = \trim($this->GetActionParam('idToDelete', '')); + if (empty($sId)) { + throw new ClientException(Notifications::UnknownError); + } + + $this->IdentitiesProvider()->DeleteIdentity($oAccount, $sId); + return $this->TrueResponse(); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoAccountsAndIdentitiesSortOrder(): array + { + $aAccounts = $this->GetActionParam('Accounts', null); + $aIdentities = $this->GetActionParam('Identities', null); + + if (!\is_array($aAccounts) && !\is_array($aIdentities)) { + return $this->FalseResponse(); + } + + if (\is_array($aAccounts) && 1 < \count($aAccounts)) { + $oAccount = $this->getMainAccountFromToken(); + $aAccounts = \array_filter(\array_merge( + \array_fill_keys($aAccounts, null), + $this->GetAccounts($oAccount) + )); + $this->SetAccounts($oAccount, $aAccounts); + } + + return $this->DefaultResponse($this->LocalStorageProvider()->Put( + $this->getAccountFromToken(), + StorageType::CONFIG, + 'identities_order', + \json_encode(array( + 'Identities' => \is_array($aIdentities) ? $aIdentities : array() + )) + )); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoAccountsAndIdentities(): array + { + // https://github.com/the-djmaze/snappymail/issues/571 + return $this->DefaultResponse(array( + 'Accounts' => \array_values(\array_map(function($value){ + return [ + 'email' => IDN::emailToUtf8($value['email'] ?? $value[1]), + 'name' => $value['name'] ?? '' + ]; + }, + $this->GetAccounts($this->getMainAccountFromToken()) + )), + 'Identities' => $this->GetIdentities($this->getAccountFromToken()) + )); + } + + /** + * @return Identity[] + */ + public function GetIdentities(Account $oAccount): array + { + // A custom name for a single identity is also stored in this system + $allowMultipleIdentities = $this->GetCapa(Capa::IDENTITIES); + + // Get all identities + $identities = $this->IdentitiesProvider()->GetIdentities($oAccount, $allowMultipleIdentities); + + // Sort identities + $orderString = $this->LocalStorageProvider()->Get($oAccount, StorageType::CONFIG, 'identities_order'); + $old = false; + if (!$orderString) { + $orderString = $this->StorageProvider()->Get($oAccount, StorageType::CONFIG, 'accounts_identities_order'); + $old = !!$orderString; + } + + $order = \json_decode($orderString, true) ?? []; + if (isset($order['Identities']) && \is_array($order['Identities']) && 1 < \count($order['Identities'])) { + $list = \array_map(function ($item) { + return ('' === $item) ? '---' : $item; + }, $order['Identities']); + + \usort($identities, function ($a, $b) use ($list) { + return \array_search($a->Id(true), $list) < \array_search($b->Id(true), $list) ? -1 : 1; + }); + } + + if ($old) { + $this->LocalStorageProvider()->Put( + $oAccount, + StorageType::CONFIG, + 'identities_order', + \json_encode(array('Identities' => empty($order['Identities']) ? [] : $order['Identities'])) + ); + $this->StorageProvider()->Clear($oAccount, StorageType::CONFIG, 'accounts_identities_order'); + } + + return $identities; + } + + public function GetIdentityByID(Account $oAccount, string $sID, bool $bFirstOnEmpty = false): ?Identity + { + $aIdentities = $this->GetIdentities($oAccount); + + foreach ($aIdentities as $oIdentity) { + if ($oIdentity && $sID === $oIdentity->Id()) { + return $oIdentity; + } + } + + return $bFirstOnEmpty && isset($aIdentities[0]) ? $aIdentities[0] : null; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php new file mode 100644 index 0000000000..970703a779 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php @@ -0,0 +1,42 @@ +Config()->Get('security', 'allow_admin_panel', true)) { + $sAdminKey = $this->getAdminAuthKey(); + if ($sAdminKey && $this->Cacher(null, true)->Get(KeyPathHelper::SessionAdminKey($sAdminKey))) { + return true; + } + } + + if ($bThrowExceptionOnFalse) { + throw new ClientException(Notifications::AuthError); + } + + return false; + } + + protected function getAdminAuthKey() : string + { + $cookie = \SnappyMail\Cookies::get(static::$AUTH_ADMIN_TOKEN_KEY); + if ($cookie) { + $aAdminHash = Utils::DecodeKeyValuesQ($cookie); + if (!empty($aAdminHash[1]) && 'token' === $aAdminHash[0]) { + return $aAdminHash[1]; + } + \SnappyMail\Cookies::clear(static::$AUTH_ADMIN_TOKEN_KEY); + } + return ''; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminDomains.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminDomains.php new file mode 100644 index 0000000000..bb037054b8 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminDomains.php @@ -0,0 +1,235 @@ +IsAdminLoggined(); + $mResult = false; + $oDomain = $this->DomainProvider()->Load($this->GetActionParam('name', ''), false, false); + if ($oDomain) { + $mResult = $oDomain->jsonSerialize(); + $mResult['name'] = $oDomain->Name(); + } + return $this->DefaultResponse($mResult); + } + + public function DoAdminDomainList() : array + { + $this->IsAdminLoggined(); + $bIncludeAliases = !empty($this->GetActionParam('includeAliases', '1')); + return $this->DefaultResponse($this->DomainProvider()->GetList($bIncludeAliases)); + } + + public function DoAdminDomainDelete() : array + { + $this->IsAdminLoggined(); + return $this->DefaultResponse($this->DomainProvider()->Delete($this->GetActionParam('name', ''))); + } + + public function DoAdminDomainDisable() : array + { + $this->IsAdminLoggined(); + + return $this->DefaultResponse($this->DomainProvider()->Disable( + (string) $this->GetActionParam('name', ''), + !empty($this->GetActionParam('disabled', '0')) + )); + } + + public function DoAdminDomainSave() : array + { + $this->IsAdminLoggined(); + + $oDomain = $this->DomainProvider()->LoadOrCreateNewFromAction($this); + + return $this->DefaultResponse($oDomain ? $this->DomainProvider()->Save($oDomain) : false); + } + + public function DoAdminDomainAliasSave() : array + { + $this->IsAdminLoggined(); + + return $this->DefaultResponse($this->DomainProvider()->SaveAlias( + (string) $this->GetActionParam('name', ''), + (string) $this->GetActionParam('alias', '') + )); + } + + public function DoAdminDomainMatch() : array + { + $sCredentials = $this->resolveLoginCredentials( + $this->GetActionParam('username'), + new \SnappyMail\SensitiveString('********') + ); + $sEmail = $sCredentials['email']; + return $this->DefaultResponse(array( + 'email' => $sEmail, + 'login' => $sCredentials['imapUser'], + 'name' => $sCredentials['domain'] ? $sCredentials['domain']->Name() : null, +// 'domain' => $sCredentials['domain'], + 'whitelist' => $sCredentials['domain'] ? $sCredentials['domain']->ValidateWhiteList($sEmail) : null + )); + } + + public function DoAdminDomainAutoconfig() : array + { + $this->IsAdminLoggined(); +// $sDomain = \SnappyMail\IDN::toAscii($this->GetActionParam('domain')); + $sDomain = \strtolower(\idn_to_ascii($this->GetActionParam('domain'))); + $sEmail = "test@{$sDomain}"; + return $this->DefaultResponse(array( + 'email' => $sEmail, + 'config' => \RainLoop\Providers\Domain\Autoconfig::discover($sEmail) + )); + } + + public function DoAdminDomainTest() : array + { + $this->IsAdminLoggined(); + + $mImapResult = false; + $sImapErrorDesc = ''; + $mSmtpResult = false; + $sSmtpErrorDesc = ''; + $mSieveResult = false; + $sSieveErrorDesc = ''; + + $oDomain = $this->DomainProvider()->LoadOrCreateNewFromAction($this, 'test.example.com'); + if ($oDomain) { + $aAuth = $this->GetActionParam('auth'); + + try + { + $oImapClient = new \MailSo\Imap\ImapClient(); + $oImapClient->SetLogger($this->Logger()); + + $oSettings = $oDomain->ImapSettings(); + $oImapClient->Connect($oSettings); + $mImapResult = [ + 'connectCapa' => \array_values(\array_diff($oImapClient->Capabilities(), ['STARTTLS'])) + ]; + if (!empty($aAuth['user'])) { + $oSettings->username = $aAuth['user']; + $oSettings->passphrase = $aAuth['pass']; + $oImapClient->Login($oSettings); + $mImapResult['authCapa'] = \array_values(\array_unique(\array_map(function($n){ + return \str_starts_with($n, 'THREAD=') ? 'THREAD' : $n; + }, + $oImapClient->Capabilities() + ))); + } + + $oImapClient->Disconnect(); + } + catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException) + { + $this->logException($oException, \LOG_ERR); + $sImapErrorDesc = $oException->getSocketMessage(); + if (empty($sImapErrorDesc)) { + $sImapErrorDesc = $oException->getMessage(); + } + } + catch (\Throwable $oException) + { + $this->logException($oException, \LOG_ERR); + $sImapErrorDesc = $oException->getMessage(); + } + + if ($oDomain->SmtpSettings()->usePhpMail) { + $mSmtpResult = \MailSo\Base\Utils::FunctionCallable('mail'); + if (!$mSmtpResult) { + $sSmtpErrorDesc = 'PHP: mail() function is undefined'; + } + } else { + try + { + $oSmtpClient = new \MailSo\Smtp\SmtpClient(); + $oSmtpClient->SetLogger($this->Logger()); + + $oSettings = $oDomain->SmtpSettings(); + $oSettings->Ehlo = \MailSo\Smtp\SmtpClient::EhloHelper(); + $oSmtpClient->Connect($oSettings); + $mSmtpResult = [ + 'connectCapa' => $oSmtpClient->Capability() + ]; + + if (!empty($aAuth['user'])) { + $oSettings->username = $aAuth['user']; + $oSettings->passphrase = $aAuth['pass']; + $oSmtpClient->Login($oSettings); + $mSmtpResult['authCapa'] = $oSmtpClient->Capability(); + } + + $oSmtpClient->Disconnect(); + } + catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException) + { + $this->logException($oException, \LOG_ERR); + $sSmtpErrorDesc = $oException->getSocketMessage(); + if (empty($sSmtpErrorDesc)) { + $sSmtpErrorDesc = $oException->getMessage(); + } + } + catch (\Throwable $oException) + { + $this->logException($oException, \LOG_ERR); + $sSmtpErrorDesc = $oException->getMessage(); + } + } + + if ($oDomain->SieveSettings()->enabled) { + try + { + $oSieveClient = new \MailSo\Sieve\SieveClient(); + $oSieveClient->SetLogger($this->Logger()); + + $oSettings = $oDomain->SieveSettings(); + $oSieveClient->Connect($oSettings); + $mSieveResult = [ + 'connectCapa' => $oSieveClient->Capability() + ]; + + if (!empty($aAuth['user'])) { + $oSettings->username = $aAuth['user']; + $oSettings->passphrase = $aAuth['pass']; + $oSieveClient->Login($oSettings); + $mSieveResult['authCapa'] = $oSieveClient->Capability(); + } + + $oSieveClient->Disconnect(); + } + catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException) + { + $this->logException($oException, \LOG_ERR); + $sSieveErrorDesc = $oException->getSocketMessage(); + if (empty($sSieveErrorDesc)) { + $sSieveErrorDesc = $oException->getMessage(); + } + } + catch (\Throwable $oException) + { + $this->logException($oException, \LOG_ERR); + $sSieveErrorDesc = $oException->getMessage(); + } + } else { + $mSieveResult = true; + } + } + + return $this->DefaultResponse(array( + 'Imap' => $mImapResult ? true : $sImapErrorDesc, + 'Smtp' => $mSmtpResult ? true : $sSmtpErrorDesc, + 'Sieve' => $mSieveResult ? true : $sSieveErrorDesc, + 'ImapResult' => $mImapResult, + 'SmtpResult' => $mSmtpResult, + 'SieveResult' => $mSieveResult + )); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminExtensions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminExtensions.php new file mode 100644 index 0000000000..3caea3168b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminExtensions.php @@ -0,0 +1,158 @@ +DefaultResponse(Repository::getPackagesList()); + } + + public function DoAdminPackageDelete() : array + { + return $this->DefaultResponse(Repository::deletePackage($this->GetActionParam('id', ''))); + } + + public function DoAdminPackageInstall() : array + { + $sType = $this->GetActionParam('type', ''); + $bResult = Repository::installPackage( + $sType, + $this->GetActionParam('id', ''), + $this->GetActionParam('file', '') + ); + return $this->DefaultResponse($bResult ? + ('plugin' !== $sType ? array('Reload' => true) : true) : false); + } + + public function DoAdminPluginDisable() : array + { + $this->IsAdminLoggined(); + + $sId = (string) $this->GetActionParam('id', ''); + $bDisable = '1' === (string) $this->GetActionParam('disabled', '1'); + + if (!$bDisable) { + $oPlugin = $this->Plugins()->CreatePluginByName($sId); + if ($oPlugin) { + $sValue = $oPlugin->Supported(); + if (\strlen($sValue)) { + return $this->FalseResponse(Notifications::UnsupportedPluginPackage, $sValue); + } + } else { + return $this->FalseResponse(Notifications::InvalidPluginPackage); + } + } + + return $this->DefaultResponse(Repository::enablePackage($sId, !$bDisable)); + } + + public function DoAdminPluginLoad() : array + { + $this->IsAdminLoggined(); + + $mResult = false; + $sId = (string) $this->GetActionParam('id', ''); + + if (!empty($sId)) { + $oPlugin = $this->Plugins()->CreatePluginByName($sId); + if ($oPlugin) { + $mResult = array( + '@Object' => 'Object/Plugin', + 'id' => $sId, + 'name' => $oPlugin->Name(), + 'readme' => $oPlugin->Description(), + 'config' => array(), + + 'author' => $oPlugin::AUTHOR, + 'url' => $oPlugin::URL, + 'version' => $oPlugin::VERSION, + 'released' => $oPlugin::RELEASE +/* + $oPlugin::NAME + $oPlugin::REQUIRED + $oPlugin::DEPRECATED + $oPlugin::CATEGORY + $oPlugin::LICENSE + $oPlugin::DESCRIPTION +*/ + ); + + $aMap = $oPlugin->ConfigMap(); + if (\is_array($aMap)) { + $oConfig = $oPlugin->Config(); + foreach ($aMap as $oItem) { + if ($oItem) { + if ($oItem instanceof \RainLoop\Plugins\Property) { + if (PluginPropertyType::PASSWORD === $oItem->Type()) { + $oItem->SetValue(static::APP_DUMMY); + } else { + $oItem->SetValue($oConfig->Get('plugin', $oItem->Name(), '')); + } + $mResult['config'][] = $oItem; + } else if ($oItem instanceof \RainLoop\Plugins\PropertyCollection) { + foreach ($oItem as $oSubItem) { + if ($oSubItem && $oSubItem instanceof \RainLoop\Plugins\Property) { + if (PluginPropertyType::PASSWORD === $oSubItem->Type()) { + $oSubItem->SetValue(static::APP_DUMMY); + } else { + $oSubItem->SetValue($oConfig->Get('plugin', $oSubItem->Name(), '')); + } + } + } + $mResult['config'][] = $oItem; + } + } + } + } + } + } + + return $this->DefaultResponse($mResult); + } + + public function DoAdminPluginSettingsUpdate() : array + { + $this->IsAdminLoggined(); + + $sId = (string) $this->GetActionParam('id', ''); + + if (!empty($sId)) { + $oPlugin = $this->Plugins()->CreatePluginByName($sId); + if ($oPlugin) { + $oConfig = $oPlugin->Config(); + $aMap = $oPlugin->ConfigMap(true); + if (\is_array($aMap)) { + $aSettings = (array) $this->GetActionParam('settings', []); + foreach ($aMap as $oItem) { + $sKey = $oItem->Name(); + $mValue = $aSettings[$sKey] ?? $oConfig->Get('plugin', $sKey); + if (PluginPropertyType::PASSWORD !== $oItem->Type() || static::APP_DUMMY !== $mValue) { + $oItem->SetValue($mValue); + $mValue = $oItem->Value(); + if (null !== $mValue) { + if ($oItem->encrypted) { + $oConfig->setEncrypted('plugin', $sKey, $mValue); + } else { + $oConfig->Set('plugin', $sKey, $mValue); + } + } + } + } + } + if ($oConfig->Save()) { + return $this->TrueResponse(); + } + } + } + + throw new ClientException(Notifications::CantSavePluginSettings); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Attachments.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Attachments.php new file mode 100644 index 0000000000..007c6171d3 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Attachments.php @@ -0,0 +1,194 @@ +GetActionParam('target', ''); + $sFolder = $this->GetActionParam('folder', ''); + $sFilename = \MailSo\Base\Utils::SecureFileName($this->GetActionParam('filename', '')); + $aHashes = $this->GetActionParam('hashes', null); + $oFilesProvider = $this->FilesProvider(); + if (empty($sAction)) { + $this->logWrite('AttachmentsAction target not set'); + } + if (!$this->GetCapa(Capa::ATTACHMENTS_ACTIONS)) { + $this->logWrite('AttachmentsAction disabled'); + } + if (!$oFilesProvider || !$oFilesProvider->IsActive()) { + $this->logWrite('AttachmentsAction FilesProvider inactive'); + } + if (empty($sAction) || !$this->GetCapa(Capa::ATTACHMENTS_ACTIONS) || !$oFilesProvider || !$oFilesProvider->IsActive()) { + return $this->FalseResponse(); + } + + $oAccount = $this->initMailClientConnection(); + + $bError = false; + $aData = []; + $mUIDs = []; + + if (\is_array($aHashes) && \count($aHashes)) { + foreach ($aHashes as $sZipHash) { + $aResult = $this->getMimeFileByHash($oAccount, $sZipHash); + if (empty($aResult['data']) && empty($aResult['fileHash'])) { + $this->logWrite('AttachmentsAction data/fileHash not set: ' . \print_r($aResult,1)); + $bError = true; + break; + } + $aData[] = $aResult; + if (!empty($aResult['mimeIndex'])) { + $mUIDs[$aResult['uid']] = $aResult['uid']; + } + } + } + $mUIDs = 1 < \count($mUIDs); + + if ($bError || !\count($aData)) { + return $this->FalseResponse(); + } + + $mResult = false; + switch (\strtolower($sAction)) + { + case 'zip': + + $sZipHash = \MailSo\Base\Utils::Sha1Rand(); + $sZipFileName = $oFilesProvider->GenerateLocalFullFileName($oAccount, $sZipHash); + + if (!empty($sZipFileName)) { + if (\class_exists('ZipArchive')) { + $oZip = new \ZipArchive(); + $oZip->open($sZipFileName, \ZIPARCHIVE::CREATE | \ZIPARCHIVE::OVERWRITE); + $oZip->setArchiveComment('SnappyMail/'.APP_VERSION); + foreach ($aData as $aItem) { + $sFileName = ($mUIDs ? "{$aItem['uid']}/" : ($sFolder ? "{$aItem['uid']}-" : '')) . $aItem['fileName']; + if (isset($aItem['data'])) { + if (!$oZip->addFromString($sFileName, $aItem['data'])) { + $bError = true; + } + } else { + $sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $aItem['fileHash']); + if (!$oZip->addFile($sFullFileNameHash, $sFileName)) { + $bError = true; + } + } + } + + if ($bError) { + $oZip->close(); + } else { + $bError = !$oZip->close(); + } +/* + } else { + @\unlink($sZipFileName); + $oZip = new \SnappyMail\Stream\ZIP($sZipFileName); +// $oZip->setArchiveComment('SnappyMail/'.APP_VERSION); + foreach ($aData as $aItem) { + if ($aItem['fileHash']) { + $sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $aItem['fileHash']); + if (!$oZip->addFile($sFullFileNameHash, $aItem['fileName'])) { + $bError = true; + } + } + } + $oZip->close(); +*/ + } else { + @\unlink($sZipFileName); + $oZip = new \PharData($sZipFileName . '.zip', 0, null, \Phar::ZIP); + $oZip->compressFiles(\Phar::GZ); + foreach ($aData as $aItem) { + if ($aItem['data']) { + if (!$oZip->addFromString($sFileName, \MailSo\Base\Utils::UrlSafeBase64Decode($aItem['data']))) { + $bError = true; + } + } else { + $oZip->addFile( + $oFilesProvider->GetFileName($oAccount, $aItem['fileHash']), + ($mUIDs ? "{$aItem['uid']}/" : ($sFolder ? "{$aItem['uid']}-" : '')) . $aItem['fileName'] + ); + } + } + $oZip->compressFiles(\Phar::GZ); + unset($oZip); + \rename($sZipFileName . '.zip', $sZipFileName); + } + + if (!$bError) { + $mResult = array( + 'fileHash' => $this->encodeRawKey(array( + 'fileName' => ($sFilename ?: ($sFolder ? 'messages' : 'attachments')) . \date('-YmdHis') . '.zip', + 'mimeType' => 'application/zip', + 'fileHash' => $sZipHash + )) + ); + } + } + break; + + default: + $data = new \SnappyMail\AttachmentsAction; + $data->action = $sAction; + $data->items = $aData; + $data->filesProvider = $oFilesProvider; + $data->account = $oAccount; + $this->Plugins()->RunHook('json.attachments', array($data)); + $mResult = $data->result; + break; + } + + foreach ($aData as $aItem) { + $aItem['fileHash'] && $oFilesProvider->Clear($oAccount, $aItem['fileHash']); + } + + return $this->DefaultResponse($bError ? false : $mResult); + } + + private function getMimeFileByHash(\RainLoop\Model\Account $oAccount, string $sHash) : array + { + $aValues = $this->decodeRawKey($sHash); + $aValues['fileName'] = empty($aValues['fileName']) ? 'file.dat' : (string) $aValues['fileName']; + $aValues['fileHash'] = null; + if (isset($aValues['data'])) { + $aValues['data'] = \MailSo\Base\Utils::UrlSafeBase64Decode($aValues['data']); + } else if (!empty($aValues['folder']) && !empty($aValues['uid'])) { + $sFolder = (string) $aValues['folder']; + $iUid = (int) $aValues['uid']; + $sMimeIndex = (string) $aValues['mimeIndex'] ?: ''; + $oFileProvider = $this->FilesProvider(); + $this->MailClient()->MessageMimeStream( + function ($rResource, $sContentType, $sFileName, $sMimeIndex = '') + use ($oAccount, $oFileProvider, &$aValues) { + unset($sContentType, $sFileName, $sMimeIndex); + if (\is_resource($rResource)) { + $sContentTypeIn = isset($aValues['mimeType']) ? (string) $aValues['mimeType'] : ''; + $sHash = \MailSo\Base\Utils::Sha1Rand($aValues['fileName'].'~'.$sContentTypeIn); + $rTempResource = $oFileProvider->GetFile($oAccount, $sHash, 'wb+'); + if (\is_resource($rTempResource)) { + if (-1 < \MailSo\Base\Utils::WriteStream($rResource, $rTempResource)) { + $sResultHash = $sHash; + $aValues['fileHash'] = $sHash; + } + \fclose($rTempResource); + } + } + }, + $sFolder, + $iUid, + $sMimeIndex + ); + } + + return $aValues; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php new file mode 100644 index 0000000000..3e6dd5a079 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php @@ -0,0 +1,378 @@ +oAddressBookProvider) { + $oDriver = null; + try { +// if ($this->oConfig->Get('contacts', 'enable', false)) { + if ($this->GetCapa(Capa::CONTACTS)) { + $oDriver = $this->fabrica('address-book', $oAccount); + } + if ($oAccount && $oDriver) { + $oDriver->SetEmail($this->GetMainEmail($oAccount)); + $oDriver->setDAVClientConfig($this->getContactsSyncData($oAccount)); + } + } catch (\Throwable $e) { + \SnappyMail\LOG::error('AddressBook', $e->getMessage()."\n".$e->getTraceAsString()); + $oDriver = null; +// $oDriver = new \RainLoop\Providers\AddressBook\PdoAddressBook(); + } + $this->oAddressBookProvider = new \RainLoop\Providers\AddressBook($oDriver); + $this->oAddressBookProvider->SetLogger($this->oLogger); + } + + return $this->oAddressBookProvider; + } + + public function DoSaveContactsSyncData() : array + { + $oAccount = $this->getAccountFromToken(); + + $oAddressBookProvider = $this->AddressBookProvider($oAccount); + if (!$oAddressBookProvider || !$oAddressBookProvider->IsActive()) { + return $this->FalseResponse(); + } + + $sPassword = $this->GetActionParam('Password', ''); + + $mData = $this->getContactsSyncData($oAccount); + + $bResult = $this->setContactsSyncData($oAccount, array( + 'Mode' => \intval($this->GetActionParam('Mode', '0')), + 'User' => $this->GetActionParam('User', ''), + 'Password' => static::APP_DUMMY === $sPassword + ? (isset($mData['Password']) ? $mData['Password'] : '') + : $sPassword, + 'Url' => $this->GetActionParam('Url', '') + )); + + return $this->DefaultResponse($bResult); + } + + public function DoTestContactsSyncData() : array + { + if (!$this->GetCapa(Capa::CONTACTS)) { + throw new ClientException(\RainLoop\Notifications::ContactsSyncError, null, 'Disallowed'); + } + + $oAccount = $this->getAccountFromToken(); + + $sPassword = $this->GetActionParam('Password', ''); + if (static::APP_DUMMY === $sPassword) { + $mData = $this->getContactsSyncData($oAccount); + $sPassword = isset($mData['Password']) ? $mData['Password'] : ''; + } + $sPasswordHMAC = null; + if ($sPassword) { + $oMainAccount = $this->getMainAccountFromToken(); + $sPassword = \SnappyMail\Crypt::EncryptToJSON($sPassword, $oMainAccount->CryptKey()); + if ($sPassword) { + $sPasswordHMAC = \hash_hmac('sha1', $sPassword, $oMainAccount->CryptKey()); + } + } + + $oDriver = $this->fabrica('address-book', $oAccount); + if (!$oDriver) { + throw new ClientException(\RainLoop\Notifications::ContactsSyncError, null, 'No driver'); + } + $oDriver->SetEmail($this->GetMainEmail($oAccount)); + $oDriver->setDAVClientConfig([ + 'Mode' => 2, // readonly + 'User' => $this->GetActionParam('User', ''), + 'Password' => $sPassword, + 'Url' => $this->GetActionParam('Url', ''), + 'PasswordHMAC' => $sPasswordHMAC + ]); + + $oClient = $oDriver->getDavClient(); + if (!$oClient) { + throw new ClientException(\RainLoop\Notifications::ContactsSyncError, null, 'No client'); + } + $oClient->propFind($oClient->urlPath, [ + '{DAV:}getlastmodified', + '{DAV:}resourcetype', + '{DAV:}getetag' + ], 1); + + return $this->TrueResponse(); + } + + public function DoContactsSync() : array + { + $oAccount = $this->getAccountFromToken(); + $oAddressBookProvider = $this->AddressBookProvider($oAccount); + if (!$oAddressBookProvider) { + throw new ClientException(\RainLoop\Notifications::ContactsSyncError, null, 'No AddressBookProvider'); + } + \ignore_user_abort(true); + \SnappyMail\HTTP\Stream::start(/*$binary = false*/); + \SnappyMail\HTTP\Stream::JSON(['messsage'=>'start']); + if (!$oAddressBookProvider->Sync()) { + throw new ClientException(\RainLoop\Notifications::ContactsSyncError, null, 'AddressBookProvider->Sync() failed'); + } + return $this->TrueResponse(); + } + + public function DoContacts() : array + { + $oAccount = $this->getAccountFromToken(); + + $sSearch = \trim($this->GetActionParam('Search', '')); + $iOffset = (int) $this->GetActionParam('Offset', 0); + $iLimit = (int) $this->GetActionParam('Limit', 20); + $iOffset = 0 > $iOffset ? 0 : $iOffset; + $iLimit = 0 > $iLimit ? 20 : $iLimit; + + $iResultCount = 0; + $mResult = array(); + + $oAbp = $this->AddressBookProvider($oAccount); + if ($oAbp->IsActive()) { + $iResultCount = 0; + $mResult = $oAbp->GetContacts($iOffset, $iLimit, $sSearch, $iResultCount); + } + + return $this->DefaultResponse(array( + 'Offset' => $iOffset, + 'Limit' => $iLimit, + 'Count' => $iResultCount, + 'Search' => $sSearch, + 'List' => $mResult + )); + } + + public function DoContactsDelete() : array + { + $oAccount = $this->getAccountFromToken(); + $aUids = \explode(',', (string) $this->GetActionParam('uids', '')); + + $aFilteredUids = \array_filter(\array_map('intval', $aUids)); + + $bResult = false; + if (\count($aFilteredUids) && $this->AddressBookProvider($oAccount)->IsActive()) { + $bResult = $this->AddressBookProvider($oAccount)->DeleteContacts($aFilteredUids); + } + + return $this->DefaultResponse($bResult); + } + + public function DoContactSave() : array + { + $oAccount = $this->getAccountFromToken(); + + $bResult = false; + + if ($this->HasActionParam('uid') && $this->HasActionParam('jCard')) { + $oAddressBookProvider = $this->AddressBookProvider($oAccount); + if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) { + $vCard = \Sabre\VObject\Reader::readJson($this->GetActionParam('jCard')); + if ($vCard && $vCard instanceof \Sabre\VObject\Component\VCard) { + $vCard->REV = \gmdate('Ymd\\THis\\Z'); + $vCard->PRODID = 'SnappyMail-'.APP_VERSION; + $sUid = \trim($this->GetActionParam('uid')); + $oContact = $sUid ? $oAddressBookProvider->GetContactByID($sUid) : null; + if (!$oContact) { + $oContact = new \RainLoop\Providers\AddressBook\Classes\Contact(); + } + $oContact->setVCard($vCard); + $bResult = $oAddressBookProvider->ContactSave($oContact); + } + } + } + + return $this->DefaultResponse(array( + 'ResultID' => $bResult ? $oContact->id : '', + 'Result' => $bResult + )); + } + + public function UploadContacts(?array $aFile, int $iError) : array + { + $oAccount = $this->getAccountFromToken(); + + $mResponse = false; + + if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) { + $sSavedName = 'upload-post-'.\md5($aFile['name'].$aFile['tmp_name']); + if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) { + $iError = \RainLoop\Enumerations\UploadError::ON_SAVING; + } else { + \ini_set('auto_detect_line_endings', '1'); + $mData = $this->FilesProvider()->GetFile($oAccount, $sSavedName); + if ($mData) { + $sFileStart = \fread($mData, 128); + \rewind($mData); + if (false !== $sFileStart) { + $sFileStart = \trim($sFileStart); + if (false !== \strpos($sFileStart, 'BEGIN:VCARD')) { + $mResponse = $this->importContactsFromVcfFile($oAccount, $mData); + } else if (false !== \strpos($sFileStart, ',') || false !== \strpos($sFileStart, ';')) { + $mResponse = $this->importContactsFromCsvFile($oAccount, $mData, $sFileStart); + } + } + } + + if (\is_resource($mData)) { + \fclose($mData); + } + + unset($mData); + $this->FilesProvider()->Clear($oAccount, $sSavedName); + + \ini_set('auto_detect_line_endings', '0'); + } + } + + if (UPLOAD_ERR_OK !== $iError) { + $iClientError = 0; + $sError = \RainLoop\Enumerations\UploadError::getUserMessage($iError, $iClientError); + if (!empty($sError)) { + return $this->FalseResponse($iClientError, $sError); + } + } + + return $this->DefaultResponse($mResponse); + } + + public function setContactsSyncData(\RainLoop\Model\Account $oAccount, array $aData) : bool + { + if (!isset($aData['Mode'])) { + $aData['Mode'] = empty($aData['Enable']) ? 0 : 1; + } +// $oAccount = $this->getAccountFromToken(); + $oMainAccount = $this->getMainAccountFromToken(); + if ($aData['Password']) { + $aData['Password'] = \SnappyMail\Crypt::EncryptToJSON($aData['Password'], $oMainAccount->CryptKey()); + } + $aData['PasswordHMAC'] = $aData['Password'] ? \hash_hmac('sha1', $aData['Password'], $oMainAccount->CryptKey()) : null; + return $this->StorageProvider()->Put( + $oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'contacts_sync', + \json_encode($aData) + ); + } + + protected function getContactsSyncData(\RainLoop\Model\Account $oAccount) : ?array + { +// $oAccount = $this->getAccountFromToken(); + $sData = $this->StorageProvider()->Get($oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'contacts_sync' + ); + if (!empty($sData)) { + $aData = \json_decode($sData, true); + if ($aData) { + if ($aData['Password']) { + $oMainAccount = $this->getMainAccountFromToken(); + // Verify oAccount password hasn't changed so that Password can be decrypted + if ($aData['PasswordHMAC'] !== \hash_hmac('sha1', $aData['Password'], $oMainAccount->CryptKey())) { + // Failed + $aData['Password'] = null; + } else { + // Success + $aData['Password'] = \SnappyMail\Crypt::DecryptFromJSON( + $aData['Password'], + $oMainAccount->CryptKey() + ); + } + } + if (!isset($aData['Mode'])) { + $aData['Mode'] = empty($aData['Enable']) ? 0 : 1; + } + return $aData; + } + + return \SnappyMail\Upgrade::ConvertInsecureContactsSync($this, $oAccount); + } + return null; + } + + public function RawContactsVcf() : bool + { + $oAccount = $this->getAccountFromToken(); + + \header('Content-Type: text/x-vcard; charset=UTF-8'); + \header('Content-Disposition: attachment; filename="contacts.vcf"'); + \header('Accept-Ranges: none'); + \header('Content-Transfer-Encoding: binary'); + + $this->Http()->ServerNoCache(); + + $oAddressBookProvider = $this->AddressBookProvider($oAccount); + return $oAddressBookProvider->IsActive() ? + $oAddressBookProvider->Export('vcf') : false; + } + + public function RawContactsCsv() : bool + { + $oAccount = $this->getAccountFromToken(); + + \header('Content-Type: text/csv; charset=UTF-8'); + \header('Content-Disposition: attachment; filename="contacts.csv"'); + \header('Accept-Ranges: none'); + \header('Content-Transfer-Encoding: binary'); + + $this->Http()->ServerNoCache(); + + $oAddressBookProvider = $this->AddressBookProvider($oAccount); + return $oAddressBookProvider->IsActive() ? + $oAddressBookProvider->Export('csv') : false; + } + + private function importContactsFromVcfFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile): int + { + $iCount = 0; + $oAddressBookProvider = $this->AddressBookProvider($oAccount); + if (\is_resource($rFile) && $oAddressBookProvider && $oAddressBookProvider->IsActive()) { + try + { + $this->logWrite('Import contacts from vcf'); + foreach (\RainLoop\Providers\AddressBook\Utils::VcfStreamToContacts($rFile) as $oContact) { + if ($oAddressBookProvider->ContactSave($oContact)) { + ++$iCount; + } + } + } + catch (\Throwable $oExc) + { + $this->logException($oExc); + } + } + return $iCount; + } + + private function importContactsFromCsvFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile, string $sFileStart): int + { + $iCount = 0; + $oAddressBookProvider = $this->AddressBookProvider($oAccount); + if (\is_resource($rFile) && $oAddressBookProvider && $oAddressBookProvider->IsActive()) { + try + { + $this->logWrite('Import contacts from csv'); + $sDelimiter = ((int)\strpos($sFileStart, ',') > (int)\strpos($sFileStart, ';')) ? ',' : ';'; + foreach (\RainLoop\Providers\AddressBook\Utils::CsvStreamToContacts($rFile, $sDelimiter) as $oContact) { + if ($oAddressBookProvider->ContactSave($oContact)) { + ++$iCount; + } + } + } + catch (\Throwable $oExc) + { + $this->logException($oExc); + } + } + return $iCount; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Filters.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Filters.php new file mode 100644 index 0000000000..1d2b025212 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Filters.php @@ -0,0 +1,86 @@ +getAccountFromToken(); + + if (!$this->GetCapa(Capa::SIEVE, $oAccount)) { + return $this->FalseResponse(); + } + + return $this->DefaultResponse($this->FiltersProvider()->Load($oAccount)); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoFiltersScriptSave() : array + { + $oAccount = $this->getAccountFromToken(); + + if (!$this->GetCapa(Capa::SIEVE, $oAccount)) { + return $this->FalseResponse(); + } + + $sName = $this->GetActionParam('name', ''); + + if ($this->GetActionParam('active', false)) { +// $this->FiltersProvider()->ActivateScript($oAccount, $sName); + } + + return $this->DefaultResponse($this->FiltersProvider()->Save( + $oAccount, $sName, $this->GetActionParam('body', '') + )); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoFiltersScriptActivate() : array + { + $oAccount = $this->getAccountFromToken(); + + if (!$this->GetCapa(Capa::SIEVE, $oAccount)) { + return $this->FalseResponse(); + } + + return $this->DefaultResponse($this->FiltersProvider()->ActivateScript( + $oAccount, $this->GetActionParam('name', '') + )); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoFiltersScriptDelete() : array + { + $oAccount = $this->getAccountFromToken(); + + if (!$this->GetCapa(Capa::SIEVE, $oAccount)) { + return $this->FalseResponse(); + } + + return $this->DefaultResponse($this->FiltersProvider()->DeleteScript( + $oAccount, $this->GetActionParam('name', '') + )); + } + + protected function FiltersProvider() : \RainLoop\Providers\Filters + { + if (!$this->oFiltersProvider) { + $this->oFiltersProvider = new \RainLoop\Providers\Filters($this->fabrica('filters')); + } + return $this->oFiltersProvider; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php new file mode 100644 index 0000000000..a88d371167 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php @@ -0,0 +1,448 @@ +initMailClientConnection(); + + $sFolderFullName = $this->GetActionParam('folder', ''); + + if (!$this->oConfig->Get('labs', 'allow_message_append', false)) { + return $this->FalseResponse(999, 'Permission denied'); + } + + if (empty($_FILES['appendFile'])) { + return $this->FalseResponse(999, 'No file'); + } + + if (\UPLOAD_ERR_OK != $_FILES['appendFile']['error']) { + return $this->FalseResponse($iErrorCode, \RainLoop\Enumerations\UploadError::getMessage($iErrorCode)); + } + + if ($oAccount && !empty($sFolderFullName) && \is_uploaded_file($_FILES['appendFile']['tmp_name'])) { + $sSavedName = 'append-post-' . \md5($sFolderFullName . $_FILES['appendFile']['name'] . $_FILES['appendFile']['tmp_name']); + if ($this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $_FILES['appendFile']['tmp_name'])) { + $iMessageStreamSize = $this->FilesProvider()->FileSize($oAccount, $sSavedName); + $rMessageStream = $this->FilesProvider()->GetFile($oAccount, $sSavedName); + $this->ImapClient()->MessageAppendStream($sFolderFullName, $rMessageStream, $iMessageStreamSize); + $this->FilesProvider()->Clear($oAccount, $sSavedName); + return $this->TrueResponse(); + } + } + return $this->FalseResponse(999); + } + + public function DoFolders() : array + { + $oAccount = $this->initMailClientConnection(); + + $HideUnsubscribed = false; + $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); + if ($oSettingsLocal instanceof \RainLoop\Settings) { + $HideUnsubscribed = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $HideUnsubscribed); + } + + $oFolderCollection = $this->MailClient()->Folders('', '*', $HideUnsubscribed); + + $oNamespaces = $this->ImapClient()->GetNamespaces(); + if ($oNamespaces) { + if (isset($oNamespaces->aOtherUsers[0])) try { + $oCollection = $this->MailClient()->Folders($oNamespaces->aOtherUsers[0]['prefix'], '*', $HideUnsubscribed); + if ($oCollection) { + foreach ($oCollection as $oFolder) { + $oFolderCollection[$oFolder->FullName] = $oFolder; + } + } + } catch (\Throwable $e) { + // https://github.com/the-djmaze/snappymail/issues/1438 + // $oAccount->Domain()->ImapSettings()->disabled_capabilities[] = 'NAMESPACE'; + // $this->DomainProvider()->Save($oAccount->Domain()); + } + if (isset($oNamespaces->aShared[0])) try { + $oCollection = $this->MailClient()->Folders($oNamespaces->aShared[0]['prefix'], '*', $HideUnsubscribed); + if ($oCollection) { + foreach ($oCollection as $oFolder) { + $oFolderCollection[$oFolder->FullName] = $oFolder; + } + } + } catch (\Throwable $e) { + // https://github.com/the-djmaze/snappymail/issues/1438 + // $oAccount->Domain()->ImapSettings()->disabled_capabilities[] = 'NAMESPACE'; + // $this->DomainProvider()->Save($oAccount->Domain()); + } + } + + if ($oFolderCollection) { + try { +// $aQuota = $this->ImapClient()->Quota(); + $aQuota = $this->ImapClient()->QuotaRoot(); + } catch (\Throwable $oException) { + // ignore + } + + $aCapabilities = \array_values(\array_filter($this->ImapClient()->Capability() ?: [], function ($item) { + return !\preg_match('/^(IMAP|AUTH|LOGIN|SASL)/', $item); + })); + + $oFolderCollection = \array_merge( + $oFolderCollection->jsonSerialize(), + array( + 'quotaUsage' => $aQuota ? $aQuota[0] * 1024 : null, + 'quotaLimit' => $aQuota ? $aQuota[1] * 1024 : null, + 'namespace' => $oNamespaces ? $oNamespaces->GetPersonalPrefix() : '', + 'namespaces' => $oNamespaces, + 'capabilities' => $aCapabilities + ) + ); + } + + return $this->DefaultResponse($oFolderCollection); + } + + public function DoFolderCreate() : array + { + $this->initMailClientConnection(); + + try + { + $oFolder = $this->MailClient()->FolderCreate( + $this->GetActionParam('folder', ''), + $this->GetActionParam('parent', ''), + !empty($this->GetActionParam('subscribe', 0)) + ); + +// FolderInformation(string $sFolderName, int $iPrevUidNext = 0, array $aUids = array()) + return $this->DefaultResponse($oFolder); + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::CantCreateFolder, $oException); + } + } + + public function DoFolderSetMetadata() : array + { + $this->initMailClientConnection(); + $sFolderFullName = $this->GetActionParam('folder'); + $sMetadataKey = $this->GetActionParam('key'); + if ($sFolderFullName && $sMetadataKey) { + $this->ImapClient()->FolderSetMetadata($sFolderFullName, [ + $sMetadataKey => $this->GetActionParam('value') ?: null + ]); + } + return $this->TrueResponse(); + } + + public function DoFolderSettings() : array + { + $this->initMailClientConnection(); + + $sFolderFullName = $this->GetActionParam('folder', ''); + + // DoFolderSubscribe + try + { + $bSubscribe = !empty($this->GetActionParam('subscribe', 0)); + $this->ImapClient()->{$bSubscribe ? 'FolderSubscribe' : 'FolderUnsubscribe'}($sFolderFullName); + } + catch (\Throwable $oException) + { + } + + // DoFolderCheckable + $this->SetFolderCheckable($sFolderFullName, !empty($this->GetActionParam('checkable'))); + + // DoFolderSetMetadata + try + { + $aKolab = $this->GetActionParam('kolab'); + if ($aKolab['type']) { + $this->ImapClient()->FolderSetMetadata($sFolderFullName, [ + $aKolab['type'] => $aKolab['value'] ?: null + ]); + } + } + catch (\Throwable $oException) + { + } + + return $this->TrueResponse(); + } + + public function DoFolderSubscribe() : array + { + $this->initMailClientConnection(); + + $sFolderFullName = $this->GetActionParam('folder', ''); + $bSubscribe = !empty($this->GetActionParam('subscribe', 0)); + + try + { + $this->ImapClient()->{$bSubscribe ? 'FolderSubscribe' : 'FolderUnsubscribe'}($sFolderFullName); + } + catch (\Throwable $oException) + { + throw new ClientException( + $bSubscribe ? Notifications::CantSubscribeFolder : Notifications::CantUnsubscribeFolder, + $oException + ); + } + + return $this->TrueResponse(); + } + + protected function SetFolderCheckable(string $sFolderFullName, bool $bCheckable) : bool + { + $oAccount = $this->getAccountFromToken(); + $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); + + $aCheckableFolders = \json_decode($oSettingsLocal->GetConf('CheckableFolder', '[]')); + if (!\is_array($aCheckableFolders)) { + $aCheckableFolders = array(); + } + + if ($bCheckable) { + $aCheckableFolders[] = $sFolderFullName; + } else if (($key = \array_search($sFolderFullName, $aCheckableFolders)) !== false) { + \array_splice($aCheckableFolders, $key, 1); + } + + $oSettingsLocal->SetConf('CheckableFolder', \json_encode(\array_unique($aCheckableFolders))); + + return $oSettingsLocal->save(); + } + + public function DoFolderCheckable() : array + { + return $this->DefaultResponse( + $this->SetFolderCheckable( + $this->GetActionParam('folder', ''), + !empty($this->GetActionParam('checkable')) + ) + ); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoFolderRename() : array + { + $this->initMailClientConnection(); + + try + { + $sOldName = $this->GetActionParam('oldName', ''); + $sNewName = $this->GetActionParam('newName', ''); + $sDelimiter = $this->ImapClient()->FolderHierarchyDelimiter($sOldName); + + $this->MailClient()->FolderRename($sOldName, $sNewName); + + // DoFolderSubscribe + try + { + $bSubscribe = !empty($this->GetActionParam('subscribe', 0)); + $this->ImapClient()->{$bSubscribe ? 'FolderSubscribe' : 'FolderUnsubscribe'}($sNewName); + } + catch (\Throwable $oException) + { + } + + // DoFolderCheckable + $oAccount = $this->getAccountFromToken(); + $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); + $aCheckableFolders = \json_decode($oSettingsLocal->GetConf('CheckableFolder', '[]')); + $aRemoveFolders = []; + if (\is_array($aCheckableFolders)) { + foreach ($aCheckableFolders as $sFolder) { + if (\str_starts_with($sFolder . $sDelimiter, $sOldName . $sDelimiter)) { + $aRemoveFolders[] = $sFolder; + if ($sFolder !== $sOldName) { + $aCheckableFolders[] = $sNewName . $sDelimiter . \substr($sFolder, \strlen($sOldName) + 1); + } + } + } + $aCheckableFolders = \array_diff($aCheckableFolders, $aRemoveFolders); + } else { + $aCheckableFolders = []; + } + if ($this->GetActionParam('checkable')) { + $aCheckableFolders[] = $sNewName; + } + $oSettingsLocal->SetConf('CheckableFolder', \json_encode(\array_unique($aCheckableFolders))); + $oSettingsLocal->save(); + + // DoFolderSetMetadata + try + { + $aKolab = $this->GetActionParam('kolab'); + if ($aKolab['type']) { + $this->ImapClient()->FolderSetMetadata($sNewName, [ + $aKolab['type'] => $aKolab['value'] ?: null + ]); + } + } + catch (\Throwable $oException) + { + } + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::CantRenameFolder, $oException); + } + + return $this->TrueResponse(); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoFolderDelete() : array + { + $this->initMailClientConnection(); + + try + { + $this->ImapClient()->FolderDelete($this->GetActionParam('folder', '')); + } + catch (\MailSo\Mail\Exceptions\NonEmptyFolder $oException) + { + throw new ClientException(Notifications::CantDeleteNonEmptyFolder, $oException); + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::CantDeleteFolder, $oException); + } + + return $this->TrueResponse(); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoFolderClear() : array + { + $this->initMailClientConnection(); + + try + { + $this->ImapClient()->FolderClear($this->GetActionParam('folder', '')); + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::MailServerError, $oException); + } + + return $this->TrueResponse(); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoFolderInformation() : array + { + $this->initMailClientConnection(); + + try + { + return $this->DefaultResponse($this->MailClient()->FolderInformation( + $this->GetActionParam('folder', ''), + (int) $this->GetActionParam('uidNext', 0), + new \MailSo\Imap\SequenceSet($this->GetActionParam('flagsUids', [])) + )); + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::MailServerError, $oException); + } + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoFolderInformationMultiply() : array + { + $aResult = array(); + + $aFolders = $this->GetActionParam('folders', null); + if (\is_array($aFolders)) { + $this->initMailClientConnection(); + + $aFolders = \array_unique($aFolders); + foreach ($aFolders as $sFolder) { + try + { + $aResult[] = $this->MailClient()->FolderInformation($sFolder); + } + catch (\Throwable $oException) + { + $this->logException($oException); + } + } + } + + return $this->DefaultResponse($aResult); + } + + public function DoSystemFoldersUpdate() : array + { + $oAccount = $this->getAccountFromToken(); + + $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); + + $oSettingsLocal->SetConf('SentFolder', $this->GetActionParam('sent', '')); + $oSettingsLocal->SetConf('DraftsFolder', $this->GetActionParam('drafts', '')); + $oSettingsLocal->SetConf('JunkFolder', $this->GetActionParam('junk', '')); + $oSettingsLocal->SetConf('TrashFolder', $this->GetActionParam('trash', '')); + $oSettingsLocal->SetConf('ArchiveFolder', $this->GetActionParam('archive', '')); + + return $this->DefaultResponse($oSettingsLocal->save()); + } + + public function DoFolderACL() : array + { + $this->initMailClientConnection(); + return $this->DefaultResponse([ + '@Object' => 'Collection/FolderACL', + '@Collection' => $this->ImapClient()->FolderGetACL( + $this->GetActionParam('folder', '') + ) + ]); + } + + public function DoFolderDeleteACL() : array + { + $this->initMailClientConnection(); + $this->ImapClient()->FolderDeleteACL( + $this->GetActionParam('folder', ''), + $this->GetActionParam('identifier', '') + ); + return $this->TrueResponse(); + } + + public function DoFolderSetACL() : array + { +// $oImapClient->FolderSetACL('INBOX', 'demo@snappymail.eu', 'lrwstipekxacd'); +// $oImapClient->FolderSetACL($sFolderFullName, 'demo@snappymail.eu', 'lrwstipekxacd'); +// $oImapClient->FolderSetACL($sFolderFullName, 'foobar@snappymail.eu', 'lr'); + $this->initMailClientConnection(); + $this->ImapClient()->FolderSetACL( + $this->GetActionParam('folder', ''), + $this->GetActionParam('identifier', ''), + $this->GetActionParam('rights', '') + ); + return $this->TrueResponse(); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php new file mode 100644 index 0000000000..d0b2069ac8 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php @@ -0,0 +1,116 @@ +Config(); + if ($bAdmin) { + $sLanguage = $oConfig->Get('admin_panel', 'language', 'en'); + } else { + $sLanguage = $oConfig->Get('webmail', 'language', 'en'); + if ($oAccount = $this->getAccountFromToken(false)) { + if ($oConfig->Get('login', 'determine_user_language', true)) { + $sLanguage = $this->ValidateLanguage($this->detectClientLanguage($bAdmin), $sLanguage, false); + } + if ($oConfig->Get('webmail', 'allow_languages_on_settings', true) + && ($oSettings = $this->SettingsProvider()->Load($oAccount))) { + $sLanguage = $oSettings->GetConf('language', $sLanguage); + } + } else if ($oConfig->Get('login', 'allow_languages_on_login', true) && $oConfig->Get('login', 'determine_user_language', true)) { + $sLanguage = $this->ValidateLanguage($this->detectClientLanguage($bAdmin), $sLanguage, false); + } + } + $sHookLanguage = $sLanguage = $this->ValidateLanguage($sLanguage, '', $bAdmin) ?: 'en'; + $this->Plugins()->RunHook('filter.language', array(&$sHookLanguage, $bAdmin)); + return $this->ValidateLanguage($sHookLanguage, $sLanguage, $bAdmin); + } + + public function ValidateLanguage(string $sLanguage, string $sDefault = '', bool $bAdmin = false, bool $bAllowEmptyResult = false): string + { + $sResult = \SnappyMail\L10n::validLanguage($sLanguage, $bAdmin) + ?: \SnappyMail\L10n::validLanguage($sDefault, $bAdmin); + + if ($sResult || $bAllowEmptyResult) { + return $sResult ?: ''; + } + + $sResult = $this->Config()->Get($bAdmin ? 'admin_panel' : 'webmail', 'language', 'en'); + return \SnappyMail\L10n::validLanguage($sResult, $bAdmin) ? $sResult : 'en'; + } + + public function detectClientLanguage(bool $bAdmin): string + { + $aLangs = $aList = array(); + + $sAcceptLang = \strtolower(\MailSo\Base\Http::GetServer('HTTP_ACCEPT_LANGUAGE', 'en')); + if (!empty($sAcceptLang) && \preg_match_all('/([a-z]{1,8}(?:-[a-z]{1,8})?)(?:;q=([0-9.]+))?/', $sAcceptLang, $aList)) { + $aLangs = \array_combine($aList[1], $aList[2]); + foreach ($aLangs as $n => $v) { + $aLangs[$n] = $v ? $v : 1; + } + + \arsort($aLangs, SORT_NUMERIC); + } + + foreach (\array_keys($aLangs) as $sLang) { + $sLang = $this->ValidateLanguage($sLang, '', $bAdmin, true); + if (!empty($sLang)) { + return $sLang; + } + } + + return ''; + } + + public function StaticI18N(string $sKey): string + { + static $sLang = null; + static $aLang = null; + + if (null === $sLang) { + $sLang = $this->GetLanguage(); + } + + if (null === $aLang) { + $sLang = $this->ValidateLanguage($sLang, 'en'); + $aLang = \SnappyMail\L10n::load($sLang, 'static'); + $this->Plugins()->ReadLang($sLang, $aLang); + } + + return $aLang[$sKey] ?? $sKey; + } + + public function compileLanguage(string $sLanguage, bool $bAdmin = false) : string + { + $sLanguage = \strtr($sLanguage, '_', '-'); + + $aResultLang = \json_decode(\file_get_contents(APP_VERSION_ROOT_PATH.'app/localization/langs.json'), true); + $langs = \array_flip(\SnappyMail\L10n::getLanguages($bAdmin)); + $aResultLang['LANGS_NAMES'] = \array_intersect_key($aResultLang['LANGS_NAMES'], $langs); + $aResultLang['LANGS_NAMES_EN'] = \array_intersect_key($aResultLang['LANGS_NAMES_EN'], $langs); + + $aResultLang = \array_replace_recursive( + $aResultLang, + \SnappyMail\L10n::load($sLanguage, ($bAdmin ? 'admin' : 'user')) + ); + + $this->Plugins()->ReadLang($sLanguage, $aResultLang); + + $sResult = \json_encode($aResultLang, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK); + + $sTimeFormat = ''; + $options = [$sLanguage, \substr($sLanguage, 0, 2), 'en']; + foreach ($options as $lang) { + $sFileName = APP_VERSION_ROOT_PATH.'app/localization/'.$lang.'/relativetimeformat.js'; + if (\is_file($sFileName)) { + $sTimeFormat = \preg_replace('/^\\s+/', '', \file_get_contents($sFileName)); + break; + } + } + + return "document.documentElement.lang = '{$sLanguage}';\nrl.I18N={$sResult};\nrl.relativeTime = {$sTimeFormat};"; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php new file mode 100644 index 0000000000..8b04bbdad2 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -0,0 +1,1401 @@ +decodeRawKey($this->GetActionParam('RawKey', '')); + $sHash = ''; + if ($aValues && 6 < \count($aValues)) { + // GET + $sHash = (string) $aValues['hash']; + $oParams->sFolderName = (string) $aValues['folder']; + $oParams->iLimit = $aValues['limit']; + $oParams->iOffset = $aValues['offset']; + $oParams->sSearch = (string) $aValues['search']; + $oParams->sSort = (string) $aValues['sort']; + if (isset($aValues['uidNext'])) { + $oParams->iPrevUidNext = $aValues['uidNext']; + } + $oParams->bUseThreads = !empty($aValues['useThreads']); + if ($oParams->bUseThreads) { + if (isset($aValues['threadUid'])) { + $oParams->iThreadUid = $aValues['threadUid']; + } + if (isset($aValues['threadAlgorithm'])) { + $oParams->sThreadAlgorithm = $aValues['threadAlgorithm']; + } + } + } else { + // POST + $oParams->sFolderName = $this->GetActionParam('folder', ''); + $oParams->iOffset = $this->GetActionParam('offset', 0); + $oParams->iLimit = $this->GetActionParam('limit', 10); + $oParams->sSearch = $this->GetActionParam('search', ''); + $oParams->sSort = $this->GetActionParam('sort', ''); + $oParams->iPrevUidNext = $this->GetActionParam('uidNext', 0); + $oParams->bUseThreads = !empty($this->GetActionParam('useThreads', '0')); + if ($oParams->bUseThreads) { + $oParams->iThreadUid = $this->GetActionParam('threadUid', ''); + $oParams->sThreadAlgorithm = $this->GetActionParam('threadAlgorithm', ''); + } + } + + if (!\strlen($oParams->sFolderName)) { + throw new ClientException(Notifications::CantGetMessageList); + } + + $oAccount = $this->initMailClientConnection(); + + $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); + if ($oSettingsLocal instanceof \RainLoop\Settings) { + $oParams->bHideDeleted = !empty($oSettingsLocal->GetConf('HideDeleted', 1)); + } + +// $oParams->bUseSort = $this->ImapClient()->hasCapability('SORT'); + $oParams->bUseSort = true; + + if ($sHash) { + $oInfo = $this->ImapClient()->FolderStatusAndSelect($oParams->sFolderName); + $aRequestHash = \explode('-', $sHash); + $sNewHash = $oParams->hash() . '-' . $oInfo->etag; + if ($aRequestHash[1] == $oInfo->etag) { + $this->verifyCacheByKey($sNewHash); + } + $sHash = $sNewHash; + } + + try + { + if ($this->Config()->Get('cache', 'enable', true) && $this->Config()->Get('cache', 'server_uids', false)) { + $oParams->oCacher = $this->Cacher($oAccount); + } + +// \ignore_user_abort(true); + $oMessageList = $this->MailClient()->MessageList($oParams); + if ($sHash) { + $this->cacheByKey($sHash); + } + return $this->DefaultResponse($oMessageList); + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::CantGetMessageList, $oException); + } + } + + public function DoSaveMessage() : array + { + $oAccount = $this->initMailClientConnection(); + + $sDraftFolder = $this->GetActionParam('saveFolder', ''); + if (!\strlen($sDraftFolder)) { + throw new ClientException(Notifications::UnknownError); + } + + $oMessage = $this->buildMessage($oAccount, true); + + $this->Plugins()->RunHook('filter.save-message', array($oMessage)); + + $mResult = false; + if ($oMessage) { + $rMessageStream = \MailSo\Base\ResourceRegistry::CreateMemoryResource(); + + $iMessageStreamSize = \MailSo\Base\Utils::WriteStream($oMessage->ToStream(false), $rMessageStream, 8192, true); + + if (false !== $iMessageStreamSize) { + $sMessageId = $oMessage->MessageId(); + + \rewind($rMessageStream); + + $iNewUid = $this->ImapClient()->MessageAppendStream( + $sDraftFolder, $rMessageStream, $iMessageStreamSize, array(MessageFlag::SEEN) + ); + + if (!empty($sMessageId) && (null === $iNewUid || 0 === $iNewUid)) { + $iNewUid = $this->MailClient()->FindMessageUidByMessageId($sDraftFolder, $sMessageId); + } + + $mResult = true; + + $sMessageFolder = $this->GetActionParam('messageFolder', ''); + $iMessageUid = (int) $this->GetActionParam('messageUid', 0); + if (\strlen($sMessageFolder) && 0 < $iMessageUid) { + $this->ImapClient()->MessageDelete($sMessageFolder, new SequenceSet($iMessageUid)); + } + + if (null !== $iNewUid && 0 < $iNewUid) { + $mResult = array( + 'folder' => $sDraftFolder, + 'uid' => $iNewUid + ); + } + } + } + + return $this->DefaultResponse($mResult); + } + + public function DoSendMessage() : array + { + $oAccount = $this->initMailClientConnection(); +/* + $aAuth = $this->GetActionParam('auth', null); + if ($aAuth) { + $oAccount->setSmtpUser($aAuth['username']); + $oAccount->setSmtpPass(new \SnappyMail\SensitiveString($aAuth['password'])); +// if ($oAccount instanceof AdditionalAccount && !empty($aAuth['remember'])) { +// $oMainAccount = $this->getMainAccountFromToken(); +// $aAccounts = $this->GetAccounts($oMainAccount); +// $this->SetAccounts($oMainAccount, $aAccounts); +// } + } +*/ + $oConfig = $this->Config(); + + $sSaveFolder = $this->GetActionParam('saveFolder', ''); + $aDraftInfo = $this->GetActionParam('draftInfo', null); + + $oMessage = $this->buildMessage($oAccount, false); + + $this->Plugins()->RunHook('filter.send-message', array($oMessage)); + + $mResult = false; + try + { + if ($oMessage) { + $rMessageStream = \MailSo\Base\ResourceRegistry::CreateMemoryResource(); + + $iMessageStreamSize = \MailSo\Base\Utils::WriteStream( + $oMessage->ToStream(true), $rMessageStream, 8192, true, true + ); + + if (false !== $iMessageStreamSize) { + $this->smtpSendMessage($oAccount, $oMessage, $rMessageStream, $iMessageStreamSize, true, + !empty($this->GetActionParam('dsn', 0)), + !empty($this->GetActionParam('requireTLS', 0)) + ); + + if (\is_array($aDraftInfo) && 3 === \count($aDraftInfo)) { + $sDraftInfoType = $aDraftInfo[0]; + $iDraftInfoUid = (int) $aDraftInfo[1]; + $sDraftInfoFolder = $aDraftInfo[2]; + + try + { + switch (\strtolower($sDraftInfoType)) + { + case 'reply': + case 'reply-all': + $this->MailClient()->MessageSetFlag($sDraftInfoFolder, new SequenceSet($iDraftInfoUid), MessageFlag::ANSWERED); + break; + case 'forward': + $this->MailClient()->MessageSetFlag($sDraftInfoFolder, new SequenceSet($iDraftInfoUid), MessageFlag::FORWARDED); + break; + } + } + catch (\Throwable $oException) + { + $this->logException($oException, \LOG_ERR); + } + } + + if (\strlen($sSaveFolder)) { + $rAppendMessageStream = $rMessageStream; + $iAppendMessageStreamSize = $iMessageStreamSize; + try + { + if ($oMessage->GetBcc()) { + $rAppendMessageStream = \MailSo\Base\ResourceRegistry::CreateMemoryResource(); + $iAppendMessageStreamSize = \MailSo\Base\Utils::WriteStream( + $oMessage->ToStream(false), $rAppendMessageStream, 8192, true, true + ); + } else { + if (\is_resource($rMessageStream)) { + \rewind($rMessageStream); + } + } + } + catch (\Throwable $oException) + { + $this->logException($oException, \LOG_ERR); + throw new ClientException(Notifications::CantSaveMessage, $oException); + } + + try + { + $this->Plugins()->RunHook('filter.send-message-stream', + array($oAccount, &$rAppendMessageStream, &$iAppendMessageStreamSize)); + } + catch (\Throwable $oException) + { + $this->logException($oException, \LOG_ERR); + } + + try + { + $this->ImapClient()->MessageAppendStream( + $sSaveFolder, $rAppendMessageStream, $iAppendMessageStreamSize, + array(MessageFlag::SEEN) + ); + } + catch (\Throwable $oException) + { + // Save folder not the same as default Sent folder, so try again + $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); + if ($oSettingsLocal instanceof \RainLoop\Settings) { + $sSentFolder = (string) $oSettingsLocal->GetConf('SentFolder', ''); + if (\strlen($sSentFolder) && $sSentFolder !== $sSaveFolder) { + $oException = null; + try + { + $this->ImapClient()->MessageAppendStream( + $sSentFolder, $rAppendMessageStream, $iAppendMessageStreamSize, + array(MessageFlag::SEEN) + ); + } + catch (\Throwable $oException) + { + } + } + } + if ($oException) { + $this->logException($oException, \LOG_ERR); + throw new ClientException(Notifications::CantSaveMessage, $oException); + } + } + + if (\is_resource($rAppendMessageStream) && $rAppendMessageStream !== $rMessageStream) { + \fclose($rAppendMessageStream); + } + } + + if (\is_resource($rMessageStream)) { + \fclose($rMessageStream); + } + + $this->deleteMessageAttachments($oAccount); + + $sDraftFolder = $this->GetActionParam('messageFolder', ''); + $iDraftUid = (int) $this->GetActionParam('messageUid', 0); + if (\strlen($sDraftFolder) && 0 < $iDraftUid) { + try + { + $this->ImapClient()->MessageDelete($sDraftFolder, new SequenceSet($iDraftUid)); + } + catch (\Throwable $oException) + { + $this->logException($oException, \LOG_ERR); + } + } + + $mResult = true; + } + } + } + catch (\MailSo\Smtp\Exceptions\LoginBadCredentialsException $oException) + { + throw new ClientException(Notifications::AuthError, $oException); + } + catch (ClientException $oException) + { + throw $oException; + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::CantSendMessage, $oException); + } + + if (false === $mResult) { + throw new ClientException(Notifications::CantSendMessage); + } + + try + { + if ($oMessage && $this->AddressBookProvider($oAccount)->IsActive()) { + $aArrayToFrec = array(); + $oToCollection = $oMessage->GetTo(); + if ($oToCollection) { + foreach ($oToCollection as /* @var $oEmail \MailSo\Mime\Email */ $oEmail) { + $aArrayToFrec[$oEmail->GetEmail(true)] = $oEmail->ToString(false, true); + } + } + + if (\count($aArrayToFrec)) { + $oSettings = $this->SettingsProvider()->Load($oAccount); + + $this->AddressBookProvider($oAccount)->IncFrec( + \array_values($aArrayToFrec), + !!$oSettings->GetConf('ContactsAutosave', !!$oConfig->Get('defaults', 'contacts_autosave', true)) + ); + } + } + } + catch (\Throwable $oException) + { + $this->logException($oException); + } + + return $this->TrueResponse(); + } + + public function DoSendReadReceiptMessage() : array + { + $oAccount = $this->initMailClientConnection(); + + $oMessage = $this->buildReadReceiptMessage($oAccount); + + $this->Plugins()->RunHook('filter.send-read-receipt-message', array($oMessage, $oAccount)); + + $mResult = false; + try + { + if ($oMessage) { + $rMessageStream = \MailSo\Base\ResourceRegistry::CreateMemoryResource(); + + $iMessageStreamSize = \MailSo\Base\Utils::WriteStream( + $oMessage->ToStream(true), $rMessageStream, 8192, true, true + ); + + if (false !== $iMessageStreamSize) { + $this->smtpSendMessage($oAccount, $oMessage, $rMessageStream, $iMessageStreamSize, false); + + if (\is_resource($rMessageStream)) { + \fclose($rMessageStream); + } + + $mResult = true; + + $sFolderFullName = $this->GetActionParam('messageFolder', ''); + $iUid = (int) $this->GetActionParam('messageUid', 0); + + if (\strlen($sFolderFullName) && 0 < $iUid) { + try + { + $this->MailClient()->MessageSetFlag($sFolderFullName, new SequenceSet($iUid), MessageFlag::MDNSENT, true, true); + } + catch (\Throwable $oException) + { + $this->Cacher($oAccount)->Set(\RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $sFolderFullName, $iUid), '1'); + } + } + } + } + } + catch (ClientException $oException) + { + throw $oException; + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::CantSendMessage, $oException); + } + + if (false === $mResult) { + throw new ClientException(Notifications::CantSendMessage); + } + + return $this->TrueResponse(); + } + + public function DoMessageSetSeen() : array + { + return $this->messageSetFlag(MessageFlag::SEEN); + } + + public function DoMessageSetSeenToAll() : array + { + $this->initMailClientConnection(); + + $sThreadUids = \trim($this->GetActionParam('threadUids', '')); + + try + { + $this->MailClient()->MessageSetFlag( + $this->GetActionParam('folder', ''), + empty($sThreadUids) ? new SequenceSet('1:*', false) : new SequenceSet(\explode(',', $sThreadUids)), + MessageFlag::SEEN, + !empty($this->GetActionParam('setAction', '0')) + ); + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::MailServerError, $oException); + } + + return $this->TrueResponse(); + } + + public function DoMessageSetFlagged() : array + { + return $this->messageSetFlag(MessageFlag::FLAGGED, true); + } + + public function DoMessageSetDeleted() : array + { + return $this->messageSetFlag(MessageFlag::DELETED, true); + } + + public function DoMessageSetKeyword() : array + { + return $this->messageSetFlag($this->GetActionParam('keyword', ''), true); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoMessage() : array + { + $aValues = $this->decodeRawKey((string) $this->GetActionParam('RawKey', '')); + if ($aValues && 2 <= \count($aValues)) { + $sFolder = (string) $aValues[0]; + $iUid = (int) $aValues[1]; +// $useThreads = !empty($aValues[2]); +// $accountHash = $aValues[3]; + } else { + $sFolder = $this->GetActionParam('folder', ''); + $iUid = (int) $this->GetActionParam('uid', 0); + } + + $oAccount = $this->initMailClientConnection(); + + try + { + $oMessage = $this->MailClient()->Message($sFolder, $iUid, true, $this->Cacher($oAccount)); + if (!$oMessage) { + throw new \RuntimeException('Message not found'); + } + + $bAutoVerify = $this->Config()->Get('security', 'auto_verify_signatures', false); + + // S/MIME signed. Verify it, so we have the raw mime body to show + if ($oMessage->smimeSigned && ($bAutoVerify || !$oMessage->smimeSigned['detached'])) try { + $bOpaque = !$oMessage->smimeSigned['detached']; + $sBody = $this->ImapClient()->FetchMessagePart( + $oMessage->Uid, + $oMessage->smimeSigned['partId'] + ); + $result = (new \SnappyMail\SMime\OpenSSL(''))->verify($sBody, null, $bOpaque); + if ($result) { + if ($bOpaque) { + $oMessage->smimeSigned['body'] = $result['body']; + } + $oMessage->smimeSigned['success'] = $result['success']; + } + } catch (\Throwable $e) { + $this->logException($e); + } + + if ($bAutoVerify && $oMessage->pgpSigned) try { + $GPG = $this->GnuPG(); + if ($GPG) { + if ($oMessage->pgpSigned['sigPartId']) { + $sPartId = $oMessage->pgpSigned['partId']; + $sSigPartId = $oMessage->pgpSigned['sigPartId']; + $aParts = [ + FetchType::BODY_PEEK.'['.$sPartId.']', + // An empty section specification refers to the entire message, including the header. + // But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME]. + FetchType::BODY_PEEK.'['.$sPartId.'.MIME]', + FetchType::BODY_PEEK.'['.$sSigPartId.']' + ]; + $oFetchResponse = $this->ImapClient()->Fetch($aParts, $oMessage->Uid, true)[0]; + $sBodyMime = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.'.MIME]'); + $info = $this->GnuPG()->verify( + \preg_replace('/\\r?\\n/su', "\r\n", + $sBodyMime . $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']') + ), + \preg_replace('/[^\x00-\x7F]/', '', + $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sSigPartId.']') + ) + ); + } else { + // clearsigned text + $info = $this->GnuPG()->verify($oMessage->sPlain, ''); + } + if (!empty($info[0]) && 0 == $info[0]['status']) { + $info = $info[0]; + $oMessage->pgpSigned = [ + 'fingerprint' => $info['fingerprint'], + 'success' => true + ]; + } + } + } catch (\Throwable $e) { + $this->logException($e); + } +/* + if (!$oMessage->sPlain && !$oMessage->sHtml && !$oMessage->pgpEncrypted && !$oMessage->smimeEncrypted) { + $aAttachments = $oMessage->Attachments ?: []; + foreach ($aAttachments as $oAttachment) { +// \in_array($oAttachment->ContentType(), ['application/vnd.ms-tnef', 'application/ms-tnef']) + if ('winmail.dat' === \strtolower($oAttachment->FileName())) { + $sData = $this->ImapClient()->FetchMessagePart( + $oMessage->Uid, + $oAttachment->PartID() + ); + $oTNEF = new \TNEFDecoder\TNEFAttachment; + $oTNEF->decodeTnef($sData); + foreach ($oTNEF->getFiles() as $oFile) { + if (\in_array($oFile->type, ['application/rtf', 'text/rtf'])) { + $rtf = new \SnappyMail\Rtf\Document($oFile->content); + $oMessage->setHtml($rtf->toHTML()); + } else { + // List as attachment? + $oMapiAttachment = new \MailSo\Mail\Attachment($sFolder, $iUid, BodyStructure); + $oMessage->Attachments->append($oMapiAttachment); + } + } + break; + } + } + } +*/ + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::CantGetMessage, $oException); + } + + $ETag = $oMessage->ETag($this->getAccountFromToken()->ImapUser()); + $this->verifyCacheByKey($ETag); + $this->Plugins()->RunHook('filter.result-message', array($oMessage)); + $this->cacheByKey($ETag); + return $this->DefaultResponse($oMessage); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoMessageDelete() : array + { + $this->initMailClientConnection(); + + $sFolder = $this->GetActionParam('folder', ''); + $aUids = \explode(',', (string) $this->GetActionParam('uids', '')); + + try + { + $this->ImapClient()->MessageDelete($sFolder, new SequenceSet($aUids), true); + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::CantDeleteMessage, $oException); + } + + $sHash = $this->MailClient()->FolderHash($sFolder); + + return $this->DefaultResponse($sHash ? array($sFolder, $sHash) : array($sFromFolder)); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoMessageMove() : array + { + $this->initMailClientConnection(); + + $sFromFolder = $this->GetActionParam('fromFolder', ''); + $sToFolder = $this->GetActionParam('toFolder', ''); + + $oUids = new SequenceSet(\explode(',', (string) $this->GetActionParam('uids', ''))); + + if (!empty($this->GetActionParam('markAsRead', '0'))) { + try + { + $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::SEEN); + } + catch (\Throwable $oException) + { + unset($oException); + } + } + + $sLearning = $this->GetActionParam('learning', ''); + if ($sLearning) { + try + { + if ('SPAM' === $sLearning) { +// $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, '\\junk'); + $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::JUNK); + $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::NOTJUNK, false); + } else if ('HAM' === $sLearning) { + $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::NOTJUNK); + $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::JUNK, false); +// $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, '\\junk', false); + } + } + catch (\Throwable $oException) + { + unset($oException); + } + } + + try + { + $this->ImapClient()->MessageMove($sFromFolder, $sToFolder, $oUids); + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::CantMoveMessage, $oException); + } + + $sHash = $this->MailClient()->FolderHash($sFromFolder); + + return $this->DefaultResponse($sHash ? array($sFromFolder, $sHash) : array($sFromFolder)); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoMessageCopy() : array + { + $this->initMailClientConnection(); + + $sToFolder = $this->GetActionParam('toFolder', ''); + + try + { + $this->ImapClient()->MessageCopy( + $this->GetActionParam('fromFolder', ''), + $sToFolder, + new SequenceSet(\explode(',', (string) $this->GetActionParam('uids', ''))) + ); + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::CantCopyMessage, $oException); + } + + $sHash = $this->MailClient()->FolderHash($sToFolder); + + return $this->DefaultResponse($sHash ? array($sToFolder, $sHash) : array($sToFolder)); + } + + public function DoMessageUploadAttachments() : array + { + $oAccount = $this->initMailClientConnection(); + + $mResult = false; + $self = $this; + + try + { + $aAttachments = $this->GetActionParam('attachments', array()); + if (!\is_array($aAttachments)) { + $aAttachments = []; + } + if (\count($aAttachments)) { + $oFilesProvider = $this->FilesProvider(); + foreach ($aAttachments as $mIndex => $sAttachment) { + $aAttachments[$mIndex] = false; + if ($aValues = $this->decodeRawKey($sAttachment)) { + $sFolder = isset($aValues['folder']) ? (string) $aValues['folder'] : ''; + $iUid = isset($aValues['uid']) ? (int) $aValues['uid'] : 0; + $sMimeIndex = isset($aValues['mimeIndex']) ? (string) $aValues['mimeIndex'] : ''; + + $sTempName = \sha1($sAttachment); + if (!$oFilesProvider->FileExists($oAccount, $sTempName)) { + $this->MailClient()->MessageMimeStream( + function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use ($oAccount, $sTempName, $self, &$aAttachments, $mIndex) { + if (\is_resource($rResource)) { + $sContentType = + $sContentType + ?: \SnappyMail\File\MimeType::fromStream($rResource, $sFileName) + ?: \SnappyMail\File\MimeType::fromFilename($sFileName) + ?: 'application/octet-stream'; // 'text/plain' + + $sTempName .= \SnappyMail\File\MimeType::toExtension($sContentType); + + if ($self->FilesProvider()->PutFile($oAccount, $sTempName, $rResource)) { + $aAttachments[$mIndex] = [ +// 'name' => $sFileName, + 'tempName' => $sTempName, + 'mimeType' => $sContentType +// 'size' => 0 + ]; + } + } + }, $sFolder, $iUid, $sMimeIndex); + } else { + $rResource = $oFilesProvider->GetFile($oAccount, $sTempName); + $sContentType = \SnappyMail\File\MimeType::fromStream($rResource, $sTempName) + ?: \SnappyMail\File\MimeType::fromFilename($sTempName) + ?: 'application/octet-stream'; // 'text/plain' + $aAttachments[$mIndex] = [ +// 'name' => '', + 'tempName' => $sTempName, + 'mimeType' => $sContentType +// 'size' => $oFilesProvider->FileSize($oAccount, $sTempName) + ]; + } + } + } + } + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::MailServerError, $oException); + } + + return $this->DefaultResponse($aAttachments); + } + + /** + * @throws \RainLoop\Exceptions\ClientException + * @throws \MailSo\Net\Exceptions\ConnectionException + */ + private function smtpSendMessage(Account $oAccount, \MailSo\Mime\Message $oMessage, + /*resource*/ &$rMessageStream, int &$iMessageStreamSize, bool $bAddHiddenRcpt = true, + bool $bDsn = false, bool $bRequireTLS = false) + { + $oRcpt = $oMessage->GetRcpt(); + if (!$oRcpt || !$oRcpt->count()) { + throw new ClientException(Notifications::InvalidRecipients); + } + + $this->Plugins()->RunHook('filter.smtp-message-stream', + array($oAccount, &$rMessageStream, &$iMessageStreamSize)); + + $this->Plugins()->RunHook('filter.message-rcpt', array($oAccount, $oRcpt)); + + $oSmtpClient = null; + try + { + $oFrom = $oMessage->GetFrom(); + $sFrom = $oFrom instanceof \MailSo\Mime\Email ? $oFrom->GetEmail() : ''; + $sFrom = empty($sFrom) ? $oAccount->Email() : $sFrom; + + $this->Plugins()->RunHook('filter.smtp-from', array($oAccount, $oMessage, &$sFrom)); + + $aHiddenRcpt = array(); + if ($bAddHiddenRcpt) { + $this->Plugins()->RunHook('filter.smtp-hidden-rcpt', array($oAccount, $oMessage, &$aHiddenRcpt)); + } + + $oSmtpClient = new \MailSo\Smtp\SmtpClient(); + $oSmtpClient->SetLogger($this->Logger()); + + $oAccount->SmtpConnectAndLogin($this->Plugins(), $oSmtpClient); + + if ($oSmtpClient->Settings->usePhpMail) { + if (!$sFrom || !\MailSo\Base\Utils::FunctionCallable('mail')) { + throw new ClientException(Notifications::CantSendMessage); + } + $oToCollection = $oMessage->GetTo(); + if (!$oToCollection) { + throw new ClientException(Notifications::CantSendMessage); + } + $sRawBody = \stream_get_contents($rMessageStream); + if (empty($sRawBody)) { + throw new ClientException(Notifications::CantSendMessage); + } + $sMailTo = \trim($oToCollection->ToString(true)); + $sMailSubject = \trim($oMessage->GetSubject()); + $sMailSubject = \strlen($sMailSubject) ? \MailSo\Base\Utils::EncodeHeaderValue($sMailSubject) : ''; + + $sMailHeaders = $sMailBody = ''; + list($sMailHeaders, $sMailBody) = \explode("\r\n\r\n", $sRawBody, 2); + unset($sRawBody); + + if ($this->Config()->Get('labs', 'mail_func_clear_headers', true)) { + $sMailHeaders = \MailSo\Base\Utils::RemoveHeaderFromHeaders($sMailHeaders, array( + MimeEnumHeader::TO_, + MimeEnumHeader::SUBJECT + )); + } + + $this->Logger()->WriteDump(array( + $sMailTo, $sMailSubject, $sMailBody, $sMailHeaders + ), \LOG_DEBUG); + + $bR = $this->Config()->Get('labs', 'mail_func_additional_parameters', false) ? + \mail($sMailTo, $sMailSubject, $sMailBody, $sMailHeaders, '-f'.$sFrom) : + \mail($sMailTo, $sMailSubject, $sMailBody, $sMailHeaders); + if (!$bR) { + throw new ClientException(Notifications::CantSendMessage); + } + } else if ($oSmtpClient->IsConnected()) { + if ($iMessageStreamSize && $oSmtpClient->maxSize() && $iMessageStreamSize * 1.33 > $oSmtpClient->maxSize()) { + throw new ClientException(Notifications::ClientViewError, null, 'Message size '. ($iMessageStreamSize * 1.33) . ' bigger then max ' . $oSmtpClient->maxSize()); + } + + if (!empty($sFrom)) { + $oSmtpClient->MailFrom($sFrom, 0, $bDsn, $bRequireTLS); + } + + foreach ($oRcpt as /* @var $oEmail \MailSo\Mime\Email */ $oEmail) { + $oSmtpClient->Rcpt($oEmail->GetEmail(), $bDsn); + } + + if ($bAddHiddenRcpt && \is_array($aHiddenRcpt) && \count($aHiddenRcpt)) { + foreach ($aHiddenRcpt as $sEmail) { + if (\preg_match('/^[^@\s]+@[^@\s]+$/', $sEmail)) { + $oSmtpClient->Rcpt($sEmail); + } + } + } + + $oSmtpClient->DataWithStream($rMessageStream); + + $oSmtpClient->Disconnect(); + } + } + catch (\MailSo\Net\Exceptions\ConnectionException $oException) + { + if ($oSmtpClient && $oSmtpClient->Settings->viewErrors) { + throw new ClientException(Notifications::ClientViewError, $oException); + } + throw new ClientException(Notifications::ConnectionError, $oException); + } + catch (\MailSo\Smtp\Exceptions\LoginException $oException) + { + throw new ClientException(Notifications::AuthError, $oException); + } + catch (\Throwable $oException) + { + if ($oSmtpClient && $oSmtpClient->Settings->viewErrors) { + throw new ClientException(Notifications::ClientViewError, $oException); + } + throw $oException; + } + } + + private function messageSetFlag(string $sMessageFlag, bool $bSkipUnsupportedFlag = false) : array + { + $this->initMailClientConnection(); + + try + { + $this->MailClient()->MessageSetFlag( + $this->GetActionParam('folder', ''), + new SequenceSet(\explode(',', (string) $this->GetActionParam('uids', ''))), + $sMessageFlag, + !empty($this->GetActionParam('setAction', '0')), + $bSkipUnsupportedFlag + ); + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::MailServerError, $oException); + } + + return $this->TrueResponse(); + } + + private function deleteMessageAttachments(Account $oAccount) : void + { + $aAttachments = $this->GetActionParam('attachments', null); + + if (\is_array($aAttachments)) { + foreach (\array_keys($aAttachments) as $sTempName) { + if ($this->FilesProvider()->FileExists($oAccount, $sTempName)) { + $this->FilesProvider()->Clear($oAccount, $sTempName); + } + } + } + } + + private function buildReadReceiptMessage(Account $oAccount) : \MailSo\Mime\Message + { + $sReadReceipt = $this->GetActionParam('readReceipt', ''); + $sSubject = $this->GetActionParam('subject', ''); + $sText = $this->GetActionParam('plain', ''); + + $oIdentity = $this->GetIdentityByID($oAccount, '', true); + + if (empty($sReadReceipt) || empty($sSubject) || empty($sText) || !$oIdentity) { + throw new ClientException(Notifications::UnknownError); + } + + $oMessage = new \MailSo\Mime\Message(); + + if ($this->Config()->Get('security', 'hide_x_mailer_header', true)) { + $oMessage->DoesNotAddDefaultXMailer(); + } + + $oMessage->SetFrom(new \MailSo\Mime\Email($oIdentity->Email(), $oIdentity->Name())); + + $oFrom = $oMessage->GetFrom(); + $oMessage->RegenerateMessageId($oFrom ? $oFrom->GetDomain() : ''); + + $sReplyTo = $oIdentity->ReplyTo(); + if (!empty($sReplyTo)) { + $oReplyTo = new \MailSo\Mime\EmailCollection($sReplyTo); + if ($oReplyTo && $oReplyTo->count()) { + $oMessage->SetReplyTo($oReplyTo); + } + } + + $oMessage->SetSubject($sSubject); + + $oToEmails = new \MailSo\Mime\EmailCollection($sReadReceipt); + if ($oToEmails && $oToEmails->count()) { + $oMessage->SetTo($oToEmails); + } + + $this->Plugins()->RunHook('filter.read-receipt-message-plain', array($oAccount, $oMessage, &$sText)); + + $oPart = new MimePart; + $oPart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'text/plain; charset="utf-8"'); + $oPart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'quoted-printable'); + $oPart->Body = \MailSo\Base\StreamWrappers\Binary::CreateStream( + \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sText))), + 'convert.quoted-printable-encode' + ); + $oMessage->SubParts->append($oPart); + + $this->Plugins()->RunHook('filter.build-read-receipt-message', array($oMessage, $oAccount)); + + return $oMessage; + } + + /** + * called by DoSaveMessage and DoSendMessage + */ + private function buildMessage(Account $oAccount, bool $bWithDraftInfo = true) : \MailSo\Mime\Message + { + $oMessage = new \MailSo\Mime\Message(); + + if ($this->Config()->Get('security', 'hide_x_mailer_header', true)) { + $oMessage->DoesNotAddDefaultXMailer(); + } + + $oMessage->SetFrom(\MailSo\Mime\Email::Parse($this->GetActionParam('from', ''))); + $oFrom = $oMessage->GetFrom(); + +/* + $oIdentity = $this->GetIdentityByID($oAccount, $this->GetActionParam('identityID', '')); + if ($oIdentity) + { + $oMessage->SetFrom(new \MailSo\Mime\Email( + $oIdentity->Email(), $oIdentity->Name())); + if ($oAccount->Domain()->SmtpSettings()->setSender) { + $oMessage->SetSender(\MailSo\Mime\Email::Parse($oAccount->Email())); + } + } + else + { + $oMessage->SetFrom(\MailSo\Mime\Email::Parse($oAccount->Email())); + } +*/ + $oMessage->RegenerateMessageId($oFrom ? $oFrom->GetDomain() : ''); + + $oMessage->SetReplyTo(new \MailSo\Mime\EmailCollection($this->GetActionParam('replyTo', ''))); + + if (!empty($this->GetActionParam('readReceiptRequest', 0))) { + // Read Receipts Reference Main Account Email, Not Identities #147 +// $oMessage->SetReadReceipt(($oIdentity ?: $oAccount)->Email()); + $oMessage->SetReadReceipt($oFrom->GetEmail()); + } + + if (empty($this->GetActionParam('requireTLS', 0))) { + $oMessage->SetCustomHeader('TLS-Required', 'No'); + } + + if (!empty($this->GetActionParam('markAsImportant', 0))) { + $oMessage->SetPriority(\MailSo\Mime\Enumerations\MessagePriority::HIGH); + } + + $oMessage->SetSubject($this->GetActionParam('subject', '')); + + $oMessage->SetTo(new \MailSo\Mime\EmailCollection($this->GetActionParam('to', ''))); + $oMessage->SetCc(new \MailSo\Mime\EmailCollection($this->GetActionParam('cc', ''))); + $oMessage->SetBcc(new \MailSo\Mime\EmailCollection($this->GetActionParam('bcc', ''))); + + $aDraftInfo = $this->GetActionParam('draftInfo', null); + if ($bWithDraftInfo && \is_array($aDraftInfo) && !empty($aDraftInfo[0]) && !empty($aDraftInfo[1]) && !empty($aDraftInfo[2])) { + $oMessage->SetDraftInfo($aDraftInfo[0], $aDraftInfo[1], $aDraftInfo[2]); + } + + $oMessage->SetInReplyTo($this->GetActionParam('inReplyTo', '')); + $oMessage->SetReferences($this->GetActionParam('references', '')); + + $aAutocrypt = $this->GetActionParam('autocrypt', []); + $oMessage->SetAutocrypt( + \array_map(fn($header)=>"addr={$header['addr']}; keydata={$header['keydata']}", $aAutocrypt) + ); + + $aFoundCids = array(); + $aFoundDataURL = array(); + $aFoundContentLocationUrls = array(); + $oPart; + + if ($sSigned = $this->GetActionParam('signed', '')) { + $aSigned = \explode("\r\n\r\n", $sSigned, 2); +// $sBoundary = \preg_replace('/^.+boundary="([^"]+)".+$/Dsi', '$1', $aSigned[0]); + $sBoundary = $this->GetActionParam('boundary', ''); +// \preg_match('/protocol="(application/[^"]+)"/', $aSigned[0], $match); +// $sProtocol = $match[1][0]; + $sProtocol = 'application/pgp-signature'; + + $oPart = new MimePart; + $oPart->Headers->AddByName( + MimeEnumHeader::CONTENT_TYPE, + 'multipart/signed; micalg="pgp-sha256"; protocol="'.$sProtocol.'"; boundary="'.$sBoundary.'"' + ); + $oPart->Body = $aSigned[1]; + $oMessage->SubParts->append($oPart); + $oMessage->SubParts->SetBoundary($sBoundary); + + unset($oAlternativePart); + unset($sSigned); + + } else if ($sEncrypted = $this->GetActionParam('encrypted', '')) { + $oMessage->addPgpEncrypted(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sEncrypted))); + unset($sEncrypted); + + } else if ($sHtml = $this->GetActionParam('html', '')) { + $oPart = new MimePart; + $oPart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'multipart/alternative'); + $oMessage->SubParts->append($oPart); + + $sHtml = \MailSo\Base\HtmlUtils::BuildHtml($sHtml, $aFoundCids, $aFoundDataURL, $aFoundContentLocationUrls); + + $aLinkedData = $this->GetActionParam('linkedData', []); + if ($aLinkedData) { + $sHtml = \str_replace('', '', $sHtml); + } + + $this->Plugins()->RunHook('filter.message-html', array($oAccount, $oMessage, &$sHtml)); + + // First add plain + $sPlain = $this->GetActionParam('plain', '') ?: \MailSo\Base\HtmlUtils::ConvertHtmlToPlain($sHtml); + $this->Plugins()->RunHook('filter.message-plain', array($oAccount, $oMessage, &$sPlain)); + $oPart->addPlain($sPlain); + unset($sPlain); + + // Now add HTML + $oAlternativePart = new MimePart; + $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'text/html; charset=utf-8'); + $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'quoted-printable'); + $oAlternativePart->Body = \MailSo\Base\StreamWrappers\Binary::CreateStream( + \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sHtml))), + 'convert.quoted-printable-encode' + ); + $oPart->SubParts->append($oAlternativePart); + + unset($oAlternativePart); + unset($sHtml); + + } else { + $sPlain = $this->GetActionParam('plain', ''); +/* + if ($sSignature = $this->GetActionParam('pgpSignature', null)) { + $oPart = new MimePart; + $oPart->Headers->AddByName( + MimeEnumHeader::CONTENT_TYPE, + 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"' + ); + $oMessage->SubParts->append($oPart); + + $oAlternativePart = new MimePart; + $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'text/plain; charset="utf-8"; protected-headers="v1"'); + $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64'); + $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sPlain))); + $oPart->SubParts->append($oAlternativePart); + + $oAlternativePart = new MimePart; + $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pgp-signature; name="signature.asc"'); + $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sSignature))); + $oPart->SubParts->append($oAlternativePart); + + unset($sSignature); + } else { +*/ + $this->Plugins()->RunHook('filter.message-plain', array($oAccount, $oMessage, &$sPlain)); + $oMessage->addPlain($sPlain); + unset($sPlain); + } + unset($oPart); + + $aAttachments = $this->GetActionParam('attachments', null); + if (\is_array($aAttachments)) { + foreach ($aAttachments as $sTempName => $aData) { + $sFileName = (string) $aData['name']; + $bIsInline = (bool) $aData['inline']; + $sCID = (string) $aData['cId']; + $sContentLocation = (string) $aData['location']; + $sMimeType = (string) $aData['type']; + + $rResource = $this->FilesProvider()->GetFile($oAccount, $sTempName); + if (\is_resource($rResource)) { + $iFileSize = $this->FilesProvider()->FileSize($oAccount, $sTempName); + + $oMessage->Attachments()->append( + new \MailSo\Mime\Attachment($rResource, $sFileName, $iFileSize, $bIsInline, + \in_array(trim(trim($sCID), '<>'), $aFoundCids), + $sCID, array(), $sContentLocation, $sMimeType + ) + ); + } + } + } +/* + // OpenPGP https://github.com/the-djmaze/snappymail/issues/1587 + $sPublicKey = $this->GetActionParam('publicKey', ''); + if ($sPublicKey) { + $oMessage->Attachments()->append( + new \MailSo\Mime\Attachment( + \fopen("data://text/plain,{$sPublicKey}", 'r'), + 'OpenPGP_'.\md5($sPublicKey).'.asc', + \strlen($sPublicKey), + false, + false, + '', + array(), + '', + 'application/pgp-keys' + ) + ); + } +*/ + foreach ($aFoundDataURL as $sCidHash => $sDataUrlString) { + $aMatch = array(); + $sCID = '<'.$sCidHash.'>'; + if (\preg_match('/^data:(image\/[a-zA-Z0-9]+);base64,(.+)$/i', $sDataUrlString, $aMatch) && + !empty($aMatch[1]) && !empty($aMatch[2])) + { + $sRaw = \MailSo\Base\Utils::Base64Decode($aMatch[2]); + $iFileSize = \strlen($sRaw); + if (0 < $iFileSize) { + $sFileName = \preg_replace('/[^a-z0-9]+/i', '.', $aMatch[1]); + $rResource = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString($sRaw); + + $sRaw = ''; + unset($sRaw); + unset($aMatch); + + $oMessage->Attachments()->append( + new \MailSo\Mime\Attachment($rResource, $sFileName, $iFileSize, true, true, $sCID) + ); + } + } + } + + $oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('signPassphrase', '')); + + // GnuPG + $sFingerprint = $this->GetActionParam('signFingerprint', ''); + if ($sFingerprint) { + $GPG = $this->GnuPG(); +/* + // https://github.com/the-djmaze/snappymail/issues/1587 + if ($this->GetActionParam('attachPublicKey', false)) { + $sPublicKey = $GPG->export($sFingerprint); + $oMessage->Attachments()->append( + new \MailSo\Mime\Attachment( + \fopen("data://text/plain,{$sPublicKey}", 'r'), + "OpenPGP_0x{$sFingerprint}.asc", + \strlen($sPublicKey), + false, + false, + '', + array(), + '', + 'application/pgp-keys' + ) + ); + } +*/ + $oBody = $oMessage->GetRootPart(); + $resource = $oBody->ToStream(); + + \MailSo\Base\StreamFilters\LineEndings::appendTo($resource); + $fp = \fopen('php://temp', 'r+b'); +// \stream_copy_to_stream($resource, $fp); // Fails + while (!\feof($resource)) \fwrite($fp, \fread($resource, 8192)); + + $oBody->Body = null; + $oBody->SubParts->Clear(); + $oMessage->SubParts->Clear(); + $oMessage->Attachments()->Clear(); + + $GPG->addSignKey($sFingerprint, $oPassphrase); + $GPG->setsignmode(GNUPG_SIG_MODE_DETACH); + $sSignature = $GPG->signStream($fp); + if (!$sSignature) { + throw new \Exception('GnuPG sign() failed'); + } + + $oPart = new MimePart; + $oPart->Headers->AddByName( + MimeEnumHeader::CONTENT_TYPE, + 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"' + ); + $oMessage->SubParts->append($oPart); + + \rewind($fp); + $oBody->Raw = $fp; + $oPart->SubParts->append($oBody); + + $oSignaturePart = new MimePart; + $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pgp-signature; name="signature.asc"'); + $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oSignaturePart->Body = $sSignature; + $oPart->SubParts->append($oSignaturePart); + } else { + $sCertificate = $this->GetActionParam('signCertificate', ''); + $sPrivateKey = $this->GetActionParam('signPrivateKey', ''); + if ('S/MIME' === $this->GetActionParam('sign', '')) { + $sID = $this->GetActionParam('identityID', ''); + foreach ($this->GetIdentities($oAccount) as $oIdentity) { + if ($oIdentity && $oIdentity->smimeCertificate && $oIdentity->smimeKey + && ($oIdentity->Id() === $sID || $oIdentity->Email() === $oFrom->GetEmail()) + ) { + $sCertificate = $oIdentity->smimeCertificate; + $sPrivateKey = $oIdentity->smimeKey; + break; + } + } + } +/* + // https://github.com/the-djmaze/snappymail/issues/1587 + if ($sCertificate && $this->GetActionParam('attachCertificate', false)) { + $oMessage->Attachments()->append( + new \MailSo\Mime\Attachment( + \fopen("data://text/plain,{$sCertificate}", 'r'), + 'certificate.pem', + \strlen($sCertificate), + false, + false, + '', + array(), + '', + 'application/pem-certificate-chain' + ) + ); + } +*/ + if ($sCertificate && $sPrivateKey) { + $oBody = $oMessage->GetRootPart(); + + $resource = $oBody->ToStream(); + \MailSo\Base\StreamFilters\LineEndings::appendTo($resource); + $tmp = new \SnappyMail\File\Temporary('mimepart'); + $tmp->writeFromStream($resource); + + $oBody->Body = null; + $oBody->SubParts->Clear(); + $oMessage->SubParts->Clear(); + $oMessage->Attachments()->Clear(); + + $detached = true; + + $SMIME = $this->SMIME(); + $SMIME->setCertificate($sCertificate); + $SMIME->setPrivateKey($sPrivateKey, $oPassphrase); + $sSignature = $SMIME->sign($tmp, $detached); + + if (!$sSignature) { + throw new \RuntimeException('S/MIME sign() failed'); + } + + $oPart = new MimePart; + $oMessage->SubParts->append($oPart); + if ($detached) { + $oPart->Headers->AddByName( + MimeEnumHeader::CONTENT_TYPE, + 'multipart/signed; micalg="sha-256"; protocol="application/pkcs7-signature"' + ); + + $fp = $tmp->fopen(); + \rewind($fp); + $oBody->Raw = $fp; + $oPart->SubParts->append($oBody); + + $oSignaturePart = new MimePart; + $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pkcs7-signature; name="signature.p7s"'); + $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64'); + $oSignaturePart->Body = $sSignature; + $oPart->SubParts->append($oSignaturePart); + } else { + $oPart->Headers->AddByName( + MimeEnumHeader::CONTENT_TYPE, + 'application/pkcs7-mime; smime-type=signed-data; name="smime.p7m"' + ); + $oPart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64'); + $oPart->Body = $sSignature; + } + } + } + + $aFingerprints = \json_decode($this->GetActionParam('encryptFingerprints', ''), true); + if ($aFingerprints) { + $resource = $oMessage->GetRootPart()->ToStream(); + $fp = \fopen('php://temp', 'r+b'); +// \stream_copy_to_stream($resource, $fp); // Fails + while (!\feof($resource)) \fwrite($fp, \fread($resource, 8192)); + + $oMessage->SubParts->Clear(); + $oMessage->Attachments()->Clear(); + + $GPG = $this->GnuPG(); + foreach ($aFingerprints as $sFingerprint) { + $GPG->addEncryptKey($sFingerprint); + } + $oMessage->addPgpEncrypted($GPG->encryptStream($fp)); + } else { + $aCertificates = $this->GetActionParam('encryptCertificates', []); + if ($aCertificates) { + $oBody = $oMessage->GetRootPart(); + + $resource = $oBody->ToStream(); + \MailSo\Base\StreamFilters\LineEndings::appendTo($resource); + $tmp = new \SnappyMail\File\Temporary('mimepart'); + $tmp->writeFromStream($resource); + + $oBody->Body = null; + $oBody->SubParts->Clear(); + $oMessage->SubParts->Clear(); + $oMessage->Attachments()->Clear(); + + $SMIME = $this->SMIME(); + $certificates = $SMIME->certificates(); + // Load certificates by id + foreach ($aCertificates as &$sCertificate) { + if (!\str_contains($sCertificate, '-----BEGIN CERTIFICATE-----')) { + foreach ($certificates as $certificate) { + if ($certificate['id'] === $sCertificate) { + $sCertificate = $SMIME->getCertificate($certificate['file']); + } + } + } + } + $sEncrypted = $SMIME->encrypt($tmp, $aCertificates); + + $oPart = new MimePart; + $oMessage->SubParts->append($oPart); + $oPart->Headers->AddByName( + MimeEnumHeader::CONTENT_TYPE, + 'application/pkcs7-mime; smime-type=enveloped-data; name="smime.p7m"' + ); + $oPart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64'); + $oPart->Body = $sEncrypted; + } + } + + $this->Plugins()->RunHook('filter.build-message', array($oMessage)); + + return $oMessage; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php new file mode 100644 index 0000000000..939610695a --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php @@ -0,0 +1,403 @@ +GnuPG(); + if ($GPG) { + $keys = $GPG->allKeysInfo(''); + foreach ($keys['public'] as $key) { + $key = $GPG->export($key['subkeys'][0]['fingerprint'] ?: $key['subkeys'][0]['keyid']); + if ($key) { + $result[] = $key; + } + } + } + + return $this->DefaultResponse(\array_values(\array_unique($result))); + } + + public function DoPgpSearchKey() : array + { + $result = Keyservers::get( + $this->GetActionParam('query', '') + ); + return $this->DefaultResponse($result ?: false); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function GnuPG() : ?\SnappyMail\PGP\PGPInterface + { + $oAccount = $this->getMainAccountFromToken(); + if (!$oAccount) { + return null; + } + + $homedir = \rtrim($this->StorageProvider()->GenerateFilePath( + $oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::ROOT + ), '/') . '/.gnupg'; + + \MailSo\Base\Utils::mkdir($homedir); + if (!\is_writable($homedir)) { + throw new \Exception("gpg homedir '{$homedir}' not writable"); + } + + /** + * Workaround error: socket name for '/very/long/path/to/.gnupg/S.gpg-agent.extra' is too long + * BSD 4.4 max length = 104 + */ + if (80 < \strlen($homedir)) { + \clearstatcache(); + // First try a symbolic link + $tmpdir = \sys_get_temp_dir() . '/snappymail'; +// if (\RainLoop\Utils::inOpenBasedir($tmpdir) && + \is_dir($tmpdir) || \mkdir($tmpdir, 0700); + if (\is_dir($tmpdir) && \is_writable($tmpdir)) { + $link = $tmpdir . '/' . \md5($homedir); + if (\is_link($link) || \symlink($homedir, $link)) { + $homedir = $link; + } else { + $this->logWrite("symlink('{$homedir}', '{$link}') failed", \LOG_WARNING, 'GnuPG'); + } + } + // Else try ~/.gnupg/ + hash(email address) + if (80 < \strlen($homedir)) { + $tmpdir = ($_SERVER['HOME'] ?: \exec('echo ~') ?: \dirname(\getcwd())) . '/.gnupg/'; + if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) { + $tmpdir .= \sha1($oAccount->ParentEmail()); + } else { + $tmpdir .= \sha1($oAccount->Email()); + } +// if (\RainLoop\Utils::inOpenBasedir($tmpdir) && + if (\is_dir($tmpdir) || \is_link($tmpdir) || \symlink($homedir, $tmpdir) || \mkdir($tmpdir, 0700, true)) { + $homedir = $tmpdir; + } + } + } + + return GnuPG::getInstance($homedir); + } + + public function DoGnupgDecrypt() : array + { + $GPG = $this->GnuPG(); + if (!$GPG) { + return $this->FalseResponse(); + } + + $oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', '')); + + $GPG->addDecryptKey($this->GetActionParam('keyId', ''), $oPassphrase); + + $sData = $this->GetActionParam('data', ''); + $oPart = null; + $result = [ + 'data' => null, + 'signatures' => [] + ]; + if ($sData) { + $result = $GPG->decrypt($sData); +// $oPart = \MailSo\Mime\Part::FromString($result); + } else { + $this->initMailClientConnection(); + $this->MailClient()->MessageMimeStream( + function ($rResource) use ($GPG, &$result, &$oPart) { + if (\is_resource($rResource)) try { + $result['data'] = $GPG->decryptStream($rResource); +// $oPart = \MailSo\Mime\Part::FromString($result); +// $GPG->decryptStream($rResource, $rStreamHandle); +// $oPart = \MailSo\Mime\Part::FromStream($rStreamHandle); + } catch (\Throwable $e) { + $result = $e; + } + }, + $this->GetActionParam('folder', ''), + (int) $this->GetActionParam('uid', ''), + $this->GetActionParam('partId', '') + ); + } + + if ($oPart && $oPart->isPgpSigned()) { +// $GPG->verifyStream($oPart->SubParts[0]->Body, \stream_get_contents($oPart->SubParts[1]->Body)); +// $result['signatures'] = $oPart->SubParts[0]; + } + + if ($result instanceof \Throwable) { + throw $result; + } + + return $this->DefaultResponse($result); + } + + public function DoGnupgGetKeys() : array + { + $GPG = $this->GnuPG(); + return $this->DefaultResponse($GPG ? $GPG->allKeysInfo('') : false); + } + + public function DoGnupgExportKey() : array + { + $oPassphrase = $this->GetActionParam('isPrivate', '') + ? new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', '')) + : null; + $GPG = $this->GnuPG(); + return $this->DefaultResponse($GPG ? $GPG->export( + $this->GetActionParam('keyId', ''), + $oPassphrase + ) : false); + } + + public function DoGnupgGenerateKey() : array + { + $fingerprint = false; + $GPG = $this->GnuPG(); + if ($GPG) { + $sName = $this->GetActionParam('name', ''); + $sEmail = $this->GetActionParam('email', ''); + $oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', '')); + $fingerprint = $GPG->generateKey( + $sName ? "{$sName} <{$sEmail}>" : $sEmail, + $oPassphrase + ); + } + return $this->DefaultResponse($fingerprint); + } + + public function DoGnupgDeleteKey() : array + { + $GPG = $this->GnuPG(); + $sKeyId = $this->GetActionParam('keyId', ''); + $bPrivate = !!$this->GetActionParam('isPrivate', 0); + return $this->DefaultResponse($GPG ? $GPG->deleteKey($sKeyId, $bPrivate) : false); + } + + public function DoPgpImportKey() : array + { + $sKey = $this->GetActionParam('key', ''); + $sKeyId = $this->GetActionParam('keyId', ''); + $sEmail = $this->GetActionParam('email', ''); + + if (!$sKey) { + try { + if (!$sKeyId) { + if (\preg_match('/[^\\s<>]+@[^\\s<>]+/', $sEmail, $aMatch)) { + $sEmail = $aMatch[0]; + } + if ($sEmail) { + $aKeys = Keyservers::index($sEmail); + if ($aKeys) { + $sKeyId = $aKeys[0]['keyid']; + } + } + } + if ($sKeyId) { + $sKey = Keyservers::get($sKeyId); + } + } catch (\Throwable $e) { + // ignore + } + } + + $result = []; + if ($sKey) { + $sKey = \trim($sKey); + $result['backup'] = $this->GetActionParam('backup', '') && Backup::PGPKey($sKey); + $result['gnuPG'] = $this->GetActionParam('gnuPG', '') && ($GPG = $this->GnuPG()) && $GPG->import($sKey); + } + + return $this->DefaultResponse($result); + } + + /** + * Used to import keys in OpenPGP.js + * Handy when using multiple browsers + */ + public function DoGetStoredPGPKeys() : array + { + return $this->DefaultResponse(Backup::getKeys()); + } + + /** + * Used to store generated armored key pair from OpenPGP.js + * Handy when using multiple browsers + */ + public function DoPgpStoreKeyPair() : array + { + $publicKey = $this->GetActionParam('publicKey', ''); + $privateKey = $this->GetActionParam('privateKey', ''); + + $result = [ + 'onServer' => [false, false], + 'inGnuPG' => [false, false] + ]; + + $onServer = (int) $this->GetActionParam('onServer', 0); + if ($publicKey && $onServer & 1) { + $result['onServer'][0] = Backup::PGPKey($publicKey); + } + if ($privateKey && $onServer & 2) { + $result['onServer'][1] = Backup::PGPKey($privateKey); + } + + $inGnuPG = (int) $this->GetActionParam('inGnuPG', 0); + if ($inGnuPG) { + $GPG = $this->GnuPG(); + if ($publicKey && $inGnuPG & 1) { + $result['inGnuPG'][0] = $GPG->import($publicKey); + } + if ($privateKey && $inGnuPG & 2) { + $result['inGnuPG'][1] = $GPG->import($privateKey); + } + } + +// $revocationCertificate = $this->GetActionParam('revocationCertificate', ''); + return $this->DefaultResponse($result); + } + + /** + * Used to store key from OpenPGP.js + * Handy when using multiple browsers + */ + public function DoStorePGPKey() : array + { + $key = $this->GetActionParam('key', ''); + $keyId = $this->GetActionParam('keyId', ''); + return $this->DefaultResponse(($key && $keyId && Backup::PGPKey($key, $keyId))); + } + + /** + * https://datatracker.ietf.org/doc/html/rfc3156#section-5 + */ + public function DoPgpVerifyMessage() : array + { + $sBodyPart = $this->GetActionParam('bodyPart', ''); + if ($sBodyPart) { + $result = [ + 'text' => \preg_replace('/\\r?\\n/su', "\r\n", $sBodyPart), + 'signature' => $this->GetActionParam('sigPart', '') + ]; + } else { + $sFolderName = $this->GetActionParam('folder', ''); + $iUid = (int) $this->GetActionParam('uid', 0); + $sPartId = $this->GetActionParam('partId', ''); + $sSigPartId = $this->GetActionParam('sigPartId', ''); +// $sMicAlg = $this->GetActionParam('micAlg', ''); + + $this->initMailClientConnection(); + $oImapClient = $this->ImapClient(); + $oImapClient->FolderExamine($sFolderName); + + $aParts = [ + FetchType::BODY_PEEK.'['.$sPartId.']', + // An empty section specification refers to the entire message, including the header. + // But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME]. + FetchType::BODY_PEEK.'['.$sPartId.'.MIME]' + ]; + if ($sSigPartId) { + $aParts[] = FetchType::BODY_PEEK.'['.$sSigPartId.']'; + } + + $oFetchResponse = $oImapClient->Fetch($aParts, $iUid, true)[0]; + + $sBodyMime = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.'.MIME]'); + if ($sSigPartId) { + $result = [ + 'text' => \preg_replace('/\\r?\\n/su', "\r\n", + $sBodyMime . $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']') + ), + 'signature' => \preg_replace('/[^\x00-\x7F]/', '', + $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sSigPartId.']') + ) + ]; + } else { + // clearsigned text + $result = [ + 'text' => $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']'), + 'signature' => '' + ]; + $decode = (new \MailSo\Mime\HeaderCollection($sBodyMime))->ValueByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING); + if ('base64' === $decode) { + $result['text'] = \base64_decode($result['text']); + } else if ('quoted-printable' === $decode) { + $result['text'] = \quoted_printable_decode($result['text']); + } + } + } + + // Try by default as OpenPGP.js sets useGnuPG to 0 + if ($this->GetActionParam('tryGnuPG', 1)) { + $GPG = $this->GnuPG(); + if ($GPG) { + $info = $this->GnuPG()->verify($result['text'], $result['signature']); +// $info = $this->GnuPG()->verifyStream($fp, $result['signature']); + if (empty($info[0])) { + $result = false; + } else { + $info = $info[0]; + + /** + * https://code.woboq.org/qt5/include/gpg-error.h.html + * status: + 0 = GPG_ERR_NO_ERROR + 1 = GPG_ERR_GENERAL + 9 = GPG_ERR_NO_PUBKEY + 117440513 = General error + 117440520 = Bad signature + */ + + $summary = \defined('GNUPG_SIGSUM_VALID') ? [ + GNUPG_SIGSUM_VALID => 'The signature is fully valid.', + GNUPG_SIGSUM_GREEN => 'The signature is good but one might want to display some extra information. Check the other bits.', + GNUPG_SIGSUM_RED => 'The signature is bad. It might be useful to check other bits and display more information, i.e. a revoked certificate might not render a signature invalid when the message was received prior to the cause for the revocation.', + GNUPG_SIGSUM_KEY_REVOKED => 'The key or at least one certificate has been revoked.', + GNUPG_SIGSUM_KEY_EXPIRED => 'The key or one of the certificates has expired. It is probably a good idea to display the date of the expiration.', + GNUPG_SIGSUM_SIG_EXPIRED => 'The signature has expired.', + GNUPG_SIGSUM_KEY_MISSING => 'Can’t verify due to a missing key or certificate.', + GNUPG_SIGSUM_CRL_MISSING => 'The CRL (or an equivalent mechanism) is not available.', + GNUPG_SIGSUM_CRL_TOO_OLD => 'Available CRL is too old.', + GNUPG_SIGSUM_BAD_POLICY => 'A policy requirement was not met.', + GNUPG_SIGSUM_SYS_ERROR => 'A system error occurred.', +// GNUPG_SIGSUM_TOFU_CONFLICT = 'A TOFU conflict was detected.', + ] : []; + + // Verified, so no need to return $result['text'] and $result['signature'] + $result = [ + 'fingerprint' => $info['fingerprint'], + 'validity' => $info['validity'], + 'status' => $info['status'], + 'summary' => $info['summary'], + 'message' => \implode("\n", \array_filter($summary, function($k) use ($info) { + return $info['summary'] & $k; + }, ARRAY_FILTER_USE_KEY)) + ]; + } + } else { + $result = false; + } + } + + return $this->DefaultResponse($result); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php new file mode 100644 index 0000000000..ef73f9c7f9 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php @@ -0,0 +1,323 @@ +getAccountFromToken(); + $sRawKey = $this->GetActionParam('RawKey', ''); + $aValues = $this->decodeRawKey($sRawKey); + if (!empty($aValues['folder']) && !empty($aValues['uid']) + && !empty($aValues['accountHash']) && $aValues['accountHash'] === $oAccount->Hash() + ) { + $this->verifyCacheByKey($sRawKey); + $this->initMailClientConnection(); + \header('Content-Type: text/plain'); + return $this->MailClient()->MessageMimeStream( + function ($rResource) use ($sRawKey) { + if (\is_resource($rResource)) { + $this->cacheByKey($sRawKey); + \MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource); + } + }, + (string) $aValues['folder'], + (int) $aValues['uid'], + isset($aValues['mimeIndex']) ? (string) $aValues['mimeIndex'] : '' + ); + } + return false; + } + + public function RawDownload() : bool + { + return $this->rawSmart(true); + } + + public function RawView() : bool + { + return $this->rawSmart(false); + } + + public function RawViewThumbnail() : bool + { + return $this->rawSmart(false, true); + } + + public function RawUserBackground() : bool + { + $sRawKey = (string) $this->GetActionParam('RawKey', ''); + $this->verifyCacheByKey($sRawKey); + + $oAccount = $this->getAccountFromToken(); + + $mData = $this->StorageProvider()->Get($oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'background' + ); + + if (!empty($mData)) { + $mData = \json_decode($mData, true); + if (!empty($mData['ContentType']) && !empty($mData['Raw'])) { + $this->cacheByKey($sRawKey); + + \header('Content-Type: '.$mData['ContentType']); + echo \base64_decode($mData['Raw']); + unset($mData); + + return true; + } + } + + return false; + } + + private function clearFileName(string $sFileName, string $sContentType, string $sMimeIndex, int $iMaxLength = 250): string + { + $sFileName = !\strlen($sFileName) ? \preg_replace('/[^a-zA-Z0-9]/', '.', (empty($sMimeIndex) ? '' : $sMimeIndex . '.') . $sContentType) : $sFileName; + $sClearedFileName = \MailSo\Base\Utils::StripSpaces(\preg_replace('/[\.]+/', '.', $sFileName)); + $sExt = \MailSo\Base\Utils::GetFileExtension($sClearedFileName); + + if (10 < $iMaxLength && $iMaxLength < \strlen($sClearedFileName) - \strlen($sExt)) { + $sClearedFileName = \substr($sClearedFileName, 0, $iMaxLength) . (empty($sExt) ? '' : '.' . $sExt); + } + + return \MailSo\Base\Utils::SecureFileName(\MailSo\Base\Utils::Utf8Clear($sClearedFileName)); + } + + /** + * Message, Message Attachment or Zip + */ + private function rawSmart(bool $bDownload, bool $bThumbnail = false) : bool + { + $sRawKey = (string) $this->GetActionParam('RawKey', ''); + + $oAccount = $this->getAccountFromToken(); + $aValues = $this->decodeRawKey($sRawKey); + if (empty($aValues['accountHash']) || $aValues['accountHash'] !== $oAccount->Hash()) { + return false; + } + + $sRange = \MailSo\Base\Http::GetHeader('Range'); + $aMatch = array(); + $sRangeStart = $sRangeEnd = ''; + $bIsRangeRequest = false; + if (!empty($sRange) && 'bytes=0-' !== \strtolower($sRange) + && \preg_match('/^bytes=([0-9]+)-([0-9]*)/i', \trim($sRange), $aMatch)) + { + $sRangeStart = $aMatch[1]; + $sRangeEnd = $aMatch[2]; + $bIsRangeRequest = true; + } else { + $this->verifyCacheByKey($sRawKey); + } + + $sMimeIndex = isset($aValues['mimeIndex']) ? (string) $aValues['mimeIndex'] : ''; + $sContentTypeIn = isset($aValues['mimeType']) ? (string) $aValues['mimeType'] : ''; + $sFileNameIn = isset($aValues['fileName']) ? (string) $aValues['fileName'] : ''; + + if (!empty($aValues['fileHash'])) { + $rResource = $this->FilesProvider()->GetFile($oAccount, (string) $aValues['fileHash']); + if (!\is_resource($rResource)) { + return false; + } + // https://github.com/the-djmaze/snappymail/issues/144 + if ('.pdf' === \substr($sFileNameIn,-4)) { + $sContentTypeOut = 'application/pdf'; // application/octet-stream + } else { + $sContentTypeOut = $sContentTypeIn + ?: \SnappyMail\File\MimeType::fromFilename($sFileNameIn) + ?: 'application/octet-stream'; + } + $sFileNameOut = $this->clearFileName($sFileNameIn, $sContentTypeIn, $sMimeIndex); + \header('Content-Type: '.$sContentTypeOut); + \MailSo\Base\Http::setContentDisposition('attachment', ['filename' => $sFileNameOut]); + \header('Accept-Ranges: none'); + \header('Content-Transfer-Encoding: binary'); + \MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource); + return true; + } + + $sFolder = isset($aValues['folder']) ? (string) $aValues['folder'] : ''; + $iUid = isset($aValues['uid']) ? (int) $aValues['uid'] : 0; + if (empty($sFolder) || 1 > $iUid) { + return false; + } + + $this->initMailClientConnection(); + + $self = $this; + return $this->MailClient()->MessageMimeStream( + function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use ( + $self, $sRawKey, $sContentTypeIn, $sFileNameIn, $bDownload, $bThumbnail, + $bIsRangeRequest, $sRangeStart, $sRangeEnd + ) { + if (\is_resource($rResource)) { + \MailSo\Base\Utils::ResetTimeLimit(); + + $self->cacheByKey($sRawKey); + + $self->logWrite(\print_r([ + $sFileName, + $sContentType, + $sFileNameIn, + $sContentTypeIn + ],true), \LOG_DEBUG, 'RAW'); + + if ($sFileNameIn) { + $sFileName = $sFileNameIn; + } + $sFileName = $self->clearFileName($sFileName, $sContentType, $sMimeIndex); + + if ('.pdf' === \substr($sFileName, -4)) { + // https://github.com/the-djmaze/snappymail/issues/144 + $sContentType = 'application/pdf'; + } else { + $sContentType = $sContentTypeIn + ?: $sContentType +// ?: \SnappyMail\File\MimeType::fromStream($rResource, $sFileName) + ?: \SnappyMail\File\MimeType::fromFilename($sFileName) + ?: 'application/octet-stream'; + } + + if (!$bDownload) { + $bDetectImageOrientation = $self->Config()->Get('labs', 'image_exif_auto_rotate', false) + // Mostly only JPEG has EXIF metadata + && 'image/jpeg' == $sContentType; + try + { + if ($bThumbnail) { + $oImage = static::loadImage($rResource, $bDetectImageOrientation, 48); + \MailSo\Base\Http::setContentDisposition('inline', ['filename' => $sFileName.'_thumb60x60.png']); + $oImage->show('png'); +// $oImage->show('webp'); // Little Britain: "Safari says NO" + exit; + } else if ($bDetectImageOrientation) { + $oImage = static::loadImage($rResource, $bDetectImageOrientation); + \MailSo\Base\Http::setContentDisposition('inline', ['filename' => $sFileName]); + $oImage->show(); +// $oImage->show('webp'); // Little Britain: "Safari says NO" + exit; + } + } + catch (\Throwable $oException) + { + $self->Logger()->WriteExceptionShort($oException); + \MailSo\Base\Http::StatusHeader(500); + exit; + } + } + + if (!\headers_sent()) { + \header('Content-Type: '.$sContentType); + \MailSo\Base\Http::setContentDisposition($bDownload ? 'attachment' : 'inline', ['filename' => $sFileName]); + \header('Accept-Ranges: bytes'); + \header('Content-Transfer-Encoding: binary'); + } + + $sLoadedData = null; + if ($bIsRangeRequest || !$bDownload) { + $sLoadedData = \stream_get_contents($rResource); + } + + \MailSo\Base\Utils::ResetTimeLimit(); + + if ($sLoadedData) { + if ($bIsRangeRequest && (\strlen($sRangeStart) || \strlen($sRangeEnd))) { + $iFullContentLength = \strlen($sLoadedData); + + \MailSo\Base\Http::StatusHeader(206); + + $iRangeStart = \max(0, \intval($sRangeStart)); + $iRangeEnd = \max(0, \intval($sRangeEnd)); + + if ($iRangeEnd && $iRangeStart < $iRangeEnd) { + $sLoadedData = \substr($sLoadedData, $iRangeStart, $iRangeEnd - $iRangeStart); + } else if ($iRangeStart) { + $sLoadedData = \substr($sLoadedData, $iRangeStart); + } + + $iContentLength = \strlen($sLoadedData); + + if (0 < $iContentLength) { + \header('Content-Length: '.$iContentLength); + \header('Content-Range: bytes '.$sRangeStart.'-'.($iRangeEnd ?: $iFullContentLength - 1).'/'.$iFullContentLength); + } + } else { + \header('Content-Length: '.\strlen($sLoadedData)); + } + + echo $sLoadedData; + + unset($sLoadedData); + } else { + \MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource); + } + } + }, $sFolder, $iUid, $sMimeIndex + ); + } + + private static function loadImage($resource, bool $bDetectImageOrientation = false, int $iThumbnailBoxSize = 0) : \SnappyMail\Image + { + if (\extension_loaded('gmagick')) { $handler = 'gmagick'; } + else if (\extension_loaded('imagick')) { $handler = 'imagick'; } + else if (\extension_loaded('gd')) { $handler = 'gd2'; } + else { return null; } + $handler = 'SnappyMail\\Image\\'.$handler.'::createFromStream'; + $oImage = $handler($resource); + + if (!$oImage->valid()) { + throw new \Exception('Loading image failed'); + } + + // rotateImageByOrientation + if ($bDetectImageOrientation) { + switch ($oImage->getOrientation()) + { + case 2: // flip horizontal + $oImage->flopImage(); + break; + + case 3: // rotate 180 + $oImage->rotate(180); + break; + + case 4: // flip vertical + $oImage->flipImage(); + break; + + case 5: // vertical flip + 90 rotate + $oImage->flipImage(); + $oImage->rotate(90); + break; + + case 6: // rotate 90 + $oImage->rotate(90); + break; + + case 7: // horizontal flip + 90 rotate + $oImage->flopImage(); + $oImage->rotate(90); + break; + + case 8: // rotate 270 + $oImage->rotate(270); + break; + } + } + + if (0 < $iThumbnailBoxSize) { + $oImage->cropThumbnailImage($iThumbnailBoxSize, $iThumbnailBoxSize); + } + + $oImage->stripImage(); + + return $oImage; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php new file mode 100644 index 0000000000..84310f213c --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php @@ -0,0 +1,123 @@ + APP_VERSION, + 'Action' => $sActionName, + 'Result' => $this->responseObject($mResult) + ), $aAdditionalParams); + } + + public function TrueResponse(array $aAdditionalParams = array()) : array + { + return $this->DefaultResponse(true, $aAdditionalParams); + } + + public function FalseResponse(int $iErrorCode = 0, string $sErrorMessage = '', string $sAdditionalErrorMessage = '') : array + { + return $this->DefaultResponse(false, [ + 'code' => $iErrorCode, + 'message' => $sErrorMessage, + 'messageAdditional' => $sAdditionalErrorMessage + ]); + } + + public function ExceptionResponse(\Throwable $oException) : array + { + $iErrorCode = Notifications::UnknownError; + $sErrorMessage = $oException->getMessage(); + $sErrorMessageAdditional = ''; + $iExceptionCode = 0; + + if ($oException instanceof \RainLoop\Exceptions\ClientException) { + $iErrorCode = $oException->getCode(); + if ($iErrorCode === Notifications::ClientViewError) { + $sErrorMessage = $oException->getMessage(); + } + $sErrorMessageAdditional = $oException->getAdditionalMessage(); + } else { + $iExceptionCode = $oException->getCode(); + } + + $this->logException($oException->getPrevious() ?: $oException); + + return $this->DefaultResponse(false, [ + 'code' => $iErrorCode, + 'message' => $sErrorMessage, + 'messageAdditional' => $sErrorMessageAdditional, + 'ExceptionCode' => $iExceptionCode + ]); + } + + /** + * @param mixed $mResponse + * + * @return mixed + */ + private $aCheckableFolder = null; + private function responseObject($mResponse, string $sParent = '') + { + if (!($mResponse instanceof \JsonSerializable)) { + if (\is_object($mResponse)) { + return '["'.\get_class($mResponse).'"]'; + } + + if (\is_array($mResponse)) { + foreach ($mResponse as $iKey => $oItem) { + $mResponse[$iKey] = $this->responseObject($oItem, 'Array'); + } + } + + return $mResponse; + } + + if ($mResponse instanceof \MailSo\Mail\Message) { + $aResult = $mResponse->jsonSerialize(); + if (!$sParent && \strlen($aResult['readReceipt']) && !\in_array('$mdnsent', $aResult['flags']) && !\in_array('\\answered', $aResult['flags'])) { + $oAccount = $this->getAccountFromToken(); + if ('1' === $this->Cacher($oAccount)->Get(\RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $aResult['folder'], $aResult['uid']), '0')) { + $aResult['readReceipt'] = ''; + } + } + return $aResult; + } + + if ($mResponse instanceof \MailSo\Imap\Folder) { + $aResult = $mResponse->jsonSerialize(); + + if (null === $this->aCheckableFolder) { + $aCheckable = \json_decode( + $this->SettingsProvider(true) + ->Load($this->getAccountFromToken()) + ->GetConf('CheckableFolder', '[]') + ); + $this->aCheckableFolder = \is_array($aCheckable) ? $aCheckable : array(); + } + $aResult['checkable'] = \in_array($mResponse->FullName, $this->aCheckableFolder); + + return $aResult; + } + + return $mResponse; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/SMime.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/SMime.php new file mode 100644 index 0000000000..124f3c9745 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/SMime.php @@ -0,0 +1,215 @@ +SMIME) { + $oAccount = $this->getMainAccountFromToken(); + if (!$oAccount) { + return null; + } + + $homedir = \rtrim($this->StorageProvider()->GenerateFilePath( + $oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::ROOT + ), '/') . '/.smime'; + + \MailSo\Base\Utils::mkdir($homedir); + if (!\is_writable($homedir)) { + throw new \Exception("smime homedir '{$homedir}' not writable"); + } + + $this->SMIME = new OpenSSL($homedir); + } + return $this->SMIME; + } + + public function DoGetSMimeCertificate() : array + { + $result = [ + 'key' => '', + 'pkey' => '', + 'cert' => '' + ]; + return $this->DefaultResponse(\array_values(\array_unique($result))); + } + + // Like DoGnupgGetKeys + public function DoSMimeGetCertificates() : array + { + return $this->DefaultResponse( + $this->SMIME()->certificates() + ); + } + + /** + * Can be used by Identity + */ + public function DoSMimeCreateCertificate() : array + { + $oAccount = $this->getAccountFromToken(); + + $oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', '')); + + $cert = new Certificate(); + $cert->distinguishedName['commonName'] = $this->GetActionParam('name', '') ?: $oAccount->Name(); + $cert->distinguishedName['emailAddress'] = $this->GetActionParam('email', '') ?: $oAccount->Email(); + $result = $cert->createSelfSigned($oPassphrase, $this->GetActionParam('privateKey', '')); + return $this->DefaultResponse($result ?: false); + } + + public function DoSMimeExportPrivateKey() : array + { + $SMIME = $this->SMIME(); + $SMIME->setPrivateKey( + $this->GetActionParam('privateKey'), + new \SnappyMail\SensitiveString($this->GetActionParam('oldPassphrase', '')) + ); + $result = $SMIME->exportPrivateKey( + new \SnappyMail\SensitiveString($this->GetActionParam('newPassphrase', '')) + ); + + return $this->DefaultResponse($result); + } + + public function DoSMimeDecryptMessage() : array + { + $sFolderName = $this->GetActionParam('folder', ''); + $iUid = (int) $this->GetActionParam('uid', 0); + $sPartId = $this->GetActionParam('partId', ''); + $sCertificate = $this->GetActionParam('certificate', ''); + $sPrivateKey = $this->GetActionParam('privateKey', ''); + $oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', '')); + + $this->initMailClientConnection(); + $oImapClient = $this->ImapClient(); + $oImapClient->FolderExamine($sFolderName); + + if ('TEXT' === $sPartId) { + $oFetchResponse = $oImapClient->Fetch([ + FetchType::BODY_PEEK.'['.$sPartId.']', + // An empty section specification refers to the entire message, including the header. + // But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME]. + FetchType::BODY_HEADER_PEEK + ], $iUid, true)[0]; + $sBody = $oFetchResponse->GetFetchValue(FetchType::BODY_HEADER); + } else { + $oFetchResponse = $oImapClient->Fetch([ + FetchType::BODY_PEEK.'['.$sPartId.']', + // An empty section specification refers to the entire message, including the header. + // But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME]. + FetchType::BODY_PEEK.'['.$sPartId.'.MIME]' + ], $iUid, true)[0]; + $sBody = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.'.MIME]'); + } + $sBody .= $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']'); + + $SMIME = $this->SMIME(); + $SMIME->setCertificate($sCertificate); + $SMIME->setPrivateKey($sPrivateKey, $oPassphrase); + $result = $SMIME->decrypt($sBody); + if ($result) { + $result = ['data' => $result]; + if (\str_contains($result['data'], 'multipart/signed') + || \preg_match('/smime-type=["\']?signed-data/', $result['data']) + ) { + $signed = $SMIME->verify($result['data'], null, true); + $result['signed'] = [ + 'success' => !empty($signed['success']) + ]; + if (!empty($signed['body'])) { + $result['data'] = $signed['body']; + } + } + } + + return $this->DefaultResponse($result ?: false); + } + + public function DoSMimeVerifyMessage() : array + { + $sBody = $this->GetActionParam('bodyPart', ''); + $sPartId = $this->GetActionParam('partId', ''); + $bDetached = !empty($this->GetActionParam('detached', 0)); + if (!$sBody && $sPartId) { + $iUid = (int) $this->GetActionParam('uid', 0); +// $sMicAlg = $this->GetActionParam('micAlg', ''); + $this->initMailClientConnection(); + $oImapClient = $this->ImapClient(); + $oImapClient->FolderExamine($this->GetActionParam('folder', '')); + $sBody = $oImapClient->FetchMessagePart($iUid, $sPartId); + } + + $result = $this->SMIME()->verify($sBody, null, !$bDetached); + + // Import the certificates automatically + $sBody = $this->GetActionParam('sigPart', ''); + $sPartId = $this->GetActionParam('sigPartId', '') ?: $sPartId; + if (!$sBody && $sPartId && $oImapClient) { + $sBody = $oImapClient->Fetch( + [FetchType::BODY_PEEK.'['.$sPartId.']'], + $iUid, + true + )[0]->GetFetchValue(FetchType::BODY.'['.$sPartId.']'); + } + if ($sBody) { + $sBody = \trim($sBody); + $certificates = []; + \openssl_pkcs7_read( + "-----BEGIN PKCS7-----\n\n{$sBody}\n-----END PKCS7-----", + $certificates + ) || $this->logWrite("openssl_pkcs7_read: " . \openssl_error_string(), \LOG_ERR, 'OpenSSL'); + foreach ($certificates as $certificate) { + $this->SMIME()->storeCertificate($certificate); + } + } + + return $this->DefaultResponse($result); + } + + public function DoSMimeImportCertificate() : array + { + return $this->DefaultResponse( + $this->SMIME()->storeCertificate( + $this->GetActionParam('pem', '') + ) + ); + } + + public function DoSMimeImportCertificatesFromMessage() : array + { +/* + $sBody = $this->GetActionParam('sigPart', ''); + if (!$sBody) { + $sPartId = $this->GetActionParam('sigPartId', '') ?: $this->GetActionParam('partId', ''); + $this->initMailClientConnection(); + $oImapClient = $this->ImapClient(); + $oImapClient->FolderExamine($this->GetActionParam('folder', '')); + $sBody = $oImapClient->Fetch([ + FetchType::BODY_PEEK.'['.$sPartId.']' + ], (int) $this->GetActionParam('uid', 0), true)[0] + ->GetFetchValue(FetchType::BODY.'['.$sPartId.']'); + } + $sBody = \trim($sBody); + $certificates = []; + \openssl_pkcs7_read( + "-----BEGIN PKCS7-----\n\n{$sBody}\n-----END PKCS7-----", + $certificates + ); + + foreach ($certificates as $certificate) { + $this->SMIME()->storeCertificate($certificate); + } + + return $this->DefaultResponse($certificates); +*/ + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Themes.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Themes.php new file mode 100644 index 0000000000..9d70fbbbd4 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Themes.php @@ -0,0 +1,230 @@ +Config()->Get('webmail', 'theme', 'Default'); + if (!$bAdmin + && ($oAccount = $this->getAccountFromToken(false)) + && $this->GetCapa(Capa::THEMES) + && ($oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount))) { + $sTheme = (string) $oSettingsLocal->GetConf('Theme', $sTheme); + } + $sTheme = $this->ValidateTheme($sTheme); + } + return $sTheme; + } + + /** + * @staticvar array $aCache + */ + public function GetThemes(): array + { + static $aCache = array(); + if ($aCache) { + return $aCache; + } + + $bClear = false; + $bDefault = false; + $aCache = array(); + $sDir = APP_VERSION_ROOT_PATH . 'themes'; + if (\is_dir($sDir)) { + $rDirH = \opendir($sDir); + if ($rDirH) { + while (($sFile = \readdir($rDirH)) !== false) { + if ('.' !== $sFile[0] && \is_dir($sDir . '/' . $sFile) + && (\file_exists("{$sDir}/{$sFile}/styles.css") || \file_exists("{$sDir}/{$sFile}/styles.less"))) { + if ('Default' === $sFile) { + $bDefault = true; + } else if ('Clear' === $sFile) { + $bClear = true; + } else { + $aCache[] = $sFile; + } + } + } + closedir($rDirH); + } + } + + $sDir = APP_INDEX_ROOT_PATH . 'themes'; // custom user themes + if (\is_dir($sDir)) { + if ($rDirH = \opendir($sDir)) { + while (($sFile = \readdir($rDirH)) !== false) { + if ('.' !== $sFile[0] && \is_dir($sDir . '/' . $sFile) + && (\file_exists("{$sDir}/{$sFile}/styles.css") || \file_exists("{$sDir}/{$sFile}/styles.less"))) { + $aCache[] = $sFile . '@custom'; + } + } + \closedir($rDirH); + } else { + $this->logWrite("{$sDir} not readable", \LOG_DEBUG, 'Themes'); + } + } + + if (\class_exists('OC', false)) { + $sDir = \OC::$SERVERROOT . '/themes'; // custom user themes + if (\is_dir($sDir) && ($rDirH = \opendir($sDir))) { + while (($sFile = \readdir($rDirH)) !== false) { + if ('.' !== $sFile[0] && \is_dir("{$sDir}/{$sFile}") && \file_exists("{$sDir}/{$sFile}/snappymail/style.css")) { + $aCache[] = $sFile . '@nextcloud'; + } + } + \closedir($rDirH); + } + } + + $aCache = \array_unique($aCache); + \sort($aCache); + + if ($bDefault) { + \array_unshift($aCache, 'Default'); + } + + if ($bClear) { + \array_push($aCache, 'Clear'); + } + + return $aCache; + } + + public function ValidateTheme(string $sTheme): string + { + if (!\in_array($sTheme, $this->GetThemes())) { + $sTheme = $this->Config()->Get('webmail', 'theme', 'Default'); + if (!\in_array($sTheme, $this->GetThemes())) { + $sTheme = 'Default'; + } + } + return $sTheme; + } + + public function compileCss(string $sTheme, bool $bAdmin, bool $bMinified = false) : string + { + $mResult = array(); + $bLess = false; + + if ('@nextcloud' === \substr($sTheme, -10)) { + $sBase = \OC::$WEBROOT . '/'; + $sThemeCSSFile = \OC::$SERVERROOT . '/themes/' . \str_replace('@nextcloud', '/snappymail/style.css', $sTheme); + } else { + $bCustomTheme = '@custom' === \substr($sTheme, -7); + if ($bCustomTheme) { + $sTheme = \substr($sTheme, 0, -7); + $sBase = \RainLoop\Utils::WebPath(); + } else { + $sBase = \RainLoop\Utils::WebVersionPath(); + } + $sBase .= "themes/{$sTheme}/"; + $sThemeCSSFile = ($bCustomTheme ? APP_INDEX_ROOT_PATH : APP_VERSION_ROOT_PATH).'themes/'.$sTheme.'/styles.css'; + if (!\is_file($sThemeCSSFile)) { + $sThemeCSSFile = \str_replace('styles.css', 'styles.less', $sThemeCSSFile); + if (\is_file($sThemeCSSFile)) { + $bLess = true; + $mResult[] = "@base: \"{$sBase}\";"; + $mResult[] = \file_get_contents($sThemeCSSFile); + } + } + } + if (\is_file($sThemeCSSFile)) { + $mResult[] = \file_get_contents($sThemeCSSFile); + } + + $mResult[] = $this->Plugins()->CompileCss($bAdmin, $bLess, $bMinified); + + $mResult = \preg_replace('@(url\(["\']?)(\\./)?([a-z]+[^:a-z])@', + "\$1{$sBase}\$3", + \str_replace('@{base}', $sBase, \implode("\n", $mResult))); + + return $bLess ? (new \LessPHP\lessc())->compile($mResult) : $mResult; +// : \str_replace(';}', '}', \preg_replace('/\\s*([:;{},])\\s*/', '\1', \preg_replace('/\\s+/', ' ', \preg_replace('#/\\*.*?\\*/#s', '', $mResult)))); + } + + public function UploadBackground(?array $aFile, int $iError): array + { + $oAccount = $this->getAccountFromToken(); + + if (!$this->GetCapa(Capa::USER_BACKGROUND)) { + return $this->FalseResponse(); + } + + $sName = ''; + $sHash = ''; + + if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) { + $sMimeType = \SnappyMail\File\MimeType::fromFile($aFile['tmp_name'], $aFile['name']) + ?: \SnappyMail\File\MimeType::fromFilename($aFile['name']) + ?: $aFile['type']; + if (\in_array($sMimeType, array('image/png', 'image/jpg', 'image/jpeg', 'image/webp'))) { + $sSavedName = 'upload-post-' . \md5($aFile['name'] . $aFile['tmp_name']) + . \SnappyMail\File\MimeType::toExtension($sMimeType); + if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) { + $iError = \RainLoop\Enumerations\UploadError::ON_SAVING; + } else { + $rData = $this->FilesProvider()->GetFile($oAccount, $sSavedName); + if (\is_resource($rData)) { + $sData = \stream_get_contents($rData); + if (!empty($sData) && \strlen($sData)) { + $sName = $aFile['name']; + if (empty($sName)) { + $sName = '_'; + } + + if ($this->StorageProvider()->Put($oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'background', + // Used by RawUserBackground() + \RainLoop\Utils::jsonEncode(array( + 'ContentType' => $sMimeType, + 'Raw' => \base64_encode($sData) + )) + )) { + $oSettings = $this->SettingsProvider()->Load($oAccount); + if ($oSettings) { + $sHash = \MailSo\Base\Utils::Sha1Rand($sName . APP_VERSION . APP_SALT); + + $oSettings->SetConf('UserBackgroundName', $sName); + $oSettings->SetConf('UserBackgroundHash', $sHash); + $oSettings->save(); + } + } + } + + unset($sData); + } + + if (\is_resource($rData)) { + \fclose($rData); + } + + unset($rData); + } + + $this->FilesProvider()->Clear($oAccount, $sSavedName); + } else { + $iError = \RainLoop\Enumerations\UploadError::FILE_TYPE; + } + } + + if (UPLOAD_ERR_OK !== $iError) { + $iClientError = 0; + $sError = \RainLoop\Enumerations\UploadError::getUserMessage($iError, $iClientError); + if (!empty($sError)) { + return $this->FalseResponse($iClientError, $sError); + } + } + + return $this->DefaultResponse(!empty($sName) && !empty($sHash) ? array( + 'name' => $sName, + 'hash' => $sHash + ) : false); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php new file mode 100644 index 0000000000..f1a19c172f --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php @@ -0,0 +1,301 @@ +oSuggestionsProvider) { + $this->oSuggestionsProvider = new Suggestions($this->fabrica('suggestions')); + } + + return $this->oSuggestionsProvider; + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoLogin() : array + { + try { + $oAccount = $this->LoginProcess( + \MailSo\Base\Utils::Trim($this->GetActionParam('Email', '')), + new \SnappyMail\SensitiveString($this->GetActionParam('Password', '')) + ); + } catch (\Throwable $oException) { + $this->loginErrorDelay(); + throw $oException; + } + + empty($this->GetActionParam('signMe', 0)) || $this->SetSignMeToken($oAccount); + + $sLanguage = $this->GetActionParam('language', ''); + if ($oAccount && $sLanguage) { + $oSettings = $this->SettingsProvider()->Load($oAccount); + if ($oSettings) { + $sLanguage = $this->ValidateLanguage($sLanguage); + $sCurrentLanguage = $oSettings->GetConf('language', ''); + + if ($sCurrentLanguage !== $sLanguage) { + $oSettings->SetConf('language', $sLanguage); + $oSettings->save(); + } + } + } + + return $this->DefaultResponse($this->AppData(false)); + } + + public function DoLogout() : array + { + $bMain = true; // empty($_COOKIE[self::AUTH_ADDITIONAL_TOKEN_KEY]); + $this->Logout($bMain); + $bMain && $this->ClearSignMeData(); + return $this->TrueResponse(); + } + + public function DoAppDelayStart() : array + { + Utils::UpdateConnectionToken(); + + $bMainCache = false; + $bFilesCache = false; + + $iOneDay1 = 3600 * 23; + $iOneDay2 = 3600 * 25; + + $sTimers = $this->StorageProvider()->Get(null, + \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY, 'Cache/Timers', ''); + + $aTimers = \explode(',', $sTimers); + + $iMainCacheTime = !empty($aTimers[0]) && \is_numeric($aTimers[0]) ? (int) $aTimers[0] : 0; + $iFilesCacheTime = !empty($aTimers[1]) && \is_numeric($aTimers[1]) ? (int) $aTimers[1] : 0; + + if (0 === $iMainCacheTime || $iMainCacheTime + $iOneDay1 < \time()) { + $bMainCache = true; + $iMainCacheTime = \time(); + } + + if (0 === $iFilesCacheTime || $iFilesCacheTime + $iOneDay2 < \time()) { + $bFilesCache = true; + $iFilesCacheTime = \time(); + } + + if ($bMainCache || $bFilesCache) { + if (!$this->StorageProvider()->Put(null, + \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY, 'Cache/Timers', + \implode(',', array($iMainCacheTime, $iFilesCacheTime)))) + { + $bMainCache = $bFilesCache = false; + } + } + + if ($bMainCache) { + $this->logWrite('Cacher GC: Begin'); + $this->Cacher()->GC(48); + $this->logWrite('Cacher GC: End'); + + $this->logWrite('Storage GC: Begin'); + $this->StorageProvider()->GC(); + $this->logWrite('Storage GC: End'); + } else if ($bFilesCache) { + $this->logWrite('Files GC: Begin'); + $this->FilesProvider()->GC(48); + $this->logWrite('Files GC: End'); + } + + return $this->TrueResponse(); + } + + public function DoSettingsUpdate() : array + { + $oAccount = $this->getAccountFromToken(); + + $self = $this; + $oConfig = $this->Config(); + + $oSettings = $this->SettingsProvider()->Load($oAccount); + $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); + + if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)) { + $this->setSettingsFromParams($oSettings, 'language', 'string', function ($sLanguage) use ($self) { + return $self->ValidateLanguage($sLanguage); + }); + } else { +// $oSettings->SetConf('language', $this->ValidateLanguage($oConfig->Get('webmail', 'language', 'en'))); + } + $this->setSettingsFromParams($oSettings, 'hourCycle', 'string'); + + if ($this->GetCapa(Capa::THEMES)) { + $this->setSettingsFromParams($oSettingsLocal, 'Theme', 'string', function ($sTheme) use ($self) { + return $self->ValidateTheme($sTheme); + }); + $this->setSettingsFromParams($oSettings, 'fontSansSerif', 'string'); + $this->setSettingsFromParams($oSettings, 'fontSerif', 'string'); + $this->setSettingsFromParams($oSettings, 'fontMono', 'string'); + } else { +// $oSettingsLocal->SetConf('Theme', $this->ValidateTheme($oConfig->Get('webmail', 'theme', 'Default'))); + } + + $this->setSettingsFromParams($oSettings, 'MessagesPerPage', 'int', function ($iValue) { + return \min(100, \max(10, $iValue)); + }); + + $this->setSettingsFromParams($oSettings, 'Layout', 'int', function ($iValue) { + return (int) (\in_array((int) $iValue, array(\RainLoop\Enumerations\Layout::NO_PREVIEW, + \RainLoop\Enumerations\Layout::SIDE_PREVIEW, \RainLoop\Enumerations\Layout::BOTTOM_PREVIEW)) ? + $iValue : \RainLoop\Enumerations\Layout::SIDE_PREVIEW); + }); + + $this->setSettingsFromParams($oSettings, 'EditorDefaultType', 'string'); + $this->setSettingsFromParams($oSettings, 'editorWysiwyg', 'string'); + $this->setSettingsFromParams($oSettings, 'requestReadReceipt', 'bool'); + $this->setSettingsFromParams($oSettings, 'requestDsn', 'bool'); + $this->setSettingsFromParams($oSettings, 'requireTLS', 'bool'); + $this->setSettingsFromParams($oSettings, 'pgpSign', 'bool'); + $this->setSettingsFromParams($oSettings, 'pgpEncrypt', 'bool'); + $this->setSettingsFromParams($oSettings, 'allowSpellcheck', 'bool'); + + $this->setSettingsFromParams($oSettings, 'ViewHTML', 'bool'); + $this->setSettingsFromParams($oSettings, 'ViewImages', 'string'); + $this->setSettingsFromParams($oSettings, 'ViewImagesWhitelist', 'string'); + $this->setSettingsFromParams($oSettings, 'RemoveColors', 'bool'); + $this->setSettingsFromParams($oSettings, 'AllowStyles', 'bool'); + $this->setSettingsFromParams($oSettings, 'ListInlineAttachments', 'bool'); + $this->setSettingsFromParams($oSettings, 'CollapseBlockquotes', 'bool'); + $this->setSettingsFromParams($oSettings, 'MaxBlockquotesLevel', 'int'); + $this->setSettingsFromParams($oSettings, 'simpleAttachmentsList', 'bool'); + $this->setSettingsFromParams($oSettings, 'listGrouped', 'bool'); + $this->setSettingsFromParams($oSettings, 'ContactsAutosave', 'bool'); + $this->setSettingsFromParams($oSettings, 'DesktopNotifications', 'bool'); + $this->setSettingsFromParams($oSettings, 'SoundNotification', 'bool'); + $this->setSettingsFromParams($oSettings, 'NotificationSound', 'string'); + $this->setSettingsFromParams($oSettings, 'UseCheckboxesInList', 'bool'); + $this->setSettingsFromParams($oSettings, 'AllowDraftAutosave', 'bool'); + $this->setSettingsFromParams($oSettings, 'AutoLogout', 'int'); + $this->setSettingsFromParams($oSettings, 'keyPassForget', 'int'); + $this->setSettingsFromParams($oSettings, 'messageNewWindow', 'bool'); + $this->setSettingsFromParams($oSettings, 'messageReadAuto', 'bool'); + $this->setSettingsFromParams($oSettings, 'MessageReadDelay', 'int'); + $this->setSettingsFromParams($oSettings, 'MsgDefaultAction', 'int'); + $this->setSettingsFromParams($oSettings, 'showNextMessage', 'bool'); + $this->setSettingsFromParams($oSettings, 'markdown', 'bool'); + + $this->setSettingsFromParams($oSettings, 'Resizer4Width', 'int'); + $this->setSettingsFromParams($oSettings, 'Resizer5Width', 'int'); + $this->setSettingsFromParams($oSettings, 'Resizer5Height', 'int'); + + $this->setSettingsFromParams($oSettingsLocal, 'defaultSort', 'string'); + $this->setSettingsFromParams($oSettingsLocal, 'UseThreads', 'bool'); + $this->setSettingsFromParams($oSettingsLocal, 'threadAlgorithm', 'string'); + $this->setSettingsFromParams($oSettingsLocal, 'ReplySameFolder', 'bool'); + $this->setSettingsFromParams($oSettingsLocal, 'HideUnsubscribed', 'bool'); + $this->setSettingsFromParams($oSettingsLocal, 'HideDeleted', 'bool'); + $this->setSettingsFromParams($oSettingsLocal, 'UnhideKolabFolders', 'bool'); + $this->setSettingsFromParams($oSettingsLocal, 'ShowUnreadCount', 'bool'); + $this->setSettingsFromParams($oSettingsLocal, 'CheckMailInterval', 'int'); + + return $this->DefaultResponse($oSettings->save() && $oSettingsLocal->save()); + } + + public function DoQuota() : array + { + $oAccount = $this->initMailClientConnection(); + try + { + return $this->DefaultResponse($this->ImapClient()->QuotaRoot() ?: [0, 0, 0, 0]); + } + catch (\Throwable $oException) + { + throw new ClientException(Notifications::MailServerError, $oException); + } + } + + public function DoSuggestions() : array + { + $oAccount = $this->getAccountFromToken(); + + $sQuery = \trim($this->GetActionParam('Query', '')); + $iLimit = (int) $this->Config()->Get('contacts', 'suggestions_limit', 20); + + $this->Plugins()->RunHook('json.suggestions-input-parameters', array(&$sQuery, &$iLimit, $oAccount)); + + $aResult = array(); + + if ($oSuggestionsProvider = $this->SuggestionsProvider()) { + $aResult = $oSuggestionsProvider->Process($oAccount, $sQuery, $iLimit); + } + + return $this->DefaultResponse($aResult); + } + + public function DoClearUserBackground() : array + { + if (!$this->GetCapa(Capa::USER_BACKGROUND)) { + return $this->FalseResponse(); + } + + $oAccount = $this->getAccountFromToken(); + $oSettings = $this->SettingsProvider()->Load($oAccount); + if ($oAccount && $oSettings) { + $this->StorageProvider()->Clear($oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'background' + ); + + $oSettings->SetConf('UserBackgroundName', ''); + $oSettings->SetConf('UserBackgroundHash', ''); + } + + return $this->DefaultResponse($oAccount && $oSettings ? $oSettings->save() : false); + } + + private function setSettingsFromParams(\RainLoop\Settings $oSettings, string $sConfigName, string $sType = 'string', ?callable $cCallback = null) : void + { + if ($this->HasActionParam($sConfigName)) { + $sValue = $this->GetActionParam($sConfigName, ''); + switch ($sType) + { + default: + case 'string': + $sValue = (string) $sValue; + if ($cCallback) { + $sValue = $cCallback($sValue); + } + $oSettings->SetConf($sConfigName, (string) $sValue); + break; + + case 'int': + $iValue = (int) $sValue; + if ($cCallback) { + $sValue = $cCallback($iValue); + } + $oSettings->SetConf($sConfigName, $iValue); + break; + + case 'bool': + $oSettings->SetConf($sConfigName, !empty($sValue) && 'false' !== $sValue); + break; + } + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php new file mode 100644 index 0000000000..be26a590ec --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php @@ -0,0 +1,471 @@ +DefaultResponse( + $this->getMainAccountFromToken()->resealCryptKey( + new SensitiveString($this->GetActionParam('passphrase', '')) + ) + ); + } + + /** + * @throws \RainLoop\Exceptions\ClientException + */ + protected function resolveLoginCredentials(string $sEmail, SensitiveString $oPassword): array + { + $sEmail = \SnappyMail\IDN::emailToAscii(\MailSo\Base\Utils::Trim($sEmail)); + + $sNewEmail = $sEmail; + $this->Plugins()->RunHook('login.credentials.step-1', array(&$sNewEmail)); + if ($sNewEmail) { + $sEmail = $sNewEmail; + } + + $oDomain = null; + $oDomainProvider = $this->DomainProvider(); + + // When email address is missing the domain, try to add it + if (!\str_contains($sEmail, '@')) { + $this->logWrite("The email address '{$sEmail}' is incomplete", \LOG_INFO, 'LOGIN'); + if ($this->Config()->Get('login', 'determine_user_domain', false)) { + $sUserHost = \SnappyMail\IDN::toAscii($this->Http()->GetHost(false, true)); + $this->logWrite("Determined user domain: {$sUserHost}", \LOG_INFO, 'LOGIN'); + + // Determine without wildcard + $aDomainParts = \explode('.', $sUserHost); + $iLimit = \min(\count($aDomainParts), 14); + while (0 < $iLimit--) { + $sDomain = \implode('.', $aDomainParts); + $oDomain = $oDomainProvider->Load($sDomain, false); + if ($oDomain) { + $sEmail .= '@' . $sDomain; + $this->logWrite("Check '{$sDomain}': OK", \LOG_INFO, 'LOGIN'); + break; + } else { + $this->logWrite("Check '{$sDomain}': NO", \LOG_INFO, 'LOGIN'); + } + \array_shift($aDomainParts); + } + + // Else determine with wildcard + if (!$oDomain) { + $oDomain = $oDomainProvider->Load($sUserHost, true); + if ($oDomain) { + $sEmail .= '@' . $sUserHost; + $this->logWrite("Check '{$sUserHost}' with wildcard: OK", \LOG_INFO, 'LOGIN'); + } else { + $this->logWrite("Check '{$sUserHost}' with wildcard: NO", \LOG_INFO, 'LOGIN'); + } + } + + if (!$oDomain) { + $this->logWrite("Domain '{$sUserHost}' was not determined!", \LOG_INFO, 'LOGIN'); + } + } + + // Else try default domain + if (!$oDomain) { + $sDefDomain = \trim($this->Config()->Get('login', 'default_domain', '')); + if (\strlen($sDefDomain)) { + if ('HTTP_HOST' === $sDefDomain || 'SERVER_NAME' === $sDefDomain) { + $sDefDomain = \preg_replace('/:[0-9]+$/D', '', $_SERVER[$sDefDomain]); + } else if ('gethostname' === $sDefDomain) { + $sDefDomain = \gethostname(); + } + $sEmail .= '@' . $sDefDomain; + $this->logWrite("Default domain '{$sDefDomain}' will be used.", \LOG_INFO, 'LOGIN'); + } else { + $this->logWrite('Default domain not configured.', \LOG_INFO, 'LOGIN'); + } + } + } + + $sNewEmail = $sEmail; + $sPassword = $oPassword->getValue(); + $this->Plugins()->RunHook('login.credentials.step-2', array(&$sNewEmail, &$sPassword)); + $this->logMask($sPassword); + if ($sNewEmail) { + $sEmail = $sNewEmail; + } + + $sImapUser = $sEmail; + $sSmtpUser = $sEmail; + if (\str_contains($sEmail, '@') + && ($oDomain || ($oDomain = $oDomainProvider->Load(\MailSo\Base\Utils::getEmailAddressDomain($sEmail), true))) + ) { + $sEmail = $oDomain->ImapSettings()->fixUsername($sEmail, false); + $sImapUser = $oDomain->ImapSettings()->fixUsername($sImapUser); + $sSmtpUser = $oDomain->SmtpSettings()->fixUsername($sSmtpUser); + } + + $sNewEmail = $sEmail; + $sNewImapUser = $sImapUser; + $sNewSmtpUser = $sSmtpUser; + $this->Plugins()->RunHook('login.credentials', array(&$sNewEmail, &$sNewImapUser, &$sPassword, &$sNewSmtpUser)); + + $oPassword->setValue($sPassword); + + return [ + 'email' => $sNewEmail ?: $sEmail, + 'domain' => $oDomain, + 'imapUser' => $sNewImapUser ?: $sImapUser, + 'smtpUser' => $sNewSmtpUser ?: $sSmtpUser, + 'pass' => $oPassword + ]; + } + + /** + * @throws \RainLoop\Exceptions\ClientException + */ + public function LoginProcess(string $sEmail, SensitiveString $oPassword, bool $bMainAccount = true): Account + { + $aCredentials = $this->resolveLoginCredentials($sEmail, $oPassword); + + if (!\str_contains($aCredentials['email'], '@') || !\strlen($oPassword)) { + throw new ClientException(Notifications::InvalidInputArgument); + } + + $oDomain = $this->DomainProvider()->getByEmailAddress($aCredentials['email']); + + $oAccount = null; + try { + $oAccount = $bMainAccount ? new MainAccount : new AdditionalAccount; + $oAccount->setCredentials( + $aCredentials['domain'], + $aCredentials['email'], + $aCredentials['imapUser'], + $oPassword, + $aCredentials['smtpUser'] +// ,new SensitiveString($oPassword) + ); + $this->Plugins()->RunHook('filter.account', array($oAccount)); + if (!$oAccount) { + throw new ClientException(Notifications::AccountFilterError); + } + } catch (\Throwable $oException) { + $this->LoggerAuthHelper($oAccount, $sEmail); + throw $oException; + } + + $this->imapConnect($oAccount, true); + if ($bMainAccount) { + $this->StorageProvider()->Put($oAccount, StorageType::SESSION, Utils::GetSessionToken(), 'true'); + + // Must be here due to bug #1241 + $this->SetMainAuthAccount($oAccount); + $this->Plugins()->RunHook('login.success', array($oAccount)); + + $this->SetAuthToken($oAccount); + $this->SetAdditionalAuthToken(null); + } + + return $oAccount; + } + + public function switchAccount(string $sEmail) : bool + { + $this->Http()->ServerNoCache(); + $oMainAccount = $this->getMainAccountFromToken(false); + if ($sEmail && $oMainAccount && $this->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) { + $oAccount = null; + if ($oMainAccount->Email() !== $sEmail) { + $sEmail = \SnappyMail\IDN::emailToAscii($sEmail); + $aAccounts = $this->GetAccounts($oMainAccount); + if (!isset($aAccounts[$sEmail])) { + throw new ClientException(Notifications::AccountDoesNotExist); + } + try { + $oAccount = AdditionalAccount::NewInstanceFromTokenArray( + $this, $aAccounts[$sEmail], true + ); + } catch (\Throwable $e) { + throw new ClientException(Notifications::AccountSwitchFailed, $e); + } + if (!$oAccount) { + throw new ClientException(Notifications::AccountSwitchFailed); + } + + // Test the login + $oImapClient = new \MailSo\Imap\ImapClient; + $oImapClient->SetLogger($this->Logger()); + $this->imapConnect($oAccount, false, $oImapClient); + } + $this->SetAdditionalAuthToken($oAccount); + return true; + } + return false; + } + + /** + * Returns RainLoop\Model\AdditionalAccount when it exists, + * else returns RainLoop\Model\MainAccount when it exists, + * else null + * + * @throws \RainLoop\Exceptions\ClientException + */ + public function getAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Account + { + $this->getMainAccountFromToken($bThrowExceptionOnFalse); + + if (false === $this->oAdditionalAuthAccount && isset($_COOKIE[self::AUTH_ADDITIONAL_TOKEN_KEY])) { + $aData = Cookies::getSecure(self::AUTH_ADDITIONAL_TOKEN_KEY); + if ($aData) { + $this->oAdditionalAuthAccount = AdditionalAccount::NewInstanceFromTokenArray( + $this, + $aData, + $bThrowExceptionOnFalse + ); + } + if (!$this->oAdditionalAuthAccount) { + $this->oAdditionalAuthAccount = null; + Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY); + } + } + + return $this->oAdditionalAuthAccount ?: $this->oMainAuthAccount; + } + + /** + * @throws \RainLoop\Exceptions\ClientException + */ + public function getMainAccountFromToken(bool $bThrowExceptionOnFalse = true): ?MainAccount + { + if (false === $this->oMainAuthAccount) try { + $this->oMainAuthAccount = null; + + $aData = Cookies::getSecure(self::AUTH_SPEC_TOKEN_KEY); + if ($aData) { + /** + * Server side control/kickout of logged in sessions + * https://github.com/the-djmaze/snappymail/issues/151 + */ + $sToken = Utils::GetSessionToken(false); + if (!$sToken) { +// \MailSo\Base\Http::StatusHeader(401); + if (isset($_COOKIE[Utils::SESSION_TOKEN])) { + \SnappyMail\Log::notice('TOKENS', 'SESSION_TOKEN invalid'); + } else { + \SnappyMail\Log::notice('TOKENS', 'SESSION_TOKEN not set'); + } + } else { + $oMainAuthAccount = MainAccount::NewInstanceFromTokenArray( + $this, + $aData, + $bThrowExceptionOnFalse + ); + if ($oMainAuthAccount) { + $sTokenValue = $this->StorageProvider()->Get($oMainAuthAccount, StorageType::SESSION, $sToken); + if ($sTokenValue) { + $this->oMainAuthAccount = $oMainAuthAccount; + } else { + $this->StorageProvider()->Clear($oMainAuthAccount, StorageType::SESSION, $sToken); + \SnappyMail\Log::notice('TOKENS', 'SESSION_TOKEN value invalid: ' . \get_debug_type($sTokenValue)); + } + } else { + \SnappyMail\Log::notice('TOKENS', 'AUTH_SPEC_TOKEN_KEY invalid'); + } + } + if (!$this->oMainAuthAccount) { + Cookies::clear(Utils::SESSION_TOKEN); +// \MailSo\Base\Http::StatusHeader(401); + $this->Logout(true); +// $sAdditionalMessage = $this->StaticI18N('SESSION_GONE'); + throw new ClientException(Notifications::InvalidToken, null, 'Session gone'); + } + } else { + $oAccount = $this->GetAccountFromSignMeToken(); + if ($oAccount) { + $this->StorageProvider()->Put( + $oAccount, + StorageType::SESSION, + Utils::GetSessionToken(), + 'true' + ); + $this->SetAuthToken($oAccount); + } + } + + if (!$this->oMainAuthAccount) { + throw new ClientException(Notifications::InvalidToken, null, 'Account undefined'); + } + } catch (\Throwable $e) { + if ($bThrowExceptionOnFalse) { + throw $e; + } + } + + return $this->oMainAuthAccount; + } + + public function SetMainAuthAccount(MainAccount $oAccount): void + { + $this->oAdditionalAuthAccount = false; + $this->oMainAuthAccount = $oAccount; + } + + public function SetAuthToken(MainAccount $oAccount): void + { + $this->SetMainAuthAccount($oAccount); + Cookies::setSecure(self::AUTH_SPEC_TOKEN_KEY, $oAccount); + } + + public function SetAdditionalAuthToken(?AdditionalAccount $oAccount): void + { + $this->oAdditionalAuthAccount = $oAccount ?: false; + Cookies::setSecure(self::AUTH_ADDITIONAL_TOKEN_KEY, $oAccount); + } + + /** + * SignMe methods used for the "remember me" cookie + */ + + private static function GetSignMeToken(): ?array + { + $sSignMeToken = Cookies::get(self::AUTH_SIGN_ME_TOKEN_KEY); + if ($sSignMeToken) { + \SnappyMail\Log::notice(self::AUTH_SIGN_ME_TOKEN_KEY, 'decrypt'); + $aResult = \SnappyMail\Crypt::DecryptUrlSafe($sSignMeToken, 'signme'); + if (isset($aResult['e'], $aResult['u']) && \SnappyMail\UUID::isValid($aResult['u'])) { + if (!isset($aResult['c'])) { + $aResult['c'] = \array_key_last($aResult); + $aResult['d'] = \end($aResult); + } + return $aResult; + } + \SnappyMail\Log::notice(self::AUTH_SIGN_ME_TOKEN_KEY, 'invalid'); + Cookies::clear(self::AUTH_SIGN_ME_TOKEN_KEY); + } + return null; + } + + public function SetSignMeToken(MainAccount $oAccount): void + { + $this->ClearSignMeData(); + $uuid = \SnappyMail\UUID::generate(); + $data = \SnappyMail\Crypt::Encrypt($oAccount, 'signme'); + Cookies::set( + self::AUTH_SIGN_ME_TOKEN_KEY, + \SnappyMail\Crypt::EncryptUrlSafe([ + 'e' => $oAccount->Email(), + 'u' => $uuid, + 'c' => $data[0], + 'd' => \base64_encode($data[1]) + ], 'signme'), + \time() + 3600 * 24 * 30 // 30 days + ); + $this->StorageProvider()->Put($oAccount, StorageType::SIGN_ME, $uuid, $data[2]); + } + + public function GetAccountFromSignMeToken(): ?MainAccount + { + $aTokenData = static::GetSignMeToken(); + if ($aTokenData) { + try + { + $sAuthToken = $this->StorageProvider()->Get( + $aTokenData['e'], + StorageType::SIGN_ME, + $aTokenData['u'] + ); + if (!$sAuthToken) { + throw new \RuntimeException("server token not found for {$aTokenData['e']}/.sign_me/{$aTokenData['u']}"); + } + $aAccountHash = \SnappyMail\Crypt::Decrypt([ + $aTokenData['c'], + \base64_decode($aTokenData['d']), + $sAuthToken + ], 'signme'); + if (!\is_array($aAccountHash)) { + throw new \RuntimeException('token decrypt failed'); + } + $oAccount = MainAccount::NewInstanceFromTokenArray($this, $aAccountHash); + if (!$oAccount) { + throw new \RuntimeException('token has no account'); + } + $this->imapConnect($oAccount); + // Update lifetime + $this->SetSignMeToken($oAccount); + return $oAccount; + } + catch (\Throwable $oException) + { + \SnappyMail\Log::warning(self::AUTH_SIGN_ME_TOKEN_KEY, $oException->getMessage()); + $this->ClearSignMeData(); + } + } + return null; + } + + protected function ClearSignMeData() : void + { + $aTokenData = static::GetSignMeToken(); + if ($aTokenData) { + $this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']); + } + Cookies::clear(self::AUTH_SIGN_ME_TOKEN_KEY); + } + + /** + * Logout methods + */ + + public function Logout(bool $bMain) : void + { +// Cookies::clear(Utils::SESSION_TOKEN); + Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY); + $bMain && Cookies::clear(self::AUTH_SPEC_TOKEN_KEY); + // TODO: kill SignMe data to prevent automatic login? + } + + /** + * @throws \RainLoop\Exceptions\ClientException + */ + protected function imapConnect(Account $oAccount, bool $bAuthLog = false, ?\MailSo\Imap\ImapClient $oImapClient = null): void + { + try { + if (!$oImapClient) { + $oImapClient = $this->ImapClient(); + } + $oAccount->ImapConnectAndLogin($this->Plugins(), $oImapClient, $this->Config()); + } catch (ClientException $oException) { + throw $oException; + } catch (\MailSo\Net\Exceptions\ConnectionException $oException) { + throw new ClientException(Notifications::ConnectionError, $oException); + } catch (\MailSo\Imap\Exceptions\LoginBadCredentialsException $oException) { + if ($bAuthLog) { + $this->LoggerAuthHelper($oAccount); + } + + if ($this->Config()->Get('imap', 'show_login_alert', true)) { + throw new ClientException(Notifications::AuthError, $oException, $oException->getAlertFromStatus()); + } else { + throw new ClientException(Notifications::AuthError, $oException); + } + } catch (\Throwable $oException) { + throw new ClientException(Notifications::AuthError, $oException); + } + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php b/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php new file mode 100644 index 0000000000..54a550d35d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php @@ -0,0 +1,474 @@ +Cacher()->GC(0); + if (\is_dir(APP_PRIVATE_DATA . 'cache')) { + \MailSo\Base\Utils::RecRmDir(APP_PRIVATE_DATA.'cache'); + } + return $this->TrueResponse(); + } + + public function DoAdminSettingsGet() : array + { + $aConfig = $this->Config()->jsonSerialize(); + unset($aConfig['version']); + $aConfig['logs']['time_zone'][1] = ''; + $aConfig['logs']['time_zone'][2] = \DateTimeZone::listIdentifiers(); + $aConfig['login']['sign_me_auto'][2] = ['DefaultOff','DefaultOn','Unused']; + $aConfig['defaults']['view_images'][2] = ['ask','match','always']; + return $this->DefaultResponse($aConfig); + } + + public function DoAdminSettingsSet() : array + { + $oConfig = $this->Config(); + foreach ($this->GetActionParam('config', []) as $sSection => $aItems) { + foreach ($aItems as $sKey => $mValue) { + $oConfig->Set($sSection, $sKey, $mValue); + } + } + return $this->DefaultResponse($oConfig->Save()); + } + + public function DoAdminSettingsUpdate() : array + { +// sleep(3); +// return $this->DefaultResponse(false); + + $this->IsAdminLoggined(); + + $oConfig = $this->Config(); + + $self = $this; + + $this->setConfigFromParams($oConfig, 'language', 'webmail', 'language', 'string', function ($sLanguage) use ($self) { + return $self->ValidateLanguage($sLanguage, '', false); + }); + + $this->setConfigFromParams($oConfig, 'languageAdmin', 'admin_panel', 'language', 'string', function ($sLanguage) use ($self) { + return $self->ValidateLanguage($sLanguage, '', true); + }); + + $this->setConfigFromParams($oConfig, 'Theme', 'webmail', 'theme', 'string', function ($sTheme) use ($self) { + return $self->ValidateTheme($sTheme); + }); + + $this->setConfigFromParams($oConfig, 'proxyExternalImages', 'labs', 'use_local_proxy_for_external_images', 'bool'); + $this->setConfigFromParams($oConfig, 'autoVerifySignatures', 'security', 'auto_verify_signatures', 'bool'); + + $this->setConfigFromParams($oConfig, 'allowLanguagesOnSettings', 'webmail', 'allow_languages_on_settings', 'bool'); + $this->setConfigFromParams($oConfig, 'allowLanguagesOnLogin', 'login', 'allow_languages_on_login', 'bool'); + $this->setConfigFromParams($oConfig, 'attachmentLimit', 'webmail', 'attachment_size_limit', 'int'); + + $this->setConfigFromParams($oConfig, 'loginDefaultDomain', 'login', 'default_domain', 'string'); + + $this->setConfigFromParams($oConfig, 'contactsEnable', 'contacts', 'enable', 'bool'); + $this->setConfigFromParams($oConfig, 'contactsSync', 'contacts', 'allow_sync', 'bool'); + $this->setConfigFromParams($oConfig, 'contactsPdoDsn', 'contacts', 'pdo_dsn', 'string'); + $this->setConfigFromParams($oConfig, 'contactsPdoUser', 'contacts', 'pdo_user', 'string'); + $this->setConfigFromParams($oConfig, 'contactsPdoPassword', 'contacts', 'pdo_password', 'dummy'); + $this->setConfigFromParams($oConfig, 'contactsMySQLSSLCA', 'contacts', 'mysql_ssl_ca', 'string'); + $this->setConfigFromParams($oConfig, 'contactsMySQLSSLVerify', 'contacts', 'mysql_ssl_verify', 'bool'); + $this->setConfigFromParams($oConfig, 'contactsMySQLSSLCiphers', 'contacts', 'mysql_ssl_ciphers', 'string'); + $this->setConfigFromParams($oConfig, 'contactsSQLiteGlobal', 'contacts', 'sqlite_global', 'bool'); + $this->setConfigFromParams($oConfig, 'contactsSuggestionsLimit', 'contacts', 'suggestions_limit', 'int'); + $this->setConfigFromParams($oConfig, 'contactsPdoType', 'contacts', 'type', 'string', function ($sType) use ($self) { + return Providers\AddressBook\PdoAddressBook::validPdoType($sType); + }); + + $this->setConfigFromParams($oConfig, 'CapaAdditionalAccounts', 'webmail', 'allow_additional_accounts', 'bool'); + $this->setConfigFromParams($oConfig, 'CapaIdentities', 'webmail', 'allow_additional_identities', 'bool'); + $this->setConfigFromParams($oConfig, 'CapaAttachmentThumbnails', 'interface', 'show_attachment_thumbnail', 'bool'); + $this->setConfigFromParams($oConfig, 'CapaThemes', 'webmail', 'allow_themes', 'bool'); + $this->setConfigFromParams($oConfig, 'CapaUserBackground', 'webmail', 'allow_user_background', 'bool'); + $this->setConfigFromParams($oConfig, 'capaGnuPG', 'security', 'gnupg', 'bool'); + $this->setConfigFromParams($oConfig, 'capaOpenPGP', 'security', 'openpgp', 'bool'); + + $this->setConfigFromParams($oConfig, 'determineUserLanguage', 'login', 'determine_user_language', 'bool'); + $this->setConfigFromParams($oConfig, 'determineUserDomain', 'login', 'determine_user_domain', 'bool'); + + $this->setConfigFromParams($oConfig, 'title', 'webmail', 'title', 'string'); + $this->setConfigFromParams($oConfig, 'loadingDescription', 'webmail', 'loading_description', 'string'); + $this->setConfigFromParams($oConfig, 'faviconUrl', 'webmail', 'favicon_url', 'string'); + + $this->setConfigFromParams($oConfig, 'pluginsEnable', 'plugins', 'enable', 'bool'); + + return $this->DefaultResponse($oConfig->Save()); + } + + /** + * @throws \MailSo\RuntimeException + */ + public function DoAdminLogin() : array + { + $sLogin = trim($this->GetActionParam('Login', '')); + $oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('Password', '')); + + $totp = $this->Config()->Get('security', 'admin_totp', ''); + + // \explode(':',`getent shadow root`)[1]; + if (!\strlen($sLogin) || !\strlen($oPassword) || + !$this->Config()->Get('security', 'allow_admin_panel', true) || + $sLogin !== $this->Config()->Get('security', 'admin_login', '') || + !$this->Config()->ValidatePassword($oPassword) + || ($totp && !\SnappyMail\TOTP::Verify($totp, $this->GetActionParam('TOTP', '')))) + { + $this->LoggerAuthHelper(null, $sLogin, true); + $this->loginErrorDelay(); + throw new ClientException(Notifications::AuthError); + } + + $sToken = $this->setAdminAuthToken(); + + return $this->DefaultResponse($sToken ? $this->AppData(true) : false); + } + + public function DoAdminLogout() : array + { + $sAdminKey = $this->getAdminAuthKey(); + if ($sAdminKey) { + $this->Cacher(null, true)->Delete(KeyPathHelper::SessionAdminKey($sAdminKey)); + } + \SnappyMail\Cookies::clear(static::$AUTH_ADMIN_TOKEN_KEY); + return $this->TrueResponse(); + } + + public function DoAdminContactsTest() : array + { + $this->IsAdminLoggined(); + + $oConfig = $this->Config(); + $this->setConfigFromParams($oConfig, 'PdoDsn', 'contacts', 'pdo_dsn', 'string'); + $this->setConfigFromParams($oConfig, 'PdoUser', 'contacts', 'pdo_user', 'string'); + $this->setConfigFromParams($oConfig, 'PdoPassword', 'contacts', 'pdo_password', 'dummy'); + $this->setConfigFromParams($oConfig, 'PdoType', 'contacts', 'type', 'string', function ($sType) { + return Providers\AddressBook\PdoAddressBook::validPdoType($sType); + }); + $this->setConfigFromParams($oConfig, 'MySQLSSLCA', 'contacts', 'mysql_ssl_ca', 'string'); + $this->setConfigFromParams($oConfig, 'MySQLSSLVerify', 'contacts', 'mysql_ssl_verify', 'bool'); + $this->setConfigFromParams($oConfig, 'MySQLSSLCiphers', 'contacts', 'mysql_ssl_ciphers', 'string'); + $this->setConfigFromParams($oConfig, 'SQLiteGlobal', 'contacts', 'sqlite_global', 'bool'); + + $sTestMessage = ''; + try { + $AddressBook = new Providers\AddressBook(new Providers\AddressBook\PdoAddressBook()); + $AddressBook->SetLogger($this->oLogger); + $sTestMessage = $AddressBook->Test(); + } catch (\Throwable $e) { + \SnappyMail\LOG::error('AddressBook', $e->getMessage()."\n".$e->getTraceAsString()); + $sTestMessage = $e->getMessage(); + } + + return $this->DefaultResponse(array( + 'Result' => '' === $sTestMessage, + 'Message' => \MailSo\Base\Utils::Utf8Clear($sTestMessage) + )); + } + + public function DoAdminPasswordUpdate() : array + { + $this->IsAdminLoggined(); + + $bResult = false; + $oConfig = $this->Config(); + + $oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('Password', '')); + + $oNewPassword = new \SnappyMail\SensitiveString($this->GetActionParam('newPassword', '')); + + $passfile = APP_PRIVATE_DATA.'admin_password.txt'; + + if ($oConfig->ValidatePassword($oPassword)) { + $sLogin = \trim($this->GetActionParam('Login', '')); + if (\strlen($sLogin)) { + $oConfig->Set('security', 'admin_login', $sLogin); + } + + $oConfig->Set('security', 'admin_totp', $this->GetActionParam('TOTP', '')); + + if (\strlen($oNewPassword)) { + $oConfig->SetPassword($oNewPassword); + if (\is_file($passfile) && \trim(\file_get_contents($passfile)) !== (string) $oNewPassword) { + \unlink($passfile); + } + } + + $bResult = $oConfig->Save(); + } + + return $this->DefaultResponse($bResult + ? array('Weak' => \is_file($passfile)) + : false); + } + + // /?admin/Backup + public function DoAdminBackup() : void + { + try { + $this->IsAdminLoggined(); + $file = \SnappyMail\Upgrade::backup(); + \header('Content-Type: application/gzip'); + \MailSo\Base\Http::setContentDisposition('attachment', ['filename' => \basename($file)]); + \header('Content-Transfer-Encoding: binary'); + \header('Content-Length: ' . \filesize($file)); + $fp = \fopen($file, 'rb'); + \fpassthru($fp); + \unlink($file); + } catch (\Throwable $e) { + if (102 == $e->getCode()) { + \MailSo\Base\Http::StatusHeader(403); + } + echo $e->getMessage(); + } + exit; + } + + public function DoAdminInfo() : array + { + $this->IsAdminLoggined(); + + $info = \SnappyMail\Repository::getLatestCoreInfo(); + + $sVersion = empty($info->version) ? '' : $info->version; + + $bShowWarning = false; + if (!empty($info->warnings) && !SNAPPYMAIL_DEV) { + foreach ($info->warnings as $sWarningVersion) { + $sWarningVersion = \trim($sWarningVersion); + + if (\version_compare(APP_VERSION, $sWarningVersion, '<') + && \version_compare($sVersion, $sWarningVersion, '>=')) + { + $bShowWarning = true; + break; + } + } + } + + $aWarnings = []; + if (!\version_compare(APP_VERSION, '2.0', '>')) { + $aWarnings[] = APP_VERSION; + } + if (!\is_writable(\dirname(APP_VERSION_ROOT_PATH))) { + $aWarnings[] = 'Can not write into: ' . \dirname(APP_VERSION_ROOT_PATH); + } + if (!\is_writable(APP_INDEX_ROOT_PATH . 'index.php')) { + $aWarnings[] = 'Can not edit: ' . APP_INDEX_ROOT_PATH . 'index.php'; + } + + $aResult = [ + 'system' => [ + 'load' => \is_callable('sys_getloadavg') ? \sys_getloadavg() : null + ], + 'core' => [ + 'updatable' => \SnappyMail\Repository::canUpdateCore(), + 'warning' => $bShowWarning, + 'version' => $sVersion, + 'versionCompare' => \version_compare(APP_VERSION, $sVersion), + 'warnings' => $aWarnings + ], + 'php' => [ + [ + 'name' => 'PHP ' . PHP_VERSION, + 'loaded' => true, + 'version' => PHP_VERSION + ], + [ + 'name' => 'PHP 64bit', + 'loaded' => PHP_INT_SIZE == 8, + 'version' => PHP_INT_SIZE + ] + ] + ]; + + foreach (['APCu', 'cURL','Fileinfo','iconv','intl','LDAP','redis','Tidy','uuid','Zip'] as $name) { + $aResult['php'][] = [ + 'name' => $name, + 'loaded' => \extension_loaded(\strtolower($name)), + 'version' => \phpversion($name) + ]; + } + + $aResult['php'][] = [ + 'name' => 'Phar', + 'loaded' => \class_exists('PharData'), + 'version' => \phpversion('phar') + ]; + + $aResult['php'][] = [ + 'name' => 'Contacts database:', + 'loaded' => \extension_loaded('pdo_mysql') || \extension_loaded('pdo_pgsql') || \extension_loaded('pdo_sqlite'), + 'version' => 0 + ]; + foreach (['pdo_mysql','pdo_pgsql','pdo_sqlite'] as $name) { + $aResult['php'][] = [ + 'name' => "- {$name}", + 'loaded' => \extension_loaded(\strtolower($name)), + 'version' => \phpversion($name) + ]; + } + + $aResult['php'][] = [ + 'name' => 'Crypt:', + 'loaded' => true, + 'version' => 0 + ]; + foreach (['Sodium','OpenSSL','XXTEA','GnuPG'] as $name) { + $aResult['php'][] = [ + 'name' => '- ' . (('OpenSSL' === $name && \defined('OPENSSL_VERSION_TEXT')) ? OPENSSL_VERSION_TEXT : $name), + 'loaded' => \extension_loaded(\strtolower($name)), + 'version' => \phpversion($name) + ]; + } + + $aResult['php'][] = [ + 'name' => 'Image processing:', + 'loaded' => \extension_loaded('gd') || \extension_loaded('gmagick') || \extension_loaded('imagick'), + 'version' => 0 + ]; + foreach (['GD','Gmagick','Imagick'] as $name) { + $aResult['php'][] = [ + 'name' => "- {$name}", + 'loaded' => \extension_loaded(\strtolower($name)), + 'version' => \phpversion($name) + ]; + } + + return $this->DefaultResponse($aResult); + } + + public function DoAdminUpgradeCore() : array + { + \header('Connection: close'); + return $this->DefaultResponse(\SnappyMail\Upgrade::core()); + } + + public function DoAdminQRCode() : array + { + $user = (string) $this->GetActionParam('username', ''); + $secret = (string) $this->GetActionParam('TOTP', ''); + $issuer = \rawurlencode(API::Config()->Get('webmail', 'title', 'SnappyMail')); + $QR = \SnappyMail\QRCode::getMinimumQRCode( + "otpauth://totp/{$issuer}:{$user}?secret={$secret}&issuer={$issuer}", +// "otpauth://totp/{$user}?secret={$secret}", + \SnappyMail\QRCode::ERROR_CORRECT_LEVEL_M + ); + return $this->DefaultResponse($QR->__toString()); + } + + private function setAdminAuthToken() : string + { + $sRand = \MailSo\Base\Utils::Sha1Rand(); + if (!$this->Cacher(null, true)->Set(KeyPathHelper::SessionAdminKey($sRand), \time())) { + throw new \RuntimeException('Failed to store admin token'); + } + $sToken = Utils::EncodeKeyValuesQ(array('token', $sRand)); + if (!$sToken) { + throw new \RuntimeException('Failed to encode admin token'); + } + \SnappyMail\Cookies::set(static::$AUTH_ADMIN_TOKEN_KEY, $sToken); + return $sToken; + } + + private function setConfigFromParams(Config\Application $oConfig, string $sParamName, string $sConfigSector, string $sConfigName, string $sType = 'string', ?callable $mStringCallback = null): void + { + if ($this->HasActionParam($sParamName)) { + $sValue = $this->GetActionParam($sParamName, ''); + switch ($sType) { + default: + case 'string': + $sValue = (string)$sValue; + if ($mStringCallback && is_callable($mStringCallback)) { + $sValue = $mStringCallback($sValue); + } + + $oConfig->Set($sConfigSector, $sConfigName, $sValue); + break; + + case 'dummy': + $sValue = (string) $this->GetActionParam($sParamName, static::APP_DUMMY); + if (static::APP_DUMMY !== $sValue) { + $oConfig->Set($sConfigSector, $sConfigName, $sValue); + } + break; + + case 'int': + $iValue = (int)$sValue; + $oConfig->Set($sConfigSector, $sConfigName, $iValue); + break; + + case 'bool': + $oConfig->Set($sConfigSector, $sConfigName, !empty($sValue) && 'false' !== $sValue); + break; + } + } + } + + public static function AdminAppData(Actions $oActions, array &$aResult): void + { + $oConfig = $oActions->Config(); + $aResult['Admin'] = [ + 'host' => '' !== $oConfig->Get('admin_panel', 'host', ''), + 'path' => $oConfig->Get('admin_panel', 'key', '') ?: 'admin', + 'allowed' => (bool)$oConfig->Get('security', 'allow_admin_panel', true) + ]; + + $aResult['Auth'] = $oActions->IsAdminLoggined(false); + if ($aResult['Auth']) { + $aResult['adminLogin'] = (string)$oConfig->Get('security', 'admin_login', ''); + $aResult['adminTOTP'] = (string)$oConfig->Get('security', 'admin_totp', ''); + $aResult['pluginsEnable'] = (bool)$oConfig->Get('plugins', 'enable', false); + + $aResult['loginDefaultDomain'] = $oConfig->Get('login', 'default_domain', ''); + $aResult['determineUserLanguage'] = (bool)$oConfig->Get('login', 'determine_user_language', true); + $aResult['determineUserDomain'] = (bool)$oConfig->Get('login', 'determine_user_domain', false); + + $aResult['supportedPdoDrivers'] = \RainLoop\Pdo\Base::getAvailableDrivers(); + + $aResult['contactsEnable'] = (bool)$oConfig->Get('contacts', 'enable', false); + $aResult['contactsSync'] = (bool)$oConfig->Get('contacts', 'allow_sync', false); + $aResult['contactsPdoType'] = Providers\AddressBook\PdoAddressBook::validPdoType($oConfig->Get('contacts', 'type', 'sqlite')); + $aResult['contactsPdoDsn'] = (string)$oConfig->Get('contacts', 'pdo_dsn', ''); + $aResult['contactsPdoType'] = (string)$oConfig->Get('contacts', 'type', ''); + $aResult['contactsPdoUser'] = (string)$oConfig->Get('contacts', 'pdo_user', ''); + $aResult['contactsPdoPassword'] = static::APP_DUMMY; + $aResult['contactsMySQLSSLCA'] = (string) $oConfig->Get('contacts', 'mysql_ssl_ca', ''); + $aResult['contactsMySQLSSLVerify'] = !!$oConfig->Get('contacts', 'mysql_ssl_verify', true); + $aResult['contactsMySQLSSLCiphers'] = (string) $oConfig->Get('contacts', 'mysql_ssl_ciphers', ''); + $aResult['contactsSQLiteGlobal'] = !!$oConfig->Get('contacts', 'sqlite_global', \is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite')); + $aResult['contactsSuggestionsLimit'] = (int)$oConfig->Get('contacts', 'suggestions_limit', 20); + + $aResult['faviconUrl'] = $oConfig->Get('webmail', 'favicon_url', ''); + + $aResult['weakPassword'] = \is_file(APP_PRIVATE_DATA.'admin_password.txt'); + + $aResult['Admin']['language'] = $oActions->ValidateLanguage($oConfig->Get('admin_panel', 'language', 'en'), '', true); + $aResult['Admin']['languages'] = \SnappyMail\L10n::getLanguages(true); + $aResult['Admin']['clientLanguage'] = $oActions->ValidateLanguage($oActions->detectClientLanguage(true), '', true, true); + + $gnupg = \SnappyMail\PGP\GnuPG::getInstance(''); + $aResult['gnupg'] = $gnupg ? $gnupg->getEngineInfo()['version'] : null; + } else { + $passfile = APP_PRIVATE_DATA.'admin_password.txt'; + $sPassword = $oConfig->Get('security', 'admin_password', ''); + if (!$sPassword) { + $sPassword = \substr(\base64_encode(\random_bytes(16)), 0, 12); + Utils::saveFile($passfile, $sPassword . "\n"); +// \chmod($passfile, 0600); + $oConfig->SetPassword(new \SnappyMail\SensitiveString($sPassword)); + $oConfig->Save(); + } + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php new file mode 100644 index 0000000000..f73d0017e5 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php @@ -0,0 +1,134 @@ +Load()) { + \usleep(10000); + $oConfig->Load(); + } +// \ini_set('display_errors', '0'); + if ($oConfig->Get('debug', 'enable', false)) { + \error_reporting(E_ALL); +// \ini_set('display_errors', '1'); + \ini_set('log_errors', '1'); + } + \MailSo\Config::$BoundaryPrefix = \trim($oConfig->Get('labs', 'boundary_prefix', '')); + } + return $oConfig; + } + + public static function getCSP(?string $sScriptNonce = null) : \SnappyMail\HTTP\CSP + { + $oConfig = static::Config(); + $CSP = new \SnappyMail\HTTP\CSP(\trim($oConfig->Get('security', 'content_security_policy', ''))); + $CSP->report = $oConfig->Get('security', 'csp_report', false); + $CSP->report_only = $oConfig->Get('debug', 'enable', false); // || SNAPPYMAIL_DEV + + // Allow https: due to remote images in e-mails or use proxy + if (!$oConfig->Get('labs', 'use_local_proxy_for_external_images', '')) { + $CSP->add('img-src', 'https:'); + $CSP->add('img-src', 'http:'); + } + if ($sScriptNonce) { + $CSP->add('script-src', "'nonce-{$sScriptNonce}'"); + } + + static::Actions()->Plugins()->RunHook('main.content-security-policy', array($CSP)); + + return $CSP; + } + + public static function Logger() : \MailSo\Log\Logger + { + static $oLogger = null; + if (!$oLogger) { + $oConfig = static::Config(); + $oLogger = new \MailSo\Log\Logger(true); + $oLogger->SetShowSecrets(!$oConfig->Get('logs', 'hide_passwords', true)); + if ($oConfig->Get('debug', 'enable', false)) { + $oLogger->SetLevel(\LOG_DEBUG); + } else if ($oConfig->Get('logs', 'enable', false)) { + $oLogger->SetLevel(\max(3, \RainLoop\Api::Config()->Get('logs', 'level', \LOG_WARNING))); + } + } + return $oLogger; + } + + public static function Version() : string + { + return APP_VERSION; + } + + public static function CreateUserSsoHash(string $sEmail, + #[\SensitiveParameter] + string $sPassword, + array $aAdditionalOptions = array(), bool $bUseTimeout = true + ) : ?string + { + $sSsoHash = \MailSo\Base\Utils::Sha1Rand(\sha1($sPassword.$sEmail)); + + return static::Actions()->Cacher()->Set( + KeyPathHelper::SsoCacherKey($sSsoHash), + \SnappyMail\Crypt::EncryptToJSON(array( + 'Email' => $sEmail, + 'Password' => $sPassword, + 'AdditionalOptions' => $aAdditionalOptions, + 'Time' => $bUseTimeout ? \time() : 0 + ), $sSsoHash) + ) ? $sSsoHash : null; + } + + public static function ClearUserSsoHash(string $sSsoHash) : bool + { + return static::Actions()->Cacher()->Delete(KeyPathHelper::SsoCacherKey($sSsoHash)); + } + + public static function ClearUserData(string $sEmail) : bool + { + if (\strlen($sEmail)) { + $sEmail = \SnappyMail\IDN::emailToAscii($sEmail); + + $oStorageProvider = static::Actions()->StorageProvider(); + if ($oStorageProvider && $oStorageProvider->IsActive()) { + $oStorageProvider->DeleteStorage($sEmail); + } + + $oConfig = static::Config(); + $sqlite_global = $oConfig->Get('contacts', 'sqlite_global', false); + if ('sqlite' != $oConfig->Get('contacts', 'type', '') || \is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite')) { + $oConfig->Set('contacts', 'sqlite_global', true); + $oAddressBookProvider = static::Actions()->AddressBookProvider(); + $oAddressBookProvider && $oAddressBookProvider->DeleteAllContacts($sEmail); + $oConfig->Set('contacts', 'sqlite_global', !!$sqlite_global); + } + + return true; + } + + return false; + } + + public static function LogoutCurrentLogginedUser() : bool + { + static::Actions()->Logout(true); + return true; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/AbstractConfig.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/AbstractConfig.php new file mode 100644 index 0000000000..2a87abf368 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/AbstractConfig.php @@ -0,0 +1,321 @@ +sFile = \APP_PRIVATE_DATA.'configs/'.\trim($sFileName); + + $sAdditionalFileName = \trim($sAdditionalFileName); + if ($sAdditionalFileName) { + $sAdditionalFileName = \APP_PRIVATE_DATA.'configs/'.$sAdditionalFileName; + if (\file_exists($this->sAdditionalFile)) { + $this->sAdditionalFile = $sAdditionalFileName; + } + } + + $this->sFileHeader = $sFileHeader; + $this->aData = $this->defaultValues(); + + $this->bUseApcCache = defined('APP_USE_APCU_CACHE') && APP_USE_APCU_CACHE && + \MailSo\Base\Utils::FunctionsCallable(array('apcu_fetch', 'apcu_store')); + } + + public function offsetExists($offset) : bool + { + } + + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + $offset = \explode('/', $offset, 2); + $this->Get($offset[0], $offset[1]); + } + + public function offsetSet($offset, $value) : void + { + $offset = \explode('/', $offset, 2); + $this->Set($offset[0], $offset[1], $value); + } + + public function offsetUnset($offset) : void + { + } + + protected abstract function defaultValues() : array; + + public function IsInited() : bool + { + return \is_array($this->aData) && \count($this->aData); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->aData;; + } + + /** + * @param mixed $mDefault = null + * + * @return mixed + */ + public function Get(string $sSection, string $sName, $mDefault = null) + { + return isset($this->aData[$sSection][$sName][0]) + ? $this->aData[$sSection][$sName][0] + : $mDefault; + } + + /** + * @param mixed $mParamValue + */ + public function Set(string $sSectionKey, string $sParamKey, $mParamValue) : void + { + if (isset($this->aData[$sSectionKey][$sParamKey][0])) { + if (!\is_scalar($mParamValue)) { + $mParamValue = null; + } + switch (\get_debug_type($this->aData[$sSectionKey][$sParamKey][0])) + { + case 'bool': + $this->aData[$sSectionKey][$sParamKey][0] = (bool) $mParamValue; + break; + case 'float': + $this->aData[$sSectionKey][$sParamKey][0] = (float) $mParamValue; + break; + case 'int': + $this->aData[$sSectionKey][$sParamKey][0] = (int) $mParamValue; + break; + case 'string': + default: + $this->aData[$sSectionKey][$sParamKey][0] = (string) $mParamValue; + break; + } + } else if ('custom' === $sSectionKey) { + $this->aData[$sSectionKey][$sParamKey] = array((string) $mParamValue); + } + } + + public function getDecrypted(string $sSection, string $sName, $mDefault = null) + { + // $salt = \basename($this->sFile) not possible due to RainLoop\Plugins\Property + if (!empty($this->aData[$sSection][$sName][0])) try { + return \SnappyMail\Crypt::DecryptFromJSON($this->aData[$sSection][$sName][0], \APP_SALT); + } catch (\Throwable $e) { + } + return $mDefault; + } + + public function setEncrypted(string $sSectionKey, string $sParamKey, $mParamValue) : void + { + // $salt = \basename($this->sFile) not possible due to RainLoop\Plugins\Property + $mParamValue = \SnappyMail\Crypt::EncryptToJSON($mParamValue, \APP_SALT); + $this->Set($sSectionKey, $sParamKey, $mParamValue); + } + + private function cacheKey() : string + { + return 'config:'.\sha1($this->sFile).':'.\sha1($this->sAdditionalFile).':'; + } + + private function loadDataFromCache() : bool + { + if ($this->bUseApcCache) { + $iMTime = \filemtime($this->sFile); + $iMTime = \is_int($iMTime) && 0 < $iMTime ? $iMTime : 0; + + $iATime = $this->sAdditionalFile ? \filemtime($this->sAdditionalFile) : 0; + $iATime = \is_int($iATime) && 0 < $iATime ? $iATime : 0; + + if (0 < $iMTime) { + $sKey = $this->cacheKey(); + + $sTimeHash = \apcu_fetch($sKey.'time'); + if ($sTimeHash && $sTimeHash === \md5($iMTime.'/'.$iATime)) { + $aFetchData = \apcu_fetch($sKey.'data'); + if (\is_array($aFetchData)) { + $this->aData = $aFetchData; + return true; + } + } + } + } + + return false; + } + + private function storeDataToCache() : bool + { + if ($this->bUseApcCache) { + $iMTime = \filemtime($this->sFile); + $iMTime = \is_int($iMTime) && 0 < $iMTime ? $iMTime : 0; + + $iATime = $this->sAdditionalFile ? \filemtime($this->sAdditionalFile) : 0; + $iATime = \is_int($iATime) && 0 < $iATime ? $iATime : 0; + + if (0 < $iMTime) { + $sKey = $this->cacheKey(); + + \apcu_store($sKey.'time', \md5($iMTime.'/'.$iATime)); + \apcu_store($sKey.'data', $this->aData); + + return true; + } + } + + return false; + } + + private function clearCache() : bool + { + if ($this->bUseApcCache) { + $sKey = $this->cacheKey(); + + \apcu_delete($sKey.'time'); + \apcu_delete($sKey.'data'); + + return true; + } + + return false; + } + + public function Load() : bool + { + $sFile = $this->sFile; + if (!\file_exists($sFile) && \str_ends_with($sFile, '.json')) { + $sFile = \str_replace('.json', '.ini', $sFile); + } + if (\file_exists($sFile) && \is_readable($sFile)) { + if ($this->loadDataFromCache()) { + return true; + } + + if (\str_ends_with($sFile, '.json')) { + $aData = \json_decode(\file_get_contents($sFile), true); + } else { + $aData = \parse_ini_file($sFile, true); + } + if ($aData && \count($aData)) { + foreach ($aData as $sSectionKey => $aSectionValue) { + if (\is_array($aSectionValue)) { + foreach ($aSectionValue as $sParamKey => $mParamValue) { + $this->Set($sSectionKey, $sParamKey, $mParamValue); + } + } + } + + unset($aData); + + if (\file_exists($this->sAdditionalFile) && \is_readable($this->sAdditionalFile)) { + if (\str_ends_with($this->sAdditionalFile, '.json')) { + $aData = \json_decode(\file_get_contents($this->sAdditionalFile), true); + } else { + $aData = \parse_ini_file($this->sAdditionalFile, true); + } + if ($aData && \count($aData)) { + foreach ($aData as $sSectionKey => $aSectionValue) { + if (\is_array($aSectionValue)) { + foreach ($aSectionValue as $sParamKey => $mParamValue) { + $this->Set($sSectionKey, $sParamKey, $mParamValue); + } + } + } + } + + unset($aData); + } + + $this->storeDataToCache(); + + return true; + } + } + + return $this->Save(); + } + + public function Save() : bool + { + if (\file_exists($this->sFile) && !\is_writable($this->sFile)) { + return false; + } + + if (\str_ends_with($this->sFile, '.json')) { + $this->clearCache(); + \RainLoop\Utils::saveFile($this->sFile, \json_encode($this, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); + // Remove old .ini file + $sFile = \str_replace('.json', '.ini', $this->sFile); + \file_exists($sFile) && \unlink($sFile); + return true; + } + + $sNewLine = "\n"; + + $aResultLines = array(); + + foreach ($this->aData as $sSectionKey => $aSectionValue) { + if (\is_array($aSectionValue)) { + $aResultLines[] = ''; + $aResultLines[] = '['.$sSectionKey.']'; + $bFirst = true; + + foreach ($aSectionValue as $sParamKey => $mParamValue) { + if (\is_array($mParamValue)) { + // Add comments + if (!empty($mParamValue[1])) { + if (!$bFirst) { + $aResultLines[] = ''; + } + foreach (\explode("\n", \str_replace("\r", '', $mParamValue[1])) as $sLine) { + $aResultLines[] = '; ' . $sLine; + } + } + + // Add value + $bFirst = false; + + $sValue = '""'; + switch (\get_debug_type($mParamValue[0])) + { + case 'bool': + $sValue = $mParamValue[0] ? 'On' : 'Off'; + break; + case 'float': + case 'int': + $sValue = $mParamValue[0]; + break; + case 'string': + default: + $sValue = '"'.\addcslashes($mParamValue[0], '\\"').'"'; + break; + } + + $aResultLines[] = $sParamKey.' = '.$sValue; + } + } + } + } + + $this->clearCache(); + + \RainLoop\Utils::saveFile($this->sFile, + (\strlen($this->sFileHeader) ? $this->sFileHeader : ''). + $sNewLine.\implode($sNewLine, $aResultLines)); + + return true; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php new file mode 100644 index 0000000000..0621cff843 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php @@ -0,0 +1,463 @@ +Get('security', 'max_sys_getloadavg', 0)); + if ($max && \is_callable('sys_getloadavg')) { + $load = \sys_getloadavg(); + if ($load && $load[0] > $max) { + \header('HTTP/1.1 503 Service Unavailable', true, 503); + \header('Retry-After: 120'); + exit("Mailserver too busy ({$load[0]}). Please try again later."); + } + } + + $this->aReplaceEnv = null; + if ((isset($_ENV) && \is_array($_ENV) && \count($_ENV)) || + (isset($_SERVER) && \is_array($_SERVER) && \count($_SERVER))) + { + $sEnvNames = $this->Get('labs', 'replace_env_in_configuration', ''); + if (\strlen($sEnvNames)) { + $this->aReplaceEnv = \explode(',', $sEnvNames); + if (\is_array($this->aReplaceEnv)) { + $this->aReplaceEnv = \array_map('trim', $this->aReplaceEnv); + $this->aReplaceEnv = \array_map('strtolower', $this->aReplaceEnv); + } + } + } + + if (!\is_array($this->aReplaceEnv) || !\count($this->aReplaceEnv)) { + $this->aReplaceEnv = null; + } + + $sCipher = $this->Get('security', 'encrypt_cipher', ''); + if (!$sCipher || !\SnappyMail\Crypt::cipherSupported($sCipher)) { + $sCipher && \SnappyMail\Log::warning('Crypt', "OpenSSL no support for cipher '{$sCipher}'"); + $aCiphers = \SnappyMail\Crypt::listCiphers(); + $sCipher2 = $aCiphers ? $aCiphers[\array_rand($aCiphers)] : ''; + if ($sCipher !== $sCipher2) { + $this->Set('security', 'encrypt_cipher', $sCipher2); + $this->Save(); + } + } + + if (!\in_array($this->Get('logs', 'time_zone', ''), \DateTimeZone::listIdentifiers())) { + $this->Set('logs', 'time_zone', 'UTC'); + } + + return $bResult; + } + + /** + * @param mixed $mDefault = null + * + * @return mixed + */ + public function Get(string $sSection, string $sName, $mDefault = null) + { + $mResult = parent::Get($sSection, $sName, $mDefault); + if ($this->aReplaceEnv && \is_string($mResult)) { + $sKey = \strtolower($sSection.'.'.$sName); + if (\in_array($sKey, $this->aReplaceEnv) && false !== strpos($mResult, '$')) { + $mResult = \preg_replace_callback('/\$([^\s]+)/', function($aMatch) { + if (!empty($aMatch[0]) && !empty($aMatch[1])) { + if (!empty($_ENV[$aMatch[1]])) { + return $_ENV[$aMatch[1]]; + } + if (!empty($_SERVER[$aMatch[1]])) { + return $_SERVER[$aMatch[1]]; + } + return $aMatch[0]; + } + return ''; + }, $mResult); + } + } + return $mResult; + } + + public function Set(string $sSectionKey, string $sParamKey, $mParamValue) : void + { + // Workarounds for the changed application structure + if ('webmail' === $sSectionKey) { + if ('language_admin' === $sParamKey) { + $sSectionKey = 'admin_panel'; + $sParamKey = 'language'; + } + } + if ('security' === $sSectionKey) { + if (\str_starts_with($sParamKey, 'admin_panel_')) { + $sSectionKey = 'admin_panel'; + $sParamKey = \str_replace('admin_panel_', '', $sParamKey); + } + } + if ('labs' === $sSectionKey) { + if (\str_starts_with($sParamKey, 'imap_')) { + $sSectionKey = 'imap'; + $sParamKey = \str_replace('imap_', '', $sParamKey); + } + if (\str_starts_with($sParamKey, 'use_app_debug_')) { + $sSectionKey = 'debug'; + $sParamKey = \str_replace('use_app_debug_js', 'javascript', $sParamKey); + $sParamKey = \str_replace('use_app_debug_css', 'css', $sParamKey); + } + if ('cache_system_data' === $sParamKey) { + $sSectionKey = 'cache'; + $sParamKey = 'system_data'; + } + if ('force_https' === $sParamKey) { + $sSectionKey = 'security'; + } + if ('check_new_messages' === $sParamKey) { + $sSectionKey = 'imap'; + $sParamKey = 'fetch_new_messages'; + } + if ('login_fault_delay' === $sParamKey) { + $sSectionKey = 'login'; + $sParamKey = 'fault_delay'; + } + if ('log_ajax_response_write_limit' === $sParamKey) { + $sSectionKey = 'logs'; + $sParamKey = 'json_response_write_limit'; + } + } + if ('language' === $sParamKey) { + $mParamValue = \SnappyMail\L10n::validLanguage($mParamValue, 'admin_panel' === $sSectionKey) ?: 'en'; + } + parent::Set($sSectionKey, $sParamKey, $mParamValue); + } + + public function SetPassword(\SnappyMail\SensitiveString $oPassword) : void + { + $this->Set('security', 'admin_password', \password_hash($oPassword, PASSWORD_DEFAULT)); + } + + public function ValidatePassword(\SnappyMail\SensitiveString $oPassword) : bool + { + return \strlen($oPassword) && \password_verify($oPassword, $this->Get('security', 'admin_password', '')); + } + + public function Save() : bool + { + $this->Set('version', 'current', APP_VERSION); + $this->Set('version', 'saved', \gmdate('r')); + + return parent::Save(); + } + + protected function defaultValues() : array + { + $value = \ini_get('upload_max_filesize'); + $upload_max_filesize = \intval($value); + switch (\strtoupper(\substr($value, -1))) { + case 'G': $upload_max_filesize *= 1024; + case 'M': $upload_max_filesize *= 1024; + case 'K': $upload_max_filesize *= 1024; + } + $upload_max_filesize = $upload_max_filesize / 1024 / 1024; + + return array( + + 'webmail' => array( + + 'title' => array('SnappyMail Webmail', 'Text displayed as page title'), + 'loading_description' => array('SnappyMail', 'Text displayed on startup'), + 'favicon_url' => array(''), + 'app_path' => array(''), + + 'theme' => array('Default', 'Theme used by default'), + 'allow_themes' => array(true, 'Allow theme selection on settings screen'), + 'allow_user_background' => array(false), + + 'language' => array('en', 'Language used by default'), + 'allow_languages_on_settings' => array(true, 'Allow language selection on settings screen'), + + 'allow_additional_accounts' => array(true), + 'allow_additional_identities' => array(true), + 'popup_identity' => array(true, 'When identity is not set yet, open identity popup after login'), + + 'messages_per_page' => array(20, 'Number of messages displayed on page by default'), + 'message_read_delay' => array(5, 'Mark message read after N seconds'), + + 'min_refresh_interval' => array(5, 'Minimal check for new messages interval in minutes'), + + 'attachment_size_limit' => array(\min($upload_max_filesize, 25), 'File size limit (MB) for file upload on compose screen +0 for unlimited.'), + + 'compress_output' => array(false, 'brotli or gzip compress the output. +Warning: only enable when server does not do this, else double compression errors occur') + ), + + 'interface' => array( + 'show_attachment_thumbnail' => array(true) + ), + + 'contacts' => array( + 'enable' => array(false, 'Enable contacts'), + 'allow_sync' => array(false), + 'sync_interval' => array(20), + 'type' => array('sqlite'), + 'pdo_dsn' => array('host=127.0.0.1;port=3306;dbname=snappymail'), + 'pdo_user' => array('root'), + 'pdo_password' => array(''), + 'mysql_ssl_ca' => array('', 'PEM format certificate'), + 'mysql_ssl_verify' => array(true), + 'mysql_ssl_ciphers' => array('', 'HIGH'), + 'sqlite_global' => array(\is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite')), + 'suggestions_limit' => array(20) + ), + + 'security' => array( + 'custom_server_signature' => array('SnappyMail'), + 'x_xss_protection_header' => array('1; mode=block'), + + 'gnupg' => array(true), + 'openpgp' => array(true), + 'auto_verify_signatures' => array(false), + + 'allow_admin_panel' => array(true, 'Access settings'), + 'admin_login' => array('admin', 'Login and password for web admin panel'), + 'admin_password' => array(''), + 'admin_totp' => array(''), + 'insecure_cryptkey' => array(false, 'Use email address instead of login password for encrypting sensitive data (like account passwords)'), + + 'force_https' => array(false), + 'hide_x_mailer_header' => array(true), + 'max_sys_getloadavg' => array(0.0, 'https://en.m.wikipedia.org/wiki/Load_(computing)'), + 'content_security_policy' => array('', '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'), + 'csp_report' => array(false, 'Report CSP errors to PHP and/or SnappyMail Log'), + 'encrypt_cipher' => array('aes-256-cbc-hmac-sha1', 'A valid cipher method from https://php.net/openssl_get_cipher_methods'), + 'cookie_samesite' => array('Strict', 'Strict, Lax or None'), + 'secfetch_allow' => array('', '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"') + ), + + 'admin_panel' => array( +/* + 'enabled' => array(true, 'Access settings'), + 'login' => array('admin', 'Login and password for web admin panel'), + 'password' => array(''), + 'totp' => array(''), +*/ + 'host' => array(''), + 'key' => array('admin'), + 'allow_update' => array(false), + 'language' => array('en', 'Admin Panel interface language'), + ), + + 'ssl' => array( + 'verify_certificate' => array(true, 'Require verification of SSL certificate used.'), + 'allow_self_signed' => array(false, 'Allow self-signed certificates. Requires verify_certificate.'), + 'security_level' => array(1, 'https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_security_level.html'), + 'cafile' => array('', 'Location of Certificate Authority file on local filesystem (/etc/ssl/certs/ca-certificates.crt)'), + 'capath' => array('', 'capath must be a correctly hashed certificate directory. (/etc/ssl/certs/)'), + 'local_cert' => array('', 'Location of client certificate file (pem format with private key) on local filesystem'), + 'disable_compression' => array(true, 'This can help mitigate the CRIME attack vector.') + ), + + 'capa' => array( + 'dangerous_actions' => array(true, 'Allow clear folder and delete messages without moving to trash'), + 'attachments_actions' => array(true, 'Allow download attachments as Zip (and optionally others)') + ), + + 'login' => array( + + 'default_domain' => array('', + '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. +'), + + 'allow_languages_on_login' => array(true, + 'Allow language selection on webmail login screen'), + + 'determine_user_language' => array(true, 'Detect language from browser header `Accept-Language`'), + 'determine_user_domain' => array(false, 'Like default_domain but then HTTP_HOST/SERVER_NAME without www.'), + + 'sign_me_auto' => array(\RainLoop\Enumerations\SignMeType::DefaultOff, + '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'), + + 'fault_delay' => array(5, 'When login fails, wait N seconds before responding'), + ), + + 'plugins' => array( + 'enable' => array(false, 'Enable plugin support'), + 'enabled_list' => array('', 'Comma-separated list of enabled plugins'), + ), + + 'defaults' => array( + 'view_editor_type' => array('Html', 'Editor mode used by default (Plain, Html)'), + 'view_layout' => array(1, 'layout: 0 - no preview, 1 - side preview, 2 - bottom preview'), + 'view_use_checkboxes' => array(true), + 'view_show_next_message' => array(true, 'Show next message when (re)move current message'), + 'autologout' => array(30), + 'view_html' => array(true), + 'show_images' => array(false), + 'view_images' => array('ask', 'View external images: + "ask" - always ask + "match" - whitelist or ask + "always" - show always'), + 'contacts_autosave' => array(true), + 'mail_list_grouped' => array(false), + 'mail_use_threads' => array(false), + 'allow_draft_autosave' => array(true), + 'mail_reply_same_folder' => array(false), + 'msg_default_action' => array(1, '1 - reply, 2 - reply all'), + 'collapse_blockquotes' => array(true), + 'allow_spellcheck' => array(false) + ), + + 'logs' => array( + + 'enable' => array(false, 'Enable logging'), + + 'path' => array('', 'Path where log files will be stored'), + + 'level' => array(4, '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'), + + 'hide_passwords' => array(true, 'Required for development purposes only. +Disabling this option is not recommended.'), + + 'time_zone' => array('UTC'), + + 'filename' => array('log-{date:Y-m-d}.txt', + '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 = "stderr"'), + + 'auth_logging' => array(false, 'Enable auth logging in a separate file (for fail2ban)'), + 'auth_logging_filename' => array('fail2ban/auth-{date:Y-m-d}.txt'), + 'auth_logging_format' => array('[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}'), + 'auth_syslog' => array(false, 'Enable auth logging to syslog for fail2ban'), + + 'json_response_write_limit' => array(300), + ), + + 'debug' => array( + 'enable' => array(false, 'Special option required for development purposes'), + // use_app_debug_js + 'javascript' => array(false), + // use_app_debug_css + 'css' => array(false) + ), + + 'cache' => array( + 'enable' => array(true, + 'The section controls caching of the entire application. + +Enables caching in the system'), + + 'path' => array('', 'Path where cache files will be stored'), + + 'index' => array('v1', 'Additional caching key. If changed, cache is purged'), + + 'fast_cache_index' => array('v1', 'Additional caching key. If changed, fast cache is purged'), + + 'http' => array(true, 'Browser-level cache. If enabled, caching is maintainted without using files'), + 'http_expires' => array(3600, 'Browser-level cache time (seconds, Expires header)'), + + 'server_uids' => array(true, 'Caching message UIDs when searching and sorting (threading)'), + + 'system_data' => array(true) + ), + + 'imap' => array( + 'use_force_selection' => array(false), + 'use_expunge_all_on_delete' => array(false), + 'message_list_fast_simple_search' => array(true), + 'message_list_permanent_filter' => array(''), + 'message_all_headers' => array(false), + 'show_login_alert' => array(true), + 'fetch_new_messages' => array(true), + ), + + 'labs' => array( + 'allow_message_append' => array(false, 'Allow drag & drop .eml files from system into messages list'), + 'smtp_show_server_errors' => array(false), + 'mail_func_clear_headers' => array(true, 'PHP mail() remove To and Subject headers'), + 'mail_func_additional_parameters' => array(false, 'PHP mail() set -f emailaddress'), + 'folders_spec_limit' => array(50), + 'curl_proxy' => array(''), + 'curl_proxy_auth' => array(''), + 'custom_login_link' => array(''), + 'custom_logout_link' => array(''), + 'http_client_ip_check_proxy' => array(false), + 'use_local_proxy_for_external_images' => array(true), + 'image_exif_auto_rotate' => array(false), + 'cookie_default_path' => array(''), + 'cookie_default_secure' => array(false), + 'replace_env_in_configuration' => array(''), + 'boundary_prefix' => array(''), + 'dev_email' => array(''), + 'dev_password' => array('') + ), + + 'version' => array( + 'current' => array(''), + 'saved' => array('') + ) + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Plugin.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Plugin.php new file mode 100644 index 0000000000..59f5d845b6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Plugin.php @@ -0,0 +1,53 @@ +DefaultValue(); + $aResultMap[$oProperty->Name()] = array( + \is_array($mDefaultValue) ? '' : $mDefaultValue, + '' + ); + } + } + + if (\count($aResultMap)) { + $this->aMap = array( + 'plugin' => $aResultMap + ); + } + } + +// parent::__construct('plugin-'.$sPluginName.'.ini', '; SnappyMail plugin ('.$sPluginName.')'); + parent::__construct('plugin-'.$sPluginName.'.json'); + } + + protected function defaultValues() : array + { + return $this->aMap; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $aData = []; + foreach (parent::jsonSerialize() as $sSectionKey => $aSectionValue) { + if (\is_array($aSectionValue)) { + $aData[$sSectionKey] = []; + foreach ($aSectionValue as $sParamKey => $mParamValue) { + $aData[$sSectionKey][$sParamKey] = $mParamValue[0]; + } + } + } + return $aData; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/Capa.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/Capa.php new file mode 100644 index 0000000000..5637e97386 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/Capa.php @@ -0,0 +1,18 @@ + 'Filesize exceeds the upload_max_filesize directive in php.ini', + /*2*/\UPLOAD_ERR_FORM_SIZE => 'Filesize exceeds the MAX_FILE_SIZE directive that was specified in the html form', + /*3*/\UPLOAD_ERR_PARTIAL => 'File was only partially uploaded', + /*4*/\UPLOAD_ERR_NO_FILE => 'No file was uploaded', + /*6*/\UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder', + /*7*/\UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk', + /*8*/\UPLOAD_ERR_EXTENSION => 'File upload stopped by extension', + 98 => 'Invalid file type', + 99 => 'Unknown error', + 1001 => 'Filesize exceeds the config setting', + 1002 => 'Error saving file', + 1003 => 'File is empty' + ]; + + public static function getMessage(int $code): string + { + return isset(static::$messages[$code]) ? static::$messages[$code] : ''; + } + + public static function getUserMessage(int $iError, int &$iClientError): string + { + $iClientError = $iError; + switch ($iError) { + case \UPLOAD_ERR_OK: + case \UPLOAD_ERR_PARTIAL: + case \UPLOAD_ERR_NO_FILE: + case static::FILE_TYPE: + case static::EMPTY_FILE: + break; + + case \UPLOAD_ERR_INI_SIZE: + case \UPLOAD_ERR_FORM_SIZE: + case static::CONFIG_SIZE: + return 'File is too big'; + + case \UPLOAD_ERR_NO_TMP_DIR: + case \UPLOAD_ERR_CANT_WRITE: + case \UPLOAD_ERR_EXTENSION: + case static::ON_SAVING: + $iClientError = static::ON_SAVING; + break; + + default: + $iClientError = static::UNKNOWN; + break; + } + + return static::getMessage($iClientError); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Exceptions/ClientException.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Exceptions/ClientException.php new file mode 100644 index 0000000000..6d7d824afa --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Exceptions/ClientException.php @@ -0,0 +1,39 @@ +sAdditionalMessage = $sAdditionalMessage ?: ($oPrevious ? $oPrevious->getMessage() : ''); + } + + public function getAdditionalMessage() : string + { + return $this->sAdditionalMessage; + } + + public function __toString() : string + { + $message = $this->getMessage(); + if ($this->sAdditionalMessage) { + $message .= " ({$this->sAdditionalMessage})"; + } + return "{$message}\r\n{$this->getFile()}#{$this->getLine()}\r\n{$this->getTraceAsString()}"; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/KeyPathHelper.php b/snappymail/v/0.0.0/app/libraries/RainLoop/KeyPathHelper.php new file mode 100644 index 0000000000..db07fc51e7 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/KeyPathHelper.php @@ -0,0 +1,31 @@ +sEmail; + } + + public function Name() : string + { + return $this->sName; + } + + public function ImapUser() : string + { + return $this->sImapUser; + } + + public function ImapPass() : string + { + return $this->oImapPass ? $this->oImapPass->getValue() : ''; + } + + public function SmtpUser() : string + { + return $this->sSmtpUser ?: ($this->oDomain ? $this->oDomain->SmtpSettings()->fixUsername($this->sEmail) : ''); +// return $this->sSmtpUser ?: $this->sEmail ?: $this->sImapUser; + } + + public function Domain() : ?Domain + { + return $this->oDomain; + } + + public function Hash() : string + { + return \sha1(\implode(APP_SALT, [ + $this->sEmail, + $this->sImapUser, +// \json_encode($this->Domain()), +// $this->oImapPass + ])); + } + + public function setImapUser(string $sImapUser) : void + { + $this->sImapUser = $sImapUser; + } + + public function setImapPass(SensitiveString $oPassword) : void + { + $this->oImapPass = $oPassword; + } + + public function setSmtpUser(string $sSmtpUser) : void + { + $this->sSmtpUser = $sSmtpUser; + } + + public function setSmtpPass(SensitiveString $oPassword) : void + { + $this->oSmtpPass = $oPassword; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $result = [ + 'email' => $this->sEmail, + 'login' => $this->sImapUser, + 'pass' => $this->ImapPass(), + 'name' => $this->sName, + 'smtp' => [] + ]; + if ($this->sSmtpUser) { + $result['smtp']['user'] = $this->sSmtpUser; + } + if ($this->oSmtpPass) { + $result['smtp']['pass'] = $this->oSmtpPass->getValue(); + } + return $result; + } + + public function setCredentials( + Domain $oDomain, + string $sEmail, + string $sImapUser, + SensitiveString $oImapPass, + string $sSmtpUser = '', + ?SensitiveString $oSmtpPass = null + ) { + $this->sEmail = $sEmail; + $this->oDomain = $oDomain; + $this->sImapUser = $sImapUser; + $this->oImapPass = $oImapPass; + $this->sSmtpUser = $sSmtpUser; + $this->oSmtpPass = $oSmtpPass; + } + + /** + * Converts old numeric array to new associative array + */ + public static function convertArray(array $aAccount) : array + { + if (isset($aAccount['email'])) { + return $aAccount; + } + if (empty($aAccount[0]) || 'account' != $aAccount[0] || 7 > \count($aAccount)) { + return []; + } + return [ + 'email' => $aAccount[1] ?: '', + 'login' => $aAccount[2] ?: '', + 'pass' => $aAccount[3] ?: '' + ]; + } + + public static function NewInstanceFromTokenArray( + \RainLoop\Actions $oActions, + array $aAccountHash, + bool $bThrowExceptionOnFalse = false): ?self + { + $oAccount = null; + $aAccountHash = static::convertArray($aAccountHash); + try { +/* + if (empty($aAccountHash['email'])) { + throw new ClientException(Notifications::InvalidToken, null, 'TokenArray missing email'); + } + if (empty($aAccountHash['login'])) { + throw new ClientException(Notifications::InvalidToken, null, 'TokenArray missing login'); + } + if (empty($aAccountHash['pass'])) { + throw new ClientException(Notifications::InvalidToken, null, 'TokenArray missing pass'); + } +*/ + if (empty($aAccountHash['email']) || empty($aAccountHash['login']) || empty($aAccountHash['pass'])) { + throw new \RuntimeException("Invalid TokenArray"); + } + $oDomain = $oActions->DomainProvider()->getByEmailAddress($aAccountHash['email']); + if ($oDomain) { +// $aAccountHash['email'] = $oDomain->ImapSettings()->fixUsername($aAccountHash['email'], false); +// $aAccountHash['login'] = $oDomain->ImapSettings()->fixUsername($aAccountHash['login']); + $oAccount = new static; + $oAccount->sEmail = \SnappyMail\IDN::emailToAscii($aAccountHash['email']); +// $oAccount->sImapUser = \SnappyMail\IDN::emailToAscii($aAccountHash['login']); + $oAccount->sImapUser = $aAccountHash['login']; + $oAccount->setImapPass(new SensitiveString($aAccountHash['pass'])); + $oAccount->oDomain = $oDomain; + $oActions->Plugins()->RunHook('filter.account', array($oAccount)); + if (!$oAccount) { + throw new ClientException(Notifications::AccountFilterError); + } + if (isset($aAccountHash['name'])) { + $oAccount->sName = $aAccountHash['name']; + } + // init smtp user/password + if (isset($aAccountHash['smtp']['user'])) { + $oAccount->sSmtpUser = $aAccountHash['smtp']['user']; + } + if (isset($aAccountHash['smtp']['pass'])) { + $oAccount->setSmtpPass(new SensitiveString($aAccountHash['smtp']['pass'])); + } + } + } catch (\Throwable $e) { + \SnappyMail\Log::debug('ACCOUNT', $e->getMessage()); + if ($bThrowExceptionOnFalse) { + throw $e; + } + } + return $oAccount; + } + + public function ImapConnectAndLogin(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Imap\ImapClient $oImapClient, \RainLoop\Config\Application $oConfig) : bool + { + $oSettings = $this->Domain()->ImapSettings(); + $oSettings->timeout = \max($oSettings->timeout, (int) $oConfig->Get('imap', 'timeout', $oSettings->timeout)); + $oSettings->username = $this->ImapUser(); + + $oSettings->expunge_all_on_delete |= !!$oConfig->Get('imap', 'use_expunge_all_on_delete', false); + $oSettings->fast_simple_search = !(!$oSettings->fast_simple_search || !$oConfig->Get('imap', 'message_list_fast_simple_search', true)); + $oSettings->fetch_new_messages = !(!$oSettings->fetch_new_messages || !$oConfig->Get('imap', 'fetch_new_messages', true)); + $oSettings->force_select |= !!$oConfig->Get('imap', 'use_force_selection', false); + $oSettings->message_all_headers |= !!$oConfig->Get('imap', 'message_all_headers', false); + $oSettings->search_filter = $oSettings->search_filter ?: \trim($oConfig->Get('imap', 'message_list_permanent_filter', '')); +// $oSettings->body_text_limit = \min($oSettings->body_text_limit, (int) $oConfig->Get('imap', 'body_text_limit', 50)); +// $oSettings->thread_limit = \min($oSettings->thread_limit, (int) $oConfig->Get('imap', 'large_thread_limit', 50)); + + $oImapClient->Settings = $oSettings; + + $oPlugins->RunHook('imap.before-connect', array($this, $oImapClient, $oSettings)); + $oImapClient->Connect($oSettings); + $oPlugins->RunHook('imap.after-connect', array($this, $oImapClient, $oSettings)); + + $oSettings->passphrase = $this->oImapPass; + return $this->netClientLogin($oImapClient, $oPlugins); + } + + public function SmtpConnectAndLogin(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Smtp\SmtpClient $oSmtpClient) : bool + { + $oSettings = $this->Domain()->SmtpSettings(); + $oSettings->username = $this->SmtpUser(); + $oSettings->Ehlo = \MailSo\Smtp\SmtpClient::EhloHelper(); + + $oSmtpClient->Settings = $oSettings; + + $oPlugins->RunHook('smtp.before-connect', array($this, $oSmtpClient, $oSettings)); + if ($oSettings->usePhpMail) { + $oSettings->useAuth = false; + return true; + } + $oSmtpClient->Connect($oSettings); + $oPlugins->RunHook('smtp.after-connect', array($this, $oSmtpClient, $oSettings)); +/* + if ($this->oDomain->OutAskCredentials() && !($this->oSmtpPass && $this->sSmtpUser)) { + throw new RequireCredentialsException + } +*/ + $oSettings->passphrase = $this->oSmtpPass ?: $this->oImapPass; + return $this->netClientLogin($oSmtpClient, $oPlugins); + } + + public function SieveConnectAndLogin(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Sieve\SieveClient $oSieveClient, \RainLoop\Config\Application $oConfig) + { + $oSettings = $this->Domain()->SieveSettings(); + $oSettings->username = $this->ImapUser(); + + $oSieveClient->Settings = $oSettings; + + $oPlugins->RunHook('sieve.before-connect', array($this, $oSieveClient, $oSettings)); + $oSieveClient->Connect($oSettings); + $oPlugins->RunHook('sieve.after-connect', array($this, $oSieveClient, $oSettings)); + + $oSettings->passphrase = $this->oImapPass; + return $this->netClientLogin($oSieveClient, $oPlugins); + } + + private function netClientLogin(\MailSo\Net\NetClient $oClient, \RainLoop\Plugins\Manager $oPlugins) : bool + { +/* + $encrypted = !empty(\stream_get_meta_data($oClient->ConnectionResource())['crypto']); + [crypto] => Array( + [protocol] => TLSv1.3 + [cipher_name] => TLS_AES_256_GCM_SHA384 + [cipher_bits] => 256 + [cipher_version] => TLSv1.3 + ) +*/ + $oSettings = $oClient->Settings; + + $client_name = \strtolower($oClient->getLogName()); + + $oPlugins->RunHook("{$client_name}.before-login", array($this, $oClient, $oSettings)); + $bResult = !$oSettings->useAuth || $oClient->Login($oSettings); + $oPlugins->RunHook("{$client_name}.after-login", array($this, $oClient, $bResult, $oSettings)); + return $bResult; + } + +/* + // Stores settings in AdditionalAccount else MainAccount + public function settingsLocal() : \RainLoop\Settings + { + return \RainLoop\Api::Actions()->SettingsProvider(true)->Load($this); + } +*/ + + /** + * @deprecated since v2.36.1 + */ + public function IncLogin() : string + { + return $this->ImapUser(); + } + public function IncPassword() : string + { + return $this->ImapPass(); + } + public function OutLogin() : string + { + return $this->SmtpUser(); + } + public function SetPassword(SensitiveString $oPassword) : void + { + $this->oImapPass = $oPassword; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/AdditionalAccount.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/AdditionalAccount.php new file mode 100644 index 0000000000..6515025bce --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/AdditionalAccount.php @@ -0,0 +1,70 @@ +getMainAccountFromToken()->Email()); + } + + public function Hash() : string + { + return \sha1(parent::Hash() . $this->ParentEmail()); + } + + public static function convertArray(array $aAccount) : array + { + $aResult = parent::convertArray($aAccount); + $iCount = \count($aAccount); + if ($aResult && 7 < $iCount && 9 >= $iCount) { + $aResult['hmac'] = \array_pop($aAccount); + } + return $aResult; + } + + public function asTokenArray(MainAccount $oMainAccount) : array + { + $sHash = $oMainAccount->CryptKey(); + $aData = $this->jsonSerialize(); + $aData['pass'] = \SnappyMail\Crypt::EncryptUrlSafe($aData['pass'], $sHash); // sPassword + if (!empty($aData['smtp']['pass'])) { + $aData['smtp']['pass'] = \SnappyMail\Crypt::EncryptUrlSafe($aData['smtp']['pass'], $sHash); + } + $aData['hmac'] = \hash_hmac('sha1', $aData['pass'], $sHash); + return $aData; + } + + public static function NewInstanceFromTokenArray( + \RainLoop\Actions $oActions, + array $aAccountHash, + bool $bThrowExceptionOnFalse = false) : ?Account /* PHP7.4: ?self*/ + { + $aAccountHash = static::convertArray($aAccountHash); + if (!empty($aAccountHash['email'])) { + $sHash = $oActions->getMainAccountFromToken()->CryptKey(); + // hmac only set when asTokenArray() was used + $sPasswordHMAC = $aAccountHash['hmac'] ?? null; + if ($sPasswordHMAC) { + if ($sPasswordHMAC === \hash_hmac('sha1', $aAccountHash['pass'], $sHash)) { + $aAccountHash['pass'] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash['pass'], $sHash); + if (!empty($aData['smtp']['pass'])) { + $aAccountHash['smtp']['pass'] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash['smtp']['pass'], $sHash); + } + } else { + $aAccountHash['pass'] = ''; + if (!empty($aData['smtp']['pass'])) { + $aAccountHash['smtp']['pass'] = ''; + } + } + } + return parent::NewInstanceFromTokenArray($oActions, $aAccountHash, $bThrowExceptionOnFalse); + } + return null; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php new file mode 100644 index 0000000000..b9c7d3c869 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php @@ -0,0 +1,250 @@ +Name = \SnappyMail\IDN::toAscii($sName); + $this->Name = \strtolower(\idn_to_ascii($sName)); + $this->IMAP = new \MailSo\Imap\Settings; + $this->SMTP = new \MailSo\Smtp\Settings; + $this->Sieve = new \MailSo\Sieve\Settings; + } + + private function Normalize() + { + $this->IMAP->host = \trim($this->IMAP->host); + $this->Sieve->host = \trim($this->Sieve->host); + $this->SMTP->host = \trim($this->SMTP->host); + $this->whiteList = \trim($this->whiteList); + } + + public function Name() : string + { + return $this->Name; + } + + /** + * @deprecated + */ + public function IncHost() : string + { + \trigger_error('Deprecated function called.', \E_USER_DEPRECATED); + return $this->IMAP->host; + } + + /** + * @deprecated + */ + public function IncPort() : int + { + \trigger_error('Deprecated function called.', \E_USER_DEPRECATED); + return $this->IMAP->port; + } + + /** + * @deprecated + */ + public function IncShortLogin() : bool + { + \trigger_error('Deprecated function called.', \E_USER_DEPRECATED); + return $this->IMAP->shortLogin; + } + + /** + * @deprecated + */ + public function UseSieve() : bool + { + \trigger_error('Deprecated function called.', \E_USER_DEPRECATED); + return $this->Sieve->enabled; + } + + /** + * @deprecated + */ + public function OutHost() : string + { + \trigger_error('Deprecated function called.', \E_USER_DEPRECATED); + return $this->SMTP->host; + } + + /** + * @deprecated + */ + public function OutPort() : int + { + \trigger_error('Deprecated function called.', \E_USER_DEPRECATED); + return $this->SMTP->port; + } + + public function SetAliasName(string $sAliasName) : void + { +// $this->aliasName = \SnappyMail\IDN::toAscii($sAliasName); + $this->aliasName = \strtolower(\idn_to_ascii($sAliasName)); + } + + public function ValidateWhiteList(string $sEmail) : bool + { + $sW = $this->whiteList; + if (!$sW) { + return true; + } + $sEmail = \SnappyMail\IDN::emailToAscii(\mb_strtolower($sEmail)); + $iPos = \strrpos($sEmail, '@'); + $sUserPart = \substr($sEmail, 0, $iPos); + $sUserDomain = \substr($sEmail, $iPos); + $sItem = \strtok($sW, " ;,\n"); + while (false !== $sItem) { + $sItem = \SnappyMail\IDN::emailToAscii(\mb_strtolower(\trim($sItem))); + if ($sItem === $sEmail || $sItem === $sUserPart || $sItem === $sUserDomain) { + return true; + } + $sItem = \strtok(" ;,\n"); + } + return false; + } + + public function ImapSettings() : \MailSo\Imap\Settings + { + return $this->IMAP; + } + + public function SieveSettings() : \MailSo\Sieve\Settings + { + return $this->Sieve; + } + + public function SmtpSettings() : \MailSo\Smtp\Settings + { + return $this->SMTP; + } + + /** + * See jsonSerialize() for valid values + */ + public static function fromArray(string $sName, array $aDomain) : ?self + { + if (!\strlen($sName)) { + return null; + } + $oDomain = new self($sName); + if (!empty($aDomain['IMAP'])) { + $oDomain->IMAP = \MailSo\Imap\Settings::fromArray($aDomain['IMAP']); + $oDomain->SMTP = \MailSo\Smtp\Settings::fromArray($aDomain['SMTP']); + $oDomain->Sieve = \MailSo\Sieve\Settings::fromArray($aDomain['Sieve']); + $oDomain->whiteList = (string) $aDomain['whiteList']; + } else if (\strlen($aDomain['imapHost'])) { + // Old way + $oDomain->IMAP->host = $aDomain['imapHost']; + $oDomain->IMAP->port = (int) $aDomain['imapPort']; + $oDomain->IMAP->type = (int) $aDomain['imapSecure']; + $oDomain->IMAP->shortLogin = !empty($aDomain['imapShortLogin']); + + $oDomain->Sieve->enabled = !empty($aDomain['useSieve']); + $oDomain->Sieve->host = $aDomain['sieveHost']; + $oDomain->Sieve->port = (int) $aDomain['sievePort']; + $oDomain->Sieve->type = (int) $aDomain['sieveSecure']; + + $oDomain->SMTP->host = $aDomain['smtpHost']; + $oDomain->SMTP->port = (int) $aDomain['smtpPort']; + $oDomain->SMTP->type = (int) $aDomain['smtpSecure']; + $oDomain->SMTP->shortLogin = !empty($aDomain['smtpShortLogin']); + $oDomain->SMTP->useAuth = !empty($aDomain['smtpAuth']); + $oDomain->SMTP->setSender = !empty($aDomain['smtpSetSender']); + $oDomain->SMTP->authPlainLine = !empty($aDomain['smtpAuthPlainLine']); + $oDomain->SMTP->usePhpMail = !empty($aDomain['smtpPhpMail']); + + $oDomain->whiteList = (string) $aDomain['whiteList']; + } else { + return null; + } + $oDomain->Normalize(); + return $oDomain; + } + + /** + * Used by old RainLoop ToIniString() + */ + public static function fromIniArray(string $sName, array $aDomain) : ?self + { + $oDomain = null; + if (\strlen($sName) && \strlen($aDomain['imap_host'])) { + $oDomain = new self($sName); + + $oDomain->IMAP->host = $aDomain['imap_host']; + $oDomain->IMAP->port = (int) $aDomain['imap_port']; + $oDomain->IMAP->type = self::StrConnectionSecurityTypeToCons($aDomain['imap_secure'] ?? ''); + $oDomain->IMAP->shortLogin = !empty($aDomain['imap_short_login']); + + $oDomain->Sieve->enabled = !empty($aDomain['sieve_use']); + $oDomain->Sieve->host = $aDomain['sieve_host'] ?: ''; + $oDomain->Sieve->port = (int) ($aDomain['sieve_port'] ?? 4190);; + $oDomain->Sieve->type = self::StrConnectionSecurityTypeToCons($aDomain['sieve_secure'] ?? ''); + + $oDomain->SMTP->host = $aDomain['smtp_host']; + $oDomain->SMTP->port = (int) ($aDomain['smtp_port'] ?? 25); + $oDomain->SMTP->type = self::StrConnectionSecurityTypeToCons($aDomain['smtp_secure'] ?? ''); + $oDomain->SMTP->shortLogin = !empty($aDomain['smtp_short_login']); + $oDomain->SMTP->useAuth = !empty($aDomain['smtp_auth']); + $oDomain->SMTP->setSender = !empty($aDomain['smtp_set_sender']); + $oDomain->SMTP->usePhpMail = !empty($aDomain['smtp_php_mail']); + + $oDomain->whiteList = $aDomain['white_list'] ?? ''; + + $oDomain->Normalize(); + } + return $oDomain; + } + + /** + * Use by old RainLoop fromIniArray() + */ + public static function StrConnectionSecurityTypeToCons(string $sType) : int + { + switch (\strtoupper($sType)) + { + case 'SSL': + return ConnectionSecurityType::SSL; + case 'TLS': + return ConnectionSecurityType::STARTTLS; + } + return ConnectionSecurityType::NONE; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $aResult = array( +// '@Object' => 'Object/Domain', + 'IMAP' => $this->IMAP, + 'SMTP' => $this->SMTP, + 'Sieve' => $this->Sieve, + 'whiteList' => $this->whiteList + ); + if ($this->aliasName) { + $aResult['aliasName'] = $this->aliasName; + } + return $aResult; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Identity.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Identity.php new file mode 100644 index 0000000000..f31e77a9c1 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Identity.php @@ -0,0 +1,174 @@ +sId = $sId; + $this->sEmail = $sEmail; + } + + function __get(string $name) + { + if (!\property_exists($this, $name)) { + $name = \substr($name, 1); + } + if (\property_exists($this, $name)) { + return $this->$name; + } + } + + function toMime() : \MailSo\Mime\Email + { + return new \MailSo\Mime\Email($this->sEmail, $this->sName); + } + + public function Id(bool $bFillOnEmpty = false): string + { + return $bFillOnEmpty ? ('' === $this->sId ? '---' : $this->sId) : $this->sId; + } + + public function Email(): string + { + return $this->sEmail; + } + + public function SetEmail(string $sEmail): self + { + $this->sEmail = $sEmail; + + return $this; + } + + public function Name(): string + { + return $this->sName; + } + + public function SetId(string $sId): Identity + { + $this->sId = $sId; + return $this; + } + + public function SetName(string $sName): Identity + { + $this->sName = $sName; + return $this; + } + + public function ReplyTo(): string + { + return $this->sReplyTo; + } + + public function SetBcc(string $sBcc): Identity + { + $this->sBcc = $sBcc; + return $this; + } + + public function FromJSON(array $aData, bool $bJson = false): bool + { + if (!empty($aData['Email'])) { + $this->sId = !empty($aData['Id']) ? $aData['Id'] : ''; + $this->sLabel = isset($aData['Label']) ? $aData['Label'] : ''; + $this->sEmail = $bJson ? \SnappyMail\IDN::emailToAscii($aData['Email']) : $aData['Email']; + $this->sName = isset($aData['Name']) ? $aData['Name'] : ''; + $this->sReplyTo = !empty($aData['ReplyTo']) ? $aData['ReplyTo'] : ''; + $this->sBcc = !empty($aData['Bcc']) ? $aData['Bcc'] : ''; + $this->sSignature = !empty($aData['Signature']) ? $aData['Signature'] : ''; + $this->bSignatureInsertBefore = !empty($aData['SignatureInsertBefore']); + $this->sSentFolder = isset($aData['sentFolder']) ? $aData['sentFolder'] : ''; + $this->pgpEncrypt = !empty($aData['pgpEncrypt']); + $this->pgpSign = !empty($aData['pgpSign']); + $this->smimeKey = new SensitiveString(isset($aData['smimeKey']) ? $aData['smimeKey'] : ''); + $this->smimeCertificate = isset($aData['smimeCertificate']) ? $aData['smimeCertificate'] : ''; + return true; + } + + return false; + } + + // Used to store + public function ToSimpleJSON(): array + { + return array( + 'Id' => $this->sId, + 'Label' => $this->sLabel, + 'Email' => $this->sEmail, + 'Name' => $this->sName, + 'ReplyTo' => $this->sReplyTo, + 'Bcc' => $this->sBcc, + 'Signature' => $this->sSignature, + 'SignatureInsertBefore' => $this->bSignatureInsertBefore, + 'sentFolder' => $this->sSentFolder, + 'pgpEncrypt' => $this->pgpEncrypt, + 'pgpSign' => $this->pgpSign, + 'smimeKey' => (string) $this->smimeKey, + 'smimeCertificate' => $this->smimeCertificate + ); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( + '@Object' => 'Object/Identity', + 'id' => $this->sId, + 'label' => $this->sLabel, + 'email' => \SnappyMail\IDN::emailToUtf8($this->sEmail), + 'name' => $this->sName, + 'replyTo' => $this->sReplyTo, + 'bcc' => $this->sBcc, + 'signature' => $this->sSignature, + 'signatureInsertBefore' => $this->bSignatureInsertBefore, + 'sentFolder' => $this->sSentFolder, + 'pgpEncrypt' => $this->pgpEncrypt, + 'pgpSign' => $this->pgpSign, + 'smimeKey' => (string) $this->smimeKey, + 'smimeCertificate' => $this->smimeCertificate, + 'exists' => $this->exists + ); + } + + public function Validate(): bool + { + return !empty($this->sEmail); + } + + public function IsAccountIdentities(): bool + { + return '' === $this->Id(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/MainAccount.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/MainAccount.php new file mode 100644 index 0000000000..a44ca19fea --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/MainAccount.php @@ -0,0 +1,71 @@ +StorageProvider(); + $sKey = $oStorage->Get($this, StorageType::ROOT, '.cryptkey'); + if ($sKey) { + $sKey = \SnappyMail\Crypt::DecryptFromJSON($sKey, $oOldPass); + if (!$sKey) { + throw new ClientException(Notifications::CryptKeyError); + } + $sPass = \RainLoop\Api::Config()->Get('security', 'insecure_cryptkey', false) + ? $this->Email() + : $this->ImapPass(); + $sKey = \SnappyMail\Crypt::EncryptToJSON($sKey, $sPass); + if ($sKey) { + $this->sCryptKey = null; + if (\RainLoop\Api::Actions()->StorageProvider()->Put($this, StorageType::ROOT, '.cryptkey', $sKey)) { + return true; + } + } + } + return false; + } + + public function CryptKey() : string + { + if (!$this->sCryptKey) { + // Seal the cryptkey so that people who change their login password + // can use the old password to re-seal the cryptkey + $oStorage = \RainLoop\Api::Actions()->StorageProvider(); + $sKey = $oStorage->Get($this, StorageType::ROOT, '.cryptkey'); + $sPass = \RainLoop\Api::Config()->Get('security', 'insecure_cryptkey', false) + ? $this->Email() + : $this->ImapPass(); + if (!$sKey) { + $sKey = \SnappyMail\Crypt::EncryptToJSON( + \sha1($this->ImapPass() . APP_SALT), + $sPass + ); + $oStorage->Put($this, StorageType::ROOT, '.cryptkey', $sKey); + } + $sKey = \SnappyMail\Crypt::DecryptFromJSON($sKey, $sPass); + if (!$sKey) { + throw new ClientException(Notifications::CryptKeyError); + } + $this->sCryptKey = new SensitiveString(\hex2bin($sKey)); + } + return $this->sCryptKey; + } + +/* + // Stores settings in MainAccount + public function settings() : \RainLoop\Settings + { + return \RainLoop\Api::Actions()->SettingsProvider()->Load($this); + } +*/ +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Notifications.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Notifications.php new file mode 100644 index 0000000000..036d6c0788 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Notifications.php @@ -0,0 +1,80 @@ +getMessage(); + } + + $oClass = new \ReflectionClass(__CLASS__); + return (\array_search($iCode, $oClass->getConstants(), true) ?: 'UnknownNotification') . '['.$iCode.']'; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Base.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Base.php new file mode 100644 index 0000000000..8de393fe76 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Base.php @@ -0,0 +1,348 @@ +oLogger->WriteDump(array($sStr1, $sStr2)); + return \strcmp(\mb_strtoupper($sStr1, 'UTF-8'), \mb_strtoupper($sStr2, 'UTF-8')); + } + + public static function getAvailableDrivers() : array + { + return \class_exists('PDO', false) + ? \array_values(\array_intersect(['mysql', 'pgsql', 'sqlite'], \PDO::getAvailableDrivers())) + : []; + } + + /** + * + * @throws \Exception + */ + protected function getPDO() : \PDO + { + if ($this->oPDO) { + return $this->oPDO; + } + + if (!\class_exists('PDO')) { + throw new \Exception('Class PDO does not exist'); + } + + $oSettings = $this->getPdoSettings(); + + if (!\in_array($oSettings->driver, static::getAvailableDrivers())) { + throw new \Exception('Unknown PDO SQL connection type'); + } + + if (empty($oSettings->dsn)) { + throw new \Exception('Empty PDO DSN configuration'); + } + + $this->sDbType = $oSettings->driver; + + $options = []; + if ('mysql' === $oSettings->driver) { + if ($oSettings->sslCa) { + $options[\PDO::MYSQL_ATTR_SSL_CA] = $oSettings->sslCa; + } + // PHP 8.0 https://github.com/the-djmaze/snappymail/issues/1205 + if (\defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) { + $options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $oSettings->sslVerify; + } + if ($oSettings->sslCiphers) { + $options[\PDO::MYSQL_ATTR_SSL_CIPHER] = $oSettings->sslCiphers; + } +/* + $options[\PDO::MYSQL_ATTR_SSL_CAPATH] = ''; + // mutual (two-way) authentication + $options[\PDO::MYSQL_ATTR_SSL_KEY] = ''; + $options[\PDO::MYSQL_ATTR_SSL_CERT] = ''; +*/ + } + + $oPdo = new \PDO($oSettings->dsn, $oSettings->user, $oSettings->password, $options); + $sPdoType = $oPdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + $oPdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + +// $bCaseFunc = false; + if ('mysql' === $oSettings->driver && 'mysql' === $sPdoType) { + $oPdo->exec('SET NAMES utf8mb4 COLLATE utf8mb4_general_ci'); + } +// else if ('sqlite' === $oSettings->driver && 'sqlite' === $sPdoType && $this->bSqliteCollate) { +// if (\method_exists($oPdo, 'sqliteCreateCollation')) { +// $oPdo->sqliteCreateCollation('SQLITE_NOCASE_UTF8', array($this, 'sqliteNoCaseCollationHelper')); +// $bCaseFunc = true; +// } +// } +// $this->logWrite('PDO:'.$sPdoType.($bCaseFunc ? '/SQLITE_NOCASE_UTF8' : '')); + + $this->oPDO = $oPdo; + return $oPdo; + } + + protected function lastInsertId(?string $sTabelName = null, ?string $sColumnName = null) : string + { + $mName = null; + if ('pgsql' === $this->sDbType && null !== $sTabelName && $sColumnName !== null) { + $mName = \strtolower($sTabelName.'_'.$sColumnName.'_seq'); + } + + return null === $mName ? $this->getPDO()->lastInsertId() : $this->getPDO()->lastInsertId($mName); + } + + protected function beginTransaction() : bool + { + return $this->getPDO()->beginTransaction(); + } + + protected function commit() : bool + { + return $this->getPDO()->commit(); + } + + protected function rollBack() : bool + { + return $this->getPDO()->rollBack(); + } + + protected function prepareAndExecute(string $sSql, array $aParams = array(), bool $bMultiplyParams = false, bool $bLogParams = false) : ?\PDOStatement + { + if ($this->bExplain && !$bMultiplyParams) { + $this->prepareAndExplain($sSql, $aParams); + } + + $mResult = null; + + $this->writeLog($sSql); + $oStmt = $this->getPDO()->prepare($sSql); + if ($oStmt) { + $aLogs = array(); + $aRootParams = $bMultiplyParams ? $aParams : array($aParams); + foreach ($aRootParams as $aSubParams) { + foreach ($aSubParams as $sName => $aValue) { + if ($bLogParams) { + $aLogs[$sName] = $aValue[0]; + } + $oStmt->bindValue($sName, $aValue[0], $aValue[1]); + } + $mResult = $oStmt->execute() && !$bMultiplyParams ? $oStmt : null; + } + if ($bLogParams && $aLogs) { + $this->writeLog('Params: '.\json_encode($aLogs, JSON_UNESCAPED_UNICODE)); + } + } + + return $mResult; + } + + protected function prepareAndExplain(string $sSql, array $aParams = array()) + { + $mResult = null; + if (0 === \strpos($sSql, 'SELECT ')) { + $sSql = 'EXPLAIN '.$sSql; + $this->writeLog($sSql); + $oStmt = $this->getPDO()->prepare($sSql); + if ($oStmt) { + foreach ($aParams as $sName => $aValue) { + $oStmt->bindValue($sName, $aValue[0], $aValue[1]); + } + + $mResult = $oStmt->execute() ? $oStmt : null; + } + } + + if ($mResult) { + $aFetch = $mResult->fetchAll(\PDO::FETCH_ASSOC); + $this->oLogger->WriteDump($aFetch); + + unset($aFetch); + $mResult->closeCursor(); + } + } + + /** + * @param mixed $mData + */ + protected function writeLog($mData) + { + if ($this->oLogger) { + if ($mData instanceof \Throwable) { + $this->logException($mData, \LOG_ERR, 'SQL'); + } else if (\is_scalar($mData)) { + $this->logWrite((string) $mData, \LOG_INFO, 'SQL'); + } else { + $this->oLogger->WriteDump($mData, \LOG_INFO, 'SQL'); + } + } + } + + public function quoteValue(string $sValue) : string + { + $oPdo = $this->getPDO(); + return $oPdo ? $oPdo->quote((string) $sValue, \PDO::PARAM_STR) : '\'\''; + } + + protected function getVersion(string $sName) : ?int + { + $oPdo = $this->getPDO(); + if ($oPdo) { + $sQuery = 'SELECT MAX(value_int) FROM rainloop_system WHERE sys_name = ?'; + + $this->writeLog($sQuery); + + $oStmt = $oPdo->prepare($sQuery); + if ($oStmt->execute(array($sName.'_version'))) { + $mRow = $oStmt->fetch(\PDO::FETCH_NUM); + if ($mRow && isset($mRow[0])) { + return (int) $mRow[0]; + } + + return 0; + } + } + + return null; + } + + protected function setVersion(string $sName, int $iVersion) : bool + { + $bResult = false; + $oPdo = $this->getPDO(); + if ($oPdo) { + $sQuery = 'DELETE FROM rainloop_system WHERE sys_name = ? AND value_int <= ?;'; + $this->writeLog($sQuery); + + $oStmt = $oPdo->prepare($sQuery); + $bResult = !!$oStmt->execute(array($sName.'_version', $iVersion)); + if ($bResult) { + $sQuery = 'INSERT INTO rainloop_system (sys_name, value_int) VALUES (?, ?);'; + $this->writeLog($sQuery); + + $oStmt = $oPdo->prepare($sQuery); + if ($oStmt) { + $bResult = !!$oStmt->execute(array($sName.'_version', $iVersion)); + } + } + } + + return $bResult; + } + + /** + * @throws \Exception + */ + protected function initSystemTables() + { + $bResult = true; + + $oPdo = $this->getPDO(); + if ($oPdo) { + $aQ = Schema::getForDbType($this->sDbType); + if (\count($aQ)) { + try + { + foreach ($aQ as $sQuery) { + $this->writeLog($sQuery); + $bResult = false !== $oPdo->exec($sQuery); + if (!$bResult) { + $this->writeLog('Result=false'); + break; + } else { + $this->writeLog('Result=true'); + } + } + } + catch (\Throwable $oException) + { + $this->writeLog($oException); + throw $oException; + } + } + } + + return $bResult; + } + + protected function dataBaseUpgrade(string $sName, array $aData = array()) : bool + { + $iFromVersion = null; + try + { + $iFromVersion = $this->getVersion($sName); + } + catch (\PDOException $oException) + { +// $this->writeLog($oException); + try + { + $this->initSystemTables(); + $iFromVersion = $this->getVersion($sName); + } + catch (\PDOException $oSubException) + { + $this->writeLog($oSubException); + throw $oSubException; + } + } + + $bResult = false; + if (\is_int($iFromVersion) && 0 <= $iFromVersion) { + $oPdo = false; + foreach ($aData as $iVersion => $aQuery) { + if ($iFromVersion < $iVersion) { + if (\count($aQuery)) { + if (!$oPdo) { + $oPdo = $this->getPDO(); + $bResult = true; + } + if ($oPdo) { + try + { + foreach ($aQuery as $sQuery) { + $this->writeLog($sQuery); + $bExec = $oPdo->exec($sQuery); + if (false === $bExec) { + $this->writeLog('Result: false'); + $bResult = false; + break; + } + } + } + catch (\Throwable $oException) + { + $this->writeLog($oException); + throw $oException; + } + if (!$bResult) { + break; + } + } + } + $this->setVersion($sName, $iVersion); + } + } + } + + return $bResult; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Schema.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Schema.php new file mode 100644 index 0000000000..1a34d96efd --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Schema.php @@ -0,0 +1,68 @@ +sName = static::NAME; + } + + public function Name() : string + { + return $this->sName; + } + + public function Description() : string + { + $sFile = $this->Path().'/README'; + return \is_readable($sFile) ? \file_get_contents($sFile) : static::DESCRIPTION; + } + + public function UseLangs(?bool $bLangs = null) : bool + { + if (null !== $bLangs) { + $this->bLangs = $bLangs; + } + + return $this->bLangs; + } + + protected function configMapping() : array + { + return array(); + } + + public function Supported() : string + { + return ''; + } + + public function Init() : void + { + + } + + public function FilterAppDataPluginSection(bool $bAdmin, bool $bAuth, array &$aConfig) : void + { + + } + + final public function Hash() : string + { + return static::class . '@' . static::VERSION; + } + + final public function Config() : \RainLoop\Config\Plugin + { + return $this->oPluginConfig; + } + + final public function Manager() : \RainLoop\Plugins\Manager + { + return $this->oPluginManager; + } + + final public function Path() : string + { + return $this->sPath; + } + + final public function ConfigMap(bool $flatten = false) : array + { + if (null === $this->aConfigMap) { + $this->aConfigMap = $this->configMapping(); + } + + if ($flatten) { + $result = []; + foreach ($this->aConfigMap as $oItem) { + if ($oItem) { + if ($oItem instanceof \RainLoop\Plugins\Property) { + $result[] = $oItem; + } else if ($oItem instanceof \RainLoop\Plugins\PropertyCollection) { + foreach ($oItem as $oSubItem) { + if ($oSubItem && $oSubItem instanceof \RainLoop\Plugins\Property) { + $result[] = $oSubItem; + } + } + } + } + } + return $result; + } + + return $this->aConfigMap; + } + + final public function SetPath(string $sPath) : self + { + $this->sPath = $sPath; + + return $this; + } + + final public function SetName(string $sName) : self + { + $this->sName = $sName; + + return $this; + } + + final public function SetPluginManager(\RainLoop\Plugins\Manager $oPluginManager) : self + { + $this->oPluginManager = $oPluginManager; + + return $this; + } + + final public function SetPluginConfig(\RainLoop\Config\Plugin $oPluginConfig) : self + { + $this->oPluginConfig = $oPluginConfig; + if ($oPluginConfig->IsInited() && !$oPluginConfig->Load()) { + $oPluginConfig->Save(); + } + return $this; + } + + final protected function addHook(string $sHookName, string $sFunctionName) : self + { + if ($this->oPluginManager) { + $this->oPluginManager->AddHook($sHookName, array($this, $sFunctionName)); + } + + return $this; + } + + final protected function addCss(string $sFile, bool $bAdminScope = false) : self + { + if ($this->oPluginManager) { + $this->oPluginManager->AddCss($this->sPath.'/'.$sFile, $bAdminScope); + } + + return $this; + } + + final protected function addJs(string $sFile, bool $bAdminScope = false) : self + { + if ($this->oPluginManager) { + $this->oPluginManager->AddJs($this->sPath.'/'.$sFile, $bAdminScope); + } + + return $this; + } + + final protected function addTemplate(string $sFile, bool $bAdminScope = false) : self + { + if ($this->oPluginManager) { + $this->oPluginManager->AddTemplate($this->sPath.'/'.$sFile, $bAdminScope); + } + + return $this; + } + + final protected function addPartHook(string $sActionName, string $sFunctionName) : self + { + if ($this->oPluginManager) { + $this->oPluginManager->AddAdditionalPartAction($sActionName, array($this, $sFunctionName)); + } + + return $this; + } + + final protected function addJsonHook(string $sActionName, string $sFunctionName = '') : self + { + if ($this->oPluginManager) { + $this->oPluginManager->AddAdditionalJsonAction($sActionName, array($this, $sFunctionName ?: $sActionName)); + } + + return $this; + } + + /** + * @return mixed false|string|array + */ + final protected function jsonResponse(string $sFunctionName, $mData) + { + return $this->oPluginManager + ? $this->oPluginManager->JsonResponseHelper( + $this->oPluginManager->convertPluginFolderNameToClassName($this->Name()).'::'.$sFunctionName, $mData) + : \json_encode($mData); + } + + /** + * @param mixed $mDefault = null + * + * @return mixed + */ + final public function jsonParam(string $sKey, $mDefault = null) + { + return $this->oPluginManager + ? $this->oPluginManager->Actions()->GetActionParam($sKey, $mDefault) + : $mDefault; + } + + final public function getUserSettings() : array + { + return $this->oPluginManager + ? $this->oPluginManager->GetUserPluginSettings($this->Name()) + : array(); + } + + final public function saveUserSettings(array $aSettings) : bool + { + return $this->oPluginManager + && $this->oPluginManager->SaveUserPluginSettings($this->Name(), $aSettings); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Helper.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Helper.php new file mode 100644 index 0000000000..9f85f96592 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Helper.php @@ -0,0 +1,49 @@ +oActions = $oActions; + $this->SetLogger($oActions->Logger()); + + $oConfig = $oActions->Config(); + $this->bIsEnabled = (bool) $oConfig->Get('plugins', 'enable', false); + if ($this->bIsEnabled) { + $sList = $oConfig->Get('plugins', 'enabled_list', ''); + if (\strlen($sList)) { + $aList = \SnappyMail\Repository::getEnabledPackagesNames(); + foreach ($aList as $i => $sName) { + if (!$this->loadPlugin($sName)) { + unset($aList[$i]); + } + } + $aList = \implode(',', \array_keys($this->aPlugins)); + if ($sList != $aList) { + $oConfig->Set('plugins', 'enabled_list', $aList); + $oConfig->Save(); + } + } + } + } + + public function loadPlugin(string $sName) : bool + { + if (!isset($this->aPlugins[$sName])) { + $oPlugin = $this->CreatePluginByName($sName); + if ($oPlugin) { + $oPlugin->Init(); + $this->aPlugins[$sName] = $oPlugin; + } + } + return isset($this->aPlugins[$sName]); + } + + protected static function getPluginPath(string $sName) : ?string + { + $sPath = APP_PLUGINS_PATH.$sName; + if (\is_readable("{$sPath}/index.php")) { + return $sPath; + } + if (\is_readable("{$sPath}.phar")) { + return "phar://{$sPath}.phar"; + } + return null; + } + + public function CreatePluginByName(string $sName) : ?\RainLoop\Plugins\AbstractPlugin + { + $oPlugin = null; + + $sClassName = $this->loadPluginByName($sName); + if ($sClassName) { + $oPlugin = new $sClassName(); + $oPlugin + ->SetName($sName) + ->SetPath(static::getPluginPath($sName)) + ->SetPluginManager($this) + ->SetPluginConfig(new \RainLoop\Config\Plugin($sName, $oPlugin->ConfigMap(true))) + ; + if (\method_exists($oPlugin, 'SetLogger')) { + $oPlugin->SetLogger($this->oLogger); + } + } + + return $oPlugin; + } + + public function InstalledPlugins() : array + { + $aList = array(); + + $aGlob = \glob(APP_PLUGINS_PATH.'*'); + if (\is_array($aGlob)) { + foreach ($aGlob as $sPathName) { + if (\is_dir($sPathName)) { + $sName = \basename($sPathName); + } else if ('.phar' === \substr($sPathName, -5)) { + $sName = \basename($sPathName, '.phar'); + } else { + continue; + } + $sClassName = $this->loadPluginByName($sName); + if ($sClassName) { + $aList[] = array( + $sName, + $sClassName::VERSION, + $sClassName::NAME, + $sClassName::DESCRIPTION + ); + } + } + } else { + $this->oActions->logWrite('Cannot get installed plugins from '.APP_PLUGINS_PATH, \LOG_ERR); + } + + return $aList; + } + + public function convertPluginFolderNameToClassName(string $sFolderName) : string + { + $aParts = \array_map('ucfirst', \array_map('strtolower', + \explode(' ', \preg_replace('/[^a-z0-9]+/', ' ', $sFolderName)))); + + return \implode($aParts).'Plugin'; + } + + public function loadPluginByName(string $sName) : ?string + { + if (\preg_match('/^[a-z0-9\\-]+$/', $sName)) { + $sClassName = $this->convertPluginFolderNameToClassName($sName); + if (!\class_exists($sClassName)) { + $sPath = static::getPluginPath($sName); + if (\is_readable($sPath.'/index.php')) { + include_once $sPath.'/index.php'; + } + } + if (\class_exists($sClassName) && \is_subclass_of($sClassName, 'RainLoop\\Plugins\\AbstractPlugin')) { + return $sClassName; + } + \trigger_error("Invalid plugin class {$sClassName}"); + } + + return null; + } + + public function Actions() : \RainLoop\Actions + { + return $this->oActions; + } + + public function Hash() : string + { + return \md5( + \array_reduce($this->aPlugins, function($sResult, $oPlugin){ + return $sResult . "|{$oPlugin->Hash()}"; + }, APP_VERSION) + .implode('',$this->aJs[1]).implode('',$this->aJs[0]) + .implode('',$this->aCss[1]).implode('',$this->aCss[0]) + ); + } + + public function HaveJs(bool $bAdminScope = false) : bool + { + return $this->bIsEnabled && \count($this->aJs[$bAdminScope ? 1 : 0]); + } + + public function CompileCss(bool $bAdminScope, bool &$bLess, bool $bMinified) : string + { + $aResult = array(); + if ($this->bIsEnabled) { + foreach ($this->aCss[$bAdminScope ? 1 : 0] as $sFile) { + if ($bMinified) { + $sMinFile = \str_replace('.css', '.min.css', $sFile); + if (\is_readable($sMinFile)) { + $sFile = $sMinFile; + } + } + if (\is_readable($sFile)) { + $aResult[] = \file_get_contents($sFile); + $bLess = $bLess || \str_ends_with($sFile, '.less'); + } + } + } + return \implode("\n", $aResult); + } + + public function CompileJs(bool $bAdminScope = false, bool $bMinified = false) : string + { + $aResult = array(); + if ($this->bIsEnabled) { + foreach ($this->aJs[$bAdminScope ? 1 : 0] as $sFile) { + if ($bMinified) { + $sMinFile = \str_replace('.js', '.min.js', $sFile); + if (\is_readable($sMinFile)) { + $sFile = $sMinFile; + } + } + if (\is_readable($sFile)) { + $aResult[] = \file_get_contents($sFile); + } + } + } + + return \implode("\n", $aResult); + } + + public function CompileTemplate(array &$aList, bool $bAdminScope = false) : void + { + if ($this->bIsEnabled) { + $aTemplates = $bAdminScope ? $this->aAdminTemplates : $this->aTemplates; + foreach ($aTemplates as $sFile) { + if (\is_readable($sFile)) { + $sTemplateName = \substr(\basename($sFile), 0, -5); + $aList[$sTemplateName] = $sFile; + } + } + } + } + + public function InitAppData(bool $bAdmin, array &$aAppData, ?\RainLoop\Model\Account $oAccount = null) : self + { + if ($this->bIsEnabled && isset($aAppData['Plugins']) && \is_array($aAppData['Plugins'])) { + $bAuth = !empty($aAppData['Auth']); + foreach ($this->aPlugins as $oPlugin) { + if ($oPlugin) { + $aConfig = array(); + $aMap = $oPlugin->ConfigMap(true); + if (\is_array($aMap)) { + foreach ($aMap as /* @var $oPluginProperty \RainLoop\Plugins\Property */ $oPluginProperty) { + if ($oPluginProperty && $oPluginProperty->AllowedInJs()) { + $aConfig[$oPluginProperty->Name()] = + $oPlugin->Config()->Get('plugin', + $oPluginProperty->Name(), + $oPluginProperty->DefaultValue()); + } + } + } + + $oPlugin->FilterAppDataPluginSection($bAdmin, $bAuth, $aConfig); + + if (\count($aConfig)) { + $aAppData['Plugins'][$oPlugin->Name()] = $aConfig; + } + } + } + + $this->RunHook('filter.app-data', array($bAdmin, &$aAppData)); + } + + return $this; + } + + /** + * @param mixed $mCallbak + */ + public function AddHook(string $sHookName, $mCallbak) : self + { + if ($this->bIsEnabled && \is_callable($mCallbak)) { + $sHookName = \strtolower($sHookName); + if (!isset($this->aHooks[$sHookName])) { + $this->aHooks[$sHookName] = array(); + } + $this->aHooks[$sHookName][] = $mCallbak; + } + return $this; + } + + public function AddCss(string $sFile, bool $bAdminScope = false) : self + { + if ($this->bIsEnabled) { + $this->aCss[$bAdminScope ? 1 : 0][] = $sFile; + } + return $this; + } + + public function AddJs(string $sFile, bool $bAdminScope = false) : self + { + if ($this->bIsEnabled) { + $this->aJs[$bAdminScope ? 1 : 0][$sFile] = $sFile; + } + + return $this; + } + + public function AddTemplate(string $sFile, bool $bAdminScope = false) : self + { + if ($this->bIsEnabled) { + if ($bAdminScope) { + $this->aAdminTemplates[$sFile] = $sFile; + } else { + $this->aTemplates[$sFile] = $sFile; + } + } + + return $this; + } + +/* + private static function getCallableName(callable $callable) { + if (\is_string($callable)) { + if (\str_contains($callable, '::')) { + return '[static] ' . $callable; + } + return '[function] ' . $callable; + } + if (\is_array($callable)) { + if (\is_object($callable[0])) { + return '[method] ' . \get_class($callable[0]) . '->' . $callable[1]; + } + return '[static] ' . $callable[0] . '::' . $callable[1]; + } + if ($callable instanceof \Closure) { + return '[closure]'; + } + if (\is_object($callable)) { + return '[invokable] ' . \get_class($callable); + } + return '[unknown]'; + } +*/ + + public function RunHook(string $sHookName, array $aArg = array(), bool $bLogHook = true) : self + { + if ($this->bIsEnabled) { + $sHookName = \strtolower($sHookName); + if (isset($this->aHooks[$sHookName])) { + if ($bLogHook) { + $this->WriteLog('Hook: '.$sHookName, \LOG_INFO); +// $this->WriteLog('Hooks: '.\implode(',', \array_map('self::getCallableName', $this->aHooks[$sHookName])), \LOG_DEBUG); + } + foreach ($this->aHooks[$sHookName] as $mCallback) try { + $mCallback(...$aArg); + } catch (\Throwable $e) { +// $this->WriteLog("Hook {$sHookName} {$e->getMessage()}\n".static::getCallableName($mCallback), \LOG_ERR); + throw $e; + } + } + } + return $this; + } + + /** + * @param mixed $mCallbak + */ + public function AddAdditionalPartAction(string $sActionName, $mCallbak) : self + { + if ($this->bIsEnabled && \is_callable($mCallbak)) { + $sActionName = \strtolower($sActionName); + if (!isset($this->aAdditionalParts[$sActionName])) { + $this->aAdditionalParts[$sActionName] = array(); + } + + $this->aAdditionalParts[$sActionName][] = $mCallbak; + } + + return $this; + } + + public function RunAdditionalPart(string $sActionName, array $aParts = array()) : bool + { + $bResult = false; + if ($this->bIsEnabled) { + $sActionName = \strtolower($sActionName); + if (isset($this->aAdditionalParts[$sActionName])) { + foreach ($this->aAdditionalParts[$sActionName] as $mCallbak) { + $bResult = !!$mCallbak(...$aParts) || $bResult; + } + } + } + + return $bResult; + } + + public function AddAdditionalJsonAction(string $sActionName, callable $mCallback) : self + { + $sActionName = "DoPlugin{$sActionName}"; + if ($this->bIsEnabled && \strlen($sActionName) && !isset($this->aAdditionalJson[$sActionName])) { + $this->aAdditionalJson[$sActionName] = $mCallback; + } + return $this; + } + + public function HasAdditionalJson(string $sActionName) : bool + { + return $this->bIsEnabled && isset($this->aAdditionalJson[$sActionName]); + } + + /** + * @return mixed + */ + public function RunAdditionalJson(string $sActionName) + { + return $this->HasAdditionalJson($sActionName) ? $this->aAdditionalJson[$sActionName]() : false; + } + + /** + * @param mixed $mData + */ + public function JsonResponseHelper(string $sFunctionName, $mData) : array + { + return $this->oActions->DefaultResponse($mData, [], $sFunctionName); + } + + public function GetUserPluginSettings(string $sPluginName) : array + { + $oAccount = $this->oActions->getAccountFromToken(); + if ($oAccount) { + $oSettings = $this->oActions->SettingsProvider()->Load($oAccount); + if ($oSettings) { + $aData = $oSettings->GetConf('Plugins', array()); + if (isset($aData[$sPluginName]) && \is_array($aData[$sPluginName])) { + return $aData[$sPluginName]; + } + } + } + + return array(); + } + + public function SaveUserPluginSettings(string $sPluginName, array $aSettings) : bool + { + $oAccount = $this->oActions->getAccountFromToken(); + if ($oAccount) { + $oSettings = $this->oActions->SettingsProvider()->Load($oAccount); + if ($oSettings) { + $aData = $oSettings->GetConf('Plugins', array()); + if (!\is_array($aData)) { + $aData = array(); + } + + $aPluginSettings = array(); + if (isset($aData[$sPluginName]) && \is_array($aData[$sPluginName])) { + $aPluginSettings = $aData[$sPluginName]; + } + + foreach ($aSettings as $sKey => $mValue) { + $aPluginSettings[$sKey] = $mValue; + } + + $aData[$sPluginName] = $aPluginSettings; + $oSettings->SetConf('Plugins',$aData); + + return $oSettings->save(); + } + } + + return false; + } + + public function ReadLang(string $sLang, array &$aLang) : void + { + if ($this->bIsEnabled) { + foreach ($this->aPlugins as $oPlugin) { + if ($oPlugin->UseLangs()) { + $sPath = $oPlugin->Path().'/langs/'; + $aPLang = []; + + // First get english + if (\is_file("{$sPath}en.json")) { + $aPLang = \json_decode(\file_get_contents("{$sPath}en.json"), true); + } else if (\is_file("{$sPath}en.ini")) { + $aPLang = \parse_ini_file("{$sPath}en.ini", true); + } + if ($aPLang) { + $aLang = \array_replace_recursive($aLang, $aPLang); + } + + // Now get native + if ('en' !== $sLang) { + $aPLang = []; + if (\is_file("{$sPath}{$sLang}.json")) { + $aPLang = \json_decode(\file_get_contents("{$sPath}{$sLang}.json"), true); + } else { + if (!\is_file("{$sPath}{$sLang}.ini")) { + $sLang = \strtr($sLang, '-', '_'); + } + if (\is_file("{$sPath}{$sLang}.ini")) { + $aPLang = \parse_ini_file("{$sPath}{$sLang}.ini", true); + } + } + if ($aPLang) { + $aLang = \array_replace_recursive($aLang, $aPLang); + } + } + } + } + } + } + + public function IsEnabled() : bool + { + return $this->bIsEnabled; + } + + public function count() : int + { + return $this->bIsEnabled ? \count($this->aPlugins) : 0; + } + + public function WriteLog(string $sDesc, int $iType = \LOG_INFO) : void + { + $this->logWrite($sDesc, $iType, 'PLUGIN'); + } + + public function WriteException(string $sDesc, int $iType = \LOG_INFO) : void + { + $this->logException($sDesc, $iType, 'PLUGIN'); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Property.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Property.php new file mode 100644 index 0000000000..8a6504ff4f --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Property.php @@ -0,0 +1,207 @@ +sName = $sName; + } + + public static function NewInstance(string $sName) : self + { + return new self($sName); + } + + public function SetType(int $iType) : self + { + $this->iType = (int) $iType; + return $this; + } + + /** + * @param mixed $mValue + */ + public function SetValue($mValue) : void + { + $this->mValue = null; + switch ($this->iType) { + case PluginPropertyType::INT: + $this->mValue = (int) $mValue; + break; + case PluginPropertyType::BOOL: + $this->mValue = !empty($mValue); + break; + case PluginPropertyType::SELECT: + foreach ($this->aOptions as $option) { + if ($mValue == $option['id']) { + $this->mValue = (string) $mValue; + } + } + break; + case PluginPropertyType::SELECTION: + if ($this->aOptions && \in_array($mValue, $this->aOptions)) { + $this->mValue = (string) $mValue; + } + break; + case PluginPropertyType::PASSWORD: + case PluginPropertyType::STRING: + case PluginPropertyType::STRING_TEXT: + case PluginPropertyType::URL: + $this->mValue = (string) $mValue; + break; +// case PluginPropertyType::GROUP: +// throw new \Exception('Not allowed to set group value'); + } + } + + /** + * @param mixed $mDefaultValue + */ + public function SetDefaultValue($mDefaultValue) : self + { + if (\is_array($mDefaultValue)) { + $this->aOptions = $mDefaultValue; + } else { + $this->mDefaultValue = $mDefaultValue; + } + return $this; + } + + public function SetOptions(array $aOptions) : self + { + $this->aOptions = $aOptions; + return $this; + } + + public function SetPlaceholder(string $sPlaceholder) : self + { + $this->sPlaceholder = $sPlaceholder; + return $this; + } + + public function SetLabel(string $sLabel) : self + { + $this->sLabel = $sLabel; + return $this; + } + + public function SetDescription(string $sDesc) : self + { + $this->sDesc = $sDesc; + return $this; + } + + public function SetAllowedInJs(bool $bValue = true) : self + { + $this->bAllowedInJs = $bValue; + return $this; + } + + public function SetEncrypted(bool $bValue = true) : self + { + $this->encrypted = $bValue; + return $this; + } + + public function Name() : string + { + return $this->sName; + } + + public function AllowedInJs() : bool + { + return $this->bAllowedInJs; + } + + public function Description() : string + { + return $this->sDesc; + } + + public function Label() : string + { + return $this->sLabel; + } + + public function Type() : int + { + return $this->iType; + } + + /** + * @return mixed + */ + public function DefaultValue() + { + return $this->mDefaultValue; + } + + public function Options() : array + { + return $this->aOptions; + } + + public function Placeholder() : string + { + return $this->sPlaceholder; + } + + /** + * @return mixed + */ + public function Value() + { + return $this->mValue; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $mValue = $this->mValue; + if ($this->encrypted && $mValue) try { + $mValue = \SnappyMail\Crypt::DecryptFromJSON($mValue, \APP_SALT); + } catch (\Throwable $e) { + } + return array( + '@Object' => 'Object/PluginProperty', + 'value' => $mValue, + 'placeholder' => $this->sPlaceholder, + 'name' => $this->sName, + 'type' => $this->iType, + 'label' => $this->sLabel, + 'default' => $this->mDefaultValue, + 'options' => $this->aOptions, + 'desc' => $this->sDesc + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/PropertyCollection.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/PropertyCollection.php new file mode 100644 index 0000000000..f8f2cd26f4 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/PropertyCollection.php @@ -0,0 +1,34 @@ +sLabel = $sLabel; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( + '@Object' => 'Object/PluginProperty', + 'type' => \RainLoop\Enumerations\PluginPropertyType::GROUP, + 'label' => $this->sLabel, + 'config' => $this->getArrayCopy() +/* + 'config' => [ + '@Object' => 'Collection/PropertyCollection', + '@Collection' => $this->getArrayCopy(), + ] +*/ + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AbstractProvider.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AbstractProvider.php new file mode 100644 index 0000000000..78ee2bfdb6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AbstractProvider.php @@ -0,0 +1,10 @@ +oDriver = $oDriver; + } + + public function Test() : string + { + \sleep(1); + return $this->oDriver ? $this->oDriver->Test() : 'Personal address book driver is not allowed'; + } + + public function IsActive() : bool + { + return $this->oDriver && $this->oDriver->IsSupported(); + } + + public function Sync() : bool + { + return $this->IsActive() ? $this->oDriver->Sync() : false; + } + + public function Export(string $sType = 'vcf') : bool + { + return $this->IsActive() ? $this->oDriver->Export($sType) : false; + } + + public function ContactSave(AddressBook\Classes\Contact $oContact) : bool + { + return $this->IsActive() ? $this->oDriver->ContactSave($oContact) : false; + } + + public function DeleteContacts(array $aContactIds) : bool + { + return $this->IsActive() ? $this->oDriver->DeleteContacts($aContactIds) : false; + } + + public function DeleteAllContacts(string $sEmail) : bool + { + return $this->IsActive() ? $this->oDriver->DeleteAllContacts($sEmail) : false; + } + + public function GetContacts(int $iOffset = 0, int $iLimit = 20, string $sSearch = '', int &$iResultCount = 0) : array + { + return $this->IsActive() ? $this->oDriver->GetContacts( + \max(0, $iOffset), + 0 < $iLimit ? $iLimit : 20, + \trim($sSearch), + $iResultCount + ) : array(); + } + + public function GetContactByEmail(string $sEmail) : ?AddressBook\Classes\Contact + { + return $this->IsActive() ? $this->oDriver->GetContactByEmail($sEmail) : null; + } + + public function GetContactByID($mID, bool $bIsStrID = false) : ?AddressBook\Classes\Contact + { + return $this->IsActive() ? $this->oDriver->GetContactByID($mID, $bIsStrID) : null; + } + + /** + * @throws \InvalidArgumentException + */ + public function GetSuggestions(string $sSearch, int $iLimit = 20) : array + { + return $this->IsActive() ? $this->oDriver->GetSuggestions($sSearch, $iLimit) : array(); + } + + public function IncFrec(array $aEmails, bool $bCreateAuto = true) : bool + { + return $this->IsActive() ? $this->oDriver->IncFrec($aEmails, $bCreateAuto) : false; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php new file mode 100644 index 0000000000..564a0d48b3 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php @@ -0,0 +1,36 @@ + 0]; + + public function setDAVClientConfig(?array $aConfig) + { + if (isset($aConfig['User'], $aConfig['Password'], $aConfig['Url']) && !empty($aConfig['Mode'])) { + $this->aDAVConfig = $aConfig; + } else { + $this->aDAVConfig = ['Mode' => 0]; + } + } + + protected function isDAVReadWrite() + { + return 1 == $this->aDAVConfig['Mode']; + } + + protected function prepareDavSyncData(DAVClient $oClient, string $sPath) + { + $mResult = false; + $aResponse = null; + try + { + $aResponse = $oClient->propFind($sPath, array( + '{DAV:}getlastmodified', + '{DAV:}resourcetype', + '{DAV:}getetag' + ), 1); + } + catch (\Throwable $oException) + { + $this->logException($oException); + } + + /** + * find every *.vcf with empty + */ + if (\is_array($aResponse)) { + $mResult = array(); + foreach ($aResponse as $sKey => $aItem) { + $sKey = \rtrim(\trim($sKey), '\\/'); + if (!empty($sKey) && is_array($aItem)) { + if (isset($aItem['{DAV:}getetag'])) { + $aMatch = array(); + if (\preg_match('/\/([^\/?]+)$/', $sKey, $aMatch) && !empty($aMatch[1]) + && !static::hasDAVCollection($aItem)) + { + $sVcfFileName = \urldecode(\urldecode($aMatch[1])); + $sKeyID = \preg_replace('/\.vcf$/i', '', $sVcfFileName); + + $mResult[$sKeyID] = array( + 'deleted' => false, + 'uid' => $sKeyID, + 'vcf' => $sVcfFileName, + 'etag' => \trim(\trim($aItem['{DAV:}getetag']), '"\''), + 'lastmodified' => '', + 'changed' => 0 + ); + + if (isset($aItem['{DAV:}getlastmodified'])) { + $mResult[$sKeyID]['lastmodified'] = $aItem['{DAV:}getlastmodified']; + $mResult[$sKeyID]['changed'] = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString( + $aItem['{DAV:}getlastmodified']); + } else { + $mResult[$sKeyID]['changed'] = \MailSo\Base\DateTimeHelper::TryToParseSpecEtagFormat($mResult[$sKeyID]['etag']); + $mResult[$sKeyID]['lastmodified'] = 0 < $mResult[$sKeyID]['changed'] ? + \gmdate('c', $mResult[$sKeyID]['changed']) : ''; + } + + $mResult[$sKeyID]['changed_'] = \gmdate('c', $mResult[$sKeyID]['changed']); + } + } + } + } + } + + return $mResult; + } + + protected function davClientRequest(DAVClient $oClient, string $sCmd, string $sUrl, $mData = null) : ?\SnappyMail\HTTP\Response + { + \MailSo\Base\Utils::ResetTimeLimit(); + + $this->logWrite($sCmd.' '.$sUrl.(('PUT' === $sCmd || 'POST' === $sCmd) && null !== $mData ? ' ('.\strlen($mData).')' : ''), + \LOG_INFO, 'DAV'); + + try + { + if (('PUT' === $sCmd || 'POST' === $sCmd) && null !== $mData) { + return $oClient->request($sCmd, $sUrl, $mData, array( + 'Content-Type' => 'text/vcard; charset=utf-8' + )); + } + return $oClient->request($sCmd, $sUrl); +// if ('GET' === $sCmd) { +// $this->oLogger->WriteDump($aResponse, \LOG_INFO, 'DAV'); +// } + } + catch (\Throwable $oException) + { + $this->logException($oException); + } + + return null; + } + + private function detectionPropFind(DAVClient $oClient, string $sPath) : ?array + { + try + { + return $oClient->propFind($sPath, array( + '{DAV:}current-user-principal', + '{DAV:}resourcetype', + '{DAV:}displayname', + '{urn:ietf:params:xml:ns:carddav}addressbook-home-set' + ), 1); + } + catch (\Throwable $oException) + { + $this->logException($oException); + } + + return null; + } + + private function getContactsPaths(DAVClient $oClient, string $sPath, string $sUser, + #[\SensitiveParameter] + string $sPassword, + string $sProxy = '') : array + { + $aContactsPaths = array(); + + $sCurrentUserPrincipal = ''; + $sAddressbookHomeSet = ''; + + $aResponse = $this->detectionPropFind($oClient, '/.well-known/carddav') + ?: $this->detectionPropFind($oClient, $sPath); + + $sNextPath = ''; + $sFirstNextPath = ''; + if (\is_array($aResponse)) { + foreach ($aResponse as $sPropPath => $aItem) { + if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']) + && false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'], '/calendar-proxy')) + { + $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']; + continue; + } + + if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal']['{DAV:}href'])) { + $sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal']['{DAV:}href']; + continue; + } + + if (!empty($sPropPath)) { + if (empty($sFirstNextPath)) { + $sFirstNextPath = $sPropPath; + } + + if (empty($sNextPath)) { + if (static::hasDAVCollection($aItem)) { + $sNextPath = $sPropPath; + continue; + } + } + } + } + + if (empty($sNextPath) && empty($sCurrentUserPrincipal) && empty($sAddressbookHomeSet) && !empty($sFirstNextPath)) { + $sNextPath = $sFirstNextPath; + } + } + + if (empty($sCurrentUserPrincipal) && empty($sAddressbookHomeSet)) { + if (empty($sNextPath)) { + return $aContactsPaths; + } + if (\preg_match('/^http[s]?:\/\//i', $sNextPath)) { + $oClient = $this->getDavClientFromUrl($sNextPath, $sUser, $sPassword, $sProxy); + if (!$oClient) { + return $aContactsPaths; + } + $sNextPath = $oClient->urlPath; + } + + if ($sPath != $sNextPath) { + $aResponse = $this->detectionPropFind($oClient, $sNextPath); + if (\is_array($aResponse)) { + foreach ($aResponse as $aItem) { + if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']) && + false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'], '/calendar-proxy')) + { + $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']; + continue; + } + + if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal']['{DAV:}href'])) { + $sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal']['{DAV:}href']; + continue; + } + } + } + } + } + + if (empty($sAddressbookHomeSet)) { + if (empty($sCurrentUserPrincipal)) { + return $aContactsPaths; + } + if (\preg_match('/^http[s]?:\/\//i', $sCurrentUserPrincipal)) { + $oClient = $this->getDavClientFromUrl($sCurrentUserPrincipal, $sUser, $sPassword, $sProxy); + if (!$oClient) { + return $aContactsPaths; + } + $sCurrentUserPrincipal = $oClient->urlPath; + } + + $aResponse = $this->detectionPropFind($oClient, $sCurrentUserPrincipal); + if (\is_array($aResponse)) { + foreach ($aResponse as $aItem) { + if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']) && + false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'], '/calendar-proxy')) + { + $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']; + continue; + } + } + } + } + + if (empty($sAddressbookHomeSet)) { + return $aContactsPaths; + } + if (\preg_match('/^http[s]?:\/\//i', $sAddressbookHomeSet)) { + $oClient = $this->getDavClientFromUrl($sAddressbookHomeSet, $sUser, $sPassword, $sProxy); + if (!$oClient) { + return $aContactsPaths; + } + $sAddressbookHomeSet = $oClient->urlPath; + } + + $aResponse = $this->detectionPropFind($oClient, $sAddressbookHomeSet); + if (\is_array($aResponse)) { + foreach ($aResponse as $sPropPath => $aItem) { + if (!empty($sPropPath) && static::hasDAVCollection($aItem) + && \in_array('{urn:ietf:params:xml:ns:carddav}addressbook', $aItem['{DAV:}resourcetype'])) + { + $aContactsPaths[$sPropPath] = isset($aItem['{DAV:}displayname']) ? \trim($aItem['{DAV:}displayname']) : ''; + } + } + } + + return $aContactsPaths; + } + + /** + * Checks if remote path resourcetype is an addressbook + */ + private function checkContactsPath(DAVClient $oClient, string $sPath) : bool + { + $aResponse = $oClient->propFind($sPath, array( + '{DAV:}resourcetype' + ), 1); + + $bGood = false; + if (\is_array($aResponse)) { + foreach ($aResponse as $sKey => $aItem) { + if (!empty($sKey) && static::hasDAVCollection($aItem) + && \in_array('{urn:ietf:params:xml:ns:carddav}addressbook', $aItem['{DAV:}resourcetype'])) + { + $bGood = true; + } + } + if ($bGood) { + $oClient->urlPath = $sPath; + } + } + + return $bGood; + } + + private function getDavClientFromUrl(string $sUrl, string $sUser, + #[\SensitiveParameter] + string $sPassword, + string $sProxy = '' + ) : DAVClient + { + if (!\preg_match('/^http[s]?:\/\//i', $sUrl)) { + $sUrl = \preg_replace('/^fruux\.com/i', 'dav.fruux.com', $sUrl); + $sUrl = \preg_replace('/^icloud\.com/i', 'contacts.icloud.com', $sUrl); + $sUrl = \preg_replace('/^gmail\.com/i', 'google.com', $sUrl); + $sUrl = 'https://'.$sUrl; + } + + $aUrl = \parse_url($sUrl); + if (!\is_array($aUrl)) { + $aUrl = array(); + } + + $aUrl['scheme'] = $aUrl['scheme'] ?? 'http'; + $aUrl['host'] = $aUrl['host'] ?? 'localhost'; + $aUrl['port'] = $aUrl['port'] ?? 0; + $aUrl['path'] = isset($aUrl['path']) ? \rtrim($aUrl['path'], '\\/').'/' : '/'; + + $aSettings = array( + 'baseUri' => $aUrl['scheme'].'://'.$aUrl['host'].($aUrl['port'] ? ':'.$aUrl['port'] : ''), + 'userName' => $sUser, + 'password' => $sPassword + ); + + $this->logMask($sPassword); + + if (!empty($sProxy)) { + $aSettings['proxy'] = $sProxy; + } + + $oClient = new DAVClient($aSettings); + $oClient->setVerifyPeer(false); + + $oClient->urlPath = $aUrl['path']; + + $this->logWrite('DavClient: User: '.$aSettings['userName'].', Url: '.$sUrl, \LOG_INFO, 'DAV'); + + return $oClient; + } + + public function getDavClient() : ?DAVClient + { + if (!$this->aDAVConfig['Mode']) { + return null; + } + $sUrl = $this->aDAVConfig['Url']; + $sUser = $this->aDAVConfig['User']; + $sPassword = $this->aDAVConfig['Password']; + $sProxy = ''; + + $aMatch = array(); + $sUserAddressBookNameName = ''; + + if (\preg_match('/\|(.+)$/', $sUrl, $aMatch) && !empty($aMatch[1])) { + $sUserAddressBookNameName = \trim($aMatch[1]); + $sUserAddressBookNameName = \mb_strtolower($sUserAddressBookNameName); + + $sUrl = \preg_replace('/\|(.+)$/', '', $sUrl); + } + + $oClient = $this->getDavClientFromUrl($sUrl, $sUser, $sPassword, $sProxy); + + $sPath = $oClient->urlPath; + + $bGood = true; + if ('' === $sPath || '/' === $sPath || !$this->checkContactsPath($oClient, $sPath)) { + /** + * Path is not an addressbook, try to find it + */ + $aPaths = $this->getContactsPaths($oClient, $sPath, $sUser, $sPassword, $sProxy); + $this->oLogger->WriteDump($aPaths); + + $sNewPath = ''; + if (\is_array($aPaths)) { + if (1 < \count($aPaths)) { + if ('' !== $sUserAddressBookNameName) { + foreach ($aPaths as $sKey => $sValue) { + $sValue = \mb_strtolower(\trim($sValue)); + if ($sValue === $sUserAddressBookNameName) { + $sNewPath = $sKey; + break; + } + } + } + + if (empty($sNewPath)) { + foreach ($aPaths as $sKey => $sValue) { + $sValue = \mb_strtolower($sValue); + if (\in_array($sValue, array('contacts', 'default', 'addressbook', 'address book'))) { + $sNewPath = $sKey; + break; + } + } + } + } + + if (empty($sNewPath)) { + foreach ($aPaths as $sKey => $sValue) { + $sNewPath = $sKey; + break; + } + } + } + + $bGood = $sNewPath && $this->checkContactsPath($oClient, $sNewPath); + if (!$bGood) { + throw new \RainLoop\Exceptions\ClientException( + \RainLoop\Notifications::ContactsSyncError, + null, + 'Contacts path not found at: '.$sPath + ); + } + } + + return $bGood ? $oClient : null; + } + + private static function hasDAVCollection($aItem) + { + return !empty($aItem['{DAV:}resourcetype']) + && \is_array($aItem['{DAV:}resourcetype']) + && \in_array('{DAV:}collection', $aItem['{DAV:}resourcetype']); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Contact.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Contact.php new file mode 100644 index 0000000000..8d08f28be0 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Contact.php @@ -0,0 +1,91 @@ +Changed = \time(); + } + + function __get($k) + { + if ('vCard' === $k) { + return $this->vCard; + } + } + +/* + public function GetEmails() : array + { + $aResult = array(); + foreach ($this->vCard->EMAIL as $oProperty) { + $aResult[] = $oProperty->Value; + } + return \array_unique($aResult); + } +*/ + public function setVCard(VCard $oVCard) : void + { + if ($oVCard->PHOTO && !empty($oVCard->PHOTO->parameters['ENCODING'])) { + $oVCard->VERSION = '3.0'; + } + if (VCard::VCARD40 != $oVCard->getDocumentType()) { + $oVCard = $oVCard->convert(VCard::VCARD40); + } + + // KDE KAddressBook entry and used by SnappyMail + // https://github.com/sabre-io/vobject/issues/589 + $oVCard->select('X-CRYPTO') + || $oVCard->add('X-CRYPTO', '', [ + 'allowed' => 'PGP/INLINE,PGP/MIME,S/MIME,S/MIMEOpaque', + 'signpref' => 'Ask', + 'encryptpref' => 'Ask' + ]); + + $aWarnings = $oVCard->validate(3); +// \error_log(\print_r($aWarnings,1)); + $this->vCard = $oVCard; + $sUid = (string) $oVCard->UID; + if (empty($sUid)) { + $sUid = \SnappyMail\UUID::generate(); +// $this->vCard->UID = $sUid; + } +/* + $rev = new \DateTime($oVCard->REV[0]->getJsonValue()); + $this->Changed = $rev->getTimestamp(); +*/ + $this->IdContactStr = $sUid; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( + '@Object' => 'Object/Contact', + 'id' => $this->id, + 'readOnly' => $this->ReadOnly, + 'jCard' => $this->vCard + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Property.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Property.php new file mode 100644 index 0000000000..9a496d3ac7 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Property.php @@ -0,0 +1,46 @@ +Type = $iType; + $this->Value = $sValue; + $this->TypeStr = $sTypeStr; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + throw new \Exception('Obsolete ' . __CLASS__ . '::jsonSerialize()'); + } +} diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Enumerations/PropertyType.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Enumerations/PropertyType.php similarity index 91% rename from rainloop/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Enumerations/PropertyType.php rename to snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Enumerations/PropertyType.php index 5b1bdae944..8fc64c5d2c 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Enumerations/PropertyType.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Enumerations/PropertyType.php @@ -2,7 +2,7 @@ namespace RainLoop\Providers\AddressBook\Enumerations; -class PropertyType +abstract class PropertyType { const UNKNOWN = 0; @@ -32,4 +32,6 @@ class PropertyType const NOTE = 110; const CUSTOM = 250; + + const JCARD = 251; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Legacy.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Legacy.php new file mode 100644 index 0000000000..6a125ebfd3 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Legacy.php @@ -0,0 +1,109 @@ +getValue()); + if (\strlen($sValue)) { + $oTypes = $oProp['TYPE']; + $aTypes = $oTypes ? $oTypes->getParts() : array(); + $pref = empty($oProp['PREF']) ? 100 : \min(100, \max(1, $oProp['PREF']->getValue())); + $pref = \str_pad($pref, 3, '0', \STR_PAD_LEFT); + $aTmp[$pref . $sValue] = new Property($iType, $sValue, \implode(',', $aTypes)); + } + } + \ksort($aTmp); + foreach ($aTmp as $oProp) { + yield $oProp; + } + } + + /** + * @param mixed $oProp + */ + private static function getPropertyValueHelper($oProp, bool $bOldVersion) : string + { + $sValue = \trim($oProp); + if ($bOldVersion && !isset($oProp->parameters['CHARSET'])) { + if (\strlen($sValue)) { + $sEncValue = \mb_convert_encoding($sValue, 'UTF-8', 'ISO-8859-1'); + if (\strlen($sEncValue)) { + $sValue = $sEncValue; + } + } + } + return \MailSo\Base\Utils::Utf8Clear($sValue); + } + + public static function VCardToProperties(\Sabre\VObject\Component\VCard $oVCard) : iterable + { + yield new Property(PropertyType::JCARD, \json_encode($oVCard)); + + $bOldVersion = !empty($oVCard->VERSION) && \in_array((string) $oVCard->VERSION, array('2.1', '2.0', '1.0')); + + if (isset($oVCard->FN) && '' !== \trim($oVCard->FN)) { + $sValue = static::getPropertyValueHelper($oVCard->FN, $bOldVersion); + yield new Property(PropertyType::FULLNAME, $sValue); + } + + if (isset($oVCard->N)) { + $aNames = $oVCard->N->getParts(); + foreach ($aNames as $iIndex => $sValue) { + $sValue = \trim($sValue); + if ($bOldVersion && !isset($oVCard->N->parameters['CHARSET'])) { + if (\strlen($sValue)) { + $sEncValue = \mb_convert_encoding($sValue, 'UTF-8', 'ISO-8859-1'); + if (\strlen($sEncValue)) { + $sValue = $sEncValue; + } + } + } + $sValue = \MailSo\Base\Utils::Utf8Clear($sValue); + if ($sValue) { + switch ($iIndex) { + case 0: + yield new Property(PropertyType::LAST_NAME, $sValue); + break; + case 1: + yield new Property(PropertyType::FIRST_NAME, $sValue); + break; + case 2: + yield new Property(PropertyType::MIDDLE_NAME, $sValue); + break; + case 3: + yield new Property(PropertyType::NAME_PREFIX, $sValue); + break; + case 4: + yield new Property(PropertyType::NAME_SUFFIX, $sValue); + break; + } + } + } + } + + if (isset($oVCard->EMAIL)) { + yield from static::yieldPropertyHelper($oVCard->EMAIL, PropertyType::EMAIl); + } + + if (isset($oVCard->URL)) { + yield from static::yieldPropertyHelper($oVCard->URL, PropertyType::WEB_PAGE); + } + + if (isset($oVCard->TEL)) { + yield from static::yieldPropertyHelper($oVCard->TEL, PropertyType::PHONE); + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php new file mode 100644 index 0000000000..155ea781d6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php @@ -0,0 +1,1209 @@ +driver = static::validPdoType($oConfig->Get('contacts', 'type', 'sqlite')); + if ('sqlite' === $oSettings->driver) { + $sDsn = 'sqlite:' . APP_PRIVATE_DATA . 'AddressBook.sqlite'; + if (!$oConfig->Get('contacts', 'sqlite_global', \is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite'))) { + $oAccount = \RainLoop\Api::Actions()->getMainAccountFromToken(false); + if ($oAccount) { + $homedir = \RainLoop\Api::Actions()->StorageProvider()->GenerateFilePath( + $oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::ROOT + ); + // TODO: sync data on switch? +// if (!\is_file($homedir . 'AddressBook.sqlite') && \is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite')) { +// \copy(APP_PRIVATE_DATA . '/AddressBook.sqlite', $homedir . 'AddressBook.sqlite'); +// } + $sDsn = 'sqlite:' . $homedir . 'AddressBook.sqlite'; + } + } + } else { + $sDsn = \trim($oConfig->Get('contacts', 'pdo_dsn', '')); + $oSettings->user = \trim($oConfig->Get('contacts', 'pdo_user', '')); + $oSettings->password = (string)$oConfig->Get('contacts', 'pdo_password', ''); + $sDsn = $oSettings->driver . ':' . \preg_replace('/^[a-z]+:/', '', $sDsn); + if ('mysql' === $oSettings->driver) { + $oSettings->sslCa = \trim($oConfig->Get('contacts', 'mysql_ssl_ca', '')); + $oSettings->sslVerify = !!$oConfig->Get('contacts', 'mysql_ssl_verify', true); + $oSettings->sslCiphers = \trim($oConfig->Get('contacts', 'mysql_ssl_ciphers', '')); + } + } + + $oSettings->dsn = $sDsn; + $this->settings = $oSettings; + + $this->bExplain = false; // debug + } + + public static function validPdoType(string $sType): string + { + $sType = \trim($sType); + return \in_array($sType, static::getAvailableDrivers()) ? $sType : 'sqlite'; + } + + public function IsSupported() : bool + { + $aDrivers = static::getAvailableDrivers(); + return \is_array($aDrivers) && \in_array($this->settings->driver, $aDrivers); + } + + public function SetEmail(string $sEmail) : bool + { + $this->iUserID = $this->getUserId($sEmail); + return 0 < $this->iUserID; + } + + private function flushDeletedContacts() : bool + { + return !!$this->prepareAndExecute('DELETE FROM rainloop_ab_contacts WHERE id_user = :id_user AND deleted = 1', array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT) + )); + } + + private function prepareDatabaseSyncData() : array + { + $aResult = array(); + $oStmt = $this->prepareAndExecute('SELECT id_contact, id_contact_str, changed, deleted, etag + FROM rainloop_ab_contacts + WHERE id_user = :id_user + ORDER BY deleted DESC', + array(':id_user' => array($this->iUserID, \PDO::PARAM_INT)) + ); + + if ($oStmt) { + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + if (\is_array($aFetch) && \count($aFetch)) { + foreach ($aFetch as $aItem) { + if ($aItem && isset($aItem['id_contact'], $aItem['id_contact_str'], $aItem['changed'], $aItem['deleted'], $aItem['etag']) && + !empty($aItem['id_contact_str'])) { + $sKeyID = $aItem['id_contact_str']; + + $aResult[$sKeyID] = array( + 'deleted' => !empty($aItem['deleted']), + 'id_contact' => $aItem['id_contact'], + 'uid' => $sKeyID, + 'etag' => $aItem['etag'], + 'changed' => (int) $aItem['changed'] + ); + } + } + } + } + + return $aResult; + } + + public function Sync() : bool + { + if (1 > $this->iUserID) { + \SnappyMail\Log::warning('PdoAddressBook', 'Sync() invalid $iUserID'); + return false; + } + + try { + $oClient = $this->getDavClient(); + } catch (\Throwable $e) { + \SnappyMail\Log::error('DAV', $e->getMessage()); +// $this->logException($oException); + } + if (!$oClient) { + \SnappyMail\Log::warning('PdoAddressBook', 'Sync() invalid DavClient'); + return false; + } + + $sPath = $oClient->urlPath; + + $time = \microtime(true); + $aRemoteSyncData = $this->prepareDavSyncData($oClient, $sPath); + if (false === $aRemoteSyncData) { + \SnappyMail\Log::info('PdoAddressBook', 'Sync() no data to sync'); + return false; + } + $time = \microtime(true) - $time; + \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Received ".\count($aRemoteSyncData)." remote contacts in {$time} seconds"]); + \SnappyMail\Log::info('PdoAddressBook', \count($aRemoteSyncData) . ' remote contacts'); + + $aLocalSyncData = $this->prepareDatabaseSyncData(); + \SnappyMail\Log::info('PdoAddressBook', \count($aLocalSyncData) . ' local contacts'); + +// $this->oLogger->WriteDump($aRemoteSyncData); +// $this->oLogger->WriteDump($aLocalSyncData); + + $bReadWrite = $this->isDAVReadWrite(); + + // Delete remote when Mode = read + write + if ($bReadWrite) { + \SnappyMail\Log::info('PdoAddressBook', 'Sync() is import and export'); + $iCount = 0; + foreach ($aLocalSyncData as $sKey => $aData) { + if ($aData['deleted']) { + ++$iCount; + unset($aLocalSyncData[$sKey]); + if (isset($aRemoteSyncData[$sKey], $aRemoteSyncData[$sKey]['vcf'])) { + \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Delete remote {$sKey}"]); + $this->davClientRequest($oClient, 'DELETE', $sPath.$aRemoteSyncData[$sKey]['vcf']); + } + } + } + if ($iCount) { + \SnappyMail\Log::info('PdoAddressBook', $iCount . ' remote contacts removed'); + } + } else { + \SnappyMail\Log::info('PdoAddressBook', 'Sync() is import only'); + } + + // Delete local + $aIdsForDeletion = array(); + foreach ($aLocalSyncData as $sKey => $aData) { + if (!empty($aData['etag']) && !isset($aRemoteSyncData[$sKey])) { + $aIdsForDeletion[] = $aData['id_contact']; + } + } + if (\count($aIdsForDeletion)) { + \SnappyMail\HTTP\Stream::JSON(['messsage'=>'Delete local ' . \implode(', ', $aIdsForDeletion)]); + $this->DeleteContacts($aIdsForDeletion); + \SnappyMail\Log::info('PdoAddressBook', \count($aIdsForDeletion) . ' local contacts removed'); + unset($aIdsForDeletion); + } + + $this->flushDeletedContacts(); + + // local is new or newer + if ($bReadWrite) { + foreach ($aLocalSyncData as $sKey => $aData) { + if ((empty($aData['etag']) && !isset($aRemoteSyncData[$sKey])) // new + // newer + || (!empty($aData['etag']) && isset($aRemoteSyncData[$sKey]) && + $aRemoteSyncData[$sKey]['etag'] !== $aData['etag'] && + $aRemoteSyncData[$sKey]['changed'] < $aData['changed'] + ) + ) { + \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Update remote {$sKey}"]); + $mID = $aData['id_contact']; + $oContact = $this->GetContactByID($mID); + if ($oContact) { + $sRemoteID = isset($aRemoteSyncData[$sKey]['vcf']) && !empty($aData['etag']) + ? $aRemoteSyncData[$sKey]['vcf'] : ''; + \SnappyMail\Log::info('PdoAddressBook', "Update contact {$sKey} in DAV"); + $oResponse = $this->davClientRequest($oClient, 'PUT', + $sPath . ($sRemoteID ?: $oContact->IdContactStr.'.vcf'), + $oContact->vCard->serialize() . "\r\n\r\n"); + if ($oResponse) { + $sEtag = \trim(\trim($oResponse->getHeader('etag')), '"\''); + $sDate = \trim($oResponse->getHeader('date')); + if (!empty($sEtag)) { + $iChanged = empty($sDate) ? \time() : \MailSo\Base\DateTimeHelper::ParseRFC2822DateString($sDate); + $this->prepareAndExecute('UPDATE rainloop_ab_contacts SET changed = :changed, etag = :etag '. + 'WHERE id_user = :id_user AND id_contact = :id_contact', array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':id_contact' => array($mID, \PDO::PARAM_INT), + ':changed' => array($iChanged, \PDO::PARAM_INT), + ':etag' => array($sEtag, \PDO::PARAM_STR) + ) + ); + } + } else { + \SnappyMail\Log::warning('PdoAddressBook', "Update/create remote failed"); + } + } else { + \SnappyMail\Log::warning('PdoAddressBook', "Local contact {$sKey} not found"); + } + unset($oContact); + } + } + } + + // remote is new or newer + foreach ($aRemoteSyncData as $sKey => $aData) { + if (!isset($aLocalSyncData[$sKey]) // new + // newer + || ($aLocalSyncData[$sKey]['etag'] !== $aData['etag'] && $aLocalSyncData[$sKey]['changed'] < $aData['changed']) + ) { + \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Update local {$sKey}"]); + + $oVCard = null; + + $oResponse = $this->davClientRequest($oClient, 'GET', $sPath.$aData['vcf']); + if ($oResponse) { + $sBody = \trim($oResponse->body); + + // Remove UTF-8 BOM + if ("\xef\xbb\xbf" === \substr($sBody, 0, 3)) { + $sBody = \substr($sBody, 3); + } + + if (!empty($sBody)) { + try { + $oVCard = \Sabre\VObject\Reader::read($sBody); + } catch (\Throwable $oExc) { + $this->logException($oExc); + $this->oLogger && $this->oLogger->WriteDump($sBody); + } + } + } + + if ($oVCard instanceof VCard) { + $oVCard->UID = $aData['uid']; + + $oContact = empty($aLocalSyncData[$sKey]['id_contact']) + ? null + : $this->GetContactByID($aLocalSyncData[$sKey]['id_contact']); + if ($oContact) { + \SnappyMail\Log::info('PdoAddressBook', "Update local contact {$sKey}"); + } else { + \SnappyMail\Log::info('PdoAddressBook', "Create local contact {$sKey}"); + $oContact = new Contact(); + } + + $oContact->setVCard($oVCard); + + $sEtag = \trim($oResponse->getHeader('etag'), " \n\r\t\v\x00\"'"); + if (!empty($sEtag)) { + $oContact->Etag = $sEtag; + } + + $this->ContactSave($oContact); + unset($oContact); + } else { + \SnappyMail\Log::error('PdoAddressBook', "Import remote contact {$sKey} failed"); + } + } + } + + return true; + } + + public function Export(string $sType = 'vcf') : bool + { + if (1 > $this->iUserID) { + \SnappyMail\Log::warning('PdoAddressBook', 'Export() invalid $iUserID'); + return false; + } + + $rCsv = 'csv' === $sType ? \fopen('php://output', 'w') : null; + $bCsvHeader = true; + + $aDatabaseSyncData = $this->prepareDatabaseSyncData(); + if (\count($aDatabaseSyncData)) { + foreach ($aDatabaseSyncData as $mData) { + try { +// if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) { + if ($mData && !empty($mData['id_contact'])) { + $oContact = $this->GetContactByID($mData['id_contact']); + if ($oContact) { + if ($rCsv) { + Utils::VCardToCsv($rCsv, $oContact->vCard, $bCsvHeader); + $bCsvHeader = false; + } else { + echo $oContact->vCard->serialize(); + } + } + } + } catch (\Throwable $oExc) { + $this->logException($oExc); + } + } + } + + return true; + } + + public function ContactSave(Contact $oContact) : bool + { + if (1 > $this->iUserID) { + \SnappyMail\Log::warning('PdoAddressBook', 'ContactSave() invalid $iUserID'); + return false; + } + + $iIdContact = \strlen($oContact->id) && \is_numeric($oContact->id) ? (int) $oContact->id : 0; + + $bUpdate = 0 < $iIdContact; + + $oContact->Changed = \time(); +// $oContact->vCard->REV = \gmdate('Ymd\\THis\\Z', $oContact->Changed); +// $oContact->REV = \time(); + + try { + $sFullName = (string) $oContact->vCard->FN; + + $aFreq = array(); + if ($bUpdate) { + $aFreq = $this->getContactFreq($this->iUserID, $iIdContact); + + $this->prepareAndExecute('UPDATE rainloop_ab_contacts + SET id_contact_str = :id_contact_str, display = :display, changed = :changed, etag = :etag + WHERE id_user = :id_user AND id_contact = :id_contact', + array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':id_contact' => array($iIdContact, \PDO::PARAM_INT), + ':id_contact_str' => array($oContact->IdContactStr, \PDO::PARAM_STR), + ':display' => array($sFullName, \PDO::PARAM_STR), + ':changed' => array($oContact->Changed, \PDO::PARAM_INT), + ':etag' => array($oContact->Etag, \PDO::PARAM_STR) + ) + ); + + // clear previous props + $this->prepareAndExecute( + 'DELETE FROM rainloop_ab_properties WHERE id_user = :id_user AND id_contact = :id_contact', + array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':id_contact' => array($iIdContact, \PDO::PARAM_INT) + ) + ); + } else { + $this->prepareAndExecute('INSERT INTO rainloop_ab_contacts + ( id_user, id_contact_str, display, changed, etag) + VALUES + (:id_user, :id_contact_str, :display, :changed, :etag)', + array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':id_contact_str' => array($oContact->IdContactStr, \PDO::PARAM_STR), + ':display' => array($sFullName, \PDO::PARAM_STR), + ':changed' => array($oContact->Changed, \PDO::PARAM_INT), + ':etag' => array($oContact->Etag, \PDO::PARAM_STR) + ) + ); + + $sLast = $this->lastInsertId('rainloop_ab_contacts', 'id_contact'); + if (\is_numeric($sLast) && 0 < (int) $sLast) { + $iIdContact = (int) $sLast; + $oContact->id = (string) $iIdContact; + } + } + + if (0 < $iIdContact) { + $aParams = array(); + foreach (Legacy::VCardToProperties($oContact->vCard) as /* @var $oProp Classes\Property */ $oProp) { + $iFreq = $oProp->Frec; + if (PropertyType::EMAIl === $oProp->Type && isset($aFreq[$oProp->Value])) { + $iFreq = $aFreq[$oProp->Value]; + } + $aParams[] = array( + ':id_contact' => array($iIdContact, \PDO::PARAM_INT), + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':prop_type' => array($oProp->Type, \PDO::PARAM_INT), + ':prop_type_str' => array($oProp->TypeStr, \PDO::PARAM_STR), + ':prop_value' => array($oProp->Value, \PDO::PARAM_STR), + ':prop_value_lower' => array(\mb_strtolower($oProp->Value, 'UTF-8'), \PDO::PARAM_STR), + ':prop_value_custom' => array('', \PDO::PARAM_STR), + ':prop_frec' => array($iFreq, \PDO::PARAM_INT), + ); + } + if ($aParams) { + $this->prepareAndExecute('INSERT INTO rainloop_ab_properties '. + '( id_contact, id_user, prop_type, prop_type_str, prop_value, prop_value_lower, prop_value_custom, prop_frec)'. + ' VALUES '. + '(:id_contact, :id_user, :prop_type, :prop_type_str, :prop_value, :prop_value_lower, :prop_value_custom, :prop_frec)', + $aParams, + true + ); + } + } + } + catch (\Throwable $oException) { + throw $oException; + } + + return 0 < $iIdContact; + } + + public function DeleteContacts(array $aContactIds) : bool + { + if (1 > $this->iUserID) { + \SnappyMail\Log::warning('PdoAddressBook', 'DeleteContacts() invalid $iUserID'); + return false; + } + + $aContactIds = \array_filter(\array_map('intval', $aContactIds)); + + if (!\count($aContactIds)) { + return false; + } + + $sIDs = \implode(',', $aContactIds); + $aParams = array(':id_user' => array($this->iUserID, \PDO::PARAM_INT)); + + $this->prepareAndExecute('DELETE FROM rainloop_ab_properties WHERE id_user = :id_user AND id_contact IN ('.$sIDs.')', $aParams); + + $aParams = array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':changed' => array(\time(), \PDO::PARAM_INT) + ); + + $this->prepareAndExecute('UPDATE rainloop_ab_contacts SET deleted = 1, changed = :changed '. + 'WHERE id_user = :id_user AND id_contact IN ('.$sIDs.')', $aParams); + + return true; + } + + public function DeleteAllContacts(string $sEmail) : bool + { + $iUserID = $this->getUserId($sEmail); + + $aParams = array(':id_user' => array($iUserID, \PDO::PARAM_INT)); + + $this->prepareAndExecute('DELETE FROM rainloop_ab_properties WHERE id_user = :id_user', $aParams); + $this->prepareAndExecute('DELETE FROM rainloop_ab_contacts WHERE id_user = :id_user', $aParams); + + return true; + } + + protected function getContactsFromPDO(?\PDOStatement $oStmt) : array + { + if ($oStmt) { + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + + $aContacts = array(); + $aIdContacts = array(); + if (\is_array($aFetch) && \count($aFetch)) { + foreach ($aFetch as $aItem) { + $iIdContact = $aItem && isset($aItem['id_contact']) ? (int) $aItem['id_contact'] : 0; + if (0 < $iIdContact) { + $oContact = new Contact(); + $oContact->id = (string) $iIdContact; + $oContact->IdContactStr = (string) $aItem['id_contact_str']; +// $oContact->Display = (string) $aItem['display']; + $oContact->Changed = (int) $aItem['changed']; + $oContact->Etag = (int) $aItem['etag']; + if (!empty($aItem['jcard'])) { + $oContact->setVCard( + \Sabre\VObject\Reader::readJson($aItem['jcard']) + ); +// $oContact->vCard->FN = (string) $aItem['display']; +// $oContact->vCard->REV = \gmdate('Ymd\\THis\\Z', $oContact->Changed); + } else { + $aIdContacts[] = $iIdContact; + } + $aContacts[$iIdContact] = $oContact; + } + } + } + + unset($aFetch); + + // Build vCards using old RainLoop data (missing jCard) + if (\count($aIdContacts)) { + $oStmt->closeCursor(); + + $oStmt = $this->prepareAndExecute('SELECT * FROM rainloop_ab_properties + WHERE id_contact IN ('.\implode(',', $aIdContacts).') + ORDER BY id_contact ASC'); + if ($oStmt) { + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + if (\is_array($aFetch) && \count($aFetch)) { + $aVCards = array(); + $sUid = $sFirstName = $sLastName = $sMiddleName = $sSuffix = $sPrefix = ''; + $iPrevId = 0; + foreach ($aFetch as $aItem) { + if ($aItem && isset($aItem['id_prop'], $aItem['id_contact'], $aItem['prop_type'], $aItem['prop_value'])) { + $iId = (int) $aItem['id_contact']; + if (0 < $iId) { + if ($iPrevId != $iId) { + if ($iPrevId) { + $aVCards[$iPrevId]->UID = $sUid ?: \SnappyMail\UUID::generate(); + $aVCards[$iPrevId]->N = array($sLastName, $sFirstName, $sMiddleName, $sPrefix, $sSuffix); + } + $sUid = $sFirstName = $sLastName = $sMiddleName = $sSuffix = $sPrefix = ''; + $iPrevId = $iId; + } + if (isset($aVCards[$iId])) { + $oVCard = $aVCards[$iId]; + } else { + $oVCard = new VCard; +// $oVCard = $oVCard->convert(VCard::VCARD40); + $oVCard->VERSION = '4.0'; + $oVCard->PRODID = 'SnappyMail-'.APP_VERSION; + $aVCards[$iId] = $oVCard; + } + $oVCard = $aVCards[$iId]; + $sPropValue = (string) $aItem['prop_value']; + $aTypes = array(); + if (!empty($aItem['prop_type_str'])) { + $aTypes = \explode(',', \preg_replace('/[\s]+/', '', $aItem['prop_type_str'])); + } + switch ((int) $aItem['prop_type']) + { + case PropertyType::JCARD: + break; + + case PropertyType::FULLNAME: + $oVCard->FN = $sPropValue; + break; + case PropertyType::NICK_NAME: + $oVCard->NICKNAME = $sPropValue; + break; + case PropertyType::NOTE: + $oVCard->NOTE = $sPropValue; + break; + case PropertyType::UID: + $sUid = $sPropValue; + break; + case PropertyType::FIRST_NAME: + $sFirstName = $sPropValue; + break; + case PropertyType::LAST_NAME: + $sLastName = $sPropValue; + break; + case PropertyType::MIDDLE_NAME: + $sMiddleName = $sPropValue; + break; + case PropertyType::NAME_SUFFIX: + $sSuffix = $sPropValue; + break; + case PropertyType::NAME_PREFIX: + $sPrefix = $sPropValue; + break; + + case PropertyType::EMAIl: + $oVCard->add('EMAIL', $sPropValue, \is_array($aTypes) && \count($aTypes) ? array('TYPE' => $aTypes) : null); + break; + + case PropertyType::WEB_PAGE: + $oVCard->add('URL', $sPropValue, \is_array($aTypes) && \count($aTypes) ? array('TYPE' => $aTypes) : null); + break; + + case PropertyType::PHONE: + $oVCard->add('TEL', $sPropValue, \is_array($aTypes) && \count($aTypes) ? array('TYPE' => $aTypes) : null); + break; + } + } + } + } + if ($iPrevId) { + $aVCards[$iPrevId]->UID = $sUid ?: \SnappyMail\UUID::generate(); + $aVCards[$iPrevId]->N = array($sLastName, $sFirstName, $sMiddleName, $sPrefix, $sSuffix); + } + + foreach ($aVCards as $iId => $oVCard) { + $oVCard->REV = \gmdate('Ymd\\THis\\Z', $aContacts[$iId]->Changed); + $aContacts[$iId]->setVCard($oVCard); + } + } + + unset($aFetch); + } + } + + return \array_values($aContacts); + } + + return []; + } + + public function GetContacts(int $iOffset = 0, int $iLimit = 20, string $sSearch = '', int &$iResultCount = 0) : array + { + if (1 > $this->iUserID) { + return []; + } + + $aSearchIds = array(); + + if (\strlen($sSearch)) { + $sLowerSearch = $this->specialConvertSearchValueLower($sSearch, '='); + + $sSearchTypes = \implode(',', static::$aSearchInFields); + // PropertyType::PHONE, PropertyType::WEB_PAGE + + $sSql = 'SELECT DISTINCT id_contact FROM rainloop_ab_properties '. + 'WHERE (id_user = :id_user) AND prop_type IN ('.$sSearchTypes.') AND ('. + 'prop_value LIKE :search ESCAPE \'=\''. +(\strlen($sLowerSearch) ? ' OR (prop_value_lower <> \'\' AND prop_value_lower LIKE :search_lower ESCAPE \'=\')' : ''). + ')'; + + $aParams = array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':search' => array($this->specialConvertSearchValue($sSearch, '='), \PDO::PARAM_STR) + ); + + if (\strlen($sLowerSearch)) { + $aParams[':search_lower'] = array($sLowerSearch, \PDO::PARAM_STR); + } + + $oStmt = $this->prepareAndExecute($sSql, $aParams, false, true); + if ($oStmt) { + while ($aItem = $oStmt->fetch(\PDO::FETCH_NUM)) { + if (0 < $aItem[0]) { + $aSearchIds[] = (int) $aItem[0]; + } + } + $iResultCount = \count($aSearchIds); + } + } else { + $oStmt = $this->prepareAndExecute( + 'SELECT COUNT(*) FROM rainloop_ab_contacts WHERE id_user = :id_user', + [':id_user' => array($this->iUserID, \PDO::PARAM_INT)] + ); + if ($oStmt && $aItem = $oStmt->fetch(\PDO::FETCH_NUM)) { + $iResultCount = (int) $aItem[0]; + } + } + + if (0 < $iResultCount) { + $sSql = 'SELECT + c.id_contact, + c.id_contact_str, + c.display, + c.changed, + c.etag, + p.prop_value as jcard + FROM rainloop_ab_contacts AS c + LEFT JOIN rainloop_ab_properties AS p ON (p.id_contact = c.id_contact AND p.prop_type = :prop_type) + WHERE c.deleted = 0 AND c.id_user = :id_user'; + if (\count($aSearchIds)) { + $sSql .= ' AND c.id_contact IN ('.\implode(',', $aSearchIds).')'; + } + $sSql .= ' ORDER BY display ASC LIMIT :limit OFFSET :offset'; + + $aParams = array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':prop_type' => array(PropertyType::JCARD, \PDO::PARAM_INT), + ':limit' => array($iLimit, \PDO::PARAM_INT), + ':offset' => array($iOffset, \PDO::PARAM_INT) + ); + + return $this->getContactsFromPDO( + $this->prepareAndExecute($sSql, $aParams) + ); + } + + return []; + } + + /** + * @param mixed $mID + */ + public function GetContactByEmail(string $sEmail) : ?Contact + { + $sLowerSearch = $this->specialConvertSearchValueLower($sEmail); + + $sSql = 'SELECT + DISTINCT id_contact + FROM rainloop_ab_properties + WHERE id_user = :id_user + AND prop_type = '.PropertyType::JCARD.' + AND ('. + 'prop_value LIKE :search ESCAPE \'=\'' + . (\strlen($sLowerSearch) ? ' OR (prop_value_lower <> \'\' AND prop_value_lower LIKE :search_lower ESCAPE \'=\')' : ''). + ')'; + $aParams = array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':search' => array($this->specialConvertSearchValue($sEmail, '='), \PDO::PARAM_STR) + ); + if (\strlen($sLowerSearch)) { + $aParams[':search_lower'] = array($sLowerSearch, \PDO::PARAM_STR); + } + + $oContact = null; + $iIdContact = 0; + + $aContacts = $this->getContactsFromPDO( + $this->prepareAndExecute($sSql, $aParams) + ); + + return $aContacts ? $aContacts[0] : null; + } + + /** + * @param mixed $mID + */ + public function GetContactByID($mID, bool $bIsStrID = false) : ?Contact + { + $mID = \trim($mID); + + $sSql = 'SELECT + c.id_contact, + c.id_contact_str, + c.display, + c.changed, + c.etag, + p.prop_value as jcard + FROM rainloop_ab_contacts AS c + LEFT JOIN rainloop_ab_properties AS p ON (p.id_contact = c.id_contact AND p.prop_type = :prop_type) + WHERE c.id_user = :id_user AND c.deleted = 0'; + + $aParams = array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':prop_type' => array(PropertyType::JCARD, \PDO::PARAM_INT) + ); + + if ($bIsStrID) { + $sSql .= ' AND c.id_contact_str = :id_contact_str'; + $aParams[':id_contact_str'] = array($mID, \PDO::PARAM_STR); + } else { + $sSql .= ' AND c.id_contact = :id_contact'; + $aParams[':id_contact'] = array($mID, \PDO::PARAM_INT); + } + + $sSql .= ' LIMIT 1'; + + $oContact = null; + $iIdContact = 0; + + $aContacts = $this->getContactsFromPDO( + $this->prepareAndExecute($sSql, $aParams) + ); + + return $aContacts ? $aContacts[0] : null; + } + + /** + * @throws \ValueError + */ + public function GetSuggestions(string $sSearch, int $iLimit = 20) : array + { + if (1 > $this->iUserID) { + return []; + } + + $sSearch = \trim($sSearch); + if (!\strlen($sSearch)) { + throw new \ValueError('Empty Search argument'); + } + + $sTypes = \implode(',', static::$aSearchInFields); + + $sLowerSearch = $this->specialConvertSearchValueLower($sSearch); + + $sSql = 'SELECT id_contact, id_prop, prop_type, prop_value FROM rainloop_ab_properties '. + 'WHERE (id_user = :id_user) AND prop_type IN ('.$sTypes.') AND ('. + 'prop_value LIKE :search ESCAPE \'=\''. +(\strlen($sLowerSearch) ? ' OR (prop_value_lower <> \'\' AND prop_value_lower LIKE :search_lower ESCAPE \'=\')' : ''). + ')' + ; + + $aParams = array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':limit' => array($iLimit, \PDO::PARAM_INT), + ':search' => array($this->specialConvertSearchValue($sSearch, '='), \PDO::PARAM_STR) + ); + + if (\strlen($sLowerSearch)) { + $aParams[':search_lower'] = array($sLowerSearch, \PDO::PARAM_STR); + } + + $sSql .= ' ORDER BY prop_frec DESC'; + $sSql .= ' LIMIT :limit'; + + $aResult = array(); + + $oStmt = $this->prepareAndExecute($sSql, $aParams); + if ($oStmt) { + $aIdContacts = array(); + $aIdProps = array(); + $aContactAllAccess = array(); + + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + if (\is_array($aFetch) && \count($aFetch)) { + foreach ($aFetch as $aItem) { + $iIdContact = $aItem && isset($aItem['id_contact']) ? (int) $aItem['id_contact'] : 0; + $iIdProp = $aItem && isset($aItem['id_prop']) ? (int) $aItem['id_prop'] : 0; + $iType = $aItem && isset($aItem['prop_type']) ? (int) $aItem['prop_type'] : 0; + + if (0 < $iIdContact && 0 < $iIdProp) { + $aIdContacts[$iIdContact] = $iIdContact; + $aIdProps[$iIdProp] = $iIdProp; + + if (\in_array($iType, array(PropertyType::LAST_NAME, PropertyType::FIRST_NAME, PropertyType::NICK_NAME))) { + if (!isset($aContactAllAccess[$iIdContact])) { + $aContactAllAccess[$iIdContact] = array(); + } + + $aContactAllAccess[$iIdContact][] = $iType; + } + } + } + } + + unset($aFetch); + + $aIdContacts = \array_values($aIdContacts); + if (\count($aIdContacts)) { + $oStmt->closeCursor(); + + $sTypes = \implode(',', static::$aSearchInFields); + + $sSql = 'SELECT id_prop, id_contact, prop_type, prop_value FROM rainloop_ab_properties '. + 'WHERE prop_type IN ('.$sTypes.') AND id_contact IN ('.\implode(',', $aIdContacts).')'; + + $oStmt = $this->prepareAndExecute($sSql); + if ($oStmt) { + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + if (\is_array($aFetch) && \count($aFetch)) { + $aNames = array(); + $aEmails = array(); + + foreach ($aFetch as $aItem) { + if ($aItem && isset($aItem['id_prop'], $aItem['id_contact'], $aItem['prop_type'], $aItem['prop_value'])) { + $iIdContact = (int) $aItem['id_contact']; + $iIdProp = (int) $aItem['id_prop']; + $iType = (int) $aItem['prop_type']; + + if (PropertyType::NICK_NAME === $iType) { + $aNicks[$iIdContact] = $aItem['prop_value']; + } + else if (\in_array($iType, array(PropertyType::LAST_NAME, PropertyType::FIRST_NAME))) { + if (!isset($aNames[$iIdContact])) { + $aNames[$iIdContact] = array('', ''); + } + + $aNames[$iIdContact][PropertyType::FIRST_NAME === $iType ? 0 : 1] = $aItem['prop_value']; + } + else if ((isset($aIdProps[$iIdProp]) || isset($aContactAllAccess[$iIdContact])) && + PropertyType::EMAIl === $iType) { + if (!isset($aEmails[$iIdContact])) { + $aEmails[$iIdContact] = array(); + } + + $aEmails[$iIdContact][] = $aItem['prop_value']; + } + } + } + + foreach ($aEmails as $iId => $aItems) { + if (isset($aContactAllAccess[$iId])) { + $bName = \in_array(PropertyType::FIRST_NAME, $aContactAllAccess[$iId]) || \in_array(PropertyType::LAST_NAME, $aContactAllAccess[$iId]); + $bNick = \in_array(PropertyType::NICK_NAME, $aContactAllAccess[$iId]); + + $aNameItem = isset($aNames[$iId]) && \is_array($aNames[$iId]) ? $aNames[$iId] : array('', ''); + $sNameItem = \trim($aNameItem[0].' '.$aNameItem[1]); + + $sNickItem = isset($aNicks[$iId]) ? $aNicks[$iId] : ''; + + foreach ($aItems as $sEmail) { + if ($sEmail) { + if ($bName) { + $aResult[] = array($sEmail, $sNameItem); + } + else if ($bNick) { + $aResult[] = array($sEmail, $sNickItem); + } + else { + $aResult[] = array($sEmail, ''); + } + } + } + } + else { + $aNameItem = isset($aNames[$iId]) && \is_array($aNames[$iId]) ? $aNames[$iId] : array('', ''); + $sNameItem = \trim($aNameItem[0].' '.$aNameItem[1]); + if (0 === \strlen($sNameItem)) { + $sNameItem = isset($aNicks[$iId]) ? $aNicks[$iId] : ''; + } + + foreach ($aItems as $sEmail) { + $aResult[] = array($sEmail, $sNameItem); + } + } + } + } + + unset($aFetch); + + if ($iLimit < \count($aResult)) { + $aResult = \array_slice($aResult, 0, $iLimit); + } + + return $aResult; + } + } + } + + return array(); + } + + /** + * @throws \ValueError + */ + public function IncFrec(array $aEmails, bool $bCreateAuto = true) : bool + { + if (1 > $this->iUserID) { + return false; + } + + $self = $this; + $aEmailsObjects = \array_map(function ($mItem) { + $oResult = null; + try { + $oResult = \MailSo\Mime\Email::Parse(\trim($mItem)); + } + catch (\Throwable $oException) { + unset($oException); + } + return $oResult; + }, $aEmails); + + $aEmailsObjects = \array_filter($aEmailsObjects, function ($oItem) { + return !!$oItem; + }); + + if (!\count($aEmailsObjects)) { + throw new \ValueError('Empty Emails argument'); + } + + $aExists = array(); + $aEmailsToCreate = array(); + $aEmailsToUpdate = array(); + + if ($bCreateAuto) { + $sSql = 'SELECT prop_value FROM rainloop_ab_properties WHERE id_user = :id_user AND prop_type = :prop_type'; + $oStmt = $this->prepareAndExecute($sSql, array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':prop_type' => array(PropertyType::EMAIl, \PDO::PARAM_INT) + )); + + if ($oStmt) { + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + if (\is_array($aFetch) && \count($aFetch)) { + foreach ($aFetch as $aItem) { + if ($aItem && !empty($aItem['prop_value'])) { + $aExists[] = \mb_strtolower(\trim($aItem['prop_value'])); + } + } + } + } + + $aEmailsToCreate = \array_filter($aEmailsObjects, function ($oItem) use ($aExists, &$aEmailsToUpdate) { + if ($oItem) { + $sEmail = \trim($oItem->GetEmail(true)); + if (\strlen($sEmail)) { + $aEmailsToUpdate[] = $sEmail; + return !\in_array($sEmail, $aExists); + } + } + return false; + }); + } else { + foreach ($aEmailsObjects as $oItem) { + if ($oItem) { + $sEmailUpdate = \trim($oItem->GetEmail(true)); + if (\strlen($sEmailUpdate)) { + $aEmailsToUpdate[] = $sEmailUpdate; + } + } + } + } + + unset($aEmails, $aEmailsObjects); + + if (\count($aEmailsToCreate)) { + foreach ($aEmailsToCreate as $oEmail) { + $oVCard = new VCard; + $bValid = false; + if ('' !== \trim($oEmail->GetEmail())) { + $oVCard->add('EMAIL', \trim($oEmail->GetEmail(true))); + $bValid = true; + } + if ('' !== \trim($oEmail->GetDisplayName())) { + $sFirst = $sLast = ''; + $sFullName = $oEmail->GetDisplayName(); + if (false !== \strpos($sFullName, ' ')) { + $aNames = \explode(' ', $sFullName, 2); + $sFirst = isset($aNames[0]) ? $aNames[0] : ''; + $sLast = isset($aNames[1]) ? $aNames[1] : ''; + } else { + $sFirst = $sFullName; + } + if (\strlen($sFirst) || \strlen($sLast)) { + $oVCard->N = array($sLast, $sFirst, '', '', ''); + $bValid = true; + } + } + if ($bValid) { + $oContact = new Contact(); + $oContact->setVCard($oVCard); + $this->ContactSave($oContact); + } + } + } + + $sSql = 'UPDATE rainloop_ab_properties SET prop_frec = prop_frec + 1 WHERE id_user = :id_user AND prop_type = :prop_type'; + + $aEmailsQuoted = \array_map(function ($mItem) use ($self) { + return $self->quoteValue($mItem); + }, $aEmailsToUpdate); + + if (1 === \count($aEmailsQuoted)) { + $sSql .= ' AND prop_value = '.$aEmailsQuoted[0]; + } else { + $sSql .= ' AND prop_value IN ('.\implode(',', $aEmailsQuoted).')'; + } + + return !!$this->prepareAndExecute($sSql, array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':prop_type' => array(PropertyType::EMAIl, \PDO::PARAM_INT) + )); + } + + public function Test() : string + { + $sResult = ''; + try { + $this->SyncDatabase(); + if (0 >= $this->getVersion($this->settings->driver.'-ab-version')) { + $sResult = 'Unknown database error'; + } + } + catch (\Throwable $oException) { + $sResult = $oException->getMessage(); + if (!empty($sResult) && !\MailSo\Base\Utils::IsAscii($sResult) && !\MailSo\Base\Utils::IsUtf8($sResult)) { + $sResult = \mb_convert_encoding($sResult, 'UTF-8', 'ISO-8859-1'); + } + + if (!\is_string($sResult) || empty($sResult)) { + $sResult = 'Unknown database error'; + } + } + + return $sResult; + } + + private function SyncDatabase() : bool + { + static $mCache = null; + if (null !== $mCache) { + return $mCache; + } + + $mCache = false; + switch ($this->settings->driver) { + case 'mysql': + case 'pgsql': + case 'sqlite': + $mCache = $this->dataBaseUpgrade( + $this->settings->driver.'-ab-version', + PdoSchema::getForDbType($this->settings->driver) + ); + break; + } + + return $mCache; + } + + private function getContactFreq(int $iUserID, int $iIdContact) : array + { + $aResult = array(); + + $sSql = 'SELECT prop_value, prop_frec FROM rainloop_ab_properties WHERE id_user = :id_user AND id_contact = :id_contact AND prop_type = :type'; + $aParams = array( + ':id_user' => array($iUserID, \PDO::PARAM_INT), + ':id_contact' => array($iIdContact, \PDO::PARAM_INT), + ':type' => array(PropertyType::EMAIl, \PDO::PARAM_INT) + ); + + $oStmt = $this->prepareAndExecute($sSql, $aParams); + if ($oStmt) { + $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); + if (\is_array($aFetch)) { + foreach ($aFetch as $aItem) { + if ($aItem && !empty($aItem['prop_value']) && !empty($aItem['prop_frec'])) { + $aResult[$aItem['prop_value']] = (int) $aItem['prop_frec']; + } + } + } + } + + return $aResult; + } + + private function specialConvertSearchValue(string $sSearch, string $sEscapeSign = '=') : string + { + return '%'.\str_replace(array($sEscapeSign, '_', '%'), + array($sEscapeSign.$sEscapeSign, $sEscapeSign.'_', $sEscapeSign.'%'), $sSearch).'%'; + } + + private function specialConvertSearchValueLower(string $sSearch, string $sEscapeSign = '=') : string + { + return '%'.\str_replace(array($sEscapeSign, '_', '%'), + array($sEscapeSign.$sEscapeSign, $sEscapeSign.'_', $sEscapeSign.'%'), + (string) \mb_strtolower($sSearch)).'%'; + } + + protected function getPdoSettings() : \RainLoop\Pdo\Settings + { + $sSslCa = $this->settings->sslCa; + if ($sSslCa && !\is_file($sSslCa)) { + $sFile = \APP_PRIVATE_DATA . 'configs/contacts_mysql_ssl_ca.pem'; +// $sSslCa = (\is_file($sFile) || \file_put_contents($sFile, $sSslCa)) ? $sFile : ''; + $this->settings->sslCa = \file_put_contents($sFile, $sSslCa) ? $sFile : ''; + } + return $this->settings; + } + + /** + * @throws \ValueError + */ + protected function getUserId(string $sEmail, bool $bSkipInsert = false, bool $bCache = true) : int + { + static $aCache = array(); + + $sEmail = \SnappyMail\IDN::emailToAscii(\trim($sEmail)); + if (empty($sEmail)) { + throw new \ValueError('Empty Email argument'); + } + + if ($bCache && isset($aCache[$sEmail])) { + return $aCache[$sEmail]; + } + + $this->SyncDatabase(); + + $oStmt = $this->prepareAndExecute('SELECT id_user FROM rainloop_users WHERE rl_email = :rl_email', + array( + ':rl_email' => array($sEmail, \PDO::PARAM_STR) + ) + ); + + $mRow = $oStmt->fetch(\PDO::FETCH_ASSOC); + if ($mRow && isset($mRow['id_user']) && \is_numeric($mRow['id_user'])) { + $iResult = (int) $mRow['id_user']; + if (0 >= $iResult) { + throw new \Exception('id_user <= 0'); + } + if ($bCache) { + $aCache[$sEmail] = $iResult; + } + return $iResult; + } + + if (!$bSkipInsert) { + $oStmt->closeCursor(); + $oStmt = $this->prepareAndExecute('INSERT INTO rainloop_users (rl_email) VALUES (:rl_email)', + array(':rl_email' => array($sEmail, \PDO::PARAM_STR)) + ); + return $this->getUserId($sEmail, true); + } + + throw new \Exception('id_user = 0'); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoSchema.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoSchema.php new file mode 100644 index 0000000000..8f97f84d20 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoSchema.php @@ -0,0 +1,170 @@ + [], + 2 => [ +'ALTER TABLE rainloop_ab_properties ADD prop_value_lower MEDIUMTEXT NOT NULL AFTER prop_value_custom;' + ], + 3 => [ +'ALTER TABLE rainloop_ab_properties CHANGE prop_value prop_value MEDIUMTEXT NOT NULL;', +'ALTER TABLE rainloop_ab_properties CHANGE prop_value_custom prop_value_custom MEDIUMTEXT NOT NULL;', +'ALTER TABLE rainloop_ab_properties CHANGE prop_value_lower prop_value_lower MEDIUMTEXT NOT NULL;' + ], + 4 => [ +'ALTER TABLE rainloop_ab_properties CHANGE prop_value prop_value MEDIUMTEXT NOT NULL;', +'ALTER TABLE rainloop_ab_properties CHANGE prop_value_custom prop_value_custom MEDIUMTEXT NOT NULL;', +'ALTER TABLE rainloop_ab_properties CHANGE prop_value_lower prop_value_lower MEDIUMTEXT NOT NULL;' + ] + ]; + break; + + case 'pgsql': + $aVersions = [ + 1 => [], + 2 => [ +'ALTER TABLE rainloop_ab_properties ADD prop_value_lower text NOT NULL DEFAULT \'\';' + ] + ]; + break; + + case 'sqlite': + $aVersions = [ + 1 => [], + 2 => [ +'ALTER TABLE rainloop_ab_properties ADD prop_value_lower text NOT NULL DEFAULT \'\';' + ] + ]; + $sInitial = static::{$sDbType}(); + break; + } + + if ($aVersions) { + $aList = \explode(';', \trim(static::{$sDbType}())); + foreach ($aList as $sV) { + $sV = \trim($sV); + if (\strlen($sV)) { + $aVersions[1][] = $sV; + } + } + } + + return $aVersions; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php new file mode 100644 index 0000000000..983e67aa7a --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php @@ -0,0 +1,222 @@ + 'FN', + 'name' => 'FN', + 'fullname' => 'FN', + 'displayname' => 'FN', + 'last' => 0, + 'lastname' => 0, + 'givenname' => 1, + 'first' => 1, + 'firstname' => 1, + 'middle' => 2, + 'middlename' => 2, + 'prefix' => 3, + 'nameprefix' => 3, + 'suffix' => 4, + 'namesuffix' => 4, + 'shortname' => 'NICKNAME', + 'nickname' => 'NICKNAME', + 'birthday' => 'BDAY', + + 'mobile' => array('TEL', 'CELL'), + 'mobilephone' => array('TEL', 'CELL'), + + 'businessemail' => array('EMAIL', 'WORK'), + 'businessemail2' => array('EMAIL', 'WORK'), + 'businessemail3' => array('EMAIL', 'WORK'), + 'businessphone' => array('TEL', 'WORK'), + 'businessphone2' => array('TEL', 'WORK'), + 'businessphone3' => array('TEL', 'WORK'), + 'businessmobile' => array('TEL', 'WORK,CELL'), + 'businessmobilephone' => array('TEL', 'WORK,CELL'), + 'businessweb' => array('URL', 'WORK'), + 'businesswebpage' => array('URL', 'WORK'), + 'businesswebsite' => array('URL', 'WORK'), + 'companyphone' => array('TEL', 'WORK'), + 'companymainphone' => array('TEL', 'WORK'), + + 'primaryphone' => array('TEL', 'PREF,HOME'), + 'homephone' => array('TEL', 'HOME'), + 'homephone2' => array('TEL', 'HOME'), + 'homephone3' => array('TEL', 'HOME'), + 'email' => array('EMAIL', 'HOME'), + 'email2' => array('EMAIL', 'HOME'), + 'email3' => array('EMAIL', 'HOME'), + 'homeemail' => array('EMAIL', 'HOME'), + 'homeemail2' => array('EMAIL', 'HOME'), + 'homeemail3' => array('EMAIL', 'HOME'), + 'primaryemail' => array('EMAIL', 'HOME'), + 'primaryemail2' => array('EMAIL', 'HOME'), + 'primaryemail3' => array('EMAIL', 'HOME'), + 'emailaddress' => array('EMAIL', 'HOME'), + 'email2address' => array('EMAIL', 'HOME'), + 'email3address' => array('EMAIL', 'HOME'), + 'personalemail' => array('EMAIL', 'HOME'), + 'personalemail2' => array('EMAIL', 'HOME'), + 'personalemail3' => array('EMAIL', 'HOME'), + 'personalwebsite' => array('URL', 'HOME'), + + 'otheremail' => 'EMAIL', + 'otherphone' => 'TEL', + 'notes' => 'NOTE', + 'web' => 'URL', + 'webpage' => 'URL', + 'website' => 'URL' +/* + TODO: + 'company' => '', + 'department' => '', + 'jobtitle' => '', + 'officelocation' => '', + 'homestreet' => '', + 'homecity' => '', + 'homestate' => '', + 'homepostalcode' => '', + 'homecountry' => '', + 'businessstreet' => '', + 'businesscity' => '', + 'businessstate' => '', + 'businesspostalcode' => '', + 'businesscountry' => '', +*/ + ); + + public static function CsvStreamToContacts(/*resource*/ $rFile, string $sDelimiter) : iterable + { + \setlocale(LC_CTYPE, 'en_US.UTF-8'); + + $aHeaders = \fgetcsv($rFile, 5000, $sDelimiter, '"'); + if (!$aHeaders || 3 >= \count($aHeaders)) { + return; + } + foreach ($aHeaders as $iIndex => $sItemName) { + $sItemName = \MailSo\Base\Utils::Utf8Clear($sItemName); + $sItemName = \strtoupper(\trim(\preg_replace('/[\s\-]+/', '', $sItemName))); + if (!\array_key_exists($sItemName, VCard::$propertyMap)) { + $sItemName = \strtolower($sItemName); + $sItemName = isset(static::$aMap[$sItemName]) ? static::$aMap[$sItemName] : null; + } + $aHeaders[$iIndex] = $sItemName; + } + + while (false !== ($mRow = \fgetcsv($rFile, 5000, $sDelimiter, '"'))) { + \MailSo\Base\Utils::ResetTimeLimit(); + $iCount = 0; + $oVCard = new VCard; + $aName = ['','','','','']; + foreach ($mRow as $iIndex => $sItemValue) { + $sItemName = $aHeaders[$iIndex]; + $sItemValue = \trim($sItemValue); + if (isset($sItemName) && !empty($sItemValue)) { + $mType = \is_array($sItemName) ? $sItemName[0] : $sItemName; + ++$iCount; + if (\is_int($mType)) { + $aName[$mType] = $sItemValue; + } else if (\is_array($sItemName)) { + $oVCard->add($mType, $sItemValue, ['type' => $sItemName[1]]); + } else if ('FN' === $mType || 'NICKNAME' === $mType) { + $oVCard->$mType = $sItemValue; + } else { + $oVCard->add($mType, $sItemValue); + } + } + } + if ($iCount) { + if ('' !== \implode('', $aName)) { + $oVCard->N = $aName; + } + $oContact = new Classes\Contact(); + $oContact->setVCard($oVCard); + yield $oContact; + } + } + } + + public static function VCardToCsv($stream, VCard $oVCard, bool $bWithHeader = false)/* : int|false*/ + { + $aData = array(); + if ($bWithHeader) { + \fputcsv($stream, array( + 'Title', 'First Name', 'Middle Name', 'Last Name', 'Nick Name', 'Display Name', + 'Company', 'Department', 'Job Title', 'Office Location', + 'E-mail Address', 'Notes', 'Web Page', 'Birthday', 'Mobile Phone', + 'Home Email', 'Home Phone', + 'Home Street', 'Home City', 'Home State', 'Home Postal Code', 'Home Country', + 'Business Email', 'Business Phone', + 'Business Street', 'Business City', 'Business State', 'Business Postal Code', 'Business Country' + )); + } + + $aName = isset($oVCard->N) ? $oVCard->N->getParts() : ['','','','','']; + + $adrHome = $oVCard->getByType('ADR', 'HOME'); + $adrHome = $adrHome ? $adrHome->getParts() : ['','','','','','','']; + + $adrWork = $oVCard->getByType('ADR', 'WORK'); + $adrWork = $adrWork ? $adrWork->getParts() : ['','','','','','','']; + + return \fputcsv($stream, array( + (string) $oVCard->FN, // Title + $aName[1], // First Name + $aName[2], // Middle Name + $aName[0], // Last Name + (string) $oVCard->NICKNAME, // Nick Name + (string) $oVCard->FN, // Display Name + (string) $oVCard->ORG, // Company + '', // Department + '', // Job Title + '', // Office Location + (string) $oVCard->EMAIL, // E-mail Address + (string) $oVCard->NOTE, // Notes + (string) $oVCard->URL, // Web Page + (string) $oVCard->BDAY, // Birthday + (string) $oVCard->getByType('TEL', 'CELL'), // Mobile Phone + // Home + (string) $oVCard->getByType('EMAIL', 'HOME'), // Email + (string) $oVCard->getByType('TEL', 'HOME'), // Phone + \trim($adrHome[1]."\n".$adrHome[2]), // extended address + street address + $adrHome[3], // City + $adrHome[4], // State + $adrHome[5], // Postal Code + $adrHome[6], // Country + // Business + (string) $oVCard->getByType('EMAIL', 'WORK'), // Email + (string) $oVCard->getByType('TEL', 'WORK'), // Phone + \trim($adrWork[1]."\n".$adrWork[2]), // extended address + street address + $adrWork[3], // City + $adrWork[4], // State + $adrWork[5], // Postal Code + $adrWork[6] // Country + )); + } + + public static function VcfStreamToContacts(/*resource*/ $rFile) : iterable + { + $oVCardSplitter = new \Sabre\VObject\Splitter\VCard($rFile); + if ($oVCardSplitter) { + while ($oVCard = $oVCardSplitter->getNext()) { + if ($oVCard instanceof VCard) { + \MailSo\Base\Utils::ResetTimeLimit(); + $oContact = new Classes\Contact(); + $oContact->setVCard($oVCard); + yield $oContact; + } + } + } + } +/* + public static function QrCode(string $sVcfData) : string + { + MECARD:N:djmaze;ORG:SnappyMail;TEL:+31012345678;URL:https\://snappymail.eu;EMAIL:info@snappymail.eu;ADR:address line;; + VCARD + } +*/ +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php new file mode 100644 index 0000000000..25ef02501d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php @@ -0,0 +1,91 @@ +oDriver = $oDriver; + $this->oPlugins = $oPlugins; + } + + public function Load(string $sName, bool $bFindWithWildCard = false, bool $bCheckDisabled = true, bool $bCheckAliases = true) : ?\RainLoop\Model\Domain + { + $oDomain = $this->oDriver->Load($sName, $bFindWithWildCard, $bCheckDisabled, $bCheckAliases); + $oDomain && $this->oPlugins->RunHook('filter.domain', array($oDomain)); + return $oDomain; + } + + public function Save(\RainLoop\Model\Domain $oDomain) : bool + { + return $this->oDriver->Save($oDomain); + } + + public function SaveAlias(string $sName, string $sAlias) : bool + { + if ($this->Load($sName, false, false)) { + throw new ClientException(\RainLoop\Notifications::DomainAlreadyExists); + } + return $this->oDriver->SaveAlias($sName, $sAlias); + } + + public function Delete(string $sName) : bool + { + return $this->oDriver->Delete($sName); + } + + public function Disable(string $sName, bool $bDisabled) : bool + { + return $this->oDriver->Disable($sName, $bDisabled); + } + + public function GetList(bool $bIncludeAliases = true) : array + { + return $this->oDriver->GetList($bIncludeAliases); + } + + public function LoadOrCreateNewFromAction(\RainLoop\Actions $oActions, ?string $sNameForTest = null) : ?\RainLoop\Model\Domain + { + $sName = \mb_strtolower((string) $oActions->GetActionParam('name', '')); + if (\strlen($sName) && $sNameForTest && !\str_contains($sName, '*')) { + $sNameForTest = null; + } + if (\strlen($sName) || $sNameForTest) { + if (!$sNameForTest && !empty($oActions->GetActionParam('create', 0)) && $this->Load($sName)) { + throw new ClientException(\RainLoop\Notifications::DomainAlreadyExists); + } + return \RainLoop\Model\Domain::fromArray($sNameForTest ?: $sName, [ + 'IMAP' => $oActions->GetActionParam('IMAP'), + 'SMTP' => $oActions->GetActionParam('SMTP'), + 'Sieve' => $oActions->GetActionParam('Sieve'), + 'whiteList' => $oActions->GetActionParam('whiteList') + ]); + } + return null; + } + + public function IsActive() : bool + { + return $this->oDriver instanceof Domain\DomainInterface; + } + + public function getByEmailAddress(string $sEmail) : \RainLoop\Model\Domain + { + $oDomain = $this->Load(\MailSo\Base\Utils::getEmailAddressDomain($sEmail), true); + if (!$oDomain) { + throw new ClientException(Notifications::DomainNotAllowed, null, "{$sEmail} has no domain configuration"); + } + if (!$oDomain->ValidateWhiteList($sEmail)) { + throw new ClientException(Notifications::AccountNotAllowed, null, "{$sEmail} not whitelisted"); + } + return $oDomain; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/Autoconfig.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/Autoconfig.php new file mode 100644 index 0000000000..b104276d85 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/Autoconfig.php @@ -0,0 +1,248 @@ + 'imap' === $data['@attributes']['type'] + ); + $data['outgoingServer'] = \array_filter( + isset($data['outgoingServer'][0]) ? $data['outgoingServer'] : [$data['outgoingServer']], + fn($data) => 'smtp' === $data['@attributes']['type'] + ); + $data['canonical'] = $url; + return $data; + } + } + } + return null; + } + + private static function publicsuffixes() : array + { + $oCache = \RainLoop\Api::Actions()->Cacher(); + $list = $oCache->Get('public_suffix_list'); + if ($list) { + $list = \json_decode($list, true); + if ($list[1] < \time()) { + $list = null; + } else { + $list = $list[0]; + } + } + if (null === $list) { + $list = []; + $data = \file_get_contents('https://publicsuffix.org/list/public_suffix_list.dat'); + if ($data) { + $list = \array_filter( + \explode("\n", $data), + fn($text) => \strlen($text) && '/' !== $text[0] && '*' !== $text[0] && \substr_count($text, '.') + ); + } + // Don't lookup for 24 hours + $oCache->Set('public_suffix_list', \json_encode([$list, time() + 86400])); + } + return $list ?: []; + } + + /** + * https://www.ietf.org/archive/id/draft-bucksch-autoconfig-02.html#section-4.3 + */ + private static function mx(string $domain, string $emailaddress) : ?array + { + $suffixes = static::publicsuffixes(); + foreach (\SnappyMail\DNS::MX($domain) as $hostname) { + // Extract only the second-level domain from the MX hostname + $mxbasedomain = \explode('.', $hostname); + $i = -2; + while (\in_array(\implode('.', \array_slice($mxbasedomain, $i)), $suffixes)) { + --$i; + } + $mxbasedomain = \implode('.', \array_slice($mxbasedomain, $i)); + if ($mxbasedomain) { + $mxfulldomain = $mxbasedomain; + if (\substr_count($hostname, '.') > \substr_count($mxbasedomain, '.')) { + // Remove the first component from the MX hostname + $mxfulldomain = \explode('.', $hostname, 2)[1]; + } + $hostnames[$mxfulldomain] = $mxbasedomain; + } + } + foreach ($hostnames as $mxfulldomain => $mxbasedomain) { + if ($domain != $mxfulldomain) { + $autoconfig = static::resolve($mxfulldomain, $emailaddress); + if (!$autoconfig && $mxfulldomain != $mxbasedomain && $domain != $mxbasedomain) { + $autoconfig = static::resolve($mxbasedomain, $emailaddress); + } + if ($autoconfig) { + return $autoconfig; + } + } + } + } + + /** + * https://datatracker.ietf.org/doc/html/rfc6186 + * https://datatracker.ietf.org/doc/html/rfc8314 + */ + private static function srv(string $domain) : ?array + { + $srv = \SnappyMail\DNS::SRV('_submissions._tcp.'.$domain); + if (empty($srv[0]['target'])) { + $srv = \SnappyMail\DNS::SRV('_submission._tcp.'.$domain); + } + if (!empty($srv[0]['target'])) { + $result = [ + 'incomingServer' => [], + 'outgoingServer' => [ + 'hostname' => $srv[0]['target'], + 'port' => $srv[0]['port'], + 'socketType' => (587 == $srv[0]['port']) ? 'STARTTLS' : (465 == $srv[0]['port'] ? 'SSL' : ''), + 'authentication' => 'password-cleartext', + 'username' => '%EMAILADDRESS%' + ] + ]; + $srv = \SnappyMail\DNS::SRV('_imaps._tcp.'.$domain); + if (empty($srv[0]['target'])) { + $srv = \SnappyMail\DNS::SRV('_imap._tcp.'.$domain); + } + if (!empty($srv[0]['target'])) { + $result['incomingServer'][] = [ + 'hostname' => $srv[0]['target'], + 'port' => $srv[0]['port'], + 'socketType' => 993 == $srv[0]['port'] ? 'SSL' : '', + 'authentication' => 'password-cleartext', + 'username' => '%EMAILADDRESS%' + ]; + return $result; + } + } + return null; + } + + /** + * This is Microsoft + * https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/pox-autodiscover-web-service-reference-for-exchange + */ + private static function autodiscover(string $domain) : ?array + { + foreach (\SnappyMail\DNS::SRV("_autodiscover._tcp.{$domain}") as $record) { + if (443 == $record['port']) { + $result = static::autodiscover_resolve("https://{$record['target']}", $domain); + } else if (80 == $record['port']) { + $result = static::autodiscover_resolve("http://{$record['target']}", $domain); + } else { + $result = static::autodiscover_resolve("https://{$record['target']}:{$record['port']}", $domain); + } + if ($result) { + return $result; + } + } + foreach ([ + "https://{$domain}", + "https://autodiscover.{$domain}", + "http://autodiscover.{$domain}" + ] as $host) { + $result = static::autodiscover_resolve($host, $domain); + if ($result) { + return $result; + } + } + return null; + } + + private static function autodiscover_resolve(string $host, string $domain) : ?array + { + $email = "autodiscover@{$domain}"; + $context = \stream_context_create(['http' => [ + 'method' => 'POST', + 'header' => 'Content-Type: application/xml', + 'content' => ' + + + http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a + '.$email.' + + ' + ]]); + $url = "{$host}/autodiscover/autodiscover.xml"; + $xml = \file_get_contents($url, false, $context); + if ($xml) { + $data = \json_decode( + \json_encode( + \simplexml_load_string($xml, 'SimpleXMLElement', \LIBXML_NOCDATA) + ), true); + if (!empty($data['Response']['Account']['Protocol']) && 'email' === $data['Response']['Account']['AccountType']) { + $result = [ + 'incomingServer' => [], + 'outgoingServer' => [] + ]; + foreach ($data['Response']['Account']['Protocol'] as $entry) { + if ('IMAP' === $entry['Type']) { + $result['incomingServer'][] = [ + 'hostname' => $entry['Server'], + 'port' => $entry['Port'], + 'socketType' => ('on' === $entry['SSL']) ? (993 == $entry['Port'] ? 'SSL' : 'STARTTLS') : '', + 'authentication' => 'password-cleartext', + 'username' => ($entry['LoginName'] === $email) ? '%EMAILADDRESS%' : '' + ]; + } else if ('SMTP' === $entry['Type']) { + $result['outgoingServer'][] = [ + 'hostname' => $entry['Server'], + 'port' => $entry['Port'], + 'socketType' => (587 == $entry['Port']) ? 'STARTTLS' : ('on' === $entry['SSL'] ? 'SSL' : ''), + 'authentication' => 'password-cleartext', + 'username' => ($entry['LoginName'] === $email) ? '%EMAILADDRESS%' : '' + ]; + } + } + $result['canonical'] = $url; + return $result; + } + } + return null; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/DefaultDomain.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/DefaultDomain.php new file mode 100644 index 0000000000..989e456201 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/DefaultDomain.php @@ -0,0 +1,246 @@ +sDomainPath = \rtrim(\trim($sDomainPath), '\\/'); + $this->oCacher = $oCacher; + } + + private static function decodeFileName(string $sName) : string + { + return ('default' === $sName) ? '*' : \str_replace('_wildcard_', '*', $sName); + } + + private static function encodeFileName(string $sName) : string + { +// return ('*' === $sName) ? 'default' : \str_replace('*', '_wildcard_', \SnappyMail\IDN::toAscii($sName)); + return ('*' === $sName) ? 'default' : \str_replace('*', '_wildcard_', \strtolower(\idn_to_ascii($sName))); + } + + private function getWildcardDomainsLine() : string + { + if ($this->oCacher) { + $sResult = $this->oCacher->Get(static::CACHE_KEY); + if (\is_string($sResult)) { + return $sResult; + } + } + + $aNames = array(); + +// $aList = \glob($this->sDomainPath.'/*.{ini,json,alias}', GLOB_BRACE); + $aList = \glob($this->sDomainPath.'/*.json'); + foreach ($aList as $sFile) { + $sName = \substr(\basename($sFile), 0, -5); + if ('default' === $sName || false !== \strpos($sName, '_wildcard_')) { + $aNames[] = static::decodeFileName($sName); + } + } + $aList = \glob($this->sDomainPath.'/*.ini'); + foreach ($aList as $sFile) { + $sName = \substr(\basename($sFile), 0, -4); + if ('default' === $sName || false !== \strpos($sName, '_wildcard_')) { + $aNames[] = static::decodeFileName($sName); + } + } + $aList = \glob($this->sDomainPath.'/*.alias'); + foreach ($aList as $sFile) { + $sName = \substr(\basename($sFile), 0, -6); + if ('default' === $sName || false !== \strpos($sName, '_wildcard_')) { + $aNames[] = static::decodeFileName($sName); + } + } + + $sResult = ''; + if ($aNames) { + \rsort($aNames, SORT_STRING); + $sResult = \implode(' ', \array_unique($aNames)); + } + + $this->oCacher && $this->oCacher->Set(static::CACHE_KEY, $sResult); + + return $sResult; + } + + public function Load(string $sName, bool $bFindWithWildCard = false, bool $bCheckDisabled = true, bool $bCheckAliases = true) : ?\RainLoop\Model\Domain + { +// $sName = \SnappyMail\IDN::toAscii($sName); + $sName = \strtolower(\idn_to_ascii($sName)); + if ($bCheckDisabled && \in_array($sName, $this->getDisabled())) { + return null; + } + + $sRealFileBase = $this->sDomainPath . '/' . static::encodeFileName($sName); + + if (\file_exists($sRealFileBase.'.json')) { + $aDomain = \json_decode(\file_get_contents($sRealFileBase.'.json'), true) ?: array(); + return \RainLoop\Model\Domain::fromArray($sName, $aDomain); + } + + if (\file_exists($sRealFileBase.'.ini')) { + $aDomain = \parse_ini_file($sRealFileBase.'.ini') ?: array(); + return \RainLoop\Model\Domain::fromIniArray($sName, $aDomain); + } + + if ($bCheckAliases && \file_exists($sRealFileBase.'.alias')) { + $sTarget = \trim(\file_get_contents($sRealFileBase.'.alias')); + if (!empty($sTarget)) { + $oDomain = $this->Load($sTarget, false, false, false); + $oDomain && $oDomain->SetAliasName($sName); + return $oDomain; + } + } + + if ($bFindWithWildCard) { + $sNames = $this->getWildcardDomainsLine(); + $sFoundValue = ''; + if (\strlen($sNames) + && \RainLoop\Plugins\Helper::ValidateWildcardValues($sName, $sNames, $sFoundValue) + && \strlen($sFoundValue) + ) { + return $this->Load($sFoundValue); + } + } + + return null; + } + + public function Save(\RainLoop\Model\Domain $oDomain) : bool + { + $sRealFileName = static::encodeFileName($oDomain->Name()); + if (!$sRealFileName) { + return false; + } + $this->Delete($oDomain->Name()); + \RainLoop\Utils::saveFile("{$this->sDomainPath}/{$sRealFileName}.json", \json_encode($oDomain, \JSON_PRETTY_PRINT)); + $this->oCacher && $this->oCacher->Delete(static::CACHE_KEY); + return true; + } + + public function SaveAlias(string $sName, string $sTarget) : bool + { +// $sTarget = \SnappyMail\IDN::toAscii($sTarget); +// $sTarget = static::encodeFileName($sTarget); + $sTarget = \strtolower(\idn_to_ascii($sTarget)); + $sRealFileName = static::encodeFileName($sName); + if (!$sRealFileName || !$sTarget/* || !\is_readable("{$this->sDomainPath}/{$sTarget}.json")*/) { + return false; + } +// $this->Delete($sName); + \RainLoop\Utils::saveFile("{$this->sDomainPath}/{$sRealFileName}.alias", $sTarget); + $this->oCacher && $this->oCacher->Delete(static::CACHE_KEY); + return true; + } + + protected function getDisabled() : array + { + $sFile = ''; + if (\file_exists($this->sDomainPath.'/disabled')) { + $sFile = \file_get_contents($this->sDomainPath.'/disabled'); + } + $aDisabled = array(); + // RainLoop use comma, we use newline + $sItem = \strtok($sFile, ",\n"); + while (false !== $sItem) { +// $aDisabled[] = \SnappyMail\IDN::toAscii($sItem); + $aDisabled[] = \strtolower(\idn_to_ascii($sItem)); + $sItem = \strtok(",\n"); + } + return $aDisabled; +// return \array_unique($aDisabled); + } + + public function Disable(string $sName, bool $bDisable) : bool + { +// $sName = \SnappyMail\IDN::toAscii($sName); + $sName = \strtolower(\idn_to_ascii($sName)); + if ($sName) { + $aResult = $this->getDisabled(); + if ($bDisable) { + $aResult[] = $sName; + } else { + $aResult = \array_filter($aResult, fn($v) => $v !== $sName); + } + $aResult = \array_unique($aResult); + \RainLoop\Utils::saveFile($this->sDomainPath.'/disabled', \implode("\n", $aResult)); + return $this->getDisabled() === $aResult; + } + return false; + } + + public function Delete(string $sName) : bool + { + $bResult = true; + if (\strlen($sName)) { + $sRealFileName = $this->sDomainPath . '/' . static::encodeFileName($sName); + $bResult = + (!\file_exists($sRealFileName.'.json') || \unlink($sRealFileName.'.json')) + && (!\file_exists($sRealFileName.'.ini') || \unlink($sRealFileName.'.ini')) + && (!\file_exists($sRealFileName.'.alias') || \unlink($sRealFileName.'.alias')); + $this->Disable($sName, !$bResult); + if ($this->oCacher) { + $this->oCacher->Delete(static::CACHE_KEY); + } + } + return $bResult; + } + + public function GetList(bool $bIncludeAliases = true) : array + { + $aDisabledNames = $this->getDisabled(); + $aResult = array(); + $aWildCards = array(); + $aAliases = array(); + +// $aList = \glob($this->sDomainPath.'/*.{ini,json,alias}', GLOB_BRACE); + $aList = \array_diff(\scandir($this->sDomainPath), array('.', '..')); + foreach ($aList as $sFile) { + $bAlias = '.alias' === \substr($sFile, -6); + if ($bAlias || '.json' === \substr($sFile, -5) || '.ini' === \substr($sFile, -4)) { + $sName = static::decodeFileName(\preg_replace('/\.(ini|json|alias)$/', '', $sFile)); + if ($bAlias) { + if ($bIncludeAliases) { + $aAliases[$sName] = array( + 'name' => $sName, + 'disabled' => \in_array($sName, $aDisabledNames), + 'alias' => true + ); + } + } else if (false !== \strpos($sName, '*')) { + $aWildCards[$sName] = array( + 'name' => $sName, + 'disabled' => \in_array($sName, $aDisabledNames), + 'alias' => false + ); + } else { + $aResult[$sName] = array( + 'name' => $sName, + 'disabled' => \in_array($sName, $aDisabledNames), + 'alias' => false + ); + } + } + } + + \ksort($aResult, SORT_STRING); + \ksort($aAliases, SORT_STRING); + \krsort($aWildCards, SORT_STRING); + return \array_values(\array_merge($aResult, $aAliases, $aWildCards)); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/DomainInterface.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/DomainInterface.php new file mode 100644 index 0000000000..27a5c9ea20 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/DomainInterface.php @@ -0,0 +1,16 @@ +oDriver = $oDriver; + } + + public function PutFile(\RainLoop\Model\Account $oAccount, string $sKey, /*resource*/ $rSource) : bool + { + return $this->oDriver->PutFile($oAccount, $sKey, $rSource); + } + + public function MoveUploadedFile(\RainLoop\Model\Account $oAccount, string $sKey, string $sSource) : bool + { + return $this->oDriver->MoveUploadedFile($oAccount, $sKey, $sSource); + } + + /** + * @return resource|bool + */ + public function GetFile(\RainLoop\Model\Account $oAccount, string $sKey, string $sOpenMode = 'rb') + { + return $this->oDriver->GetFile($oAccount, $sKey, $sOpenMode); + } + + public function GetFileName(\RainLoop\Model\Account $oAccount, string $sKey) : string + { + return $this->oDriver->GetFileName($oAccount, $sKey); + } + + public function Clear(\RainLoop\Model\Account $oAccount, string $sKey) : bool + { + return $this->oDriver->Clear($oAccount, $sKey); + } + + public function FileSize(\RainLoop\Model\Account $oAccount, string $sKey) : int + { + return $this->oDriver->FileSize($oAccount, $sKey); + } + + public function FileExists(\RainLoop\Model\Account $oAccount, string $sKey) : bool + { + return $this->oDriver->FileExists($oAccount, $sKey); + } + + public function GC(int $iTimeToClearInHours = 24) : bool + { + return $this->oDriver ? $this->oDriver->GC($iTimeToClearInHours) : false; + } + + public function CloseAllOpenedFiles() : bool + { + return $this->oDriver && \method_exists($this->oDriver, 'CloseAllOpenedFiles') ? + $this->oDriver->CloseAllOpenedFiles() : false; + } + + public function GenerateLocalFullFileName(\RainLoop\Model\Account $oAccount, string $sKey) : string + { + return $this->oDriver ? $this->oDriver->GenerateLocalFullFileName($oAccount, $sKey) : ''; + } + + public function IsActive() : bool + { + return $this->oDriver instanceof \RainLoop\Providers\Files\IFiles; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php new file mode 100644 index 0000000000..ded1e1c4dc --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php @@ -0,0 +1,155 @@ +aResources = array(); + $this->sDataPath = \rtrim(\trim($sStoragePath), '\\/'); + } + + public function GenerateLocalFullFileName(\RainLoop\Model\Account $oAccount, string $sKey) : string + { + return $this->generateFullFileName($oAccount, $sKey, true); + } + + public function PutFile(\RainLoop\Model\Account $oAccount, string $sKey, /*resource*/ $rSource) : bool + { + $bResult = false; + if ($rSource) { + $rOpenOutput = \fopen($this->generateFullFileName($oAccount, $sKey, true), 'w+b'); + if ($rOpenOutput) { + $bResult = (false !== \MailSo\Base\Utils::WriteStream($rSource, $rOpenOutput)); + \fclose($rOpenOutput); + } + } + return $bResult; + } + + public function MoveUploadedFile(\RainLoop\Model\Account $oAccount, string $sKey, string $sSource) : bool + { + return \move_uploaded_file($sSource, + $this->generateFullFileName($oAccount, $sKey, true)); + } + + /** + * @return resource|bool + */ + public function GetFile(\RainLoop\Model\Account $oAccount, string $sKey, string $sOpenMode = 'rb') + { + $mResult = false; + $bCreate = !!\preg_match('/[wac]/', $sOpenMode); + + $sFileName = $this->generateFullFileName($oAccount, $sKey, $bCreate); + if ($bCreate || \file_exists($sFileName)) + { + $mResult = \fopen($sFileName, $sOpenMode); + + if (\is_resource($mResult)) + { + $this->aResources[$sFileName] = $mResult; + } + } + + return $mResult; + } + + public function GetFileName(\RainLoop\Model\Account $oAccount, string $sKey) /*: string|false*/ + { + $sFileName = $this->generateFullFileName($oAccount, $sKey); + return \file_exists($sFileName) ? $sFileName : false; + } + + public function Clear(\RainLoop\Model\Account $oAccount, string $sKey) : bool + { + $sFileName = $this->generateFullFileName($oAccount, $sKey); + if (\file_exists($sFileName)) { + if (isset($this->aResources[$sFileName]) && \is_resource($this->aResources[$sFileName])) { + \fclose($this->aResources[$sFileName]); + } + return \unlink($sFileName); + } + return false; + } + + public function FileSize(\RainLoop\Model\Account $oAccount, string $sKey) /*: int|false*/ + { + $sFileName = $this->generateFullFileName($oAccount, $sKey); + return \file_exists($sFileName) ? \filesize($sFileName) : false; + } + + public function FileExists(\RainLoop\Model\Account $oAccount, string $sKey) : bool + { + return \file_exists($this->generateFullFileName($oAccount, $sKey)); + } + + public function GC(int $iTimeToClearInHours = 24) : bool + { + if (0 < $iTimeToClearInHours) { + $iTimeToClear = 3600 * $iTimeToClearInHours; + foreach (\glob("{$this->sDataPath}/*", GLOB_ONLYDIR) as $sDomain) { + foreach (\glob("{$sDomain}/*", GLOB_ONLYDIR) as $sLocal) { + \MailSo\Base\Utils::RecTimeDirRemove("{$sLocal}/.files", $iTimeToClear); + } + } + // Old + \MailSo\Base\Utils::RecTimeDirRemove("{$this->sDataPath}/files", $iTimeToClear); + + return true; + } + + return false; + } + + public function CloseAllOpenedFiles() : bool + { + if (\is_array($this->aResources) && \count($this->aResources)) + { + foreach ($this->aResources as $sFileName => $rFile) + { + if (!empty($sFileName) && \is_resource($rFile)) + { + \fclose($rFile); + } + } + } + + return true; + } + + private function generateFullFileName(\RainLoop\Model\Account $oAccount, string $sKey, bool $bMkDir = false) : string + { + if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) { + $sEmail = $oAccount->ParentEmail(); + $sSubEmail = $oAccount->Email(); + } else { + $sEmail = $oAccount->Email(); + $sSubEmail = ''; + } + + $aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld'); + $sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : ''); + $sFilePath = $this->sDataPath + .'/'.\MailSo\Base\Utils::SecureFileName($sDomain ?: 'unknown.tld') + .'/'.\MailSo\Base\Utils::SecureFileName(\implode('@', $aEmail) ?: '.unknown') + .($sSubEmail ? '/'.\MailSo\Base\Utils::SecureFileName($sSubEmail) : '') + .'/.files/'.\sha1($sKey); + + $bMkDir && \MailSo\Base\Utils::mkdir(\dirname($sFilePath)); + + return $sFilePath; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/IFiles.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/IFiles.php new file mode 100644 index 0000000000..1706cd5f85 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/IFiles.php @@ -0,0 +1,33 @@ +oDriver = $oDriver; + } + + private static function handleException(\Throwable $oException, int $defNotification) : void + { + if ($oException instanceof \MailSo\Net\Exceptions\SocketCanNotConnectToHostException) { + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ConnectionError, $oException); + } + + if ($oException instanceof \MailSo\Sieve\Exceptions\NegativeResponseException) { + throw new \RainLoop\Exceptions\ClientException( + \RainLoop\Notifications::ClientViewError, $oException, \implode("\r\n", $oException->GetResponses()) + ); + } + + throw new \RainLoop\Exceptions\ClientException($defNotification, $oException); + } + + public function Load(\RainLoop\Model\Account $oAccount) : array + { + try + { + return $this->IsActive() ? $this->oDriver->Load($oAccount) : array(); + } + catch (\Throwable $oException) + { + static::handleException($oException, \RainLoop\Notifications::CantGetFilters); + } + } + + public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, string $sRaw) : bool + { + try + { + return $this->IsActive() + ? $this->oDriver->Save($oAccount, $sScriptName, $sRaw) + : false; + } + catch (\Throwable $oException) + { + static::handleException($oException, \RainLoop\Notifications::CantSaveFilters); + } + } + + public function ActivateScript(\RainLoop\Model\Account $oAccount, string $sScriptName) + { + try + { + return $this->IsActive() + ? $this->oDriver->Activate($oAccount, $sScriptName) + : false; + } + catch (\Throwable $oException) + { + static::handleException($oException, \RainLoop\Notifications::CantActivateFiltersScript); + } + } + + public function DeleteScript(\RainLoop\Model\Account $oAccount, string $sScriptName) + { + try + { + return $this->IsActive() + ? $this->oDriver->Delete($oAccount, $sScriptName) + : false; + } + catch (\Throwable $oException) + { + static::handleException($oException, \RainLoop\Notifications::CantDeleteFiltersScript); + } + } + + public function IsActive() : bool + { + return $this->oDriver instanceof \RainLoop\Providers\Filters\FiltersInterface; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/FiltersInterface.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/FiltersInterface.php new file mode 100644 index 0000000000..5f41ed6aad --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/FiltersInterface.php @@ -0,0 +1,14 @@ +oPlugins = $oPlugins; + $this->oConfig = $oConfig; + } + + protected function getConnection(\RainLoop\Model\Account $oAccount) : ?\MailSo\Sieve\SieveClient + { + $oSieveClient = new \MailSo\Sieve\SieveClient(); + $oSieveClient->SetLogger($this->oLogger); + return $oAccount->SieveConnectAndLogin($this->oPlugins, $oSieveClient, $this->oConfig) + ? $oSieveClient + : null; + } + + public function Load(\RainLoop\Model\Account $oAccount) : array + { + $aModules = array(); + $aScripts = array(); + + $oSieveClient = $this->getConnection($oAccount); + if ($oSieveClient) { + $aModules = $oSieveClient->Modules(); + \sort($aModules); + + $aList = $oSieveClient->ListScripts(); + + foreach ($aList as $name => $active) { + $aScripts[$name] = array( + '@Object' => 'Object/SieveScript', + 'name' => $name, + 'active' => $active, + 'body' => $oSieveClient->GetScript($name) + ); + } + + $oSieveClient->Disconnect(); + + if (!isset($aList[self::SIEVE_FILE_NAME])) { + $aScripts[self::SIEVE_FILE_NAME] = array( + '@Object' => 'Object/SieveScript', + 'name' => self::SIEVE_FILE_NAME, + 'active' => false, + 'body' => '' + ); + } + } + + \ksort($aScripts); + + return array( + 'Capa' => $aModules, + 'Scripts' => $aScripts + ); + } + + public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, string $sRaw) : bool + { + $oSieveClient = $this->getConnection($oAccount); + if ($oSieveClient) { + $oSieveClient->PutScript($sScriptName, $sRaw); + return true; + } + return false; + } + + /** + * If $sScriptName is the empty string (i.e., ""), then any active script is disabled. + */ + public function Activate(\RainLoop\Model\Account $oAccount, string $sScriptName) : bool + { + $oSieveClient = $this->getConnection($oAccount); + if ($oSieveClient) { + $oSieveClient->SetActiveScript(\trim($sScriptName)); + return true; + } + return false; + } +/* + public function Check(\RainLoop\Model\Account $oAccount, string $sScript) : bool + { + $oSieveClient = $this->getConnection($oAccount); + if ($oSieveClient) { + $oSieveClient->CheckScript($sScript); + return true; + } + return false; + } +*/ + public function Delete(\RainLoop\Model\Account $oAccount, string $sScriptName) : bool + { + $oSieveClient = $this->getConnection($oAccount); + if ($oSieveClient) { + if (isset($oSieveClient->ListScripts()[$sScriptName])) { + $oSieveClient->DeleteScript(\trim($sScriptName)); + } + return true; + } + return false; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities.php new file mode 100755 index 0000000000..6dc3686eb0 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities.php @@ -0,0 +1,150 @@ +drivers = \array_filter($drivers, function ($driver) { + return $driver instanceof IIdentities; + }); + } + + public function IsActive() : bool + { + return true; + } + + /** + * @param Account $account + * @param bool $allowMultipleIdentities + * @return Identity[] + */ + public function GetIdentities(Account $account, bool $allowMultipleIdentities): array + { + // Find all identities stored in the system + $identities = $this->MergeIdentitiesPerDriver($this->GetIdentiesPerDriver($account)); + + // Find the primary identity + $primaryIdentity = \current(\array_filter($identities, function ($identity) { + return $identity->IsAccountIdentities(); + })); + + // If no primary identity is found, generate default one from account info + if (!$primaryIdentity) { + $primaryIdentity = new Identity('', $account->Email()); + $primaryIdentity->exists = !\RainLoop\Api::Config()->Get('webmail', 'popup_identity', true); + $identities[] = $primaryIdentity; + } + + // Return only primary identity or all identities + return $allowMultipleIdentities ? $identities : [$primaryIdentity]; + } + + public function UpdateIdentity(Account $account, Identity $identity) + { + // Find all identities in the system + $identities = &$this->GetIdentiesPerDriver($account); + + $isNew = true; + foreach ($this->drivers as $driver) { + if (!$driver->SupportsStore()) continue; + + $driverIdentities = &$identities[$driver->Name()]; + if (!isset($driverIdentities[$identity->Id(true)])) + continue; + + // We update the identity in all writeable stores + $driverIdentities[$identity->Id(true)] = $identity; + $isNew = false; + + $driver->SetIdentities($account, $driverIdentities); + } + + // If it is a new identity we add it to any storage driver + if ($isNew) { + // Pick any storage driver to store the result, typically only file storage + $storageDriver = \current(\array_filter($this->drivers, function ($driver) { + return $driver->SupportsStore(); + })); + + $identities[$storageDriver->Name()][$identity->Id(true)] = $identity; + $storageDriver->SetIdentities($account, $identities[$storageDriver->Name()]); + } + } + + public function DeleteIdentity(Account $account, string $identityId) + { + // On deletion, we remove the identity from all drivers if they are writeable. + $identities = &$this->GetIdentiesPerDriver($account); + + foreach ($this->drivers as $driver) { + if (!$driver->SupportsStore()) continue; + + $driverIdentities = &$identities[$driver->Name()]; + if (!isset($driverIdentities[$identityId])) + continue; + + // We found it, so remove and update storage if relevant + $identity = $driverIdentities[$identityId]; + if ($identity->IsAccountIdentities()) continue; // never remove the primary identity + + unset($driverIdentities[$identityId]); + $driver->SetIdentities($account, $driverIdentities); + } + } + + private function &GetIdentiesPerDriver(Account $account): array + { + if (isset($this->identitiesPerDriverPerAccount[$account->Email()])) + return $this->identitiesPerDriverPerAccount[$account->Email()]; + + $identitiesPerDriver = []; + foreach ($this->drivers as $driver) { + $driverIdentities = $driver->GetIdentities($account); + + foreach ($driverIdentities as $identity) + $identitiesPerDriver[$driver->Name()][$identity->Id(true)] = $identity; + } + + $this->identitiesPerDriverPerAccount[$account->Email()] = $identitiesPerDriver; + return $this->identitiesPerDriverPerAccount[$account->Email()]; + } + + /** + * @param Identity[][] $identitiesPerDriver + * @return Identity[] + */ + private function MergeIdentitiesPerDriver(array $identitiesPerDriver): array + { + // Merge logic for the identities + $identities = []; + foreach ($this->drivers as $driver) { + // Merge and replace by key + if (isset($identitiesPerDriver[$driver->Name()])) { + foreach ($identitiesPerDriver[$driver->Name()] as $identity) { + $identities[$identity->Id(true)] = $identity; + } + } + } + + return \array_values($identities); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/FileIdentities.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/FileIdentities.php new file mode 100755 index 0000000000..389586dd62 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/FileIdentities.php @@ -0,0 +1,73 @@ +localStorageProvider = $localStorageProvider; + } + + /** + * @inheritDoc + */ + public function GetIdentities(Account $account): array + { + $data = $this->localStorageProvider->Get($account, Storage\Enumerations\StorageType::CONFIG, 'identities'); + $subIdentities = \json_decode($data, true) ?? []; + $result = []; + + foreach ($subIdentities as $subIdentity) { + $identity = new Identity(); + $identity->FromJSON($subIdentity); + + if (!$identity->Validate()) { + continue; + } + if ($identity->IsAccountIdentities()) { + $identity->SetEmail($account->Email()); + } + $result[] = $identity; + } + + return $result; + } + + /** + * @inheritDoc + */ + public function SetIdentities(Account $account, array $identities): void + { + $jsons = \array_map(function ($identity) { + return $identity->ToSimpleJSON(); + }, $identities); + $this->localStorageProvider->Put($account, Storage\Enumerations\StorageType::CONFIG, 'identities', \json_encode($jsons)); + } + + /** + * @inheritDoc + */ + public function SupportsStore(): bool + { + return true; + } + + public function Name(): string + { + return "File"; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/IIdentities.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/IIdentities.php new file mode 100755 index 0000000000..6e0fc6ab23 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Identities/IIdentities.php @@ -0,0 +1,34 @@ +oDriver = $oDriver; + } + + public function Load(Account $oAccount) : \RainLoop\Settings + { + return new \RainLoop\Settings($this, $oAccount, $this->oDriver->Load($oAccount)); + } + + public function Save(Account $oAccount, \RainLoop\Settings $oSettings) : bool + { + return $this->oDriver->Save($oAccount, $oSettings); + } + + public function IsActive() : bool + { + return true; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/DefaultSettings.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/DefaultSettings.php new file mode 100644 index 0000000000..63df9be235 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/DefaultSettings.php @@ -0,0 +1,58 @@ +oStorageProvider = $oStorageProvider; + } + + public function Load(Account $oAccount) : array + { + $sValue = $this->oStorageProvider->Get($oAccount, + StorageType::CONFIG, + $this->oStorageProvider->IsLocal() ? + self::FILE_NAME_LOCAL : + self::FILE_NAME + ); + + if (\is_string($sValue)) { + $aData = \json_decode($sValue, true); + if (\is_array($aData)) { + return $aData; + } + } + + return array(); + } + + public function Save(Account $oAccount, \RainLoop\Settings $oSettings) : bool + { + return $this->oStorageProvider->Put($oAccount, + StorageType::CONFIG, + $this->oStorageProvider->IsLocal() ? + self::FILE_NAME_LOCAL : + self::FILE_NAME, + \json_encode($oSettings)); + } + + public function Delete(Account $oAccount) : bool + { + return $this->oStorageProvider->Clear($oAccount, + StorageType::CONFIG, + $this->oStorageProvider->IsLocal() ? + self::FILE_NAME_LOCAL : + self::FILE_NAME); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/ISettings.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/ISettings.php new file mode 100644 index 0000000000..0199d1b5eb --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/ISettings.php @@ -0,0 +1,12 @@ +oDriver = $oDriver; + } + + /** + * @param \RainLoop\Model\Account|string|null $mAccount + */ + private function verifyAccount($mAccount, int $iStorageType) : bool + { + return \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY === $iStorageType + || $mAccount instanceof \RainLoop\Model\Account + || \is_string($mAccount); + } + + /** + * @param \RainLoop\Model\Account|string|null $mAccount + * @param mixed $sValue + */ + public function Put($mAccount, int $iStorageType, string $sKey, string $sValue) : bool + { + return $this->verifyAccount($mAccount, $iStorageType) + ? $this->oDriver->Put($mAccount, $iStorageType, $sKey, $sValue) + : false; + } + + /** + * @param \RainLoop\Model\Account|string|null $mAccount + * @param mixed $mDefault = false + * + * @return mixed + */ + public function Get($mAccount, int $iStorageType, string $sKey, $mDefault = false) + { + return $this->verifyAccount($mAccount, $iStorageType) + ? $this->oDriver->Get($mAccount, $iStorageType, $sKey, $mDefault) + : $mDefault; + } + + /** + * @param \RainLoop\Model\Account|string|null $mAccount + */ + public function Clear($mAccount, int $iStorageType, string $sKey) : bool + { + return $this->verifyAccount($mAccount, $iStorageType) + ? $this->oDriver->Clear($mAccount, $iStorageType, $sKey) + : false; + } + + /** + * @param \RainLoop\Model\Account|string $mAccount + */ + public function DeleteStorage($mAccount) : bool + { + return $this->oDriver->DeleteStorage($mAccount); + } + + /** + * @param \RainLoop\Model\Account|string|null $mAccount + */ + public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false) : string + { + return $this->oDriver->GenerateFilePath($mAccount, $iStorageType, $bMkDir); + } + + public function IsActive() : bool + { + return true; + } + + public function IsLocal() : bool + { + return $this->oDriver->IsLocal(); + } + + public function GC() : void + { + $this->oDriver->GC(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/Enumerations/StorageType.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/Enumerations/StorageType.php new file mode 100644 index 0000000000..f2337ba414 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/Enumerations/StorageType.php @@ -0,0 +1,19 @@ +sDataPath = \rtrim(\trim($sStoragePath), '\\/'); + $this->bLocal = $bLocal; + } + + /** + * @param \RainLoop\Model\Account|string|null $mAccount + */ + public function Put($mAccount, int $iStorageType, string $sKey, string $sValue) : bool + { + $sFileName = $this->generateFileName($mAccount, $iStorageType, $sKey, true); + try { + $sFileName && \RainLoop\Utils::saveFile($sFileName, $sValue); + return true; + } catch (\Throwable $e) { + \SnappyMail\Log::warning('FileStorage', $e->getMessage()); + } + return false; + } + + /** + * @param \RainLoop\Model\Account|string|null $mAccount + * @param mixed $mDefault = false + * + * @return mixed + */ + public function Get($mAccount, int $iStorageType, string $sKey, $mDefault = false) + { + $mValue = false; + $sFileName = $this->generateFileName($mAccount, $iStorageType, $sKey); + if ($sFileName && \file_exists($sFileName)) { + $mValue = \file_get_contents($sFileName); + // Update mtime to prevent garbage collection + if (StorageType::SESSION === $iStorageType) { + \touch($sFileName); + } + } + return false === $mValue ? $mDefault : $mValue; + } + + /** + * @param \RainLoop\Model\Account|string|null $mAccount + */ + public function Clear($mAccount, int $iStorageType, string $sKey) : bool + { + $sFileName = $this->generateFileName($mAccount, $iStorageType, $sKey); + return $sFileName && \file_exists($sFileName) && \unlink($sFileName); + } + + /** + * @param \RainLoop\Model\Account|string $mAccount + */ + public function DeleteStorage($mAccount) : bool + { + $sPath = $this->generateFileName($mAccount, StorageType::CONFIG, ''); + if ($sPath && \is_dir($sPath)) { + \MailSo\Base\Utils::RecRmDir($sPath); + } + return true; + } + + public function IsLocal() : bool + { + return $this->bLocal; + } + + /** + * @param \RainLoop\Model\Account|string|null $mAccount + */ + public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false) : string + { + $sEmail = $sSubFolder = $sFilePath = ''; + if (null === $mAccount || StorageType::NOBODY === $iStorageType) { + $sFilePath = $this->sDataPath.'/__nobody__/'; + } else { + if ($mAccount instanceof \RainLoop\Model\MainAccount) { + $sEmail = $mAccount->Email(); + } else if ($mAccount instanceof \RainLoop\Model\AdditionalAccount) { + $sEmail = $mAccount->ParentEmail(); + if ($this->bLocal) { + $sSubFolder = $mAccount->Email(); + } + } else if (\is_string($mAccount)) { + $sEmail = $mAccount; + } + + if ($sEmail) { + // these are never local + if (StorageType::SIGN_ME === $iStorageType) { + $sSubFolder = '.sign_me'; + } else if (StorageType::SESSION === $iStorageType) { + $sSubFolder = '.sessions'; + } else if (StorageType::PGP === $iStorageType) { + $sSubFolder = '.pgp'; + } else if (StorageType::ROOT === $iStorageType) { + $sSubFolder = ''; + } + } + + switch ($iStorageType) + { + case StorageType::CONFIG: + case StorageType::SIGN_ME: + case StorageType::SESSION: + case StorageType::PGP: + case StorageType::ROOT: + if (empty($sEmail)) { + return ''; + } + if (\is_dir("{$this->sDataPath}/cfg")) { + \SnappyMail\Upgrade::FileStorage($this->sDataPath); + } + $aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld'); + $sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : ''); + $sFilePath = $this->sDataPath + .'/'.\MailSo\Base\Utils::SecureFileName($sDomain ?: 'unknown.tld') + .'/'.\MailSo\Base\Utils::SecureFileName(\implode('@', $aEmail) ?: '.unknown') + .'/'.($sSubFolder ? \MailSo\Base\Utils::SecureFileName($sSubFolder).'/' : ''); + break; + default: + throw new \Exception("Invalid storage type {$iStorageType}"); + } + } + + $bMkDir && !empty($sFilePath) && \MailSo\Base\Utils::mkdir($sFilePath); + + return $sFilePath; + } + + /** + * @param \RainLoop\Model\Account|string|null $mAccount + */ + protected function generateFileName($mAccount, int $iStorageType, string $sKey, bool $bMkDir = false) : string + { + $sFilePath = $this->GenerateFilePath($mAccount, $iStorageType, $bMkDir); + if ($sFilePath) { + if (StorageType::NOBODY === $iStorageType) { + $sFilePath .= \sha1($sKey ?: \time()); + } else { + $sFilePath .= ($sKey ? \MailSo\Base\Utils::SecureFileName($sKey) : ''); + } + } + return $sFilePath; + } + + public function GC() : void + { + \clearstatcache(); + foreach (\glob("{$this->sDataPath}/*", GLOB_ONLYDIR) as $sDomain) { + foreach (\glob("{$sDomain}/*", GLOB_ONLYDIR) as $sLocal) { + \MailSo\Base\Utils::RecTimeDirRemove("{$sLocal}/.sign_me", 3600 * 24 * 30); // 30 days + \MailSo\Base\Utils::RecTimeDirRemove("{$sLocal}/.sessions", 3600 * 3); // 3 hours + } + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/IStorage.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/IStorage.php new file mode 100644 index 0000000000..66bf5b93b6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/IStorage.php @@ -0,0 +1,26 @@ +aDrivers = \array_filter($aDriver, function ($oDriver) { + return $oDriver instanceof \RainLoop\Providers\Suggestions\ISuggestions; + }); + } + } + + public function Process(\RainLoop\Model\Account $oAccount, string $sQuery, int $iLimit = 20) : array + { + if (!\strlen($sQuery)) { + return []; + } + + $iLimit = \max(5, (int) $iLimit); + $aResult = []; + + // Address Book + try + { + $oAddressBookProvider = \RainLoop\Api::Actions()->AddressBookProvider($oAccount); + if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) { + $aSuggestions = $oAddressBookProvider->GetSuggestions($sQuery, $iLimit); + foreach ($aSuggestions as $aItem) { + // Unique email address + $sLine = \mb_strtolower($aItem[0]); + if (!isset($aResult[$sLine])) { + $aResult[$sLine] = $aItem; + } + } + } + } + catch (\Throwable $oException) + { + $this->logException($oException); + } + + // Extensions/Plugins + foreach ($this->aDrivers as $oDriver) { + if ($oDriver) try { + $aSuggestions = $oDriver->Process($oAccount, $sQuery, $iLimit); + if ($aSuggestions) { + foreach ($aSuggestions as $aItem) { + // Unique email address + $sLine = \mb_strtolower($aItem[0]); + if (!isset($aResult[$sLine])) { + $aResult[$sLine] = $aItem; + } + } + if ($iLimit < \count($aResult)) { + break; + } + } + } catch (\Throwable $oException) { + $this->logException($oException); + } + } + + return \array_slice(\array_values($aResult), 0, $iLimit); + } + + public function IsActive() : bool + { + return \count($this->aDrivers); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions/ISuggestions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions/ISuggestions.php new file mode 100644 index 0000000000..bb1f2a6d87 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions/ISuggestions.php @@ -0,0 +1,10 @@ +Get('security', 'custom_server_signature', '')); + if (\strlen($sServer)) { + \header('Server: '.$sServer); + } + + \header('Referrer-Policy: no-referrer'); + \header('X-Content-Type-Options: nosniff'); + + // Google FLoC, obsolete +// \header('Permissions-Policy: interest-cohort=()'); + + static::setCSP(); + + $sXssProtectionOptionsHeader = \trim($oConfig->Get('security', 'x_xss_protection_header', '')) ?: '1; mode=block'; + \header('X-XSS-Protection: '.$sXssProtectionOptionsHeader); + + $oHttp = \MailSo\Base\Http::SingletonInstance(); + if ($oConfig->Get('security', 'force_https', false) && !$oHttp->IsSecure()) { + \MailSo\Base\Http::Location('https://'.$oHttp->GetHost(false).$oHttp->GetUrl()); + return true; + } + + // See https://github.com/kjdev/php-ext-brotli + if (!empty($_SERVER['HTTP_ACCEPT_ENCODING']) + && $oConfig->Get('webmail', 'compress_output', false) + && !\ini_get('zlib.output_compression') + && !\ini_get('brotli.output_compression') + ) { + if (\is_callable('brotli_compress_add') && false !== \stripos($_SERVER['HTTP_ACCEPT_ENCODING'], 'br')) { + \ob_start(function(string $buffer, int $phase){ + static $resource; + if ($phase & PHP_OUTPUT_HANDLER_START) { + \header('Content-Encoding: br'); + $resource = \brotli_compress_init(/*int $quality = 11, int $mode = BROTLI_GENERIC*/); + } + return \brotli_compress_add($resource, $buffer, ($phase & PHP_OUTPUT_HANDLER_FINAL) ? BROTLI_FINISH : BROTLI_PROCESS); + }); + } else { + \ob_start('ob_gzhandler'); + } + } else { + \ob_start(); + } + + $sQuery = \trim($_SERVER['QUERY_STRING'] ?? ''); + $iPos = \strpos($sQuery, '&'); + if (0 < $iPos) { + $sQuery = \substr($sQuery, 0, $iPos); + } + $sQuery = \trim(\trim($sQuery), ' /'); + $aSubQuery = $_GET['q'] ?? null; + if (\is_array($aSubQuery)) { + $aSubQuery = \array_map(function ($sS) { + return \trim(\trim($sS), ' /'); + }, $aSubQuery); + + if (\count($aSubQuery)) { + $sQuery .= '/' . \implode('/', $aSubQuery); + } + } + + $aPaths = \explode('/', $sQuery); + + $sAdminPanelHost = \trim($oConfig->Get('admin_panel', 'host', '')); + if (empty($sAdminPanelHost)) { + $bAdmin = !empty($aPaths[0]) && ($oConfig->Get('admin_panel', 'key', '') ?: 'admin') === $aPaths[0]; + $bAdmin && \array_shift($aPaths); + } else { + $bAdmin = \mb_strtolower($sAdminPanelHost) === \mb_strtolower($oHttp->GetHost()); + } + + $oActions = $bAdmin ? new ActionsAdmin() : Api::Actions(); + + $oActions->Plugins()->RunHook('filter.http-paths', array(&$aPaths)); + + if ($oHttp->IsPost()) { + $oHttp->ServerNoCache(); + } + + $oServiceActions = new ServiceActions($oHttp, $oActions); + + if ($bAdmin && !$oConfig->Get('security', 'allow_admin_panel', true)) { + \MailSo\Base\Http::StatusHeader(403); + echo $oServiceActions->ErrorTemplates('Access Denied.', + 'Access to the SnappyMail Admin Panel is not allowed!'); + + return false; + } + + $bIndex = true; + if (\count($aPaths) && !empty($aPaths[0]) && 'index' !== \strtolower($aPaths[0])) { + if ('mailto' !== \strtolower($aPaths[0]) && !\SnappyMail\HTTP\SecFetch::matchAnyRule($oConfig->Get('security', 'secfetch_allow', ''))) { + \MailSo\Base\Http::StatusHeader(403); + echo $oServiceActions->ErrorTemplates('Access Denied.', + "Disallowed Sec-Fetch + Dest: " . ($_SERVER['HTTP_SEC_FETCH_DEST'] ?? '') . " + Mode: " . ($_SERVER['HTTP_SEC_FETCH_MODE'] ?? '') . " + Site: " . ($_SERVER['HTTP_SEC_FETCH_SITE'] ?? '') . " + User: " . (\SnappyMail\HTTP\SecFetch::user() ? 'true' : 'false')); + return false; + } + + $sMethodName = 'Service'.\preg_replace('/@.+$/', '', $aPaths[0]); + $sMethodExtra = \strpos($aPaths[0], '@') ? \preg_replace('/^[^@]+@/', '', $aPaths[0]) : ''; + + if (\method_exists($oServiceActions, $sMethodName) && \is_callable(array($oServiceActions, $sMethodName))) { + $bIndex = false; + $oServiceActions->SetQuery($sQuery)->SetPaths($aPaths); + echo $oServiceActions->{$sMethodName}($sMethodExtra); + } else if ($oActions->Plugins()->RunAdditionalPart($aPaths[0], $aPaths)) { + $bIndex = false; + } + } + + if ($bIndex) { + // https://github.com/the-djmaze/snappymail/issues/1024 + $oHttp->ServerNoCache(); + + if (!$bAdmin) { + $login = $oConfig->Get('labs', 'custom_login_link', ''); + if ($login && !$oActions->getAccountFromToken(false)) { + \MailSo\Base\Http::Location($login); + return true; + } + } + +// if (!\SnappyMail\HTTP\SecFetch::isEntering()) { + \header('Content-Type: text/html; charset=utf-8'); + + if (!\is_dir(APP_DATA_FOLDER_PATH) || !\is_writable(APP_DATA_FOLDER_PATH)) { + echo $oServiceActions->ErrorTemplates( + 'Permission denied!', + 'SnappyMail can not access the data folder "'.APP_DATA_FOLDER_PATH.'"' + ); + return false; + } + + $sLanguage = $oActions->GetLanguage($bAdmin); + + $bAppDebug = $oConfig->Get('debug', 'enable', false); + $sAppJsMin = $bAppDebug || $oConfig->Get('debug', 'javascript', false) ? '' : '.min'; + $sAppCssMin = $bAppDebug || $oConfig->Get('debug', 'css', false) ? '' : '.min'; + + $sFaviconUrl = (string) $oConfig->Get('webmail', 'favicon_url', ''); + + $sFaviconPngLink = $sFaviconUrl ?: Utils::WebStaticPath('apple-touch-icon.png'); + $sAppleTouchLink = $sFaviconUrl ? '' : Utils::WebStaticPath('apple-touch-icon.png'); + + $oActions = Api::Actions(); + + $sThemeName = $oActions->GetTheme($bAdmin); + + $aTemplateParameters = array( + '{{BaseAppThemeName}}' => $sThemeName, + '{{BaseAppFaviconPngLinkTag}}' => $sFaviconPngLink ? '' : '', + '{{BaseAppFaviconTouchLinkTag}}' => $sAppleTouchLink ? '' : '', + '{{BaseAppManifestLink}}' => Utils::WebStaticPath('manifest.json'), + '{{BaseFavIconSvg}}' => $sFaviconUrl ? '' : Utils::WebStaticPath('favicon.svg'), + '{{LoadingDescriptionEsc}}' => \htmlspecialchars($oConfig->Get('webmail', 'loading_description', 'SnappyMail'), ENT_QUOTES|ENT_IGNORE, 'UTF-8'), + '{{BaseAppAdmin}}' => $bAdmin ? 1 : 0 + ); + + $sCacheFileName = 'TMPL:' . \sha1( + Utils::jsonEncode(array( + $sLanguage, + $oConfig->Get('cache', 'index', ''), + $oActions->Plugins()->Hash(), + $sAppJsMin, + $sAppCssMin, + $aTemplateParameters, + APP_VERSION + )) + ); + + // https://github.com/the-djmaze/snappymail/issues/1024 +// $oActions->verifyCacheByKey($sCacheFileName); + + if ($oConfig->Get('cache', 'system_data', true)) { + $sResult = $oActions->Cacher()->Get($sCacheFileName); + } else { + $sResult = ''; + } + + if ($sResult) { + $sResult .= ''; + } else { + $aTemplateParameters['{{BaseAppBootCss}}'] = \file_get_contents(APP_VERSION_ROOT_PATH.'static/css/boot'.$sAppCssMin.'.css'); + $aTemplateParameters['{{BaseAppBootScript}}'] = \file_get_contents(APP_VERSION_ROOT_PATH.'static/js'.($sAppJsMin ? '/min' : '').'/boot'.$sAppJsMin.'.js'); + $aTemplateParameters['{{BaseAppMainCssLink}}'] = Utils::WebStaticPath('css/'.($bAdmin ? 'admin' : 'app').$sAppCssMin.'.css'); + $aTemplateParameters['{{BaseAppThemeCss}}'] = \preg_replace('/\\s*([:;{},]+)\\s*/s', '$1', $oActions->compileCss($sThemeName, $bAdmin)); + $aTemplateParameters['{{BaseLanguage}}'] = $oActions->compileLanguage($sLanguage, $bAdmin); + $aTemplateParameters['{{BaseTemplates}}'] = Utils::ClearHtmlOutput($oServiceActions->compileTemplates($bAdmin)); + $aTemplateParameters['{{NO_SCRIPT_DESC}}'] = \nl2br($oActions->StaticI18N('NO_SCRIPT_TITLE') . "\n" . $oActions->StaticI18N('NO_SCRIPT_DESC')); + $aTemplateParameters['{{NO_COOKIE_TITLE}}'] = $oActions->StaticI18N('NO_COOKIE_TITLE'); + $aTemplateParameters['{{NO_COOKIE_DESC}}'] = $oActions->StaticI18N('NO_COOKIE_DESC'); + $aTemplateParameters['{{BAD_BROWSER_TITLE}}'] = $oActions->StaticI18N('BAD_BROWSER_TITLE'); + $aTemplateParameters['{{BAD_BROWSER_DESC}}'] = \nl2br($oActions->StaticI18N('BAD_BROWSER_DESC')); + $sResult = Utils::ClearHtmlOutput(\file_get_contents(APP_VERSION_ROOT_PATH.'app/templates/Index.html')); + $sResult = \strtr($sResult, $aTemplateParameters); + if ($sCacheFileName) { + $oActions->Cacher()->Set($sCacheFileName, $sResult); + } + } + + $SameSite = \strtolower($oConfig->Get('security', 'cookie_samesite', 'Strict')); + $Secure = (isset($_SERVER['HTTPS']) || 'none' == $SameSite) ? ';secure' : ''; + $sResult = \str_replace('samesite=strict', "samesite={$SameSite}{$Secure}", $sResult); + + $sScriptNonce = \SnappyMail\UUID::generate(); + static::setCSP($sScriptNonce); + $sResult = \str_replace('nonce=""', 'nonce="'.$sScriptNonce.'"', $sResult); +/* + \preg_match(']+>(.+)', $sResult, $script); + $sScriptHash = 'sha256-'.\base64_encode(\hash('sha256', $script[1], true)); + static::setCSP(null, $sScriptHash); +*/ + // https://github.com/the-djmaze/snappymail/issues/1024 +// $oActions->cacheByKey($sCacheFileName); + + echo $sResult; + unset($sResult); + } else if (!\headers_sent()) { + \header('X-XSS-Protection: 1; mode=block'); + } + + $oActions->BootEnd(); + + return true; + } + + private static function setCSP(?string $sScriptNonce = null) : void + { + Api::getCSP($sScriptNonce)->setHeaders(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php new file mode 100644 index 0000000000..e46b953756 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php @@ -0,0 +1,664 @@ +oHttp = $oHttp; + $this->oActions = $oActions; + } + + private function Logger() : \MailSo\Log\Logger + { + return $this->oActions->Logger(); + } + + private function Plugins() : Plugins\Manager + { + return $this->oActions->Plugins(); + } + + private function Config() : Config\Application + { + return $this->oActions->Config(); + } + + private function Cacher() : \MailSo\Cache\CacheClient + { + return $this->oActions->Cacher(); + } + + private function StorageProvider() : Providers\Storage + { + return $this->oActions->StorageProvider(); + } + + private function SettingsProvider() : Providers\Settings + { + return $this->oActions->SettingsProvider(); + } + + public function SetPaths(array $aPaths) : self + { + $this->aPaths = $aPaths; + return $this; + } + + public function SetQuery(string $sQuery) : self + { + $this->sQuery = $sQuery; + return $this; + } + +/* + public function ServiceBackup() : void + { + if (\method_exists($this->oActions, 'DoAdminBackup')) { + $this->oActions->DoAdminBackup(); + } + exit; + } +*/ + + public function ServiceJson() : string + { + \ob_start(); + + $aResponse = null; + $oException = null; + + if (empty($_POST) || (!empty($_SERVER['CONTENT_TYPE']) && \str_contains($_SERVER['CONTENT_TYPE'], 'application/json'))) { + $_POST = \json_decode(\file_get_contents('php://input'), true); + } + + $sAction = $_POST['Action'] ?? ''; + if (empty($sAction) && $this->oHttp->IsGet() && !empty($this->aPaths[2])) { + $sAction = $this->aPaths[2]; + } + + $this->oActions->SetIsJson(true); + + try + { + if (empty($sAction)) { + throw new Exceptions\ClientException(Notifications::InvalidInputArgument, null, 'Action unknown'); + } + + if ('Logout' !== $sAction) { + $token = Utils::GetCsrfToken(); + if (isset($_SERVER['HTTP_X_SM_TOKEN'])) { + if ($_SERVER['HTTP_X_SM_TOKEN'] !== $token) { + $oAccount = $this->oActions->getAccountFromToken(false); + $sEmail = $oAccount ? $oAccount->Email() : 'guest'; + $this->oActions->logWrite("{$_SERVER['HTTP_X_SM_TOKEN']} !== {$token} for {$sEmail}", \LOG_ERR, 'Token'); + throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'HTTP Token mismatch'); + } + } else if ($this->oHttp->IsPost()) { + if (empty($_POST['XToken']) || $_POST['XToken'] !== $token) { + $oAccount = $this->oActions->getAccountFromToken(false); + $sEmail = $oAccount ? $oAccount->Email() : 'guest'; + $this->oActions->logWrite("{$_POST['XToken']} !== {$token} for {$sEmail}", \LOG_ERR, 'XToken'); + throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'XToken mismatch'); + } + } + } + + if ($this->oActions instanceof ActionsAdmin && 0 === \stripos($sAction, 'Admin') && !\in_array($sAction, ['AdminLogin', 'AdminLogout'])) { + $this->oActions->IsAdminLoggined(); + } + + $sMethodName = 'Do'.$sAction; + + $this->oActions->logWrite('Action: '.$sMethodName, \LOG_INFO, 'JSON'); + + if ($_POST) { + $this->oActions->SetActionParams($_POST, $sMethodName); + $aPost = $_POST; + foreach ($aPost as $key => $value) { + // password & passphrase + if (false !== \stripos($key, 'pass')) { + $aPost[$key] = '*******'; +// $this->oActions->logMask($value); + } + } + $this->oActions->logWrite(Utils::jsonEncode($aPost), \LOG_INFO, 'POST'); + } else if (3 < \count($this->aPaths) && $this->oHttp->IsGet()) { + $this->oActions->SetActionParams(array( + 'RawKey' => empty($this->aPaths[3]) ? '' : $this->aPaths[3] + ), $sMethodName); + } + + if (\method_exists($this->oActions, $sMethodName) && \is_callable(array($this->oActions, $sMethodName))) { + $this->Plugins()->RunHook("json.before-{$sAction}"); + $aResponse = $this->oActions->{$sMethodName}(); + } else if ($this->Plugins()->HasAdditionalJson($sMethodName)) { + $this->Plugins()->RunHook("json.before-{$sAction}"); + $aResponse = $this->Plugins()->RunAdditionalJson($sMethodName); + } + + if (\is_array($aResponse)) { + // Everything must converted to array + $aResponse = \json_decode(Utils::jsonEncode($aResponse), true); + $this->Plugins()->RunHook("json.after-{$sAction}", array(&$aResponse)); + } + + if (!\is_array($aResponse)) { + throw new Exceptions\ClientException(Notifications::UnknownError); + } + } + catch (\Throwable $oException) + { + \SnappyMail\Log::warning('SERVICE', "{$oException}"); + if ($e = $oException->getPrevious()) { + \SnappyMail\Log::warning('SERVICE', "- {$e}"); + } + + $aResponse = $this->oActions->ExceptionResponse($oException); + } + + $aResponse['Action'] = $sAction ?: 'Unknown'; + + if (!\headers_sent()) { + \header('Content-Type: application/json; charset=utf-8'); + } + + if (\is_array($aResponse)) { + $aResponse['epoch'] = \time(); +// if ($this->Config()->Get('debug', 'enable', false)) { +// $aResponse['rtime'] = \round(\microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'], 3); +// } + } + + $sResult = Utils::jsonEncode($aResponse); + + $sObResult = \ob_get_clean(); + + if ($this->Logger()->IsEnabled()) { + if (\strlen($sObResult)) { + $this->oActions->logWrite($sObResult, \LOG_ERR, 'OB-DATA'); + } + + if ($oException) { + $this->oActions->logException($oException, \LOG_ERR); + } + + $iLimit = (int) $this->Config()->Get('logs', 'json_response_write_limit', 0); + $this->oActions->logWrite(0 < $iLimit && $iLimit < \strlen($sResult) + ? \substr($sResult, 0, $iLimit).'...' : $sResult, \LOG_INFO, 'JSON'); + } + + return $sResult; + } + + private function privateUpload(string $sAction, int $iSizeLimit = 0) : string + { + $oConfig = $this->Config(); + + \ob_start(); + $aResponse = null; + try + { + $aFile = null; + $sInputName = 'uploader'; + $iSizeLimit = (0 < $iSizeLimit ? $iSizeLimit : ((int) $oConfig->Get('webmail', 'attachment_size_limit', 0))) * 1024 * 1024; + + $_FILES = isset($_FILES) ? $_FILES : null; + if (isset($_FILES[$sInputName], $_FILES[$sInputName]['name'], $_FILES[$sInputName]['tmp_name'], $_FILES[$sInputName]['size'])) { + $iError = (isset($_FILES[$sInputName]['error'])) ? (int) $_FILES[$sInputName]['error'] : UPLOAD_ERR_OK; +// \is_uploaded_file($_FILES[$sInputName]['tmp_name']) + + if (UPLOAD_ERR_OK === $iError && 0 < $iSizeLimit && $iSizeLimit < (int) $_FILES[$sInputName]['size']) { + $iError = Enumerations\UploadError::CONFIG_SIZE; + } + + if (UPLOAD_ERR_OK === $iError) { + $aFile = $_FILES[$sInputName]; + } + } else if (empty($_FILES)) { + $iError = UPLOAD_ERR_INI_SIZE; + } else { + $iError = Enumerations\UploadError::EMPTY_FILE; + } + + if (\method_exists($this->oActions, $sAction) && \is_callable(array($this->oActions, $sAction))) { + $aResponse = $this->oActions->{$sAction}($aFile, $iError); + } + + if (!is_array($aResponse)) { + throw new Exceptions\ClientException(Notifications::UnknownError); + } + + $this->Plugins()->RunHook('filter.upload-response', array(&$aResponse)); + } + catch (\Throwable $oException) + { + $aResponse = $this->oActions->ExceptionResponse($oException); + } + + $aResponse['Action'] = $sAction ?: 'Unknown'; + + \header('Content-Type: application/json; charset=utf-8'); + + $sResult = Utils::jsonEncode($aResponse); + + $sObResult = \ob_get_clean(); + if (\strlen($sObResult)) { + $this->oActions->logWrite($sObResult, \LOG_ERR, 'OB-DATA'); + } + + $this->oActions->logWrite($sResult, \LOG_INFO, 'UPLOAD'); + + return $sResult; + } + + public function ServiceUpload() : string + { + return $this->privateUpload('Upload'); + } + + public function ServiceUploadContacts() : string + { + return $this->privateUpload('UploadContacts', 5); + } + + public function ServiceUploadBackground() : string + { + return $this->privateUpload('UploadBackground', 1); + } + + public function ServiceProxyExternal() : string + { + $sData = empty($this->aPaths[1]) ? '' : $this->aPaths[1]; + if ($sData + && $this->Config()->Get('labs', 'use_local_proxy_for_external_images', false) + && $this->oActions->getAccountFromToken() + ) { + $this->oActions->verifyCacheByKey($sData); + $sUrl = \MailSo\Base\Utils::UrlSafeBase64Decode($sData); + if (!empty($sUrl)) { + \header('X-Content-Location: '.$sUrl); + $tmp = \tmpfile(); + $HTTP = \SnappyMail\HTTP\Request::factory(); + $HTTP->max_redirects = 2; + $HTTP->streamBodyTo($tmp); + $oResponse = $HTTP->doRequest('GET', $sUrl); + if ($oResponse) { + $sContentType = \SnappyMail\File\MimeType::fromStream($tmp) ?: $oResponse->getHeader('content-type'); + if (200 === $oResponse->status && \str_starts_with($sContentType, 'image/')) { + try { + $this->oActions->cacheByKey($sData); + \header('Content-Type: ' . $sContentType); + \header('Cache-Control: public'); + \header('Expires: '.\gmdate('D, j M Y H:i:s', 2592000 + \time()).' UTC'); + \header('X-Content-Redirect-Location: '.$oResponse->final_uri); + \rewind($tmp); + \fpassthru($tmp); + exit; + } catch (\Throwable $e) { + \header("X-Content-Error: {$e->getMessage()}"); + \SnappyMail\Log::error('Proxy', \get_class($HTTP) . ': ' . $e->getMessage()); + } + } else { + \header("X-Content-Error: {$oResponse->status} {$sContentType}"); + } + } + } + } + + \MailSo\Base\Http::StatusHeader(404); + return ''; + } + + public function ServiceCspReport() : void + { + \SnappyMail\HTTP\CSP::logReport(); + } + + public function ServiceRaw() : string + { + $sResult = ''; + $sRawError = ''; + $sAction = empty($this->aPaths[2]) ? '' : $this->aPaths[2]; + $oException = null; + + try + { + $sRawError = 'Invalid action'; + if (\strlen($sAction)) { + try { + $sMethodName = 'Raw'.$sAction; + if (\method_exists($this->oActions, $sMethodName)) { + \header('X-Raw-Action: '.$sMethodName); + \header('Content-Security-Policy: script-src \'none\'; child-src \'none\''); + + $sRawError = ''; + $this->oActions->SetActionParams(array( + 'RawKey' => empty($this->aPaths[3]) ? '' : $this->aPaths[3], + 'Params' => $this->aPaths + ), $sMethodName); + + if (!$this->oActions->{$sMethodName}()) { + $sRawError = 'False result'; + } + } else { + $sRawError = 'Unknown action "'.$sAction.'"'; + } + } catch (\Throwable $e) { +// error_log(print_r($e,1)); + $sRawError = $e->getMessage(); + } + } else { + $sRawError = 'Empty action'; + } + } + catch (Exceptions\ClientException $oException) + { + $sRawError = Notifications::AuthError == $oException->getCode() + ? 'Authentication failed' + : 'Exception as result'; + } + catch (\Throwable $oException) + { + $sRawError = 'Exception as result'; + } + + if (\strlen($sRawError)) { + $this->oActions->logWrite($sRawError, \LOG_ERR); + $this->Logger()->WriteDump($this->aPaths, \LOG_ERR, 'PATHS'); + } + + if ($oException) { + $this->oActions->logException($oException, \LOG_ERR, 'RAW'); + } + + return $sResult; + } + + public function ServiceLang() : string + { + $sResult = ''; + \header('Content-Type: application/javascript; charset=utf-8'); + + if (!empty($this->aPaths[3])) { + $bAdmin = 'Admin' === (isset($this->aPaths[2]) ? (string) $this->aPaths[2] : 'App'); + $sLanguage = $this->oActions->ValidateLanguage($this->aPaths[3], '', $bAdmin); + + $bCacheEnabled = $this->Config()->Get('cache', 'system_data', true); + $sCacheFileName = ''; + if ($bCacheEnabled) { + $sCacheFileName = KeyPathHelper::LangCache($sLanguage, $bAdmin, $this->oActions->Plugins()->Hash()); + $this->oActions->verifyCacheByKey(\md5($sCacheFileName)); + $sResult = $this->Cacher()->Get($sCacheFileName); + } + + if (!$sResult) { + $sResult = $this->oActions->compileLanguage($sLanguage, $bAdmin); + if ($sCacheFileName) { + $this->Cacher()->Set($sCacheFileName, $sResult); + } + } + + if ($sCacheFileName) { + $this->oActions->cacheByKey(\md5($sCacheFileName)); + } + } + + return $sResult; + } + + public function ServicePlugins() : string + { + $sResult = ''; + $bAdmin = !empty($this->aPaths[2]) && 'Admin' === $this->aPaths[2]; + + \header('Content-Type: application/javascript; charset=utf-8'); + + $bAppDebug = $this->Config()->Get('debug', 'enable', false); + $sMinify = ($bAppDebug || $this->Config()->Get('debug', 'javascript', false)) ? '' : 'min'; + + $bCacheEnabled = !$bAppDebug && $this->Config()->Get('cache', 'system_data', true); + $sCacheFileName = ''; + if ($bCacheEnabled) { + $sCacheFileName = KeyPathHelper::PluginsJsCache($this->oActions->Plugins()->Hash()) . $sMinify; + $this->oActions->verifyCacheByKey(\md5($sCacheFileName)); + $sResult = $this->Cacher()->Get($sCacheFileName); + } + + if (!$sResult) { + $sResult = $this->Plugins()->CompileJs($bAdmin, !!$sMinify); + if ($sCacheFileName) { + $this->Cacher()->Set($sCacheFileName, $sResult); + } + } + + if ($sCacheFileName) { + $this->oActions->cacheByKey(\md5($sCacheFileName)); + } + + return $sResult; + } + + public function ServiceCss() : string + { + $sResult = ''; + $bAdmin = !empty($this->aPaths[2]) && 'Admin' === $this->aPaths[2]; + $bJson = !empty($this->aPaths[9]) && 'Json' === $this->aPaths[9]; + + if ($bJson) { + \header('Content-Type: application/json; charset=utf-8'); + } else { + \header('Content-Type: text/css; charset=utf-8'); + } + + $sTheme = ''; + if (!empty($this->aPaths[4])) { + $sTheme = $this->oActions->ValidateTheme($this->aPaths[4]); + + $bAppDebug = $this->Config()->Get('debug', 'enable', false); + $sMinify = ($bAppDebug || $this->Config()->Get('debug', 'css', false)) ? '' : 'min'; + + $bCacheEnabled = !$bAppDebug && $this->Config()->Get('cache', 'system_data', true); + $sCacheFileName = ''; + if ($bCacheEnabled) { + $sCacheFileName = '/CssCache/'.$this->oActions->Plugins()->Hash().'/'.$sTheme.'/'.APP_VERSION.'/' . $sMinify; + $this->oActions->verifyCacheByKey(\md5($sCacheFileName . ($bJson ? 1 : 0))); + $sResult = $this->Cacher()->Get($sCacheFileName); + } + + if (!$sResult) { + try + { + $sResult = $this->oActions->compileCss($sTheme, $bAdmin); + if ($sCacheFileName) { + $this->Cacher()->Set($sCacheFileName, $sResult); + } + } + catch (\Throwable $oException) + { + $this->oActions->logException($oException, \LOG_ERR, 'LESS'); + } + } + + if ($sCacheFileName) { + $this->oActions->cacheByKey(\md5($sCacheFileName . ($bJson ? 1 : 0 ))); + } + } + + return $bJson ? Utils::jsonEncode(array($sTheme, $sResult)) : $sResult; + } + + public function ServiceAppData() : string + { + return $this->localAppData(false); + } + + public function ServiceAdminAppData() : string + { + return $this->localAppData(true); + } + + public function ServiceMailto() : string + { + $this->oHttp->ServerNoCache(); + $sTo = \trim($_GET['to'] ?? ''); + if (\preg_match('/^mailto:/i', $sTo)) { + \SnappyMail\Cookies::set( + Actions::AUTH_MAILTO_TOKEN_KEY, + Utils::EncodeKeyValuesQ(array( + 'Time' => \microtime(true), + 'MailTo' => 'MailTo', + 'To' => $sTo + )) + ); + } + \MailSo\Base\Http::Location('./'); + return ''; + } + + public function ServicePing() : string + { + $this->oHttp->ServerNoCache(); + + \header('Content-Type: text/plain; charset=utf-8'); + $this->oActions->logWrite('Pong', \LOG_INFO, 'PING'); + return 'Pong'; + } + + public function ServiceTest() : string + { + $this->oHttp->ServerNoCache(); + \SnappyMail\Integrity::test(); + return ''; + } + + /** + * Login with the \RainLoop\API::CreateUserSsoHash() generated hash + */ + public function ServiceSso() : string + { + $this->oHttp->ServerNoCache(); + + $oException = null; + $oAccount = null; + + $sSsoHash = $_REQUEST['hash'] ?? ''; + if (!empty($sSsoHash)) { + $mData = null; + + $sSsoSubData = $this->Cacher()->Get(KeyPathHelper::SsoCacherKey($sSsoHash)); + if (!empty($sSsoSubData)) { + $aData = \SnappyMail\Crypt::DecryptFromJSON($sSsoSubData, $sSsoHash); + + $this->Cacher()->Delete(KeyPathHelper::SsoCacherKey($sSsoHash)); + + if (\is_array($aData) && !empty($aData['Email']) && isset($aData['Password'], $aData['Time']) && + (0 === $aData['Time'] || \time() - 10 < $aData['Time'])) + { + $aAdditionalOptions = (isset($aData['AdditionalOptions']) && \is_array($aData['AdditionalOptions'])) + ? $aData['AdditionalOptions'] : []; + try + { + $oAccount = $this->oActions->LoginProcess( + \trim($aData['Email']), + new \SnappyMail\SensitiveString($aData['Password']) + ); + if ($aAdditionalOptions) { + $bSaveSettings = false; + + $oSettings = $this->SettingsProvider()->Load($oAccount); + if ($oSettings) { + $sLanguage = isset($aAdditionalOptions['language']) ? + $aAdditionalOptions['language'] : ''; + + if ($sLanguage) { + $sLanguage = $this->oActions->ValidateLanguage($sLanguage); + if ($sLanguage !== $oSettings->GetConf('language', '')) { + $bSaveSettings = true; + $oSettings->SetConf('language', $sLanguage); + } + } + } + + if ($bSaveSettings) { + $oSettings->save(); + } + } + } + catch (\Throwable $oException) + { + $this->oActions->logException($oException); + } + } + } + } + + \MailSo\Base\Http::Location('./'); + return ''; + } + + public function ErrorTemplates(string $sTitle, string $sDesc, bool $bShowBackLink = true) : string + { + return \strtr(\file_get_contents(APP_VERSION_ROOT_PATH.'app/templates/Error.html'), array( + '{{ErrorTitle}}' => $sTitle, + '{{ErrorHeader}}' => $sTitle, + '{{ErrorDesc}}' => $sDesc, + '{{BackLinkVisibilityStyle}}' => $bShowBackLink ? 'display:inline-block' : 'display:none', + '{{BackLink}}' => $this->oActions->StaticI18N('BACK_LINK'), + '{{BackHref}}' => './' + )); + } + + private function localError(string $sTitle, string $sDesc) : string + { + \header('Content-Type: text/html; charset=utf-8'); + return $this->ErrorTemplates($sTitle, \nl2br($sDesc)); + } + + private function localAppData(bool $bAdmin = false) : string + { + \header('Content-Type: application/json; charset=utf-8'); + $this->oHttp->ServerNoCache(); + try { + return Utils::jsonEncode($this->oActions->AppData($bAdmin)); + } catch (\Throwable $oException) { + $this->Logger()->WriteExceptionShort($oException); + \MailSo\Base\Http::StatusHeader(500); + return $oException->getMessage(); + } + } + + public function compileTemplates(bool $bAdmin = false) : string + { + $aTemplates = array(); + + foreach (['Components', ($bAdmin ? 'Admin' : 'User'), 'Common'] as $dir) { + $sNameSuffix = ('Components' === $dir) ? 'Component' : ''; + foreach (\glob(APP_VERSION_ROOT_PATH."app/templates/Views/{$dir}/*.html") as $file) { + $sTemplateName = \basename($file, '.html') . $sNameSuffix; + $aTemplates[$sTemplateName] = $file; + } + } + + $this->oActions->Plugins()->CompileTemplate($aTemplates, $bAdmin); + + $sHtml = ''; + foreach ($aTemplates as $sName => $sFile) { + $sName = \preg_replace('/[^a-zA-Z0-9]/', '', $sName); + $sHtml .= ''; + } + + return \str_replace(' ', "\xC2\xA0", $sHtml); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Settings.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Settings.php new file mode 100644 index 0000000000..bf0bf23ada --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Settings.php @@ -0,0 +1,64 @@ +aData = $aData; + $this->oAccount = $oAccount; + $this->oProvider = $oProvider; + } + + public function save() : bool + { + return $this->oProvider->Save($this->oAccount, $this); + } + + public function toArray() : array + { + return $this->aData; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->aData;; + } + + /** + * @param mixed $mDefValue = null + * + * @return mixed + */ + public function GetConf(string $sName, $mDefValue = null) + { + return isset($this->aData[$sName]) ? $this->aData[$sName] : $mDefValue; + } + + /** + * @param mixed $mValue + */ + public function SetConf(string $sName, $mValue) : void + { + $this->aData[$sName] = $mValue; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php new file mode 100644 index 0000000000..c72d127f0a --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php @@ -0,0 +1,174 @@ +Get('debug', 'enable', false)) { + $flags |= \JSON_PRETTY_PRINT; + } + */ + return \json_encode($value, $flags | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR); + } catch (\Throwable $e) { + Api::Logger()->WriteException($e, \LOG_ERR, 'JSON'); + } + return ''; + } + + public static function EncodeKeyValuesQ(array $aValues, string $sCustomKey = '') : string + { + return \SnappyMail\Crypt::EncryptUrlSafe( + $aValues, + \sha1(APP_SALT.$sCustomKey.'Q'.static::GetSessionToken()) + ); + } + + public static function DecodeKeyValuesQ(string $sEncodedValues, string $sCustomKey = '') : array + { + return \SnappyMail\Crypt::DecryptUrlSafe( + $sEncodedValues, + \sha1(APP_SALT.$sCustomKey.'Q'.static::GetSessionToken(false)) + ) ?: array(); + } + + public static function GetSessionToken(bool $generate = true) : ?string + { + $sToken = \SnappyMail\Cookies::get(self::SESSION_TOKEN); + if (!$sToken) { + if (!$generate) { + return null; + } + \SnappyMail\Log::debug('TOKENS', 'New SESSION_TOKEN'); + $sToken = \MailSo\Base\Utils::Sha1Rand(APP_SALT); + \SnappyMail\Cookies::set(self::SESSION_TOKEN, $sToken); + } + return \sha1('Session'.APP_SALT.$sToken.'Token'.APP_SALT); + } + + public static function GetConnectionToken() : string + { + $oActions = \RainLoop\Api::Actions(); + $oAccount = $oActions->getAccountFromToken(false); +// $oAccount = $oActions->getMainAccountFromToken(false); + if ($oAccount) { + if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) { + return '2-' . \sha1(APP_SALT.$oAccount->Hash()); + } + return '1-' . \sha1(APP_SALT.$oAccount->Hash()); + } + $sToken = \SnappyMail\Cookies::get(self::CONNECTION_TOKEN); + if (!$sToken) { + $sToken = \MailSo\Base\Utils::Sha1Rand(APP_SALT); + \SnappyMail\Cookies::set(self::CONNECTION_TOKEN, $sToken, \time() + 3600 * 24 * 30); + } + return '0-' . \sha1('Connection'.APP_SALT.$sToken.'Token'.APP_SALT); + } + + public static function GetCsrfToken() : string + { + return self::GetConnectionToken(); +// return \sha1('Csrf'.APP_SALT.self::GetConnectionToken().'Token'.APP_SALT); + } + + public static function UpdateConnectionToken() : void + { + $sToken = \SnappyMail\Cookies::get(self::CONNECTION_TOKEN); + if ($sToken) { + \SnappyMail\Cookies::set(self::CONNECTION_TOKEN, $sToken, \time() + 3600 * 24 * 30); + } + } + + public static function ClearHtmlOutput(string $sHtml) : string + { +// return $sHtml; + return \preg_replace('/>\\s+ <', \preg_replace( + ['@\\s*/>@', '/\\s* /i', '/ \\s*/i', '/[\\r\\n\\t]+/'], + ['>', "\xC2\xA0", "\xC2\xA0", ' '], + \trim($sHtml) + )); + } + + public static function WebPath() : string + { + static $sAppPath; + if (!$sAppPath) { + $sAppPath = \rtrim(Api::Config()->Get('webmail', 'app_path', '') + ?: \preg_replace('#index\\.php.*$#D', '', $_SERVER['SCRIPT_NAME']), + '/') . '/'; + } + return $sAppPath; + } + + public static function WebVersionPath() : string + { + return self::WebPath() . 'snappymail/v/' . APP_VERSION . '/'; + /** + * TODO: solve this to support other paths. + * https://github.com/the-djmaze/snappymail/issues/685 + */ +// return self::WebPath() . \str_replace(APP_INDEX_ROOT_PATH, '', APP_VERSION_ROOT_PATH); + } + + public static function WebStaticPath(string $path = '') : string + { + return self::WebVersionPath() . 'static/' . $path; + } + + public static function inOpenBasedir(string $name) : string + { + static $open_basedir; + if (null === $open_basedir) { + $open_basedir = \array_filter(\explode(PATH_SEPARATOR, \ini_get('open_basedir'))); + } + if ($open_basedir) { + foreach ($open_basedir as $dir) { + if (\str_starts_with($name, $dir)) { + return true; + } + } +// \SnappyMail\Log::warning('OpenBasedir', "open_basedir restriction in effect. {$name} is not within the allowed path(s): " . \ini_get('open_basedir')); + return false; + } + return true; + } + + public static function saveFile(string $filename, string $data) : void + { + $dir = \dirname($filename); + if (!\is_dir($dir) && !\mkdir($dir, 0700, true)) { + throw new \RuntimeException('Failed to create directory "'.$dir.'"'); + } + if (false === \file_put_contents($filename, $data)) { + throw new \RuntimeException('Failed to save file "'.$filename.'"'); + } + \clearstatcache(); + \chmod($filename, 0600); +/* + try { + } catch (\Throwable $oException) { + throw new \RuntimeException($oException->getMessage() . ': ' . \error_get_last()['message']); + } +*/ + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/LICENSE b/snappymail/v/0.0.0/app/libraries/Sabre/LICENSE new file mode 100644 index 0000000000..a99c8da198 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2011-2016 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/BirthdayCalendarGenerator.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/BirthdayCalendarGenerator.php new file mode 100644 index 0000000000..49793b0d46 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/BirthdayCalendarGenerator.php @@ -0,0 +1,160 @@ +setObjects($objects); + } + } + + /** + * Sets the input objects. + * + * You must either supply a vCard as a string or as a Component/VCard object. + * It's also possible to supply an array of strings or objects. + */ + public function setObjects($objects): void + { + if (!is_array($objects)) { + $objects = [$objects]; + } + + $this->objects = []; + foreach ($objects as $object) { + if (is_string($object)) { + $vObj = Reader::read($object); + if (!$vObj instanceof Component\VCard) { + throw new \InvalidArgumentException('String could not be parsed as \\Sabre\\VObject\\Component\\VCard by setObjects'); + } + + $this->objects[] = $vObj; + } elseif ($object instanceof Component\VCard) { + $this->objects[] = $object; + } else { + throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component\\VCard arguments to setObjects'); + } + } + } + + /** + * Sets the output format for the SUMMARY. + */ + public function setFormat(string $format): void + { + $this->format = $format; + } + + /** + * Parses the input data and returns a VCALENDAR. + */ + public function getResult(): VCalendar + { + $calendar = new VCalendar(); + + foreach ($this->objects as $object) { + // Skip if there is no BDAY property. + if (!$object->select('BDAY')) { + continue; + } + + // We've seen clients (ez-vcard) putting "BDAY:" properties + // without a value into vCards. If we come across those, we'll + // skip them. + if (empty($object->BDAY->getValue())) { + continue; + } + + // We're always converting to vCard 4.0, so we can rely on the + // VCardConverter handling the X-APPLE-OMIT-YEAR property for us. + $object = $object->convert(Document::VCARD40); + + // Skip if the card has no FN property. + if (!isset($object->FN)) { + continue; + } + + // Skip if the BDAY property is not of the right type. + if (!$object->BDAY instanceof Property\VCard\DateAndOrTime) { + continue; + } + + // Skip if we can't parse the BDAY value. + try { + $dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue()); + } catch (InvalidDataException $e) { + continue; + } + + // Set a year if it's not set. + $unknownYear = false; + + if (!$dateParts['year']) { + $object->BDAY = self::DEFAULT_YEAR.'-'.$dateParts['month'].'-'.$dateParts['date']; + + $unknownYear = true; + } + + // Create event. + $event = $calendar->add('VEVENT', [ + 'SUMMARY' => sprintf($this->format, $object->FN->getValue()), + 'DTSTART' => new \DateTime($object->BDAY->getValue()), + 'RRULE' => 'FREQ=YEARLY', + 'TRANSP' => 'TRANSPARENT', + ]); + + // add VALUE=date + $event->DTSTART['VALUE'] = 'DATE'; + + // Add X-SABRE-BDAY property. + if ($unknownYear) { + $event->add('X-SABRE-BDAY', 'BDAY', [ + 'X-SABRE-VCARD-UID' => $object->UID->getValue(), + 'X-SABRE-VCARD-FN' => $object->FN->getValue(), + 'X-SABRE-OMIT-YEAR' => self::DEFAULT_YEAR, + ]); + } else { + $event->add('X-SABRE-BDAY', 'BDAY', [ + 'X-SABRE-VCARD-UID' => $object->UID->getValue(), + 'X-SABRE-VCARD-FN' => $object->FN->getValue(), + ]); + } + } + + return $calendar; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Cli.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Cli.php new file mode 100644 index 0000000000..69af6f4705 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Cli.php @@ -0,0 +1,655 @@ +stderr) { + $this->stderr = fopen('php://stderr', 'w'); + } + if (!$this->stdout) { + $this->stdout = fopen('php://stdout', 'w'); + } + if (!$this->stdin) { + $this->stdin = fopen('php://stdin', 'r'); + } + + // @codeCoverageIgnoreEnd + + try { + list($options, $positional) = $this->parseArguments($argv); + + if (isset($options['q'])) { + $this->quiet = true; + } + $this->log($this->colorize('green', 'sabre/vobject ').$this->colorize('yellow', Version::VERSION)); + + foreach ($options as $name => $value) { + switch ($name) { + case 'q': + // Already handled earlier. + break; + case 'h': + case 'help': + $this->showHelp(); + + return 0; + case 'format': + switch ($value) { + // jcard/jcal documents + case 'jcard': + case 'jcal': + // specific document versions + case 'vcard21': + case 'vcard30': + case 'vcard40': + case 'icalendar20': + // specific formats + case 'json': + case 'mimedir': + // icalendar/vcard + case 'icalendar': + case 'vcard': + $this->format = $value; + break; + + default: + throw new \InvalidArgumentException('Unknown format: '.$value); + } + break; + case 'pretty': + $this->pretty = true; + break; + case 'forgiving': + $this->forgiving = true; + break; + case 'inputformat': + switch ($value) { + // json formats + case 'jcard': + case 'jcal': + case 'json': + $this->inputFormat = 'json'; + break; + + // mimedir formats + case 'mimedir': + case 'icalendar': + case 'vcard': + case 'vcard21': + case 'vcard30': + case 'vcard40': + case 'icalendar20': + $this->inputFormat = 'mimedir'; + break; + + default: + throw new \InvalidArgumentException('Unknown format: '.$value); + } + break; + default: + throw new \InvalidArgumentException('Unknown option: '.$name); + } + } + + if (0 === count($positional)) { + $this->showHelp(); + + return 1; + } + + if (1 === count($positional)) { + throw new \InvalidArgumentException('Inputfile is a required argument'); + } + + if (count($positional) > 3) { + throw new \InvalidArgumentException('Too many arguments'); + } + + if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) { + throw new \InvalidArgumentException('Unknown command: '.$positional[0]); + } + } catch (\InvalidArgumentException $e) { + $this->showHelp(); + $this->log('Error: '.$e->getMessage(), 'red'); + + return 1; + } + + $command = $positional[0]; + + $this->inputPath = $positional[1]; + $this->outputPath = $positional[2] ?? '-'; + + if ('-' !== $this->outputPath) { + $this->stdout = fopen($this->outputPath, 'w'); + } + + if (null === $this->inputFormat) { + if ('.json' === substr($this->inputPath, -5)) { + $this->inputFormat = 'json'; + } else { + $this->inputFormat = 'mimedir'; + } + } + if (null === $this->format) { + if ('.json' === substr($this->outputPath, -5)) { + $this->format = 'json'; + } else { + $this->format = 'mimedir'; + } + } + + $realCode = 0; + + try { + while ($input = $this->readInput()) { + $returnCode = $this->$command($input); + if (0 !== $returnCode) { + $realCode = $returnCode; + } + } + } catch (EofException $e) { + // end of file + } catch (\Exception $e) { + $this->log('Error: '.$e->getMessage(), 'red'); + + return 2; + } + + return $realCode; + } + + /** + * Shows the help message. + */ + protected function showHelp(): void + { + $this->log('Usage:', 'yellow'); + $this->log(' vobject [options] command [arguments]'); + $this->log(''); + $this->log('Options:', 'yellow'); + $this->log($this->colorize('green', ' -q ')."Don't output anything."); + $this->log($this->colorize('green', ' -help -h ').'Display this help message.'); + $this->log($this->colorize('green', ' --format ').'Convert to a specific format. Must be one of: vcard, vcard21,'); + $this->log($this->colorize('green', ' --forgiving ').'Makes the parser less strict.'); + $this->log(' vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir.'); + $this->log($this->colorize('green', ' --inputformat ').'If the input format cannot be guessed from the extension, it'); + $this->log(' must be specified here.'); + $this->log($this->colorize('green', ' --pretty ').'json pretty-print.'); + $this->log(''); + $this->log('Commands:', 'yellow'); + $this->log($this->colorize('green', ' validate').' source_file Validates a file for correctness.'); + $this->log($this->colorize('green', ' repair').' source_file [output_file] Repairs a file.'); + $this->log($this->colorize('green', ' convert').' source_file [output_file] Converts a file.'); + $this->log($this->colorize('green', ' color').' source_file Colorize a file, useful for debugging.'); + $this->log( + <<log('Examples:', 'yellow'); + $this->log(' vobject convert contact.vcf contact.json'); + $this->log(' vobject convert --format=vcard40 old.vcf new.vcf'); + $this->log(' vobject convert --inputformat=json --format=mimedir - -'); + $this->log(' vobject color calendar.ics'); + $this->log(''); + $this->log('https://github.com/fruux/sabre-vobject', 'purple'); + } + + /** + * Validates a VObject file. + */ + protected function validate(Component $vObj): int + { + $returnCode = 0; + + switch ($vObj->name) { + case 'VCALENDAR': + $this->log('iCalendar: '.(string) $vObj->VERSION); + break; + case 'VCARD': + $this->log('vCard: '.(string) $vObj->VERSION); + break; + } + + $warnings = $vObj->validate(); + if (!count($warnings)) { + $this->log(' No warnings!'); + } else { + $levels = [ + 1 => 'REPAIRED', + 2 => 'WARNING', + 3 => 'ERROR', + ]; + $returnCode = 2; + foreach ($warnings as $warn) { + $extra = ''; + if ($warn['node'] instanceof Property) { + $extra = ' (property: "'.$warn['node']->name.'")'; + } + $this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra); + } + } + + return $returnCode; + } + + /** + * Repairs a VObject file. + */ + protected function repair(Component $vObj): int + { + $returnCode = 0; + + switch ($vObj->name) { + case 'VCALENDAR': + $this->log('iCalendar: '.(string) $vObj->VERSION); + break; + case 'VCARD': + $this->log('vCard: '.(string) $vObj->VERSION); + break; + } + + $warnings = $vObj->validate(Node::REPAIR); + if (!count($warnings)) { + $this->log(' No warnings!'); + } else { + $levels = [ + 1 => 'REPAIRED', + 2 => 'WARNING', + 3 => 'ERROR', + ]; + $returnCode = 2; + foreach ($warnings as $warn) { + $extra = ''; + if ($warn['node'] instanceof Property) { + $extra = ' (property: "'.$warn['node']->name.'")'; + } + $this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra); + } + } + fwrite($this->stdout, $vObj->serialize()); + + return $returnCode; + } + + /** + * Converts a vObject file to a new format. + */ + protected function convert(Component $vObj): int + { + $json = false; + $convertVersion = null; + $forceInput = null; + + switch ($this->format) { + case 'json': + $json = true; + if ('VCARD' === $vObj->name) { + $convertVersion = Document::VCARD40; + } + break; + case 'jcard': + $json = true; + $forceInput = 'VCARD'; + $convertVersion = Document::VCARD40; + break; + case 'jcal': + $json = true; + $forceInput = 'VCALENDAR'; + break; + case 'mimedir': + case 'icalendar': + case 'icalendar20': + case 'vcard': + break; + case 'vcard21': + $convertVersion = Document::VCARD21; + break; + case 'vcard30': + $convertVersion = Document::VCARD30; + break; + case 'vcard40': + $convertVersion = Document::VCARD40; + break; + } + + if ($forceInput && $vObj->name !== $forceInput) { + throw new \Exception('You cannot convert a '.strtolower($vObj->name).' to '.$this->format); + } + if ($convertVersion) { + $vObj = $vObj->convert($convertVersion); + } + if ($json) { + $jsonOptions = 0; + if ($this->pretty) { + $jsonOptions = JSON_PRETTY_PRINT; + } + fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions)); + } else { + fwrite($this->stdout, $vObj->serialize()); + } + + return 0; + } + + /** + * Colorizes a file. + */ + protected function color(Component $vObj): int + { + $this->serializeComponent($vObj); + + return 0; + } + + /** + * Returns an ansi color string for a color name. + */ + protected function colorize(string $color, string $str, string $resetTo = 'default'): string + { + $colors = [ + 'cyan' => '1;36', + 'red' => '1;31', + 'yellow' => '1;33', + 'blue' => '0;34', + 'green' => '0;32', + 'default' => '0', + 'purple' => '0;35', + ]; + + return "\033[".$colors[$color].'m'.$str."\033[".$colors[$resetTo].'m'; + } + + /** + * Writes out a string in specific color. + */ + protected function cWrite(string $color, string $str): void + { + fwrite($this->stdout, $this->colorize($color, $str)); + } + + protected function serializeComponent(Component $vObj): void + { + $this->cWrite('cyan', 'BEGIN'); + $this->cWrite('red', ':'); + $this->cWrite('yellow', $vObj->name."\n"); + + /** + * Gives a component a 'score' for sorting purposes. + * + * This is solely used by the childrenSort method. + * + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accommodate elements. The $key is added to the $score to + * preserve the original relative order of elements. + */ + $sortScore = function (int $key, array $array): int { + if ($array[$key] instanceof Component) { + // We want to encode VTIMEZONE first, this is a personal + // preference. + if ('VTIMEZONE' === $array[$key]->name) { + $score = 300000000; + } else { + $score = 400000000; + } + + return $score + $key; + } + // Properties get encoded first + // VCARD version 4.0 wants the VERSION property to appear first + if ($array[$key] instanceof Property) { + if ('VERSION' === $array[$key]->name) { + $score = 100000000; + } else { + // All other properties + $score = 200000000; + } + + return $score + $key; + } + + return 0; + }; + + $children = $vObj->children(); + $tmp = $children; + uksort( + $children, + function ($a, $b) use ($sortScore, $tmp): int { + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); + + return $sA - $sB; + } + ); + + foreach ($children as $child) { + if ($child instanceof Component) { + $this->serializeComponent($child); + } else { + $this->serializeProperty($child); + } + } + + $this->cWrite('cyan', 'END'); + $this->cWrite('red', ':'); + $this->cWrite('yellow', $vObj->name."\n"); + } + + /** + * Colorizes a property. + */ + protected function serializeProperty(Property $property): void + { + if ($property->group) { + $this->cWrite('default', $property->group); + $this->cWrite('red', '.'); + } + + $this->cWrite('yellow', $property->name); + + foreach ($property->parameters as $param) { + $this->cWrite('red', ';'); + $this->cWrite('blue', $param->serialize()); + } + $this->cWrite('red', ':'); + + if ($property instanceof Property\Binary) { + $this->cWrite('default', 'embedded binary stripped. ('.strlen($property->getValue()).' bytes)'); + } else { + $parts = $property->getParts(); + $first1 = true; + // Looping through property values + foreach ($parts as $part) { + if ($first1) { + $first1 = false; + } else { + $this->cWrite('red', $property->delimiter); + } + $first2 = true; + // Looping through property sub-values + foreach ((array) $part as $subPart) { + if ($first2) { + $first2 = false; + } else { + // The sub-value delimiter is always comma + $this->cWrite('red', ','); + } + + $subPart = strtr( + $subPart, + [ + '\\' => $this->colorize('purple', '\\\\', 'green'), + ';' => $this->colorize('purple', '\;', 'green'), + ',' => $this->colorize('purple', '\,', 'green'), + "\n" => $this->colorize('purple', "\\n\n\t", 'green'), + "\r" => '', + ] + ); + + $this->cWrite('green', $subPart); + } + } + } + $this->cWrite('default', "\n"); + } + + /** + * Parses the list of arguments. + */ + protected function parseArguments(array $argv): array + { + $positional = []; + $options = []; + + for ($ii = 0; $ii < count($argv); ++$ii) { + // Skipping the first argument. + if (0 === $ii) { + continue; + } + + $v = $argv[$ii]; + + if ('--' === substr($v, 0, 2)) { + // This is a long-form option. + $optionName = substr($v, 2); + $optionValue = true; + if (strpos($optionName, '=')) { + list($optionName, $optionValue) = explode('=', $optionName); + } + $options[$optionName] = $optionValue; + } elseif ('-' === substr($v, 0, 1) && strlen($v) > 1) { + // This is a short-form option. + foreach (str_split(substr($v, 1)) as $option) { + $options[$option] = true; + } + } else { + $positional[] = $v; + } + } + + return [$options, $positional]; + } + + protected ?Parser $parser = null; + + /** + * Reads the input file. + * + * @throws EofException + * @throws ParseException + * @throws InvalidDataException + */ + protected function readInput(): ?Document + { + if (!$this->parser) { + if ('-' !== $this->inputPath) { + $this->stdin = fopen($this->inputPath, 'r'); + } + + if ('mimedir' === $this->inputFormat) { + $this->parser = new MimeDir($this->stdin, $this->forgiving ? Reader::OPTION_FORGIVING : 0); + } else { + $this->parser = new Json($this->stdin, $this->forgiving ? Reader::OPTION_FORGIVING : 0); + } + } + + return $this->parser->parse(); + } + + /** + * Sends a message to STDERR. + */ + protected function log(string $msg, string $color = 'default'): void + { + if (!$this->quiet) { + if ('default' !== $color) { + $msg = $this->colorize($color, $msg); + } + fwrite($this->stderr, $msg."\n"); + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component.php new file mode 100644 index 0000000000..95bde3d409 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component.php @@ -0,0 +1,636 @@ + + */ + protected array $children = []; + + /** + * Creates a new component. + * + * You can specify the children either in key=>value syntax, in which case + * properties will automatically be created, or you can just pass a list of + * Component and Property object. + * + * By default, a set of sensible values will be added to the component. For + * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To + * ensure that this does not happen, set $defaults to false. + * + * @param string|null $name such as VCALENDAR, VEVENT + */ + public function __construct(Document $root, ?string $name, array $children = [], bool $defaults = true) + { + $this->name = isset($name) ? strtoupper($name) : ''; + $this->root = $root; + + if ($defaults) { + // This is a terribly convoluted way to do this, but this ensures + // that the order of properties as they are specified in both + // defaults and the childrens list, are inserted in the object in a + // natural way. + $list = $this->getDefaults(); + $nodes = []; + foreach ($children as $key => $value) { + if ($value instanceof Node) { + if (isset($list[$value->name])) { + unset($list[$value->name]); + } + $nodes[] = $value; + } else { + $list[$key] = $value; + } + } + foreach ($list as $key => $value) { + $this->add($key, $value); + } + foreach ($nodes as $node) { + $this->add($node); + } + } else { + foreach ($children as $k => $child) { + if ($child instanceof Node) { + // Component or Property + $this->add($child); + } else { + // Property key=>value + $this->add($k, $child); + } + } + } + } + + /** + * Adds a new property or component, and returns the new item. + * + * This method has 3 possible signatures: + * + * add(Component $comp) // Adds a new component + * add(Property $prop) // Adds a new property + * add($name, $value, array $parameters = []) // Adds a new property + * add($name, array $children = []) // Adds a new component + * by name. + */ + public function add(): Node + { + $arguments = func_get_args(); + + if ($arguments[0] instanceof Node) { + if (isset($arguments[1])) { + throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node'); + } + $arguments[0]->parent = $this; + $newNode = $arguments[0]; + } elseif (is_string($arguments[0])) { + $newNode = call_user_func_array([$this->root, 'create'], $arguments); + } else { + throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string'); + } + + /** @var Component|Property|Parameter $newNode */ + $name = $newNode->name; + if (isset($this->children[$name])) { + $this->children[$name][] = $newNode; + } else { + $this->children[$name] = [$newNode]; + } + + return $newNode; + } + + /** + * This method removes a component or property from this component. + * + * You can either specify the item by name (like DTSTART), in which case + * all properties/components with that name will be removed, or you can + * pass an instance of a property or component, in which case only that + * exact item will be removed. + * + * @param string|Property|Component $item + */ + public function remove($item): void + { + if (is_string($item)) { + // If there's no dot in the name, it's an exact property name, + // we can just wipe out all those properties. + // + if (false === strpos($item, '.')) { + unset($this->children[strtoupper($item)]); + + return; + } + // If there was a dot, we need to ask select() to help us out and + // then we just call remove recursively. + foreach ($this->select($item) as $child) { + $this->remove($child); + } + } else { + foreach ($this->select($item->name) as $k => $child) { + if ($child === $item) { + unset($this->children[$item->name][$k]); + + return; + } + } + + throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component'); + } + } + + /** + * Returns a flat list of all the properties and components in this + * component. + */ + public function children(): array + { + $result = []; + foreach ($this->children as $childGroup) { + $result = array_merge($result, $childGroup); + } + + return $result; + } + + /** + * This method only returns a list of sub-components. Properties are + * ignored. + */ + public function getComponents(): array + { + $result = []; + + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof self) { + $result[] = $child; + } + } + } + + return $result; + } + + /** + * Returns an array with elements that match the specified name. + * + * This function is also aware of MIME-Directory groups (as they appear in + * vcards). This means that if a property is grouped as "HOME.EMAIL", it + * will also be returned when searching for just "EMAIL". If you want to + * search for a property in a specific group, you can select on the entire + * string ("HOME.EMAIL"). If you want to search on a specific property that + * has not been assigned a group, specify ".EMAIL". + */ + public function select(string $name): array + { + $group = null; + $name = strtoupper($name); + if (false !== strpos($name, '.')) { + list($group, $name) = explode('.', $name, 2); + } + if ('' === $name) { + $name = null; + } + + if (!is_null($name)) { + $result = $this->children[$name] ?? []; + + if (is_null($group)) { + return $result; + } else { + // If we have a group filter as well, we need to narrow it down + // more. + return array_filter( + $result, + function ($child) use ($group) { + return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group; + } + ); + } + } + + // If we got to this point, it means there was no 'name' specified for + // searching, implying that this is a group-only search. + $result = []; + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group) { + $result[] = $child; + } + } + } + + return $result; + } + + /** + * Turns the object back into a serialized blob. + */ + public function serialize(): string + { + $str = 'BEGIN:'.$this->name."\r\n"; + + /** + * Gives a component a 'score' for sorting purposes. + * + * This is solely used by the childrenSort method. + * + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accommodate elements. The $key is added to the $score to + * preserve the original relative order of elements. + * + * @param int $key + * @param array $array + * + * @return int + */ + $sortScore = function (int $key, array $array): ?int { + if ($array[$key] instanceof Component) { + // We want to encode VTIMEZONE first, this is a personal + // preference. + if ('VTIMEZONE' === $array[$key]->name) { + $score = 300000000; + } else { + $score = 400000000; + } + + return $score + $key; + } + // Properties get encoded first + // VCARD version 4.0 wants the VERSION property to appear first + if ($array[$key] instanceof Property) { + if ('VERSION' === $array[$key]->name) { + $score = 100000000; + } else { + // All other properties + $score = 200000000; + } + + return $score + $key; + } + + return 0; + }; + + $children = $this->children(); + $tmp = $children; + uksort( + $children, + function ($a, $b) use ($sortScore, $tmp): int { + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); + + return $sA - $sB; + } + ); + + foreach ($children as $child) { + $str .= $child->serialize(); + } + $str .= 'END:'.$this->name."\r\n"; + + return $str; + } + + /** + * This method returns an array, with the representation as it should be + * encoded in JSON. This is used to create jCard or jCal documents. + */ + #[\ReturnTypeWillChange] + public function jsonSerialize(): array + { + $components = []; + $properties = []; + + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof self) { + $components[] = $child->jsonSerialize(); + } else { + $properties[] = $child->jsonSerialize(); + } + } + } + + return [ + strtolower($this->name), + $properties, + $components, + ]; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $components = []; + $properties = []; + + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof self) { + $components[] = $child; + } else { + $properties[] = $child; + } + } + } + + $writer->startElement(strtolower($this->name)); + + if (!empty($properties)) { + $writer->startElement('properties'); + + foreach ($properties as $property) { + $property->xmlSerialize($writer); + } + + $writer->endElement(); + } + + if (!empty($components)) { + $writer->startElement('components'); + + foreach ($components as $component) { + $component->xmlSerialize($writer); + } + + $writer->endElement(); + } + + $writer->endElement(); + } + + /** + * This method should return a list of default property values. + */ + protected function getDefaults(): array + { + return []; + } + + /* Magic property accessors {{{ */ + + /** + * Using 'get' you will either get a property or component. + * + * If there were no child-elements found with the specified name, + * null is returned. + * + * To use this, this may look something like this: + * + * $event = $calendar->VEVENT; + * + * @return Property|Component + */ + public function __get(string $name): ?Node + { + if ('children' === $name) { + throw new \RuntimeException('Starting sabre/vobject 4.0 the children property is now protected. You should use the children() method instead'); + } + + $matches = $this->select($name); + if (0 === count($matches)) { + return null; + } else { + $firstMatch = current($matches); + /* @var $firstMatch Property */ + $firstMatch->setIterator(new ElementList(array_values($matches))); + + return $firstMatch; + } + } + + /** + * This method checks if a sub-element with the specified name exists. + */ + public function __isset(string $name): bool + { + $matches = $this->select($name); + + return count($matches) > 0; + } + + /** + * Using the setter method you can add properties or subcomponents. + * + * You can either pass a Component, Property + * object, or a string to automatically create a Property. + * + * If the item already exists, it will be removed. If you want to add + * a new item with the same name, always use the add() method. + */ + public function __set(string $name, $value): void + { + $name = strtoupper($name); + $this->remove($name); + if ($value instanceof self || $value instanceof Property) { + $this->add($value); + } else { + $this->add($name, $value); + } + } + + /** + * Removes all properties and components within this component with the + * specified name. + */ + public function __unset(string $name): void + { + $this->remove($name); + } + + /* }}} */ + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + */ + public function __clone() + { + foreach ($this->children as $childName => $childGroup) { + foreach ($childGroup as $key => $child) { + $clonedChild = clone $child; + $clonedChild->parent = $this; + $clonedChild->root = $this->root; + $this->children[$childName][$key] = $clonedChild; + } + } + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * It is also possible to specify defaults and severity levels for + * violating the rule. + * + * See the VEVENT implementation for getValidationRules for a more complex + * example. + */ + public function getValidationRules(): array + { + return []; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + */ + public function validate(int $options = 0): array + { + $rules = $this->getValidationRules(); + $defaults = $this->getDefaults(); + + $propertyCounters = []; + + $messages = []; + + foreach ($this->children() as $child) { + $name = strtoupper($child->name); + if (!isset($propertyCounters[$name])) { + $propertyCounters[$name] = 1; + } else { + ++$propertyCounters[$name]; + } + $messages = array_merge($messages, $child->validate($options)); + } + + foreach ($rules as $propName => $rule) { + switch ($rule) { + case '0': + if (isset($propertyCounters[$propName])) { + $messages[] = [ + 'level' => 3, + 'message' => $propName.' MUST NOT appear in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '1': + if (!isset($propertyCounters[$propName]) || 1 !== $propertyCounters[$propName]) { + $repaired = false; + if ($options & self::REPAIR && isset($defaults[$propName])) { + $this->add($propName, $defaults[$propName]); + $repaired = true; + } + $messages[] = [ + 'level' => $repaired ? 1 : 3, + 'message' => $propName.' MUST appear exactly once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '+': + if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName] < 1) { + $messages[] = [ + 'level' => 3, + 'message' => $propName.' MUST appear at least once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '*': + break; + case '?': + if (isset($propertyCounters[$propName]) && $propertyCounters[$propName] > 1) { + $level = 3; + + // We try to repair the same property appearing multiple times with the exact same value + // by removing the duplicates and keeping only one property + if ($options & self::REPAIR) { + $properties = array_unique($this->select($propName), SORT_REGULAR); + + if (1 === count($properties)) { + $this->remove($propName); + $this->add($properties[0]); + + $level = 1; + } + } + + $messages[] = [ + 'level' => $level, + 'message' => $propName.' MUST NOT appear more than once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + } + } + + return $messages; + } + + /** + * Call this method on a document if you're done using it. + * + * It's intended to remove all circular references, so PHP can easily clean + * it up. + */ + public function destroy(): void + { + parent::destroy(); + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + $child->destroy(); + } + } + $this->children = []; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/Available.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/Available.php new file mode 100644 index 0000000000..b3ba40b075 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/Available.php @@ -0,0 +1,121 @@ +DTSTART->getDateTime(); + if (isset($this->DTEND)) { + $effectiveEnd = $this->DTEND->getDateTime(); + } else { + $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); + } + + return [$effectiveStart, $effectiveEnd]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + */ + public function getValidationRules(): array + { + return [ + 'UID' => 1, + 'DTSTART' => 1, + 'DTSTAMP' => 1, + + 'DTEND' => '?', + 'DURATION' => '?', + + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'LAST-MODIFIED' => '?', + 'RECURRENCE-ID' => '?', + 'RRULE' => '?', + 'SUMMARY' => '?', + + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'RDATE' => '*', + + 'AVAILABLE' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + */ + public function validate(int $options = 0): array + { + $result = parent::validate($options); + + if (isset($this->DTEND) && isset($this->DURATION)) { + $result[] = [ + 'level' => 3, + 'message' => 'DTEND and DURATION cannot both be present', + 'node' => $this, + ]; + } + + return $result; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VAlarm.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VAlarm.php new file mode 100644 index 0000000000..abe61768ea --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VAlarm.php @@ -0,0 +1,129 @@ +TRIGGER; + if (!isset($trigger['VALUE']) || ($trigger['VALUE'] && 'DURATION' === strtoupper($trigger['VALUE']))) { + $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); + $related = (isset($trigger['RELATED']) && 'END' == strtoupper($trigger['RELATED'])) ? 'END' : 'START'; + + /** @var VEvent|VTodo $parentComponent */ + $parentComponent = $this->parent; + if ('START' === $related) { + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); + } else { + if ('VTODO' === $parentComponent->name) { + $endProp = 'DUE'; + } elseif ('VEVENT' === $parentComponent->name) { + $endProp = 'DTEND'; + } else { + throw new InvalidDataException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); + } + + if (isset($parentComponent->$endProp)) { + $effectiveTrigger = $parentComponent->$endProp->getDateTime(); + } elseif (isset($parentComponent->DURATION)) { + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); + $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION); + $effectiveTrigger = $effectiveTrigger->add($duration); + } else { + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); + } + } + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } else { + $effectiveTrigger = $trigger->getDateTime(); + } + + return $effectiveTrigger; + } + + /** + * Returns true or false depending on if the event falls in the specified + * time-range. This is used for filtering purposes. + * + * The rules used to determine if an event falls within the specified + * time-range is based on the CalDAV specification. + * + * @throws InvalidDataException + */ + public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end): bool + { + $effectiveTrigger = $this->getEffectiveTriggerTime(); + + if (isset($this->DURATION)) { + $duration = VObject\DateTimeParser::parseDuration($this->DURATION); + $repeat = (string) $this->REPEAT; + if (!$repeat) { + $repeat = 1; + } + + $period = new \DatePeriod($effectiveTrigger, $duration, (int) $repeat); + + foreach ($period as $occurrence) { + if ($start <= $occurrence && $end > $occurrence) { + return true; + } + } + + return false; + } else { + return $start <= $effectiveTrigger && $end > $effectiveTrigger; + } + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + */ + public function getValidationRules(): array + { + return [ + 'ACTION' => 1, + 'TRIGGER' => 1, + + 'DURATION' => '?', + 'REPEAT' => '?', + + 'ATTACH' => '?', + ]; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VAvailability.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VAvailability.php new file mode 100644 index 0000000000..ff6e9bd653 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VAvailability.php @@ -0,0 +1,146 @@ +getEffectiveStartEnd(); + + return + (is_null($effectiveStart) || $start < $effectiveEnd) + && (is_null($effectiveEnd) || $end > $effectiveStart) + ; + } + + /** + * Returns the 'effective start' and 'effective end' of this VAVAILABILITY + * component. + * + * We use the DTSTART and DTEND or DURATION to determine this. + * + * The returned value is an array containing DateTimeImmutable instances. + * If either the start or end is 'unbounded' its value will be null + * instead. + * + * @throws VObject\InvalidDataException + */ + public function getEffectiveStartEnd(): array + { + $effectiveStart = null; + $effectiveEnd = null; + + if (isset($this->DTSTART)) { + $effectiveStart = $this->DTSTART->getDateTime(); + } + if (isset($this->DTEND)) { + $effectiveEnd = $this->DTEND->getDateTime(); + } elseif ($effectiveStart && isset($this->DURATION)) { + $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); + } + + return [$effectiveStart, $effectiveEnd]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + */ + public function getValidationRules(): array + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'BUSYTYPE' => '?', + 'CLASS' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'DTSTART' => '?', + 'LAST-MODIFIED' => '?', + 'ORGANIZER' => '?', + 'PRIORITY' => '?', + 'SEQUENCE' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + 'DTEND' => '?', + 'DURATION' => '?', + + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + */ + public function validate(int $options = 0): array + { + $result = parent::validate($options); + + if (isset($this->DTEND) && isset($this->DURATION)) { + $result[] = [ + 'level' => 3, + 'message' => 'DTEND and DURATION cannot both be present', + 'node' => $this, + ]; + } + + return $result; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCalendar.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCalendar.php new file mode 100644 index 0000000000..7b6e66e88f --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCalendar.php @@ -0,0 +1,510 @@ + self::class, + 'VALARM' => VAlarm::class, + 'VEVENT' => VEvent::class, + 'VFREEBUSY' => VFreeBusy::class, + 'VAVAILABILITY' => VAvailability::class, + 'AVAILABLE' => Available::class, + 'VJOURNAL' => VJournal::class, + 'VTIMEZONE' => VTimeZone::class, + 'VTODO' => VTodo::class, + ]; + + /** + * List of value-types, and which classes they map to. + */ + public static array $valueMap = [ + 'BINARY' => Property\Binary::class, + 'BOOLEAN' => Property\Boolean::class, + 'CAL-ADDRESS' => Property\ICalendar\CalAddress::class, + 'DATE' => Property\ICalendar\Date::class, + 'DATE-TIME' => Property\ICalendar\DateTime::class, + 'DURATION' => Property\ICalendar\Duration::class, + 'FLOAT' => Property\FloatValue::class, + 'INTEGER' => Property\IntegerValue::class, + 'PERIOD' => Property\ICalendar\Period::class, + 'RECUR' => Property\ICalendar\Recur::class, + 'TEXT' => Property\Text::class, + 'TIME' => Property\Time::class, + 'UNKNOWN' => Property\Unknown::class, // jCard / jCal-only. + 'URI' => Property\Uri::class, + 'UTC-OFFSET' => Property\UtcOffset::class, + ]; + + /** + * List of properties, and which classes they map to. + */ + public static array $propertyMap = [ + // Calendar properties + 'CALSCALE' => Property\FlatText::class, + 'METHOD' => Property\FlatText::class, + 'PRODID' => Property\FlatText::class, + 'VERSION' => Property\FlatText::class, + + // Component properties + 'ATTACH' => Property\Uri::class, + 'CATEGORIES' => Property\Text::class, + 'CLASS' => Property\FlatText::class, + 'COMMENT' => Property\FlatText::class, + 'DESCRIPTION' => Property\FlatText::class, + 'GEO' => Property\FloatValue::class, + 'LOCATION' => Property\FlatText::class, + 'PERCENT-COMPLETE' => Property\IntegerValue::class, + 'PRIORITY' => Property\IntegerValue::class, + 'RESOURCES' => Property\Text::class, + 'STATUS' => Property\FlatText::class, + 'SUMMARY' => Property\FlatText::class, + + // Date and Time Component Properties + 'COMPLETED' => Property\ICalendar\DateTime::class, + 'DTEND' => Property\ICalendar\DateTime::class, + 'DUE' => Property\ICalendar\DateTime::class, + 'DTSTART' => Property\ICalendar\DateTime::class, + 'DURATION' => Property\ICalendar\Duration::class, + 'FREEBUSY' => Property\ICalendar\Period::class, + 'TRANSP' => Property\FlatText::class, + + // Time Zone Component Properties + 'TZID' => Property\FlatText::class, + 'TZNAME' => Property\FlatText::class, + 'TZOFFSETFROM' => Property\UtcOffset::class, + 'TZOFFSETTO' => Property\UtcOffset::class, + 'TZURL' => Property\Uri::class, + + // Relationship Component Properties + 'ATTENDEE' => Property\ICalendar\CalAddress::class, + 'CONTACT' => Property\FlatText::class, + 'ORGANIZER' => Property\ICalendar\CalAddress::class, + 'RECURRENCE-ID' => Property\ICalendar\DateTime::class, + 'RELATED-TO' => Property\FlatText::class, + 'URL' => Property\Uri::class, + 'UID' => Property\FlatText::class, + + // Recurrence Component Properties + 'EXDATE' => Property\ICalendar\DateTime::class, + 'RDATE' => Property\ICalendar\DateTime::class, + 'RRULE' => Property\ICalendar\Recur::class, + 'EXRULE' => Property\ICalendar\Recur::class, // Deprecated since rfc5545 + + // Alarm Component Properties + 'ACTION' => Property\FlatText::class, + 'REPEAT' => Property\IntegerValue::class, + 'TRIGGER' => Property\ICalendar\Duration::class, + + // Change Management Component Properties + 'CREATED' => Property\ICalendar\DateTime::class, + 'DTSTAMP' => Property\ICalendar\DateTime::class, + 'LAST-MODIFIED' => Property\ICalendar\DateTime::class, + 'SEQUENCE' => Property\IntegerValue::class, + + // Request Status + 'REQUEST-STATUS' => Property\Text::class, + + // Additions from draft-daboo-valarm-extensions-04 + 'ALARM-AGENT' => Property\Text::class, + 'ACKNOWLEDGED' => Property\ICalendar\DateTime::class, + 'PROXIMITY' => Property\Text::class, + 'DEFAULT-ALARM' => Property\Boolean::class, + + // Additions from draft-daboo-calendar-availability-05 + 'BUSYTYPE' => Property\Text::class, + ]; + + /** + * Returns the current document type. + */ + public function getDocumentType(): int + { + return self::ICALENDAR20; + } + + /** + * Returns a list of all 'base components'. For instance, if an Event has + * a recurrence rule, and one instance is overridden, the overridden event + * will have the same UID, but will be excluded from this list. + * + * VTIMEZONE components will always be excluded. + * + * @param string|null $componentName filter by component name + * + * @return VObject\Component[] + */ + public function getBaseComponents(?string $componentName = null): array + { + $isBaseComponent = function ($component): bool { + if (!$component instanceof Component) { + return false; + } + if ('VTIMEZONE' === $component->name) { + return false; + } + if (isset($component->{'RECURRENCE-ID'})) { + return false; + } + + return true; + }; + + if ($componentName) { + // Early exit + return array_filter( + $this->select($componentName), + $isBaseComponent + ); + } + + $components = []; + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if (!$child instanceof Component) { + // If one child is not a component, they all are so we skip + // the entire group. + continue 2; + } + if ($isBaseComponent($child)) { + $components[] = $child; + } + } + } + + return $components; + } + + /** + * Returns the first component that is not a VTIMEZONE, and does not have + * an RECURRENCE-ID. + * + * If there is no such component, null will be returned. + * + * @param string|null $componentName filter by component name + */ + public function getBaseComponent(?string $componentName = null): ?Component + { + $isBaseComponent = function ($component): bool { + if (!$component instanceof Component) { + return false; + } + if ('VTIMEZONE' === $component->name) { + return false; + } + if (isset($component->{'RECURRENCE-ID'})) { + return false; + } + + return true; + }; + + if ($componentName) { + foreach ($this->select($componentName) as $child) { + if ($isBaseComponent($child)) { + return $child; + } + } + + return null; + } + + // Searching all components + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($isBaseComponent($child)) { + return $child; + } + } + } + + return null; + } + + /** + * Expand all events in this VCalendar object and return a new VCalendar + * with the expanded events. + * + * If this calendar object, has events with recurrence rules, this method + * can be used to expand the event into multiple sub-events. + * + * Each event will be stripped from its recurrence information, and only + * the instances of the event in the specified time range will be left + * alone. + * + * In addition, this method will cause timezone information to be stripped, + * and normalized to UTC. + * + * @param \DateTimeZone|null $timeZone reference timezone for floating dates and + * times + * + * @throws InvalidDataException + * @throws VObject\Recur\MaxInstancesExceededException + */ + public function expand(\DateTimeInterface $start, \DateTimeInterface $end, ?\DateTimeZone $timeZone = null): VCalendar + { + $newChildren = []; + $recurringEvents = []; + + if (!$timeZone) { + $timeZone = new \DateTimeZone('UTC'); + } + + $stripTimezones = function (Component $component) use ($timeZone, &$stripTimezones): Component { + foreach ($component->children() as $componentChild) { + if ($componentChild instanceof Property\ICalendar\DateTime && $componentChild->hasTime()) { + $dt = $componentChild->getDateTimes($timeZone); + // We only need to update the first timezone, because + // setDateTimes will match all other timezones to the + // first. + $dt[0] = $dt[0]->setTimeZone(new \DateTimeZone('UTC')); + $componentChild->setDateTimes($dt); + } elseif ($componentChild instanceof Component) { + $stripTimezones($componentChild); + } + } + + return $component; + }; + + foreach ($this->children() as $child) { + if ($child instanceof Property && 'PRODID' !== $child->name) { + // We explicitly want to ignore PRODID, because we want to + // overwrite it with our own. + $newChildren[] = clone $child; + } elseif ($child instanceof Component && 'VTIMEZONE' !== $child->name) { + // We're also stripping all VTIMEZONE objects because we're + // converting everything to UTC. + if ('VEVENT' === $child->name && (isset($child->{'RECURRENCE-ID'}) || isset($child->RRULE) || isset($child->RDATE))) { + // Handle these a bit later. + $uid = (string) $child->UID; + if (!$uid) { + throw new InvalidDataException('Every VEVENT object must have a UID property'); + } + if (isset($recurringEvents[$uid])) { + $recurringEvents[$uid][] = clone $child; + } else { + $recurringEvents[$uid] = [clone $child]; + } + } elseif ('VEVENT' === $child->name && $child->isInTimeRange($start, $end)) { + $newChildren[] = $stripTimezones(clone $child); + } + } + } + + foreach ($recurringEvents as $events) { + try { + $it = new EventIterator($events, null, $timeZone); + } catch (NoInstancesException $e) { + // This event is recurring, but it doesn't have a single + // instance. We are skipping this event from the output + // entirely. + continue; + } + $it->fastForward($start); + + while ($it->valid() && $it->getDTStart() < $end) { + if ($it->getDTEnd() > $start) { + $newChildren[] = $stripTimezones($it->getEventObject()); + } + $it->next(); + } + } + + return new self($newChildren); + } + + /** + * This method should return a list of default property values. + */ + protected function getDefaults(): array + { + return [ + 'VERSION' => '2.0', + 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', + 'CALSCALE' => 'GREGORIAN', + ]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + */ + public function getValidationRules(): array + { + return [ + 'PRODID' => 1, + 'VERSION' => 1, + + 'CALSCALE' => '?', + 'METHOD' => '?', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + */ + public function validate(int $options = 0): array + { + $warnings = parent::validate($options); + + if ($ver = $this->VERSION) { + if ('2.0' !== (string) $ver) { + $warnings[] = [ + 'level' => 3, + 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.', + 'node' => $this, + ]; + } + } + + $uidList = []; + $componentsFound = 0; + $componentTypes = []; + + foreach ($this->children() as $child) { + if ($child instanceof Component) { + ++$componentsFound; + + if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'])) { + continue; + } + $componentTypes[] = $child->name; + + $uid = (string) $child->UID; + $isMaster = isset($child->{'RECURRENCE-ID'}) ? 0 : 1; + if (isset($uidList[$uid])) { + ++$uidList[$uid]['count']; + if ($isMaster && $uidList[$uid]['hasMaster']) { + $warnings[] = [ + 'level' => 3, + 'message' => 'More than one master object was found for the object with UID '.$uid, + 'node' => $this, + ]; + } + $uidList[$uid]['hasMaster'] += $isMaster; + } else { + $uidList[$uid] = [ + 'count' => 1, + 'hasMaster' => $isMaster, + ]; + } + } + } + + if (0 === $componentsFound) { + $warnings[] = [ + 'level' => 3, + 'message' => 'An iCalendar object must have at least 1 component.', + 'node' => $this, + ]; + } + + if ($options & self::PROFILE_CALDAV) { + if (count($uidList) > 1) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server may only have components with the same UID.', + 'node' => $this, + ]; + } + if (0 === count($componentTypes)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL).', + 'node' => $this, + ]; + } + if (count(array_unique($componentTypes)) > 1) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL).', + 'node' => $this, + ]; + } + + if (isset($this->METHOD)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server MUST NOT have a METHOD property.', + 'node' => $this, + ]; + } + } + + return $warnings; + } + + /** + * Returns all components with a specific UID value. + */ + public function getByUID($uid): array + { + return array_filter($this->getComponents(), function ($item) use ($uid) { + if (!$itemUid = $item->select('UID')) { + return false; + } + $itemUid = current($itemUid)->getValue(); + + return $uid === $itemUid; + }); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCard.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCard.php new file mode 100644 index 0000000000..9ec4c88ccb --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VCard.php @@ -0,0 +1,512 @@ + VCard::class, + ]; + + /** + * List of value-types, and which classes they map to. + */ + public static array $valueMap = [ + 'BINARY' => VObject\Property\Binary::class, + 'BOOLEAN' => VObject\Property\Boolean::class, + 'CONTENT-ID' => VObject\Property\FlatText::class, // vCard 2.1 only + 'DATE' => VObject\Property\VCard\Date::class, + 'DATE-TIME' => VObject\Property\VCard\DateTime::class, + 'DATE-AND-OR-TIME' => VObject\Property\VCard\DateAndOrTime::class, // vCard only + 'FLOAT' => VObject\Property\FloatValue::class, + 'INTEGER' => VObject\Property\IntegerValue::class, + 'LANGUAGE-TAG' => VObject\Property\VCard\LanguageTag::class, + 'PHONE-NUMBER' => VObject\Property\VCard\PhoneNumber::class, // vCard 3.0 only + 'TIMESTAMP' => VObject\Property\VCard\TimeStamp::class, + 'TEXT' => VObject\Property\Text::class, + 'TIME' => VObject\Property\Time::class, + 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. + 'URI' => VObject\Property\Uri::class, + 'URL' => VObject\Property\Uri::class, // vCard 2.1 only + 'UTC-OFFSET' => VObject\Property\UtcOffset::class, + ]; + + /** + * List of properties, and which classes they map to. + */ + public static array $propertyMap = [ + // vCard 2.1 properties and up + 'N' => VObject\Property\Text::class, + 'FN' => VObject\Property\FlatText::class, + 'PHOTO' => VObject\Property\Binary::class, + 'BDAY' => VObject\Property\VCard\DateAndOrTime::class, + 'ADR' => VObject\Property\Text::class, + 'LABEL' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + 'TEL' => VObject\Property\FlatText::class, + 'EMAIL' => VObject\Property\FlatText::class, + 'MAILER' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + 'GEO' => VObject\Property\FlatText::class, + 'TITLE' => VObject\Property\FlatText::class, + 'ROLE' => VObject\Property\FlatText::class, + 'LOGO' => VObject\Property\Binary::class, + // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so + // not supported at the moment + 'ORG' => VObject\Property\Text::class, + 'NOTE' => VObject\Property\FlatText::class, + 'REV' => VObject\Property\VCard\TimeStamp::class, + 'SOUND' => VObject\Property\FlatText::class, + 'URL' => VObject\Property\Uri::class, + 'UID' => VObject\Property\FlatText::class, + 'VERSION' => VObject\Property\FlatText::class, + 'KEY' => VObject\Property\FlatText::class, + 'TZ' => VObject\Property\Text::class, + + // vCard 3.0 properties + 'CATEGORIES' => VObject\Property\Text::class, + 'SORT-STRING' => VObject\Property\FlatText::class, + 'PRODID' => VObject\Property\FlatText::class, + 'NICKNAME' => VObject\Property\Text::class, + 'CLASS' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + + // rfc2739 properties + 'FBURL' => VObject\Property\Uri::class, + 'CAPURI' => VObject\Property\Uri::class, + 'CALURI' => VObject\Property\Uri::class, + 'CALADRURI' => VObject\Property\Uri::class, + + // rfc4770 properties + 'IMPP' => VObject\Property\Uri::class, + + // vCard 4.0 properties + 'SOURCE' => VObject\Property\Uri::class, + 'XML' => VObject\Property\FlatText::class, + 'ANNIVERSARY' => VObject\Property\VCard\DateAndOrTime::class, + 'CLIENTPIDMAP' => VObject\Property\Text::class, + 'LANG' => VObject\Property\VCard\LanguageTag::class, + 'GENDER' => VObject\Property\Text::class, + 'KIND' => VObject\Property\FlatText::class, + 'MEMBER' => VObject\Property\Uri::class, + 'RELATED' => VObject\Property\Uri::class, + + // rfc6474 properties + 'BIRTHPLACE' => VObject\Property\FlatText::class, + 'DEATHPLACE' => VObject\Property\FlatText::class, + 'DEATHDATE' => VObject\Property\VCard\DateAndOrTime::class, + + // rfc6715 properties + 'EXPERTISE' => VObject\Property\FlatText::class, + 'HOBBY' => VObject\Property\FlatText::class, + 'INTEREST' => VObject\Property\FlatText::class, + 'ORG-DIRECTORY' => VObject\Property\FlatText::class, + + 'X-CRYPTO' => VObject\Property\XCrypto::class, + ]; + + /** + * Returns the current document type. + */ + public function getDocumentType(): int + { + if (!$this->version) { + $version = (string) $this->VERSION; + + switch ($version) { + case '2.1': + $this->version = self::VCARD21; + break; + case '3.0': + $this->version = self::VCARD30; + break; + case '4.0': + $this->version = self::VCARD40; + break; + default: + // We don't want to cache the version if it's unknown, + // because we might get a version property in a bit. + return self::UNKNOWN; + } + } + + return $this->version; + } + + /** + * Converts the document to a different vcard version. + * + * Use one of the VCARD constants for the target. This method will return + * a copy of the vcard in the new version. + * + * At the moment the only supported conversion is from 3.0 to 4.0. + * + * If input and output version are identical, a clone is returned. + * + * @throws VObject\InvalidDataException + */ + public function convert(int $target): VCard + { + $converter = new VObject\VCardConverter(); + + return $converter->convert($this, $target); + } + + /** + * VCards with version 2.1, 3.0 and 4.0 are found. + * + * If the VCARD doesn't know its version, 2.1 is assumed. + */ + public const DEFAULT_VERSION = self::VCARD21; + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + */ + public function validate(int $options = 0): array + { + $warnings = []; + + $versionMap = [ + self::VCARD21 => '2.1', + self::VCARD30 => '3.0', + self::VCARD40 => '4.0', + ]; + + $version = $this->select('VERSION'); + if (1 === count($version)) { + $version = (string) $this->VERSION; + if ('2.1' !== $version && '3.0' !== $version && '4.0' !== $version) { + $warnings[] = [ + 'level' => 3, + 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.', + 'node' => $this, + ]; + if ($options & self::REPAIR) { + $this->VERSION = $versionMap[self::DEFAULT_VERSION]; + } + } + if ('2.1' === $version && ($options & self::PROFILE_CARDDAV)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'CardDAV servers are not allowed to accept vCard 2.1.', + 'node' => $this, + ]; + } + } + $uid = $this->select('UID'); + if (0 === count($uid)) { + if ($options & self::PROFILE_CARDDAV) { + // Required for CardDAV + $warningLevel = 3; + $message = 'vCards on CardDAV servers MUST have a UID property.'; + } else { + // Not required for regular vcards + $warningLevel = 2; + $message = 'Adding a UID to a vCard property is recommended.'; + } + if ($options & self::REPAIR) { + $this->UID = VObject\UUIDUtil::getUUID(); + $warningLevel = 1; + } + $warnings[] = [ + 'level' => $warningLevel, + 'message' => $message, + 'node' => $this, + ]; + } + + $fn = $this->select('FN'); + if (1 !== count($fn)) { + $repaired = false; + if (($options & self::REPAIR) && 0 === count($fn)) { + // We're going to try to see if we can use the contents of the + // N property. + if (isset($this->N)) { + $value = explode(';', (string) $this->N); + if (isset($value[1]) && $value[1]) { + $this->FN = $value[1].' '.$value[0]; + } else { + $this->FN = $value[0]; + } + $repaired = true; + + // Otherwise, the ORG property may work + } elseif (isset($this->ORG)) { + $this->FN = (string) $this->ORG; + $repaired = true; + + // Otherwise, the NICKNAME property may work + } elseif (isset($this->NICKNAME)) { + $this->FN = (string) $this->NICKNAME; + $repaired = true; + + // Otherwise, the EMAIL property may work + } elseif (isset($this->EMAIL)) { + $this->FN = (string) $this->EMAIL; + $repaired = true; + } + } + $warnings[] = [ + 'level' => $repaired ? 1 : 3, + 'message' => 'The FN property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ]; + } + + return array_merge( + parent::validate($options), + $warnings + ); + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + */ + public function getValidationRules(): array + { + return [ + 'ADR' => '*', + 'ANNIVERSARY' => '?', + 'BDAY' => '?', + 'CALADRURI' => '*', + 'CALURI' => '*', + 'CATEGORIES' => '*', + 'CLIENTPIDMAP' => '*', + 'EMAIL' => '*', + 'FBURL' => '*', + 'IMPP' => '*', + 'GENDER' => '?', + 'GEO' => '*', + 'KEY' => '*', + 'KIND' => '?', + 'LANG' => '*', + 'LOGO' => '*', + 'MEMBER' => '*', + 'N' => '?', + 'NICKNAME' => '*', + 'NOTE' => '*', + 'ORG' => '*', + 'PHOTO' => '*', + 'PRODID' => '?', + 'RELATED' => '*', + 'REV' => '?', + 'ROLE' => '*', + 'SOUND' => '*', + 'SOURCE' => '*', + 'TEL' => '*', + 'TITLE' => '*', + 'TZ' => '*', + 'URL' => '*', + 'VERSION' => '1', + 'XML' => '*', + + // FN is commented out, because it's already handled by the + // validate function, which may also try to repair it. + // 'FN' => '+', + 'UID' => '?', + ]; + } + + /** + * Returns a preferred field. + * + * VCards can indicate whether a field such as ADR, TEL or EMAIL is + * preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x + * being a number between 1 and 100). + * + * If neither of those parameters are specified, the first is returned, if + * a field with that name does not exist, null is returned. + */ + public function preferred(string $propertyName): ?VObject\Property + { + $preferred = null; + $lastPref = 101; + foreach ($this->select($propertyName) as $field) { + $pref = 101; + if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) { + $pref = 1; + } elseif (isset($field['PREF'])) { + $pref = $field['PREF']->getValue(); + } + + if ($pref < $lastPref || is_null($preferred)) { + $preferred = $field; + $lastPref = $pref; + } + } + + return $preferred; + } + + /** + * Returns a property with a specific TYPE value (ADR, TEL, or EMAIL). + * + * This function will return null if the property does not exist. If there are + * multiple properties with the same TYPE value, only one will be returned. + * + * @return \ArrayAccess|array|null + */ + public function getByType(string $propertyName, string $type) + { + foreach ($this->select($propertyName) as $field) { + if (isset($field['TYPE']) && $field['TYPE']->has($type)) { + return $field; + } + } + + return null; + } + + /** + * This method should return a list of default property values. + */ + protected function getDefaults(): array + { + return [ + 'VERSION' => '4.0', + 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + ]; + } + + /** + * This method returns an array, with the representation as it should be + * encoded in json. This is used to create jCard or jCal documents. + */ + #[\ReturnTypeWillChange] + public function jsonSerialize(): array + { + // A vcard does not have sub-components, so we're overriding this + // method to remove that array element. + $properties = []; + + foreach ($this->children() as $child) { + $properties[] = $child->jsonSerialize(); + } + + return [ + strtolower($this->name), + $properties, + ]; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $propertiesByGroup = []; + + foreach ($this->children() as $property) { + $group = $property->group; + + if (!isset($propertiesByGroup[$group])) { + $propertiesByGroup[$group] = []; + } + + $propertiesByGroup[$group][] = $property; + } + + $writer->startElement(strtolower($this->name)); + + foreach ($propertiesByGroup as $group => $properties) { + if (!empty($group)) { + $writer->startElement('group'); + $writer->writeAttribute('name', strtolower($group)); + } + + foreach ($properties as $property) { + switch ($property->name) { + case 'VERSION': + break; + + case 'XML': + $value = $property->getParts(); + $fragment = new Xml\Element\XmlFragment($value[0]); + $writer->write($fragment); + break; + + default: + $property->xmlSerialize($writer); + break; + } + } + + if (!empty($group)) { + $writer->endElement(); + } + } + + $writer->endElement(); + } + + /** + * Returns the default class for a property name. + */ + public function getClassNameForPropertyName(string $propertyName): string + { + $className = parent::getClassNameForPropertyName($propertyName); + + // In vCard 4, BINARY no longer exists, and we need URI instead. + if (VObject\Property\Binary::class == $className && self::VCARD40 === $this->getDocumentType()) { + return VObject\Property\Uri::class; + } + + return $className; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VEvent.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VEvent.php new file mode 100644 index 0000000000..e29e56322b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VEvent.php @@ -0,0 +1,148 @@ +RRULE) { + try { + $it = new EventIterator($this, null, $start->getTimezone()); + } catch (NoInstancesException $e) { + // If we've caught this exception, there are no instances + // for the event that fall into the specified time-range. + return false; + } + + $it->fastForward($start); + + // We fast-forwarded to a spot where the end-time of the + // recurrence instance exceeded the start of the requested + // time-range. + // + // If the start time of the recurrence did not exceed the + // end of the time range as well, we have a match. + return $it->getDTStart() < $end && $it->getDTEnd() > $start; + } + + $effectiveStart = $this->DTSTART->getDateTime($start->getTimezone()); + if (isset($this->DTEND)) { + // The DTEND property is considered non-inclusive. So for a 3-day + // event in july, dtstart and dtend would have to be July 1st and + // July 4th respectively. + // + // See: + // http://tools.ietf.org/html/rfc5545#page-54 + $effectiveEnd = $this->DTEND->getDateTime($end->getTimezone()); + } elseif (isset($this->DURATION)) { + $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); + } elseif (!$this->DTSTART->hasTime()) { + $effectiveEnd = $effectiveStart->modify('+1 day'); + } else { + $effectiveEnd = $effectiveStart; + } + + return + ($start < $effectiveEnd) && ($end > $effectiveStart) + ; + } + + /** + * This method should return a list of default property values. + */ + protected function getDefaults(): array + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + */ + public function getValidationRules(): array + { + $hasMethod = isset($this->parent->METHOD); + + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + 'DTSTART' => $hasMethod ? '?' : '1', + 'CLASS' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'GEO' => '?', + 'LAST-MODIFIED' => '?', + 'LOCATION' => '?', + 'ORGANIZER' => '?', + 'PRIORITY' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'TRANSP' => '?', + 'URL' => '?', + 'RECURRENCE-ID' => '?', + 'RRULE' => '?', + 'DTEND' => '?', + 'DURATION' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'REQUEST-STATUS' => '*', + 'RELATED-TO' => '*', + 'RESOURCES' => '*', + 'RDATE' => '*', + ]; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VFreeBusy.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VFreeBusy.php new file mode 100644 index 0000000000..a9891c92c5 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VFreeBusy.php @@ -0,0 +1,92 @@ +select('FREEBUSY') as $freebusy) { + // We are only interested in FBTYPE=BUSY (the default), + // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE. + if (isset($freebusy['FBTYPE']) && 'BUSY' !== strtoupper(substr((string) $freebusy['FBTYPE'], 0, 4))) { + continue; + } + + // The freebusy component can hold more than 1 value, separated by + // commas. + $periods = explode(',', (string) $freebusy); + + foreach ($periods as $period) { + // Every period is formatted as [start]/[end]. The start is an + // absolute UTC time, the end may be an absolute UTC time, or + // duration (relative) value. + list($busyStart, $busyEnd) = explode('/', $period); + + $busyStart = VObject\DateTimeParser::parse($busyStart); + $busyEnd = VObject\DateTimeParser::parse($busyEnd); + if ($busyEnd instanceof \DateInterval) { + $busyEnd = $busyStart->add($busyEnd); + } + + if ($start < $busyEnd && $end > $busyStart) { + return false; + } + } + } + + return true; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + */ + public function getValidationRules(): array + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CONTACT' => '?', + 'DTSTART' => '?', + 'DTEND' => '?', + 'ORGANIZER' => '?', + 'URL' => '?', + + 'ATTENDEE' => '*', + 'COMMENT' => '*', + 'FREEBUSY' => '*', + 'REQUEST-STATUS' => '*', + ]; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VJournal.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VJournal.php new file mode 100644 index 0000000000..30e40adf73 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VJournal.php @@ -0,0 +1,98 @@ +DTSTART) ? $this->DTSTART->getDateTime() : null; + if ($dtstart) { + $effectiveEnd = $dtstart; + if (!$this->DTSTART->hasTime()) { + $effectiveEnd = $effectiveEnd->modify('+1 day'); + } + + return $start <= $effectiveEnd && $end > $dtstart; + } + + return false; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + */ + public function getValidationRules(): array + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CLASS' => '?', + 'CREATED' => '?', + 'DTSTART' => '?', + 'LAST-MODIFIED' => '?', + 'ORGANIZER' => '?', + 'RECURRENCE-ID' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + + 'RRULE' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'DESCRIPTION' => '*', + 'EXDATE' => '*', + 'RELATED-TO' => '*', + 'RDATE' => '*', + ]; + } + + /** + * This method should return a list of default property values. + */ + protected function getDefaults(): array + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VTimeZone.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VTimeZone.php new file mode 100644 index 0000000000..9dcf9b9763 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VTimeZone.php @@ -0,0 +1,62 @@ +TZID, $this->root); + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + */ + public function getValidationRules(): array + { + return [ + 'TZID' => 1, + + 'LAST-MODIFIED' => '?', + 'TZURL' => '?', + + // At least 1 STANDARD or DAYLIGHT must appear. + // + // The validator is not specific yet to pick this up, so these + // rules are too loose. + 'STANDARD' => '*', + 'DAYLIGHT' => '*', + ]; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VTodo.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VTodo.php new file mode 100644 index 0000000000..cab672ce59 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Component/VTodo.php @@ -0,0 +1,187 @@ +DTSTART) ? $this->DTSTART->getDateTime() : null; + $duration = isset($this->DURATION) ? VObject\DateTimeParser::parseDuration($this->DURATION) : null; + $due = isset($this->DUE) ? $this->DUE->getDateTime() : null; + $completed = isset($this->COMPLETED) ? $this->COMPLETED->getDateTime() : null; + $created = isset($this->CREATED) ? $this->CREATED->getDateTime() : null; + + if ($dtstart) { + if ($duration) { + $effectiveEnd = $dtstart->add($duration); + + return $start <= $effectiveEnd && $end > $dtstart; + } elseif ($due) { + return + ($start < $due || $start <= $dtstart) + && ($end > $dtstart || $end >= $due); + } else { + return $start <= $dtstart && $end > $dtstart; + } + } + if ($due) { + return $start < $due && $end >= $due; + } + if ($completed && $created) { + return + ($start <= $created || $start <= $completed) + && ($end >= $created || $end >= $completed); + } + if ($completed) { + return $start <= $completed && $end >= $completed; + } + if ($created) { + return $end > $created; + } + + return true; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + */ + public function getValidationRules(): array + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CLASS' => '?', + 'COMPLETED' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'DTSTART' => '?', + 'GEO' => '?', + 'LAST-MODIFIED' => '?', + 'LOCATION' => '?', + 'ORGANIZER' => '?', + 'PERCENT-COMPLETE' => '?', + 'PRIORITY' => '?', + 'RECURRENCE-ID' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + + 'RRULE' => '?', + 'DUE' => '?', + 'DURATION' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'REQUEST-STATUS' => '*', + 'RELATED-TO' => '*', + 'RESOURCES' => '*', + 'RDATE' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @throws VObject\InvalidDataException + */ + public function validate(int $options = 0): array + { + $result = parent::validate($options); + if (isset($this->DUE) && isset($this->DTSTART)) { + $due = $this->DUE; + $dtStart = $this->DTSTART; + + if ($due->getValueType() !== $dtStart->getValueType()) { + $result[] = [ + 'level' => 3, + 'message' => 'The value type (DATE or DATE-TIME) must be identical for DUE and DTSTART', + 'node' => $due, + ]; + } elseif ($due->getDateTime() < $dtStart->getDateTime()) { + $result[] = [ + 'level' => 3, + 'message' => 'DUE must occur after DTSTART', + 'node' => $due, + ]; + } + } + + return $result; + } + + /** + * This method should return a list of default property values. + */ + protected function getDefaults(): array + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => date('Ymd\\THis\\Z'), + ]; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/DateTimeParser.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/DateTimeParser.php new file mode 100644 index 0000000000..e8777b7fe5 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/DateTimeParser.php @@ -0,0 +1,558 @@ +\+|-)?P((?\d+)W)?((?\d+)D)?(T((?\d+)H)?((?\d+)M)?((?\d+)S)?)?$/', $duration, $matches); + if (!$result) { + throw new InvalidDataException('The supplied iCalendar duration value is incorrect: '.$duration); + } + + $invert = false; + + if (isset($matches['plusminus']) && '-' === $matches['plusminus']) { + $invert = true; + } + + $parts = [ + 'week', + 'day', + 'hour', + 'minute', + 'second', + ]; + + foreach ($parts as $part) { + $matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0; + } + + // We need to re-construct the $duration string, because weeks and + // days are not supported by DateInterval in the same string. + $duration = 'P'; + $days = $matches['day']; + + if ($matches['week']) { + $days += $matches['week'] * 7; + } + + if ($days) { + $duration .= $days.'D'; + } + + if ($matches['minute'] || $matches['second'] || $matches['hour']) { + $duration .= 'T'; + + if ($matches['hour']) { + $duration .= $matches['hour'].'H'; + } + + if ($matches['minute']) { + $duration .= $matches['minute'].'M'; + } + + if ($matches['second']) { + $duration .= $matches['second'].'S'; + } + } + + if ('P' === $duration) { + $duration = 'PT0S'; + } + + $iv = new \DateInterval($duration); + + if ($invert) { + $iv->invert = true; + } + + return $iv; + } + + /** + * Parses an iCalendar (RFC5545) formatted duration value. + * + * This method will return a string suitable for strtotime or DateTime::modify. + * + * @throws InvalidDataException + */ + public static function parseDurationAsString(string $duration): string + { + $result = preg_match('/^(?\+|-)?P((?\d+)W)?((?\d+)D)?(T((?\d+)H)?((?\d+)M)?((?\d+)S)?)?$/', $duration, $matches); + if (!$result) { + throw new InvalidDataException('The supplied iCalendar duration value is incorrect: '.$duration); + } + + $parts = [ + 'week', + 'day', + 'hour', + 'minute', + 'second', + ]; + + $newDur = ''; + + foreach ($parts as $part) { + if (isset($matches[$part]) && $matches[$part]) { + $newDur .= ' '.$matches[$part].' '.$part.'s'; + } + } + + $newDur = ('-' === $matches['plusminus'] ? '-' : '+').trim($newDur); + + if ('+' === $newDur) { + $newDur = '+0 seconds'; + } + + return $newDur; + } + + /** + * Parses either a Date or DateTime, or Duration value. + * + * @param \DateTimeZone|string $referenceTz + * + * @return \DateInterval|\DateTimeImmutable + * + * @throws InvalidDataException + */ + public static function parse(string $date, $referenceTz = null) + { + if ('P' === $date[0] || ('-' === $date[0] && 'P' === $date[1])) { + return self::parseDuration($date); + } elseif (8 === strlen($date)) { + return self::parseDate($date, $referenceTz); + } else { + return self::parseDateTime($date, $referenceTz); + } + } + + /** + * This method parses a vCard date and or time value. + * + * This can be used for the DATE, DATE-TIME, TIMESTAMP and + * DATE-AND-OR-TIME value. + * + * This method returns an array, not a DateTime value. + * + * The elements in the array are in the following order: + * year, month, date, hour, minute, second, timezone + * + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the year, etc. + * + * Timezone is either returned as 'Z' or as '+0800' + * + * For any non-specified values null is returned. + * + * List of date formats that are supported: + * YYYY + * YYYY-MM + * YYYYMMDD + * --MMDD + * ---DD + * + * YYYY-MM-DD + * --MM-DD + * ---DD + * + * List of supported time formats: + * + * HH + * HHMM + * HHMMSS + * -MMSS + * --SS + * + * HH + * HH:MM + * HH:MM:SS + * -MM:SS + * --SS + * + * A full basic-format date-time string looks like : + * 20130603T133901 + * + * A full extended-format date-time string looks like : + * 2013-06-03T13:39:01 + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +1100. + * + * @throws InvalidDataException + */ + public static function parseVCardDateTime(string $date): array + { + $regex = '/^ + (?: # date part + (?: + (?: (? [0-9]{4}) (?: -)?| --) + (? [0-9]{2})? + |---) + (? [0-9]{2})? + )? + (?:T # time part + (? [0-9]{2} | -) + (? [0-9]{2} | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{4}) + + )? + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + // Attempting to parse the extended format. + $regex = '/^ + (?: # date part + (?: (? [0-9]{4}) - | -- ) + (? [0-9]{2}) - + (? [0-9]{2}) + )? + (?:T # time part + + (?: (? [0-9]{2}) : | -) + (?: (? [0-9]{2}) : | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{2}:?[0-9]{2}) + + )? + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + throw new InvalidDataException('Invalid vCard date-time string: '.$date); + } + } + $parts = [ + 'year', + 'month', + 'date', + 'hour', + 'minute', + 'second', + 'timezone', + ]; + + $result = []; + foreach ($parts as $part) { + if (empty($matches[$part])) { + $result[$part] = null; + } elseif ('-' === $matches[$part] || '--' === $matches[$part]) { + $result[$part] = null; + } else { + $result[$part] = $matches[$part]; + } + } + + return $result; + } + + /** + * This method parses a vCard TIME value. + * + * This method returns an array, not a DateTime value. + * + * The elements in the array are in the following order: + * hour, minute, second, timezone + * + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the hour etc. + * + * Timezone is either returned as 'Z' or as '+08:00' + * + * For any non-specified values null is returned. + * + * List of supported time formats: + * + * HH + * HHMM + * HHMMSS + * -MMSS + * --SS + * + * HH + * HH:MM + * HH:MM:SS + * -MM:SS + * --SS + * + * A full basic-format time string looks like : + * 133901 + * + * A full extended-format time string looks like : + * 13:39:01 + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +11:00. + * + * @throws InvalidDataException + */ + public static function parseVCardTime(string $date): array + { + $regex = '/^ + (? [0-9]{2} | -) + (? [0-9]{2} | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{4}) + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + // Attempting to parse the extended format. + $regex = '/^ + (?: (? [0-9]{2}) : | -) + (?: (? [0-9]{2}) : | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{2}:?[0-9]{2}) + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + throw new InvalidDataException('Invalid vCard time string: '.$date); + } + } + $parts = [ + 'hour', + 'minute', + 'second', + 'timezone', + ]; + + $result = []; + foreach ($parts as $part) { + if (empty($matches[$part])) { + $result[$part] = null; + } elseif ('-' === $matches[$part]) { + $result[$part] = null; + } else { + $result[$part] = $matches[$part]; + } + } + + return $result; + } + + /** + * This method parses a vCard date and or time value. + * + * This can be used for the DATE, DATE-TIME and + * DATE-AND-OR-TIME value. + * + * This method returns an array, not a DateTime value. + * The elements in the array are in the following order: + * year, month, date, hour, minute, second, timezone + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the year, etc. + * + * Timezone is either returned as 'Z' or as '+0800' + * + * For any non-specified values null is returned. + * + * List of date formats that are supported: + * 20150128 + * 2015-01 + * --01 + * --0128 + * ---28 + * + * List of supported time formats: + * 13 + * 1353 + * 135301 + * -53 + * -5301 + * --01 (unreachable, see the tests) + * --01Z + * --01+1234 + * + * List of supported date-time formats: + * 20150128T13 + * --0128T13 + * ---28T13 + * ---28T1353 + * ---28T135301 + * ---28T13Z + * ---28T13+1234 + * + * See the regular expressions for all the possible patterns. + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +1100. + * + * @throws InvalidDataException + */ + public static function parseVCardDateAndOrTime(string $date): array + { + // \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d + $valueDate = '/^(?J)(?:'. + '(?\d{4})(?\d\d)(?\d\d)'. + '|(?\d{4})-(?\d\d)'. + '|--(?\d\d)(?\d\d)?'. + '|---(?\d\d)'. + ')$/'; + + // (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)? + $valueTime = '/^(?J)(?:'. + '((?\d\d)((?\d\d)(?\d\d)?)?'. + '|-(?\d\d)(?\d\d)?'. + '|--(?\d\d))'. + '(?(Z|[+\-]\d\d(\d\d)?))?'. + ')$/'; + + // (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)? + $valueDateTime = '/^(?:'. + '((?\d{4})(?\d\d)(?\d\d)'. + '|--(?\d\d)(?\d\d)'. + '|---(?\d\d))'. + 'T'. + '(?\d\d)((?\d\d)(?\d\d)?)?'. + '(?(Z|[+\-]\d\d(\d\d?)))?'. + ')$/'; + + // date-and-or-time is date | date-time | time + // in this strict order. + + if (0 === preg_match($valueDate, $date, $matches) + && 0 === preg_match($valueDateTime, $date, $matches) + && 0 === preg_match($valueTime, $date, $matches)) { + throw new InvalidDataException('Invalid vCard date-time string: '.$date); + } + + $parts = [ + 'year' => null, + 'month' => null, + 'date' => null, + 'hour' => null, + 'minute' => null, + 'second' => null, + 'timezone' => null, + ]; + + // The $valueDateTime expression has a bug with (?J) so we simulate it. + $parts['date0'] = &$parts['date']; + $parts['date1'] = &$parts['date']; + $parts['date2'] = &$parts['date']; + $parts['month0'] = &$parts['month']; + $parts['month1'] = &$parts['month']; + $parts['year0'] = &$parts['year']; + + foreach ($parts as $part => &$value) { + if (!empty($matches[$part])) { + $value = $matches[$part]; + } + } + + unset($parts['date0']); + unset($parts['date1']); + unset($parts['date2']); + unset($parts['month0']); + unset($parts['month1']); + unset($parts['year0']); + + return $parts; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Document.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Document.php new file mode 100644 index 0000000000..02eaf13c70 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Document.php @@ -0,0 +1,232 @@ +value syntax, in which case + * properties will automatically be created, or you can just pass a list of + * Component and Property object. + * + * By default, a set of sensible values will be added to the component. For + * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To + * ensure that this does not happen, set $defaults to false. + */ + public function createComponent(string $name, ?array $children = null, bool $defaults = true): Component + { + $name = strtoupper($name); + $class = Component::class; + + if (isset(static::$componentMap[$name])) { + $class = static::$componentMap[$name]; + } + if (is_null($children)) { + $children = []; + } + + return new $class($this, $name, $children, $defaults); + } + + /** + * Factory method for creating new properties. + * + * This method automatically searches for the correct property class, based + * on its name. + * + * You can specify the parameters either in key=>value syntax, in which case + * parameters will automatically be created, or you can just pass a list of + * Parameter objects. + * + * @param string|null $valueType Force a specific valueType, such as URI or TEXT + * + * @throws InvalidDataException + */ + public function createProperty(string $name, $value = null, ?array $parameters = null, ?string $valueType = null, ?int $lineIndex = null, ?string $lineString = null): Property + { + // If there's a . in the name, it means it's prefixed by a group name. + if (false !== ($i = strpos($name, '.'))) { + $group = substr($name, 0, $i); + $name = strtoupper(substr($name, $i + 1)); + } else { + $name = strtoupper($name); + $group = null; + } + + $class = null; + + if ($valueType) { + // The valueType argument comes first to figure out the correct + // class. + $class = $this->getClassNameForPropertyValue($valueType); + } + + if (is_null($class)) { + // If a VALUE parameter is supplied, we should use that. + if (isset($parameters['VALUE'])) { + $class = $this->getClassNameForPropertyValue($parameters['VALUE']); + if (is_null($class)) { + throw new InvalidDataException('Unsupported VALUE parameter for '.$name.' property. You supplied "'.$parameters['VALUE'].'"'); + } + } else { + $class = $this->getClassNameForPropertyName($name); + } + } + if (is_null($parameters)) { + $parameters = []; + } + + return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString); + } + + /** + * This method returns a full class-name for a value parameter. + * + * For instance, DTSTART may have VALUE=DATE. In that case we will look in + * our valueMap table and return the appropriate class name. + * + * This method returns null if we don't have a specialized class. + * + * @return string|void|null + */ + public function getClassNameForPropertyValue(string $valueParam) + { + $valueParam = strtoupper($valueParam); + if (isset(static::$valueMap[$valueParam])) { + return static::$valueMap[$valueParam]; + } + } + + /** + * Returns the default class for a property name. + */ + public function getClassNameForPropertyName(string $propertyName): string + { + return static::$propertyMap[$propertyName] ?? Property\Unknown::class; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ElementList.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ElementList.php new file mode 100644 index 0000000000..9c81b77480 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ElementList.php @@ -0,0 +1,44 @@ +vevent where there's multiple VEVENT objects. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ElementList extends \ArrayIterator +{ + /* {{{ ArrayAccess Interface */ + + /** + * Sets an item through ArrayAccess. + * + * @param int $offset + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value): void + { + throw new \LogicException('You can not add new objects to an ElementList'); + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset): void + { + throw new \LogicException('You can not remove objects from an ElementList'); + } + + /* }}} */ +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/EofException.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/EofException.php new file mode 100644 index 0000000000..837af7eb7f --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/EofException.php @@ -0,0 +1,15 @@ +start = $start; + $this->end = $end; + $this->data = []; + + $this->data[] = [ + 'start' => $this->start, + 'end' => $this->end, + 'type' => 'FREE', + ]; + } + + /** + * Adds free or busy time to the data. + * + * @param string $type FREE, BUSY, BUSY-UNAVAILABLE or BUSY-TENTATIVE + */ + public function add(int $start, int $end, string $type): void + { + if ($start > $this->end || $end < $this->start) { + // This new data is outside our time range. + return; + } + + if ($start < $this->start) { + // The item starts before our requested time range + $start = $this->start; + } + if ($end > $this->end) { + // The item ends after our requested time range + $end = $this->end; + } + + // Finding out where we need to insert the new item. + $currentIndex = 0; + while ($start > $this->data[$currentIndex]['end']) { + ++$currentIndex; + } + + // The standard insertion point will be one _after_ the first + // overlapping item. + $insertStartIndex = $currentIndex + 1; + + $newItem = [ + 'start' => $start, + 'end' => $end, + 'type' => $type, + ]; + + $precedingItem = $this->data[$insertStartIndex - 1]; + if ($this->data[$insertStartIndex - 1]['start'] === $start) { + // The old item starts at the exact same point as the new item. + --$insertStartIndex; + } + + // Now we know where to insert the item, we need to know where it + // starts overlapping with items on the tail end. We need to start + // looking one item before the insertStartIndex, because it's possible + // that the new item 'sits inside' the previous old item. + if ($insertStartIndex > 0) { + $currentIndex = $insertStartIndex - 1; + } else { + $currentIndex = 0; + } + + while ($end > $this->data[$currentIndex]['end']) { + ++$currentIndex; + } + + // What we are about to insert into the array + $newItems = [ + $newItem, + ]; + + // This is the amount of items that are completely overwritten by the + // new item. + $itemsToDelete = $currentIndex - $insertStartIndex; + if ($this->data[$currentIndex]['end'] <= $end) { + ++$itemsToDelete; + } + + // If itemsToDelete was -1, it means that the newly inserted item is + // actually sitting inside an existing one. This means we need to split + // the item at the current position in two and insert the new item in + // between. + if (-1 === $itemsToDelete) { + $itemsToDelete = 0; + if ($newItem['end'] < $precedingItem['end']) { + $newItems[] = [ + 'start' => $newItem['end'] + 1, + 'end' => $precedingItem['end'], + 'type' => $precedingItem['type'], + ]; + } + } + + array_splice( + $this->data, + $insertStartIndex, + $itemsToDelete, + $newItems + ); + + $doMerge = false; + $mergeOffset = $insertStartIndex; + $mergeItem = $newItem; + $mergeDelete = 1; + + if (isset($this->data[$insertStartIndex - 1])) { + // Updating the start time of the previous item. + $this->data[$insertStartIndex - 1]['end'] = $start; + + // If the previous and the current are of the same type, we can + // merge them into one item. + if ($this->data[$insertStartIndex - 1]['type'] === $this->data[$insertStartIndex]['type']) { + $doMerge = true; + --$mergeOffset; + ++$mergeDelete; + $mergeItem['start'] = $this->data[$insertStartIndex - 1]['start']; + } + } + if (isset($this->data[$insertStartIndex + 1])) { + // Updating the start time of the next item. + $this->data[$insertStartIndex + 1]['start'] = $end; + + // If the next and the current are of the same type, we can + // merge them into one item. + if ($this->data[$insertStartIndex + 1]['type'] === $this->data[$insertStartIndex]['type']) { + $doMerge = true; + ++$mergeDelete; + $mergeItem['end'] = $this->data[$insertStartIndex + 1]['end']; + } + } + if ($doMerge) { + array_splice( + $this->data, + $mergeOffset, + $mergeDelete, + [$mergeItem] + ); + } + } + + public function getData(): array + { + return $this->data; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/FreeBusyGenerator.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/FreeBusyGenerator.php new file mode 100644 index 0000000000..4248260340 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/FreeBusyGenerator.php @@ -0,0 +1,535 @@ +setTimeRange($start, $end); + + if ($objects) { + $this->setObjects($objects); + } + if (is_null($timeZone)) { + $timeZone = new \DateTimeZone('UTC'); + } + $this->setTimeZone($timeZone); + } + + /** + * Sets the VCALENDAR object. + * + * If this is set, it will not be generated for you. You are responsible + * for setting things like the METHOD, CALSCALE, VERSION, etc.. + * + * The VFREEBUSY object will be automatically added though. + */ + public function setBaseObject(Document $vcalendar): void + { + $this->baseObject = $vcalendar; + } + + /** + * Sets a VAVAILABILITY document. + */ + public function setVAvailability(Document $vcalendar): void + { + $this->vavailability = $vcalendar; + } + + /** + * Sets the input objects. + * + * You must either specify a vcalendar object as a string, or as the parse + * Component. + * It's also possible to specify multiple objects as an array. + */ + public function setObjects($objects): void + { + if (!is_array($objects)) { + $objects = [$objects]; + } + + $this->objects = []; + foreach ($objects as $object) { + if (is_string($object) || is_resource($object)) { + $this->objects[] = Reader::read($object); + } elseif ($object instanceof Component) { + $this->objects[] = $object; + } else { + throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects'); + } + } + } + + /** + * Sets the time range. + * + * Any freebusy object falling outside this time range will be ignored. + * + * @throws \Exception + */ + public function setTimeRange(?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null): void + { + if (!$start) { + $start = new \DateTimeImmutable(Settings::$minDate); + } + if (!$end) { + $end = new \DateTimeImmutable(Settings::$maxDate); + } + $this->start = $start; + $this->end = $end; + } + + /** + * Sets the reference timezone for floating times. + */ + public function setTimeZone(\DateTimeZone $timeZone): void + { + $this->timeZone = $timeZone; + } + + /** + * Parses the input data and returns a correct VFREEBUSY object, wrapped in + * a VCALENDAR. + */ + public function getResult(): Component + { + $fbData = new FreeBusyData( + $this->start->getTimeStamp(), + $this->end->getTimeStamp() + ); + if (null !== $this->vavailability) { + $this->calculateAvailability($fbData, $this->vavailability); + } + + $this->calculateBusy($fbData, $this->objects); + + return $this->generateFreeBusyCalendar($fbData); + } + + /** + * This method takes a VAVAILABILITY component and figures out all the + * available times. + */ + protected function calculateAvailability(FreeBusyData $fbData, VCalendar $vavailability): void + { + $vavailComps = iterator_to_array($vavailability->VAVAILABILITY); + usort( + $vavailComps, + function ($a, $b) { + // We need to order the components by priority. Priority 1 + // comes first, up until priority 9. Priority 0 comes after + // priority 9. No priority implies priority 0. + // + // Yes, I'm serious. + $priorityA = isset($a->PRIORITY) ? (int) $a->PRIORITY->getValue() : 0; + $priorityB = isset($b->PRIORITY) ? (int) $b->PRIORITY->getValue() : 0; + + if (0 === $priorityA) { + $priorityA = 10; + } + if (0 === $priorityB) { + $priorityB = 10; + } + + return $priorityA - $priorityB; + } + ); + + // Now we go over all the VAVAILABILITY components and figure if + // there's any we don't need to consider. + // + // This is because of one of two reasons: either the + // VAVAILABILITY component falls outside the time we are interested in, + // or a different VAVAILABILITY component with a higher priority has + // already completely covered the time-range. + $old = $vavailComps; + $new = []; + + foreach ($old as $vavail) { + list($compStart, $compEnd) = $vavail->getEffectiveStartEnd(); + + // We don't care about date-times that are earlier or later than the + // start and end of the freebusy report, so this gets normalized + // first. + if (is_null($compStart) || $compStart < $this->start) { + $compStart = $this->start; + } + if (is_null($compEnd) || $compEnd > $this->end) { + $compEnd = $this->end; + } + + // If the item fell out of the time range, we can just skip it. + if ($compStart > $this->end || $compEnd < $this->start) { + continue; + } + + // Going through our existing list of components to see if there's + // a higher priority component that already fully covers this one. + foreach ($new as $higherVavail) { + list($higherStart, $higherEnd) = $higherVavail->getEffectiveStartEnd(); + if ( + (is_null($higherStart) || $higherStart < $compStart) + && (is_null($higherEnd) || $higherEnd > $compEnd) + ) { + // Component is fully covered by a higher priority + // component. We can skip this component. + continue 2; + } + } + + // We're keeping it! + $new[] = $vavail; + } + + // Lastly, we need to traverse the remaining components and fill in the + // freebusydata slots. + // + // We traverse the components in reverse, because we want the higher + // priority components to override the lower ones. + foreach (array_reverse($new) as $vavail) { + $busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE'; + list($vavailStart, $vavailEnd) = $vavail->getEffectiveStartEnd(); + + // Making the component size no larger than the requested free-busy + // report range. + if (!$vavailStart || $vavailStart < $this->start) { + $vavailStart = $this->start; + } + if (!$vavailEnd || $vavailEnd > $this->end) { + $vavailEnd = $this->end; + } + + // Marking the entire time range of the VAVAILABILITY component as + // busy. + $fbData->add( + $vavailStart->getTimeStamp(), + $vavailEnd->getTimeStamp(), + $busyType + ); + + // Looping over the AVAILABLE components. + if (isset($vavail->AVAILABLE)) { + foreach ($vavail->AVAILABLE as $available) { + list($availStart, $availEnd) = $available->getEffectiveStartEnd(); + $fbData->add( + $availStart->getTimeStamp(), + $availEnd->getTimeStamp(), + 'FREE' + ); + + if ($available->RRULE) { + // Our favourite thing: recurrence!! + + $rruleIterator = new Recur\RRuleIterator( + $available->RRULE->getValue(), + $availStart + ); + $rruleIterator->fastForward($vavailStart); + + $startEndDiff = $availStart->diff($availEnd); + + while ($rruleIterator->valid()) { + $recurStart = $rruleIterator->current(); + $recurEnd = $recurStart->add($startEndDiff); + + if ($recurStart > $vavailEnd) { + // We're beyond the legal time range. + break; + } + + if ($recurEnd > $vavailEnd) { + // Truncating the end if it exceeds the + // VAVAILABILITY end. + $recurEnd = $vavailEnd; + } + + $fbData->add( + $recurStart->getTimeStamp(), + $recurEnd->getTimeStamp(), + 'FREE' + ); + + $rruleIterator->next(); + } + } + } + } + } + } + + /** + * This method takes an array of iCalendar objects and applies its busy + * times on fbData. + * + * @param VCalendar[] $objects + * + * @throws InvalidDataException|Recur\MaxInstancesExceededException + */ + protected function calculateBusy(FreeBusyData $fbData, array $objects): void + { + foreach ($objects as $key => $object) { + foreach ($object->getBaseComponents() as $component) { + switch ($component->name) { + case 'VEVENT': + /** @var VEvent $component */ + $FBTYPE = 'BUSY'; + if (isset($component->TRANSP) && ('TRANSPARENT' === strtoupper($component->TRANSP))) { + break; + } + if (isset($component->STATUS)) { + $status = strtoupper($component->STATUS); + if ('CANCELLED' === $status) { + break; + } + if ('TENTATIVE' === $status) { + $FBTYPE = 'BUSY-TENTATIVE'; + } + } + + $times = []; + + if ($component->RRULE) { + try { + $iterator = new EventIterator($object, (string) $component->UID, $this->timeZone); + } catch (NoInstancesException $e) { + // This event is recurring, but it doesn't have a single + // instance. We are skipping this event from the output + // entirely. + unset($this->objects[$key]); + break; + } + + if ($this->start) { + $iterator->fastForward($this->start); + } + + $maxRecurrences = Settings::$maxRecurrences; + + while ($iterator->valid() && --$maxRecurrences) { + $startTime = $iterator->getDTStart(); + if ($this->end && $startTime > $this->end) { + break; + } + $times[] = [ + $iterator->getDTStart(), + $iterator->getDTEnd(), + ]; + + $iterator->next(); + } + } else { + $startTime = $component->DTSTART->getDateTime($this->timeZone); + if ($this->end && $startTime > $this->end) { + break; + } + $endTime = null; + if (isset($component->DTEND)) { + $endTime = $component->DTEND->getDateTime($this->timeZone); + } elseif (isset($component->DURATION)) { + $duration = DateTimeParser::parseDuration((string) $component->DURATION); + $endTime = clone $startTime; + $endTime = $endTime->add($duration); + } elseif (!$component->DTSTART->hasTime()) { + $endTime = clone $startTime; + $endTime = $endTime->modify('+1 day'); + } else { + // The event had no duration (0 seconds) + break; + } + + $times[] = [$startTime, $endTime]; + } + + foreach ($times as $time) { + if ($this->end && $time[0] > $this->end) { + break; + } + if ($this->start && $time[1] < $this->start) { + break; + } + + $fbData->add( + $time[0]->getTimeStamp(), + $time[1]->getTimeStamp(), + $FBTYPE + ); + } + break; + + case 'VFREEBUSY': + /** @var VFreeBusy $component */ + foreach ($component->FREEBUSY as $freebusy) { + $fbType = isset($freebusy['FBTYPE']) ? strtoupper($freebusy['FBTYPE']) : 'BUSY'; + + // Skipping intervals marked as 'free' + if ('FREE' === $fbType) { + continue; + } + + $values = explode(',', $freebusy); + foreach ($values as $value) { + list($startTime, $endTime) = explode('/', $value); + $startTime = DateTimeParser::parseDateTime($startTime); + + if ('P' === substr($endTime, 0, 1) || '-P' === substr($endTime, 0, 2)) { + $duration = DateTimeParser::parseDuration($endTime); + $endTime = clone $startTime; + $endTime = $endTime->add($duration); + } else { + $endTime = DateTimeParser::parseDateTime($endTime); + } + + if ($this->start && $this->start > $endTime) { + continue; + } + if ($this->end && $this->end < $startTime) { + continue; + } + $fbData->add( + $startTime->getTimeStamp(), + $endTime->getTimeStamp(), + $fbType + ); + } + } + break; + } + } + } + } + + /** + * This method takes a FreeBusyData object and generates the VCALENDAR + * object associated with it. + * + * @throws InvalidDataException + * @throws \Exception + */ + protected function generateFreeBusyCalendar(FreeBusyData $fbData): VCalendar + { + if (null !== $this->baseObject) { + $calendar = $this->baseObject; + } else { + $calendar = new VCalendar(); + } + + $vfreebusy = $calendar->createComponent('VFREEBUSY'); + $calendar->add($vfreebusy); + + if ($this->start) { + /** @var DateTime $dtstart */ + $dtstart = $calendar->createProperty('DTSTART'); + $dtstart->setDateTime($this->start); + $vfreebusy->add($dtstart); + } + if ($this->end) { + /** @var DateTime $dtend */ + $dtend = $calendar->createProperty('DTEND'); + $dtend->setDateTime($this->end); + $vfreebusy->add($dtend); + } + + $tz = new \DateTimeZone('UTC'); + /** @var DateTime $dtstamp */ + $dtstamp = $calendar->createProperty('DTSTAMP'); + $dtstamp->setDateTime(new \DateTimeImmutable('now', $tz)); + $vfreebusy->add($dtstamp); + + foreach ($fbData->getData() as $busyTime) { + $busyType = strtoupper($busyTime['type']); + + // Ignoring all the FREE parts, because those are already assumed. + if ('FREE' === $busyType) { + continue; + } + + $busyTime[0] = new \DateTimeImmutable('@'.$busyTime['start'], $tz); + $busyTime[1] = new \DateTimeImmutable('@'.$busyTime['end'], $tz); + + $prop = $calendar->createProperty( + 'FREEBUSY', + $busyTime[0]->format('Ymd\\THis\\Z').'/'.$busyTime[1]->format('Ymd\\THis\\Z') + ); + + // Only setting FBTYPE if it's not BUSY, because BUSY is the + // default anyway. + if ('BUSY' !== $busyType) { + $prop['FBTYPE'] = $busyType; + } + $vfreebusy->add($prop); + } + + return $calendar; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/Broker.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/Broker.php new file mode 100644 index 0000000000..76ee0c71e6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/Broker.php @@ -0,0 +1,976 @@ +component) { + return false; + } + + switch ($itipMessage->method) { + case 'REQUEST': + return $this->processMessageRequest($itipMessage, $existingObject); + + case 'CANCEL': + return $this->processMessageCancel($itipMessage, $existingObject); + + case 'REPLY': + return $this->processMessageReply($itipMessage, $existingObject); + + default: + // Unsupported iTip message + return; + } + } + + /** + * This function parses a VCALENDAR object and figure out if any messages + * need to be sent. + * + * A VCALENDAR object will be created from the perspective of either an + * attendee, or an organizer. You must pass a string identifying the + * current user, so we can figure out who in the list of attendees or the + * organizer we are sending this message on behalf of. + * + * It's possible to specify the current user as an array, in case the user + * has more than one identifying href (such as multiple emails). + * + * It $oldCalendar is specified, it is assumed that the operation is + * updating an existing event, which means that we need to look at the + * differences between events, and potentially send old attendees + * cancellations, and current attendees updates. + * + * If $calendar is null, but $oldCalendar is specified, we treat the + * operation as if the user has deleted an event. If the user was an + * organizer, this means that we need to send cancellation notices to + * people. If the user was an attendee, we need to make sure that the + * organizer gets the 'declined' message. + * + * @param VCalendar|string $calendar + * @param string|array $userHref + * @param VCalendar|string|null $oldCalendar + * + * @throws ITipException + * @throws InvalidDataException + * @throws ParseException + * @throws SameOrganizerForAllComponentsException + */ + public function parseEvent($calendar, $userHref, $oldCalendar = null): array + { + if ($oldCalendar) { + if (is_string($oldCalendar)) { + $oldCalendar = Reader::read($oldCalendar); + } + if (!isset($oldCalendar->VEVENT)) { + // We only support events at the moment + return []; + } + + $oldEventInfo = $this->parseEventInfo($oldCalendar); + } else { + $oldEventInfo = [ + 'organizer' => null, + 'significantChangeHash' => '', + 'attendees' => [], + ]; + } + + $userHref = (array) $userHref; + + if (!is_null($calendar)) { + if (is_string($calendar)) { + $calendar = Reader::read($calendar); + } + if (!isset($calendar->VEVENT)) { + // We only support events at the moment + return []; + } + $eventInfo = $this->parseEventInfo($calendar); + if (!$eventInfo['attendees'] && !$oldEventInfo['attendees']) { + // If there were no attendees on either side of the equation, + // we don't need to do anything. + return []; + } + if (!$eventInfo['organizer'] && !$oldEventInfo['organizer']) { + // There was no organizer before or after the change. + return []; + } + + $baseCalendar = $calendar; + + // If the new object didn't have an organizer, the organizer + // changed the object from a scheduling object to a non-scheduling + // object. We just copy the info from the old object. + if (!$eventInfo['organizer'] && $oldEventInfo['organizer']) { + $eventInfo['organizer'] = $oldEventInfo['organizer']; + $eventInfo['organizerName'] = $oldEventInfo['organizerName']; + } + } else { + // The calendar object got deleted, we need to process this as a + // cancellation / decline. + if (!$oldCalendar) { + // No old and no new calendar, there's nothing to do. + return []; + } + + $eventInfo = $oldEventInfo; + + if (in_array($eventInfo['organizer'], $userHref)) { + // This is an organizer deleting the event. + $eventInfo['attendees'] = []; + // Increasing the sequence, but only if the organizer deleted + // the event. + ++$eventInfo['sequence']; + } else { + // This is an attendee deleting the event. + foreach ($eventInfo['attendees'] as $key => $attendee) { + if (in_array($attendee['href'], $userHref)) { + $eventInfo['attendees'][$key]['instances'] = ['master' => ['id' => 'master', 'partstat' => 'DECLINED'], + ]; + } + } + } + $baseCalendar = $oldCalendar; + } + + if (in_array($eventInfo['organizer'], $userHref)) { + return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo); + } elseif ($oldCalendar) { + // We need to figure out if the user is an attendee, but we're only + // doing so if there's an oldCalendar, because we only want to + // process updates, not creation of new events. + foreach ($eventInfo['attendees'] as $attendee) { + if (in_array($attendee['href'], $userHref)) { + return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); + } + } + } + + return []; + } + + /** + * Processes incoming REQUEST messages. + * + * This is message from an organizer, and is either a new event + * invite, or an update to an existing one. + */ + protected function processMessageRequest(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar + { + if (!$existingObject) { + // This is a new invite, and we're just going to copy over + // all the components from the invite. + $existingObject = new VCalendar(); + } else { + // We need to update an existing object with all the new + // information. We can just remove all existing components + // and create new ones. + foreach ($existingObject->getComponents() as $component) { + $existingObject->remove($component); + } + } + foreach ($itipMessage->message->getComponents() as $component) { + $existingObject->add(clone $component); + } + + return $existingObject; + } + + /** + * Processes incoming CANCEL messages. + * + * This is a message from an organizer, and means that either an + * attendee got removed from an event, or an event got cancelled + * altogether. + */ + protected function processMessageCancel(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar + { + if (!$existingObject) { + // The event didn't exist in the first place, so we're just + // ignoring this message. + } else { + foreach ($existingObject->VEVENT as $vevent) { + $vevent->STATUS = 'CANCELLED'; + $vevent->SEQUENCE = $itipMessage->sequence; + } + } + + return $existingObject; + } + + /** + * Processes incoming REPLY messages. + * + * The message is a reply. This is for example an attendee telling + * an organizer he accepted the invite, or declined it. + * + * @throws InvalidDataException + * @throws MaxInstancesExceededException + * @throws NoInstancesException + */ + protected function processMessageReply(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar + { + // A reply can only be processed based on an existing object. + // If the object is not available, the reply is ignored. + if (!$existingObject) { + return null; + } + $instances = []; + $requestStatus = '2.0'; + + // Finding all the instances the attendee replied to. + foreach ($itipMessage->message->VEVENT as $vevent) { + // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence. + // The Unix timestamp will be the same for an event, even if the reply from the attendee + // used a different format/timezone to express the event date-time. + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; + $attendee = $vevent->ATTENDEE; + $instances[$recurId] = $attendee['PARTSTAT']->getValue(); + if (isset($vevent->{'REQUEST-STATUS'})) { + $requestStatus = $vevent->{'REQUEST-STATUS'}->getValue(); + list($requestStatus) = explode(';', $requestStatus); + } + } + + // Now we need to loop through the original organizer event, to find + // all the instances where we have a reply for. + $masterObject = null; + foreach ($existingObject->VEVENT as $vevent) { + // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence. + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; + if ('master' === $recurId) { + $masterObject = $vevent; + } + if (isset($instances[$recurId])) { + $attendeeFound = false; + if (isset($vevent->ATTENDEE)) { + foreach ($vevent->ATTENDEE as $attendee) { + if ($attendee->getValue() === $itipMessage->sender) { + $attendeeFound = true; + $attendee['PARTSTAT'] = $instances[$recurId]; + $attendee['SCHEDULE-STATUS'] = $requestStatus; + // Un-setting the RSVP status, because we now know + // that the attendee already replied. + unset($attendee['RSVP']); + break; + } + } + } + if (!$attendeeFound) { + // Adding a new attendee. The iTip documentation calls this + // a party crasher. + $attendee = $vevent->add('ATTENDEE', $itipMessage->sender, [ + 'PARTSTAT' => $instances[$recurId], + ]); + if ($itipMessage->senderName) { + $attendee['CN'] = $itipMessage->senderName; + } + } + unset($instances[$recurId]); + } + } + + if (!$masterObject) { + // No master object, we can't add new instances. + return null; + } + // If we got replies to instances that did not exist in the + // original list, it means that new exceptions must be created. + foreach ($instances as $recurId => $partstat) { + $recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid); + $found = false; + $iterations = 1000; + do { + $newObject = $recurrenceIterator->getEventObject(); + $recurrenceIterator->next(); + + // Compare the Unix timestamp returned by getTimestamp with the previously calculated timestamp. + // If they are the same, then this is a matching recurrence, even though its date-time may have + // been expressed in a different format/timezone. + if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() === $recurId) { + $found = true; + } + --$iterations; + } while ($recurrenceIterator->valid() && !$found && $iterations); + + // Invalid recurrence id. Skipping this object. + if (!$found) { + continue; + } + + unset( + $newObject->RRULE, + $newObject->EXDATE, + $newObject->RDATE + ); + $attendeeFound = false; + if (isset($newObject->ATTENDEE)) { + foreach ($newObject->ATTENDEE as $attendee) { + if ($attendee->getValue() === $itipMessage->sender) { + $attendeeFound = true; + $attendee['PARTSTAT'] = $partstat; + break; + } + } + } + if (!$attendeeFound) { + // Adding a new attendee + $attendee = $newObject->add('ATTENDEE', $itipMessage->sender, [ + 'PARTSTAT' => $partstat, + ]); + if ($itipMessage->senderName) { + $attendee['CN'] = $itipMessage->senderName; + } + } + $existingObject->add($newObject); + } + + return $existingObject; + } + + /** + * This method is used in cases where an event got updated, and we + * potentially need to send emails to attendees to let them know of updates + * in the events. + * + * We will detect which attendees got added, which got removed and create + * specific messages for these situations. + */ + protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo): array + { + // Merging attendee lists. + $attendees = []; + foreach ($oldEventInfo['attendees'] as $attendee) { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => $attendee['instances'], + 'newInstances' => [], + 'name' => $attendee['name'], + 'forceSend' => null, + ]; + } + foreach ($eventInfo['attendees'] as $attendee) { + if (isset($attendees[$attendee['href']])) { + $attendees[$attendee['href']]['name'] = $attendee['name']; + $attendees[$attendee['href']]['newInstances'] = $attendee['instances']; + $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend']; + } else { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => [], + 'newInstances' => $attendee['instances'], + 'name' => $attendee['name'], + 'forceSend' => $attendee['forceSend'], + ]; + } + } + + $messages = []; + + foreach ($attendees as $attendee) { + // An organizer can also be an attendee. We should not generate any + // messages for those. + if ($attendee['href'] === $eventInfo['organizer']) { + continue; + } + + $message = new Message(); + $message->uid = $eventInfo['uid']; + $message->component = 'VEVENT'; + $message->sequence = $eventInfo['sequence']; + $message->sender = $eventInfo['organizer']; + $message->senderName = $eventInfo['organizerName']; + $message->recipient = $attendee['href']; + $message->recipientName = $attendee['name']; + + // Creating the new iCalendar body. + $icalMsg = new VCalendar(); + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + + if (!$attendee['newInstances'] || 'CANCELLED' === $eventInfo['status']) { + // If there are no instances the attendee is a part of, it means + // the attendee was removed and we need to send them a CANCEL message. + // Also If the meeting STATUS property was changed to CANCELLED + // we need to send the attendee a CANCEL message. + $message->method = 'CANCEL'; + + $icalMsg->METHOD = $message->method; + + /** @var VEvent $event */ + $event = $icalMsg->add('VEVENT', [ + 'UID' => $message->uid, + 'SEQUENCE' => $message->sequence, + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]); + if (isset($calendar->VEVENT->SUMMARY)) { + $event->add('SUMMARY', $calendar->VEVENT->SUMMARY->getValue()); + } + $event->add(clone $calendar->VEVENT->DTSTART); + if (isset($calendar->VEVENT->DTEND)) { + $event->add(clone $calendar->VEVENT->DTEND); + } elseif (isset($calendar->VEVENT->DURATION)) { + $event->add(clone $calendar->VEVENT->DURATION); + } + $org = $event->add('ORGANIZER', $eventInfo['organizer']); + if ($eventInfo['organizerName']) { + $org['CN'] = $eventInfo['organizerName']; + } + $event->add('ATTENDEE', $attendee['href'], [ + 'CN' => $attendee['name'], + ]); + $message->significantChange = true; + } else { + // The attendee gets the updated event body + $message->method = 'REQUEST'; + + $icalMsg->METHOD = $message->method; + + // We need to find out that this change is significant. If it's + // not, systems may opt to not send messages. + // + // We do this based on the 'significantChangeHash' which is + // some value that changes if there's a certain set of + // properties changed in the event, or simply if there's a + // difference in instances that the attendee is invited to. + + $oldAttendeeInstances = array_keys($attendee['oldInstances']); + $newAttendeeInstances = array_keys($attendee['newInstances']); + + $message->significantChange = + 'REQUEST' === $attendee['forceSend'] + || count($oldAttendeeInstances) != count($newAttendeeInstances) + || count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 + || $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash']; + + foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) { + $currentEvent = clone $eventInfo['instances'][$instanceId]; + if ('master' === $instanceId) { + // We need to find a list of events that the attendee + // is not a part of to add to the list of exceptions. + $exceptions = []; + foreach ($eventInfo['instances'] as $instanceId => $vevent) { + if (!isset($attendee['newInstances'][$instanceId])) { + $exceptions[] = $instanceId; + } + } + + // If there were exceptions, we need to add it to an + // existing EXDATE property, if it exists. + if ($exceptions) { + if (isset($currentEvent->EXDATE)) { + $currentEvent->EXDATE->setParts(array_merge( + $currentEvent->EXDATE->getParts(), + $exceptions + )); + } else { + $currentEvent->EXDATE = $exceptions; + } + } + + // Cleaning up any scheduling information that + // shouldn't be sent along. + unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']); + unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']); + + foreach ($currentEvent->ATTENDEE as $attendee) { + unset($attendee['SCHEDULE-FORCE-SEND']); + unset($attendee['SCHEDULE-STATUS']); + + // We're adding PARTSTAT=NEEDS-ACTION to ensure that + // iOS shows an "Inbox Item" + if (!isset($attendee['PARTSTAT'])) { + $attendee['PARTSTAT'] = 'NEEDS-ACTION'; + } + } + } + + $currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z'); + $icalMsg->add($currentEvent); + } + } + + $message->message = $icalMsg; + $messages[] = $message; + } + + return $messages; + } + + /** + * Parse an event update for an attendee. + * + * This function figures out if we need to send a reply to an organizer. + * + * @return Message[] + * + * @throws InvalidDataException + */ + protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, string $attendee): array + { + if ($this->scheduleAgentServerRules && 'CLIENT' === $eventInfo['organizerScheduleAgent']) { + return []; + } + + // Don't bother generating messages for events that have already been + // cancelled. + if ('CANCELLED' === $eventInfo['status']) { + return []; + } + + $oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ? + $oldEventInfo['attendees'][$attendee]['instances'] : + []; + + $instances = []; + foreach ($oldInstances as $instance) { + $instances[$instance['id']] = [ + 'id' => $instance['id'], + 'oldstatus' => $instance['partstat'], + 'newstatus' => null, + ]; + } + foreach ($eventInfo['attendees'][$attendee]['instances'] as $instance) { + if (isset($instances[$instance['id']])) { + $instances[$instance['id']]['newstatus'] = $instance['partstat']; + } else { + $instances[$instance['id']] = [ + 'id' => $instance['id'], + 'oldstatus' => null, + 'newstatus' => $instance['partstat'], + ]; + } + } + + // We need to also look for differences in EXDATE. If there are new + // items in EXDATE, it means that an attendee deleted instances of an + // event, which means we need to send DECLINED specifically for those + // instances. + // We only need to do that though, if the master event is not declined. + if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) { + foreach ($eventInfo['exdate'] as $exDate) { + if (!in_array($exDate, $oldEventInfo['exdate'])) { + if (isset($instances[$exDate])) { + $instances[$exDate]['newstatus'] = 'DECLINED'; + } else { + $instances[$exDate] = [ + 'id' => $exDate, + 'oldstatus' => null, + 'newstatus' => 'DECLINED', + ]; + } + } + } + } + + // Gathering a few extra properties for each instance. + foreach ($instances as $recurId => $instanceInfo) { + if (isset($eventInfo['instances'][$recurId])) { + $instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART; + } else { + $instances[$recurId]['dtstart'] = $recurId; + } + } + + $message = new Message(); + $message->uid = $eventInfo['uid']; + $message->method = 'REPLY'; + $message->component = 'VEVENT'; + $message->sequence = $eventInfo['sequence']; + $message->sender = $attendee; + $message->senderName = $eventInfo['attendees'][$attendee]['name']; + $message->recipient = $eventInfo['organizer']; + $message->recipientName = $eventInfo['organizerName']; + + $icalMsg = new VCalendar(); + $icalMsg->METHOD = 'REPLY'; + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + + $hasReply = false; + + foreach ($instances as $instance) { + if ($instance['oldstatus'] == $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) { + // Skip + continue; + } + + /** @var VEvent $event */ + $event = $icalMsg->add('VEVENT', [ + 'UID' => $message->uid, + 'SEQUENCE' => $message->sequence, + ]); + $summary = isset($calendar->VEVENT->SUMMARY) ? $calendar->VEVENT->SUMMARY->getValue() : ''; + // Adding properties from the correct source instance + if (isset($eventInfo['instances'][$instance['id']])) { + $instanceObj = $eventInfo['instances'][$instance['id']]; + $event->add(clone $instanceObj->DTSTART); + if (isset($instanceObj->DTEND)) { + $event->add(clone $instanceObj->DTEND); + } elseif (isset($instanceObj->DURATION)) { + $event->add(clone $instanceObj->DURATION); + } + if (isset($instanceObj->SUMMARY)) { + $event->add('SUMMARY', $instanceObj->SUMMARY->getValue()); + } elseif ($summary) { + $event->add('SUMMARY', $summary); + } + } else { + // This branch of the code is reached, when a reply is + // generated for an instance of a recurring event, through the + // fact that the instance has disappeared by showing up in + // EXDATE + $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); + // Treat is as a DATE field + if (strlen($instance['id']) <= 8) { + $event->add('DTSTART', $dt, ['VALUE' => 'DATE']); + } else { + $event->add('DTSTART', $dt); + } + if ($summary) { + $event->add('SUMMARY', $summary); + } + } + if ('master' !== $instance['id']) { + $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); + // Treat is as a DATE field + if (strlen($instance['id']) <= 8) { + $event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']); + } else { + $event->add('RECURRENCE-ID', $dt); + } + } + $organizer = $event->add('ORGANIZER', $message->recipient); + if ($message->recipientName) { + $organizer['CN'] = $message->recipientName; + } + $attendee = $event->add('ATTENDEE', $message->sender, [ + 'PARTSTAT' => $instance['newstatus'], + ]); + if ($message->senderName) { + $attendee['CN'] = $message->senderName; + } + $hasReply = true; + } + + if ($hasReply) { + $message->message = $icalMsg; + + return [$message]; + } else { + return []; + } + } + + /** + * Returns attendee information and information about instances of an + * event. + * + * Returns an array with the following keys: + * + * 1. uid + * 2. organizer + * 3. organizerName + * 4. organizerScheduleAgent + * 5. organizerForceSend + * 6. instances + * 7. attendees + * 8. sequence + * 9. exdate + * 10. timezone - strictly the timezone on which the recurrence rule is + * based on. + * 11. significantChangeHash + * 12. status + * + * @throws ITipException + * @throws SameOrganizerForAllComponentsException + */ + protected function parseEventInfo(VCalendar $calendar): array + { + $uid = null; + $organizer = null; + $organizerName = null; + $organizerForceSend = null; + $sequence = null; + $timezone = null; + $status = null; + $organizerScheduleAgent = 'SERVER'; + + // Now we need to collect a list of attendees, and which instances they + // are a part of. + $attendees = []; + + $instances = []; + $exdate = []; + + $significantChangeEventProperties = []; + + foreach ($calendar->VEVENT as $vevent) { + $eventSignificantChangeHash = ''; + $rrule = []; + + if (is_null($uid)) { + $uid = $vevent->UID->getValue(); + } else { + if ($uid !== $vevent->UID->getValue()) { + throw new ITipException('If a calendar contained more than one event, they must have the same UID.'); + } + } + + if (!isset($vevent->DTSTART)) { + throw new ITipException('An event MUST have a DTSTART property.'); + } + + if (isset($vevent->ORGANIZER)) { + if (is_null($organizer)) { + $organizer = $vevent->ORGANIZER->getNormalizedValue(); + $organizerName = $vevent->ORGANIZER['CN'] ?? null; + } else { + if (strtoupper($organizer) !== strtoupper($vevent->ORGANIZER->getNormalizedValue())) { + throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.'); + } + } + $organizerForceSend = + isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ? + strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) : + null; + $organizerScheduleAgent = + isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ? + strtoupper((string) $vevent->ORGANIZER['SCHEDULE-AGENT']) : + 'SERVER'; + } + if (is_null($sequence) && isset($vevent->SEQUENCE)) { + $sequence = $vevent->SEQUENCE->getValue(); + } + if (isset($vevent->EXDATE)) { + foreach ($vevent->select('EXDATE') as $val) { + $exdate = array_merge($exdate, $val->getParts()); + } + sort($exdate); + } + if (isset($vevent->RRULE)) { + foreach ($vevent->select('RRULE') as $rr) { + foreach ($rr->getParts() as $key => $val) { + // ignore default values (https://github.com/sabre-io/vobject/issues/126) + if ('INTERVAL' === $key && 1 == $val) { + continue; + } + if (is_array($val)) { + $val = implode(',', $val); + } + $rrule[] = "$key=$val"; + } + } + sort($rrule); + } + if (isset($vevent->STATUS)) { + $status = strtoupper($vevent->STATUS->getValue()); + } + + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + if (is_null($timezone)) { + if ('master' === $recurId) { + $timezone = $vevent->DTSTART->getDateTime()->getTimeZone(); + } else { + $timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone(); + } + } + if (isset($vevent->ATTENDEE)) { + foreach ($vevent->ATTENDEE as $attendee) { + if ($this->scheduleAgentServerRules + && isset($attendee['SCHEDULE-AGENT']) + && 'CLIENT' === strtoupper($attendee['SCHEDULE-AGENT']->getValue()) + ) { + continue; + } + $partStat = + isset($attendee['PARTSTAT']) ? + strtoupper($attendee['PARTSTAT']) : + 'NEEDS-ACTION'; + + $forceSend = + isset($attendee['SCHEDULE-FORCE-SEND']) ? + strtoupper($attendee['SCHEDULE-FORCE-SEND']) : + null; + + if (isset($attendees[$attendee->getNormalizedValue()])) { + $attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = [ + 'id' => $recurId, + 'partstat' => $partStat, + 'forceSend' => $forceSend, + ]; + } else { + $attendees[$attendee->getNormalizedValue()] = [ + 'href' => $attendee->getNormalizedValue(), + 'instances' => [ + $recurId => [ + 'id' => $recurId, + 'partstat' => $partStat, + ], + ], + 'name' => isset($attendee['CN']) ? (string) $attendee['CN'] : null, + 'forceSend' => $forceSend, + ]; + } + } + $instances[$recurId] = $vevent; + } + + foreach ($this->significantChangeProperties as $prop) { + if (isset($vevent->$prop)) { + $propertyValues = $vevent->select($prop); + + $eventSignificantChangeHash .= $prop.':'; + + if ('EXDATE' === $prop) { + $eventSignificantChangeHash .= implode(',', $exdate).';'; + } elseif ('RRULE' === $prop) { + $eventSignificantChangeHash .= implode(',', $rrule).';'; + } else { + foreach ($propertyValues as $val) { + $eventSignificantChangeHash .= $val->getValue().';'; + } + } + } + } + $significantChangeEventProperties[] = $eventSignificantChangeHash; + } + + asort($significantChangeEventProperties); + + $significantChangeHash = implode('', $significantChangeEventProperties); + $significantChangeHash = md5($significantChangeHash); + + return compact( + 'uid', + 'organizer', + 'organizerName', + 'organizerScheduleAgent', + 'organizerForceSend', + 'instances', + 'attendees', + 'sequence', + 'exdate', + 'timezone', + 'significantChangeHash', + 'status' + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/ITipException.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/ITipException.php new file mode 100644 index 0000000000..c949cdb7c6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/ITipException.php @@ -0,0 +1,14 @@ +scheduleStatus) { + return false; + } else { + list($scheduleStatus) = explode(';', $this->scheduleStatus); + + return $scheduleStatus; + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/SameOrganizerForAllComponentsException.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/SameOrganizerForAllComponentsException.php new file mode 100644 index 0000000000..4c48625be5 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ITip/SameOrganizerForAllComponentsException.php @@ -0,0 +1,18 @@ +parent = null; + $this->root = null; + } + + /* {{{ IteratorAggregator interface */ + + /** + * Returns the iterator for this object. + */ + #[\ReturnTypeWillChange] + public function getIterator(): ?ElementList + { + if (!is_null($this->iterator)) { + return $this->iterator; + } + + return new ElementList([$this]); + } + + /** + * Sets the overridden iterator. + * + * Note that this is not actually part of the iterator interface + */ + public function setIterator(ElementList $iterator): void + { + $this->iterator = $iterator; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + */ + public function validate(int $options = 0): array + { + return []; + } + + /* }}} */ + + /* {{{ Countable interface */ + + /** + * Returns the number of elements. + */ + #[\ReturnTypeWillChange] + public function count(): int + { + $it = $this->getIterator(); + + return $it->count(); + } + + /* }}} */ + + /* {{{ ArrayAccess Interface */ + + /** + * Checks if an item exists through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset): bool + { + $iterator = $this->getIterator(); + + return $iterator->offsetExists($offset); + } + + /** + * Gets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + $iterator = $this->getIterator(); + + return $iterator->offsetGet($offset); + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value): void + { + $iterator = $this->getIterator(); + $iterator->offsetSet($offset, $value); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + + // @codeCoverageIgnoreEnd + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset): void + { + $iterator = $this->getIterator(); + $iterator->offsetUnset($offset); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + + // @codeCoverageIgnoreEnd + + /* }}} */ +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/PHPUnitAssertions.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/PHPUnitAssertions.php new file mode 100644 index 0000000000..1976c3afd8 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/PHPUnitAssertions.php @@ -0,0 +1,74 @@ +fail('Input must be a string, stream or VObject component'); + } + unset($input->PRODID); + if ($input instanceof Component\VCalendar && 'GREGORIAN' === (string) $input->CALSCALE) { + unset($input->CALSCALE); + } + + return $input; + }; + + $expected = $getObj($expected)->serialize(); + $actual = $getObj($actual)->serialize(); + + // Finding wildcards in expected. + preg_match_all('|^([A-Z]+):\\*\\*ANY\\*\\*\r$|m', $expected, $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + $actual = preg_replace( + '|^'.preg_quote($match[1], '|').':(.*)\r$|m', + $match[1].':**ANY**'."\r", + $actual + ); + } + + $this->assertEquals( + $expected, + $actual, + $message + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parameter.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parameter.php new file mode 100644 index 0000000000..c5217f1dc7 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parameter.php @@ -0,0 +1,344 @@ +root = $root; + if (is_null($name)) { + $this->noName = true; + $this->name = static::guessParameterNameByValue($value); + } else { + $this->name = strtoupper($name); + } + + // If guessParameterNameByValue() returns an empty string + // above, we're actually dealing with a parameter that has no value. + // In that case we have to move the value to the name. + if ('' === $this->name) { + $this->noName = false; + $this->name = strtoupper($value); + } else { + $this->setValue($value); + } + } + + /** + * Try to guess property name by value, can be used for vCard 2.1 nameless parameters. + * + * Figuring out what the name should have been. Note that a ton of + * these are rather silly in 2014 and would probably rarely be + * used, but we like to be complete. + */ + public static function guessParameterNameByValue(string $value): string + { + switch (strtoupper($value)) { + // Encodings + case '7-BIT': + case 'QUOTED-PRINTABLE': + case 'BASE64': + $name = 'ENCODING'; + break; + + // Common types + case 'WORK': + case 'HOME': + case 'PREF': + // Delivery Label Type + case 'DOM': + case 'INTL': + case 'POSTAL': + case 'PARCEL': + // Telephone types + case 'VOICE': + case 'FAX': + case 'MSG': + case 'CELL': + case 'PAGER': + case 'BBS': + case 'MODEM': + case 'CAR': + case 'ISDN': + case 'VIDEO': + // EMAIL types (lol) + case 'AOL': + case 'APPLELINK': + case 'ATTMAIL': + case 'CIS': + case 'EWORLD': + case 'INTERNET': + case 'IBMMAIL': + case 'MCIMAIL': + case 'POWERSHARE': + case 'PRODIGY': + case 'TLX': + case 'X400': + // Photo / Logo format types + case 'GIF': + case 'CGM': + case 'WMF': + case 'BMP': + case 'DIB': + case 'PICT': + case 'TIFF': + case 'PDF': + case 'PS': + case 'JPEG': + case 'MPEG': + case 'MPEG2': + case 'AVI': + case 'QTIME': + // Sound Digital Audio Type + case 'WAVE': + case 'PCM': + case 'AIFF': + // Key types + case 'X509': + case 'PGP': + $name = 'TYPE'; + break; + + // Value types + case 'INLINE': + case 'URL': + case 'CONTENT-ID': + case 'CID': + $name = 'VALUE'; + break; + + default: + $name = ''; + } + + return $name; + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + */ + public function setValue($value): void + { + $this->value = $value; + } + + /** + * Returns the current value. + * + * This method will always return a string, or null. If there were multiple + * values, it will automatically concatenate them (separated by comma). + */ + public function getValue(): ?string + { + if (is_array($this->value)) { + return implode(',', $this->value); + } else { + return $this->value; + } + } + + /** + * Sets multiple values for this parameter. + */ + public function setParts(array $value): void + { + $this->value = $value; + } + + /** + * Returns all values for this parameter. + * + * If there were no values, an empty array will be returned. + */ + public function getParts(): array + { + if (is_array($this->value)) { + return $this->value; + } elseif (is_null($this->value)) { + return []; + } else { + return [$this->value]; + } + } + + /** + * Adds a value to this parameter. + * + * If the argument is specified as an array, all items will be added to the + * parameter value list. + * + * @param string|array $part + */ + public function addValue($part): void + { + if (is_null($this->value)) { + $this->value = $part; + } else { + $this->value = array_merge((array) $this->value, (array) $part); + } + } + + /** + * Checks if this parameter contains the specified value. + * + * This is a case-insensitive match. It makes sense to call this for + * the TYPE parameter, for instance, to see if it contains a keyword such as + * 'WORK' or 'FAX'. + */ + public function has(string $value): bool + { + return in_array( + strtolower($value), + array_map('strtolower', (array) $this->value) + ); + } + + /** + * Turns the object back into a serialized blob. + */ + public function serialize(): string + { + $value = $this->getParts(); + + if (0 === count($value)) { + return $this->name.'='; + } + + if (Document::VCARD21 === $this->root->getDocumentType() && $this->noName) { + return implode(';', $value); + } + + return $this->name.'='.array_reduce( + $value, + function ($out, $item) { + if (!is_null($out)) { + $out .= ','; + } + + // If there's no special characters in the string, we'll use the simple + // format. + // + // The list of special characters is defined as: + // + // Any character except CONTROL, DQUOTE, ";", ":", "," + // + // by the iCalendar spec: + // https://tools.ietf.org/html/rfc5545#section-3.1 + // + // And we add ^ to that because of: + // https://tools.ietf.org/html/rfc6868 + // + // But we've found that iCal (7.0, shipped with OSX 10.9) + // severely trips on + characters not being quoted, so we + // added + as well. + if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) { + return $out.$item; + } else { + // Enclosing in double-quotes, and using RFC6868 for encoding any + // special characters + $out .= '"'.strtr( + $item, + [ + '^' => '^^', + "\n" => '^n', + '"' => '^\'', + ] + ).'"'; + + return $out; + } + } + ); + } + + /** + * This method returns an array, with the representation as it should be + * encoded in JSON. This is used to create jCard or jCal documents. + * + * @return array|string|null + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->value; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer): void + { + foreach (is_array($this->value) ? $this->value : explode(',', $this->value) as $value) { + $writer->writeElement('text', $value); + } + } + + /** + * Called when this object is being cast to a string. + */ + public function __toString(): string + { + return (string) $this->getValue(); + } + + /** + * Returns the iterator for this object. + */ + #[\ReturnTypeWillChange] + public function getIterator(): ElementList + { + if (!is_null($this->iterator)) { + return $this->iterator; + } + + return $this->iterator = new ElementList((array) $this->value); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ParseException.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ParseException.php new file mode 100644 index 0000000000..a8f497b246 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/ParseException.php @@ -0,0 +1,14 @@ +setInput($input); + } + if (is_null($this->input)) { + throw new EofException('End of input stream, or no input supplied'); + } + + if (0 !== $options) { + $this->options = $options; + } + + switch ($this->input[0]) { + case 'vcalendar': + $this->root = new VCalendar([], false); + break; + case 'vcard': + $this->root = new VCard([], false); + break; + default: + throw new ParseException('The root component must either be a vcalendar, or a vcard'); + } + foreach ($this->input[1] as $prop) { + $this->root->add($this->parseProperty($prop)); + } + if (isset($this->input[2])) { + foreach ($this->input[2] as $comp) { + $this->root->add($this->parseComponent($comp)); + } + } + + // Resetting the input so that we can throw an feof exception the next time. + $this->input = null; + + return $this->root; + } + + /** + * Parses a component. + * + * @throws InvalidDataException + */ + public function parseComponent(array $jComp): Component + { + // We can remove $self from PHP 5.4 onward. + $self = $this; + + $properties = array_map( + function ($jProp) use ($self) { + return $self->parseProperty($jProp); + }, + $jComp[1] + ); + + if (isset($jComp[2])) { + $components = array_map( + function ($jComp) use ($self) { + return $self->parseComponent($jComp); + }, + $jComp[2] + ); + } else { + $components = []; + } + + return $this->root->createComponent( + $jComp[0], + array_merge($properties, $components), + false + ); + } + + /** + * Parses properties. + * + * @throws InvalidDataException + */ + public function parseProperty(array $jProp): Property + { + list( + $propertyName, + $parameters, + $valueType, + ) = $jProp; + + $propertyName = strtoupper($propertyName); + + // This is the default class we would be using if we didn't know the + // value type. We're using this value later in this function. + $defaultPropertyClass = $this->root->getClassNameForPropertyName($propertyName); + + $parameters = (array) $parameters; + + $value = array_slice($jProp, 3); + + $valueType = strtoupper($valueType); + + if (isset($parameters['group'])) { + $propertyName = $parameters['group'].'.'.$propertyName; + unset($parameters['group']); + } + + if ('X-CRYPTO' === $propertyName) { + $propertyType = 'X-CRYPTO'; + } + + $prop = $this->root->createProperty($propertyName, null, $parameters, $valueType); + $prop->setJsonValue($value); + + // We have to do something awkward here. FlatText as well as Text + // represents TEXT values. We have to normalize these here. In the + // future we can get rid of FlatText once we're allowed to break BC + // again. + if (FlatText::class === $defaultPropertyClass) { + $defaultPropertyClass = Text::class; + } + + // If the value type we received (e.g.: TEXT) was not the default value + // type for the given property (e.g.: BDAY), we need to add a VALUE= + // parameter. + if ($defaultPropertyClass !== get_class($prop)) { + $prop['VALUE'] = $valueType; + } + + return $prop; + } + + /** + * Sets the input data. + * + * @param resource|string|array $input + */ + public function setInput($input): void + { + if (is_resource($input)) { + $input = stream_get_contents($input); + } + if (is_string($input)) { + $input = json_decode($input); + } + $this->input = $input; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/MimeDir.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/MimeDir.php new file mode 100644 index 0000000000..be5d87baf1 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/MimeDir.php @@ -0,0 +1,684 @@ +root = null; + + if (!\is_null($input)) { + $this->setInput($input); + } + + if (!\is_resource($this->input)) { + // Null was passed as input, but there was no existing input buffer + // There is nothing to parse. + throw new ParseException('No input provided to parse'); + } + + if (0 !== $options) { + $this->options = $options; + } + + $this->parseDocument(); + + return $this->root; + } + + /** + * By default, all input will be assumed to be UTF-8. + * + * However, both iCalendar and vCard might be encoded using different + * character sets. The character set is usually set in the mime-type. + * + * If this is the case, use setEncoding to specify that a different + * encoding will be used. If this is set, the parser will automatically + * convert all incoming data to UTF-8. + */ + public function setCharset(string $charset): void + { + if (!in_array($charset, self::$SUPPORTED_CHARSETS)) { + throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')'); + } + $this->charset = $charset; + } + + /** + * Sets the input buffer. Must be a string or stream. + * + * @param resource|string $input + * + * @return void + */ + public function setInput($input) + { + // Resetting the parser + $this->lineIndex = 0; + $this->startLine = 0; + + if (is_string($input)) { + // Converting to a stream. + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $input); + rewind($stream); + $this->input = $stream; + } elseif (is_resource($input)) { + $this->input = $input; + } else { + throw new \InvalidArgumentException('This parser can only read from strings or streams.'); + } + } + + /** + * Parses an entire document. + * + * @throws EofException + * @throws ParseException + */ + protected function parseDocument(): void + { + $line = $this->readLine(); + + // BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF). + // It's 0xEF 0xBB 0xBF in UTF-8 hex. + if (3 <= strlen($line) + && 0xEF === ord($line[0]) + && 0xBB === ord($line[1]) + && 0xBF === ord($line[2])) { + $line = \substr($line, 3); + } + + switch (strtoupper($line)) { + case 'BEGIN:VCALENDAR': + $class = VCalendar::$componentMap['VCALENDAR']; + break; + case 'BEGIN:VCARD': + $class = VCard::$componentMap['VCARD']; + break; + default: + throw new ParseException('This parser only supports VCARD and VCALENDAR files'); + } + + $this->root = new $class([], false); + + while (true) { + // Reading until we hit END: + try { + $line = $this->readLine(); + } catch (EofException $oEx) { + $line = 'END:'.$this->root->name; + } + if ('END:' === strtoupper(\substr($line, 0, 4))) { + break; + } + $result = $this->parseLine($line); + if ($result) { + $this->root->add($result); + } + } + + $name = strtoupper(\substr($line, 4)); + if ($name !== $this->root->name) { + throw new ParseException('Invalid MimeDir file. expected: "END:'.$this->root->name.'" got: "END:'.$name.'"'); + } + } + + /** + * Parses a line, and if it hits a component, it will also attempt to parse + * the entire component. + * + * @param string $line Unfolded line + * + * @return Node|Property|false + * + * @throws EofException + * @throws ParseException + */ + protected function parseLine(string $line) + { + // Start of a new component + if ('BEGIN:' === strtoupper(\substr($line, 0, 6))) { + if (\substr($line, 6) === $this->root->name) { + throw new ParseException('Invalid MimeDir file. Unexpected component: "'.$line.'" in document type '.$this->root->name); + } + $component = $this->root->createComponent(\substr($line, 6), [], false); + + while (true) { + // Reading until we hit END: + $line = $this->readLine(); + if ('END:' === strtoupper(\substr($line, 0, 4))) { + break; + } + $result = $this->parseLine($line); + if ($result) { + $component->add($result); + } + } + + $name = strtoupper(\substr($line, 4)); + if ($name !== $component->name) { + throw new ParseException('Invalid MimeDir file. expected: "END:'.$component->name.'" got: "END:'.$name.'"'); + } + + return $component; + } else { + // Property reader + $property = $this->readProperty($line); + if (!$property) { + // Ignored line + return false; + } + + return $property; + } + } + + /** + * We need to look ahead 1 line every time to see if we need to 'unfold' + * the next line. + * + * If that was not the case, we store it here. + */ + protected ?string $lineBuffer = null; + + /** + * The real current line number. + */ + protected int $lineIndex = 0; + + /** + * In the case of unfolded lines, this property holds the line number for + * the start of the line. + */ + protected int $startLine = 0; + + /** + * Contains a 'raw' representation of the current line. + */ + protected string $rawLine; + + /** + * Reads a single line from the buffer. + * + * This method strips any newlines and also takes care of unfolding. + * + * @throws EofException|ParseException + */ + protected function readLine(): ?string + { + if (!\is_null($this->lineBuffer)) { + $rawLine = $this->lineBuffer; + $this->lineBuffer = null; + } else { + do { + $eof = \feof($this->input); + + $rawLine = \fgets($this->input); + + if ($eof || (\feof($this->input) && false === $rawLine)) { + throw new EofException('End of document reached prematurely'); + } + if (false === $rawLine) { + throw new ParseException('Error reading from input stream'); + } + $rawLine = \rtrim($rawLine, "\r\n"); + } while ('' === $rawLine); // Skipping empty lines + ++$this->lineIndex; + } + $line = $rawLine; + + $this->startLine = $this->lineIndex; + + // Looking ahead for folded lines. + while (true) { + $nextLine = \rtrim(\fgets($this->input), "\r\n"); + ++$this->lineIndex; + if (!$nextLine) { + break; + } + if ("\t" === $nextLine[0] || ' ' === $nextLine[0]) { + $curLine = \substr($nextLine, 1); + $line .= $curLine; + $rawLine .= "\n ".$curLine; + } else { + $this->lineBuffer = $nextLine; + break; + } + } + $this->rawLine = $rawLine; + + return $line; + } + + /** + * Reads a property or component from a line. + * + * @return Property|false + * + * @throws ParseException|InvalidDataException + */ + protected function readProperty(string $line) + { + if ($this->options & self::OPTION_FORGIVING) { + $propNameToken = 'A-Z0-9\-\._\\/'; + } else { + $propNameToken = 'A-Z0-9\-\.'; + } + + $paramNameToken = 'A-Z0-9\-'; + $safeChar = '^";:,'; + $qSafeChar = '^"'; + + $regex = "/ + ^(?P [$propNameToken]+ ) (?=[;:]) # property name + | + (?<=:)(?P .+)$ # property value + | + ;(?P [$paramNameToken]+) (?=[=;:]) # parameter name + | + (=|,)(?P # parameter value + (?: [$safeChar]*) | + \"(?: [$qSafeChar]+)\" + ) (?=[;:,]) + /xi"; + + // echo $regex, "\n"; exit(); + preg_match_all($regex, $line, $matches, PREG_SET_ORDER); + + $property = [ + 'name' => null, + 'parameters' => [], + 'value' => null, + ]; + + $lastParam = null; + + /* + * Looping through all the tokens. + * + * Note that we are looping through them in reverse order, because if a + * sub-pattern matched, the subsequent named patterns will not show up + * in the result. + */ + foreach ($matches as $match) { + if (isset($match['paramValue'])) { + if ($match['paramValue'] && '"' === $match['paramValue'][0]) { + $value = \substr($match['paramValue'], 1, -1); + } else { + $value = $match['paramValue']; + } + + $value = $this->unescapeParam($value); + + if (\is_null($lastParam)) { + if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { + // When the property can't be matched and the configuration + // option is set to ignore invalid lines, we ignore this line + // This can happen when servers provide faulty data as iCloud + // frequently does with X-APPLE-STRUCTURED-LOCATION + continue; + } + throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions'); + } + if (\is_null($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam] = $value; + } elseif (is_array($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam][] = $value; + } elseif ($property['parameters'][$lastParam] === $value) { + // When the current value of the parameter is the same as the + // new one, then we can leave the current parameter as it is. + } else { + $property['parameters'][$lastParam] = [ + $property['parameters'][$lastParam], + $value, + ]; + } + continue; + } + if (isset($match['paramName'])) { + $lastParam = strtoupper($match['paramName']); + if (!isset($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam] = null; + } + continue; + } + if (isset($match['propValue'])) { + $property['value'] = $match['propValue']; + continue; + } + if (isset($match['name']) && $match['name']) { + $property['name'] = strtoupper($match['name']); + continue; + } + + // @codeCoverageIgnoreStart + throw new \LogicException('This code should not be reachable'); + // @codeCoverageIgnoreEnd + } + + if (\is_null($property['value'])) { + $property['value'] = ''; + } + if (!$property['name']) { + if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { + return false; + } + throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions'); + } + + // vCard 2.1 states that parameters may appear without a name, and only + // a value. We can deduce the value based on its name. + // + // Our parser will get those as parameters without a value instead, so + // we're filtering these parameters out first. + $namedParameters = []; + $namelessParameters = []; + + foreach ($property['parameters'] as $name => $value) { + if (!\is_null($value)) { + $namedParameters[$name] = $value; + } else { + $namelessParameters[] = $name; + } + } + + $propObj = $this->root->createProperty($property['name'], null, $namedParameters, null, $this->startLine, $line); + + foreach ($namelessParameters as $namelessParameter) { + $propObj->add(null, $namelessParameter); + } + + if (isset($propObj['ENCODING']) && 'QUOTED-PRINTABLE' === strtoupper($propObj['ENCODING'])) { + /* @var Property\Text|Property\FlatText $propObj */ + $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue()); + } else { + $charset = $this->charset; + if (Document::VCARD21 === $this->root->getDocumentType() && isset($propObj['CHARSET'])) { + // vCard 2.1 allows the character set to be specified per property. + $charset = (string) $propObj['CHARSET']; + } + switch (strtolower($charset)) { + case 'utf-8': + break; + case 'windows-1252': + case 'iso-8859-1': + $property['value'] = mb_convert_encoding($property['value'], 'UTF-8', $charset); + break; + default: + throw new ParseException('Unsupported CHARSET: '.$propObj['CHARSET']); + } + $propObj->setRawMimeDirValue($property['value']); + } + + return $propObj; + } + + /** + * Unescapes a property value. + * + * vCard 2.1 says: + * * Semi-colons must be escaped in some property values, specifically + * ADR, ORG and N. + * * Semi-colons must be escaped in parameter values, because semicolons + * are also use to separate values. + * * No mention of escaping backslashes with another backslash. + * * newlines are not escaped either, instead QUOTED-PRINTABLE is used to + * span values over more than 1 line. + * + * vCard 3.0 says: + * * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be + * escaped, all the time. + * * Commas are used for delimiters in multiple values + * * (rfc2426) Adds to this that the semicolon MUST also be escaped, + * as in some properties semicolon is used for separators. + * * Properties using semi-colons: N, ADR, GEO, ORG + * * Both ADR and N's individual parts may be broken up further with a + * comma. + * * Properties using commas: NICKNAME, CATEGORIES + * + * vCard 4.0 (rfc6350) says: + * * Commas must be escaped. + * * Semi-colons may be escaped, an unescaped semicolon _may_ be a + * delimiter, depending on the property. + * * Backslashes must be escaped + * * Newlines must be escaped as either \N or \n. + * * Some compound properties may contain multiple parts themselves, so a + * comma within a semicolon delimited property may also be unescaped + * to denote multiple parts _within_ the compound property. + * * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP. + * * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID. + * + * Even though the spec says that commas must always be escaped, the + * example for GEO in Section 6.5.2 seems to violate this. + * + * iCalendar 2.0 (rfc5545) says: + * * Commas or semicolons may be used as delimiters, depending on the + * property. + * * Commas, semi-colons, backslashes, newline (\N or \n) are always + * escaped, unless they are delimiters. + * * Colons shall not be escaped. + * * Commas can be considered the 'default delimiter' and is described as + * the delimiter in cases where the order of the multiple values is + * insignificant. + * * Semi-colons are described as the delimiter for 'structured values'. + * They are specifically used in Semi-colons are used as a delimiter in + * REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated, however. + * + * Now for the parameters + * + * If delimiter is not set (empty string) this method will just return a string. + * If it's a comma or a semicolon the string will be split on those + * characters, and always return an array. + * + * @return string|string[] + */ + public static function unescapeValue(string $input, string $delimiter = ';') + { + $regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )'; + if ($delimiter) { + $regex .= ' | ('.$delimiter.')'; + } + $regex .= ') #x'; + + $matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + $resultArray = []; + $result = ''; + + foreach ($matches as $match) { + switch ($match) { + case '\\\\': + $result .= '\\'; + break; + case '\N': + case '\n': + $result .= "\n"; + break; + case '\;': + $result .= ';'; + break; + case '\,': + $result .= ','; + break; + case $delimiter: + $resultArray[] = $result; + $result = ''; + break; + default: + $result .= $match; + break; + } + } + + $resultArray[] = $result; + + return $delimiter ? $resultArray : $result; + } + + /** + * Unescapes a parameter value. + * + * vCard 2.1: + * * Does not mention a mechanism for this. In addition, double quotes + * are never used to wrap values. + * * This means that parameters can simply not contain colons or + * semicolons. + * + * vCard 3.0 (rfc2425, rfc2426): + * * Parameters _may_ be surrounded by double quotes. + * * If this is not the case, semicolon, colon and comma may simply not + * occur (the comma used for multiple parameter values though). + * * If it is surrounded by double-quotes, it may simply not contain + * double-quotes. + * * This means that a parameter can in no case encode double-quotes, or + * newlines. + * + * vCard 4.0 (rfc6350) + * * Behavior seems to be identical to vCard 3.0 + * + * iCalendar 2.0 (rfc5545) + * * Behavior seems to be identical to vCard 3.0 + * + * Parameter escaping mechanism (rfc6868) : + * * This rfc describes a new way to escape parameter values. + * * New-line is encoded as ^n + * * ^ is encoded as ^^. + * * " is encoded as ^' + */ + private function unescapeParam(string $input): ?string + { + return + preg_replace_callback( + '#(\^(\^|n|\'))#', + function ($matches) { + switch ($matches[2]) { + case 'n': + return "\n"; + case '^': + return '^'; + case '\'': + return '"'; + + // @codeCoverageIgnoreStart + } + // @codeCoverageIgnoreEnd + }, + $input + ); + } + + /** + * Gets the full quoted printable value. + * + * We need a special method for this, because newlines have both a meaning + * in vCards, and in QuotedPrintable. + * + * This method does not do any decoding. + * + * @throws EofException + * @throws ParseException + */ + private function extractQuotedPrintableValue(): string + { + // We need to parse the raw line again to get the start of the value. + // + // We are basically looking for the first colon (:), but we need to + // skip over the parameters first, as they may contain one. + $regex = '/^ + (?: [^:])+ # Anything but a colon + (?: "[^"]")* # A parameter in double quotes + : # start of the value we really care about + (.*)$ + /xs'; + + preg_match($regex, $this->rawLine, $matches); + + $value = $matches[1]; + // Removing the first whitespace character from every line. Kind of + // like unfolding, but we keep the newline. + $value = str_replace("\n ", "\n", $value); + + // Microsoft's products don't always correctly fold lines, they may be + // missing a whitespace. So if 'forgiving' is turned on, we will take + // those as well. + if ($this->options & self::OPTION_FORGIVING) { + while ('=' === \substr($value, -1) && $this->lineBuffer) { + // Reading the line + $this->readLine(); + // Grabbing the raw form + $value .= "\n".$this->rawLine; + } + } + + return $value; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/Parser.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/Parser.php new file mode 100644 index 0000000000..29921d1826 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/Parser.php @@ -0,0 +1,71 @@ +setInput($input); + } + $this->options = $options; + } + + /** + * This method starts the parsing process. + * + * If the input was not supplied during construction, it's possible to pass + * it here instead. + * + * If either input or options are not supplied, the defaults will be used. + * + * @param resource|string|array|null $input + */ + abstract public function parse($input = null, int $options = 0): ?Document; + + /** + * Sets the input data. + * + * @param resource|string|array $input + */ + abstract public function setInput($input); +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/XML.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/XML.php new file mode 100644 index 0000000000..43e42e6d5e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/XML.php @@ -0,0 +1,394 @@ +setInput($input); + } + + if (0 !== $options) { + $this->options = $options; + } + + if (is_null($this->input)) { + throw new EofException('End of input stream, or no input supplied'); + } + + switch ($this->input['name']) { + case '{'.self::XCAL_NAMESPACE.'}icalendar': + $this->root = new VCalendar([], false); + $this->pointer = &$this->input['value'][0]; + $this->parseVCalendarComponents($this->root); + break; + + case '{'.self::XCARD_NAMESPACE.'}vcards': + foreach ($this->input['value'] as &$vCard) { + $this->root = new VCard(['version' => '4.0'], false); + $this->pointer = &$vCard; + $this->parseVCardComponents($this->root); + + // We just parse the first element. + break; + } + break; + + default: + throw new ParseException('Unsupported XML standard'); + } + + return $this->root; + } + + /** + * Parse a xCalendar component. + * + * @throws InvalidDataException + */ + protected function parseVCalendarComponents(Component $parentComponent): void + { + foreach ($this->pointer['value'] ?: [] as $children) { + switch (static::getTagName($children['name'])) { + case 'properties': + $this->pointer = &$children['value']; + $this->parseProperties($parentComponent); + break; + + case 'components': + $this->pointer = &$children; + $this->parseComponent($parentComponent); + break; + } + } + } + + /** + * Parse a xCard component. + * + * @throws InvalidDataException + */ + protected function parseVCardComponents(Component $parentComponent): void + { + $this->pointer = &$this->pointer['value']; + $this->parseProperties($parentComponent); + } + + /** + * Parse xCalendar and xCard properties. + * + * @throws InvalidDataException + */ + protected function parseProperties(Component $parentComponent, string $propertyNamePrefix = ''): void + { + foreach ($this->pointer ?: [] as $xmlProperty) { + list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']); + + $propertyName = $tagName; + $propertyValue = []; + $propertyParameters = []; + $propertyType = 'text'; + + // A property which is not part of the standard. + if (self::XCAL_NAMESPACE !== $namespace + && self::XCARD_NAMESPACE !== $namespace) { + $propertyName = 'xml'; + $value = '<'.$tagName.' xmlns="'.$namespace.'"'; + + foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) { + $value .= ' '.$attributeName.'="'.str_replace('"', '\"', $attributeValue).'"'; + } + + $value .= '>'.$xmlProperty['value'].''; + + $propertyValue = [$value]; + + $this->createProperty( + $parentComponent, + $propertyName, + $propertyParameters, + $propertyType, + $propertyValue + ); + + continue; + } + + // xCard group. + if ('group' === $propertyName) { + if (!isset($xmlProperty['attributes']['name'])) { + continue; + } + + $this->pointer = &$xmlProperty['value']; + $this->parseProperties( + $parentComponent, + strtoupper($xmlProperty['attributes']['name']).'.' + ); + + continue; + } + + // Collect parameters. + foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) { + if (!is_array($xmlPropertyChild) + || 'parameters' !== static::getTagName($xmlPropertyChild['name'])) { + continue; + } + + $xmlParameters = $xmlPropertyChild['value']; + + foreach ($xmlParameters as $xmlParameter) { + $propertyParameterValues = []; + + foreach ($xmlParameter['value'] as $xmlParameterValues) { + $propertyParameterValues[] = $xmlParameterValues['value']; + } + + $propertyParameters[static::getTagName($xmlParameter['name'])] + = implode(',', $propertyParameterValues); + } + + array_splice($xmlProperty['value'], $i, 1); + } + + $propertyNameExtended = ($this->root instanceof VCalendar + ? 'xcal' + : 'xcard').':'.$propertyName; + + switch ($propertyNameExtended) { + case 'xcal:geo': + $propertyType = 'float'; + $propertyValue['latitude'] = 0; + $propertyValue['longitude'] = 0; + + foreach ($xmlProperty['value'] as $xmlRequestChild) { + $propertyValue[static::getTagName($xmlRequestChild['name'])] + = $xmlRequestChild['value']; + } + break; + + case 'xcal:request-status': + $propertyType = 'text'; + + foreach ($xmlProperty['value'] as $xmlRequestChild) { + $propertyValue[static::getTagName($xmlRequestChild['name'])] + = $xmlRequestChild['value']; + } + break; + + case 'xcal:freebusy': + $propertyType = 'freebusy'; + // We don't break because we only want to set + // another property type. + + // no break + case 'xcal:categories': + case 'xcal:resources': + case 'xcal:exdate': + foreach ($xmlProperty['value'] as $specialChild) { + $propertyValue[static::getTagName($specialChild['name'])] + = $specialChild['value']; + } + break; + + case 'xcal:rdate': + $propertyType = 'date-time'; + + foreach ($xmlProperty['value'] as $specialChild) { + $tagName = static::getTagName($specialChild['name']); + + if ('period' === $tagName) { + $propertyParameters['value'] = 'PERIOD'; + $propertyValue[] = implode('/', $specialChild['value']); + } else { + $propertyValue[] = $specialChild['value']; + } + } + break; + + case 'xcard:x-crypto': + foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) { + if (is_array($xmlPropertyChild)) { + $propertyParameterValues = []; + foreach ($xmlPropertyChild['value'] as $xmlParameter) { + if (is_array($xmlParameter['value'])) { + foreach ($xmlParameter['value'] as $xmlParameterValues) { + $propertyParameterValues[] = $xmlParameterValues['value']; + } + } else { + $propertyParameterValues[] = $xmlParameter['value']; + } + } + $propertyParameters[static::getTagName($xmlPropertyChild['name'])] + = implode(',', $propertyParameterValues); + } + array_splice($xmlProperty['value'], $i, 1); + } + $propertyType = 'X-CRYPTO'; + break; + + default: + $propertyType = static::getTagName($xmlProperty['value'][0]['name']); + + foreach ($xmlProperty['value'] as $value) { + $propertyValue[] = $value['value']; + } + + if ('date' === $propertyType) { + $propertyParameters['value'] = 'DATE'; + } + break; + } + + $this->createProperty( + $parentComponent, + $propertyNamePrefix.$propertyName, + $propertyParameters, + $propertyType, + $propertyValue + ); + } + } + + /** + * Parse a component. + * + * @throws InvalidDataException + */ + protected function parseComponent(Component $parentComponent): void + { + $components = $this->pointer['value'] ?: []; + + foreach ($components as $component) { + $componentName = static::getTagName($component['name']); + $currentComponent = $this->root->createComponent( + $componentName, + null, + false + ); + + $this->pointer = &$component; + $this->parseVCalendarComponents($currentComponent); + + $parentComponent->add($currentComponent); + } + } + + /** + * Create a property. + * + * @throws InvalidDataException + */ + protected function createProperty(Component $parentComponent, string $name, array $parameters, string $type, $value): void + { + $property = $this->root->createProperty( + $name, + null, + $parameters, + $type + ); + $parentComponent->add($property); + $property->setXmlValue($value); + } + + /** + * Sets the input data. + * + * @param resource|string|array $input + * + * @throws SabreXml\LibXMLException + */ + public function setInput($input): void + { + if (is_resource($input)) { + $input = stream_get_contents($input); + } + + if (is_string($input)) { + $reader = new SabreXml\Reader(); + $reader->elementMap['{'.self::XCAL_NAMESPACE.'}period'] + = XML\Element\KeyValue::class; + $reader->elementMap['{'.self::XCAL_NAMESPACE.'}recur'] + = XML\Element\KeyValue::class; + $reader->xml($input); + $input = $reader->parse(); + } + + $this->input = $input; + } + + /** + * Get tag name from a Clark notation. + */ + protected static function getTagName(string $clarkedTagName): string + { + list(, $tagName) = SabreXml\Service::parseClarkNotation($clarkedTagName); + + return $tagName; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/XML/Element/KeyValue.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/XML/Element/KeyValue.php new file mode 100644 index 0000000000..207f150b22 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Parser/XML/Element/KeyValue.php @@ -0,0 +1,61 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(SabreXml\Reader $reader): array + { + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + $values = []; + $reader->read(); + + do { + if (SabreXml\Reader::ELEMENT === $reader->nodeType) { + $name = $reader->localName; + $values[$name] = $reader->parseCurrentElement()['value']; + } else { + $reader->read(); + } + } while (SabreXml\Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property.php new file mode 100644 index 0000000000..f4d0bdc0b2 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property.php @@ -0,0 +1,607 @@ +value syntax. + * + * @param Component $root The root document + * @param string|array|null $value + * @param array $parameters List of parameters + * @param string|null $group The vcard property group + */ + public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], ?string $group = null, ?int $lineIndex = null, ?string $lineString = null) + { + $this->name = $name; + $this->group = $group; + + $this->root = $root; + + foreach ($parameters as $k => $v) { + $this->add($k, $v); + } + + if (!is_null($value)) { + $this->setValue($value); + } + + if (!is_null($lineIndex)) { + $this->lineIndex = $lineIndex; + } + + if (!is_null($lineString)) { + $this->lineString = $lineString; + } + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + */ + public function setValue($value): void + { + $this->value = $value; + } + + /** + * Returns the current value. + * + * This method will always return a singular value. If this was a + * multi-value object, some decision will be made first on how to represent + * it as a string. + * + * To get the correct multi-value version, use getParts. + */ + public function getValue() + { + if (is_array($this->value)) { + if (0 == count($this->value)) { + return null; + } elseif (1 === count($this->value)) { + return $this->value[0]; + } + + return $this->getRawMimeDirValue(); + } + + return $this->value; + } + + /** + * Sets a multi-valued property. + */ + public function setParts(array $parts): void + { + $this->value = $parts; + } + + /** + * Returns a multi-valued property. + * + * This method always returns an array, if there was only a single value, + * it will still be wrapped in an array. + */ + public function getParts(): array + { + if (is_null($this->value)) { + return []; + } elseif (is_array($this->value)) { + return $this->value; + } else { + return [$this->value]; + } + } + + /** + * Adds a new parameter. + * + * If a parameter with same name already existed, the values will be + * combined. + * If nameless parameter is added, we try to guess its name. + * + * @param string|array|null $value + */ + public function add(?string $name, $value = null): void + { + $noName = false; + if (null === $name) { + $name = Parameter::guessParameterNameByValue($value); + $noName = true; + } + + if (isset($this->parameters[strtoupper($name)])) { + $this->parameters[strtoupper($name)]->addValue($value); + } else { + $param = new Parameter($this->root, $name, $value); + $param->noName = $noName; + $this->parameters[$param->name] = $param; + } + } + + /** + * Returns an iterable list of children. + */ + public function parameters(): array + { + return $this->parameters; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + abstract public function getValueType(): string; + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + */ + abstract public function setRawMimeDirValue(string $val): void; + + /** + * Returns a raw mime-dir representation of the value. + */ + abstract public function getRawMimeDirValue(): string; + + /** + * Turns the object back into a serialized blob. + */ + public function serialize(): string + { + $str = $this->name; + if ($this->group) { + $str = $this->group.'.'.$this->name; + } + + foreach ($this->parameters() as $param) { + $str .= ';'.$param->serialize(); + } + + $str .= ':'.$this->getRawMimeDirValue(); + + $str = \preg_replace( + '/( + (?:^.)? # 1 additional byte in first line because of missing single space (see next line) + .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) + (?![\x80-\xbf]) # prevent splitting multibyte characters + )/x', + "$1\r\n ", + $str + ); + + // remove single space after last CRLF + return \substr($str, 0, -1); + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + */ + public function getJsonValue(): array + { + return $this->getParts(); + } + + /** + * Sets the JSON value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value): void + { + if (1 === count($value)) { + $this->setValue(reset($value)); + } else { + $this->setValue($value); + } + } + + /** + * This method returns an array, with the representation as it should be + * encoded in JSON. This is used to create jCard or jCal documents. + */ + #[\ReturnTypeWillChange] + public function jsonSerialize(): array + { + $parameters = []; + + foreach ($this->parameters as $parameter) { + if ('VALUE' === $parameter->name) { + continue; + } + $parameters[strtolower($parameter->name)] = $parameter->jsonSerialize(); + } + // In jCard, we need to encode the property-group as a separate 'group' + // parameter. + if ($this->group) { + $parameters['group'] = $this->group; + } + + return array_merge( + [ + strtolower($this->name), + (object) $parameters, + strtolower($this->getValueType()), + ], + $this->getJsonValue() + ); + } + + /** + * Hydrate data from an XML subtree, as it would appear in a xCard or xCal + * object. + * + * @throws InvalidDataException + */ + public function setXmlValue(array $value): void + { + $this->setJsonValue($value); + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $parameters = []; + + foreach ($this->parameters as $parameter) { + if ('VALUE' === $parameter->name) { + continue; + } + + $parameters[] = $parameter; + } + + $writer->startElement(strtolower($this->name)); + + if (!empty($parameters)) { + $writer->startElement('parameters'); + + foreach ($parameters as $parameter) { + $writer->startElement(strtolower($parameter->name)); + $writer->write($parameter); + $writer->endElement(); + } + + $writer->endElement(); + } + + $this->xmlSerializeValue($writer); + $writer->endElement(); + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer): void + { + $valueType = strtolower($this->getValueType()); + + foreach ($this->getJsonValue() as $values) { + foreach ((array) $values as $value) { + $writer->writeElement($valueType, $value); + } + } + } + + /** + * Called when this object is being cast to a string. + * + * If the property only had a single value, you will get just that. In the + * case the property had multiple values, the contents will be escaped and + * combined with comma. + */ + public function __toString(): string + { + return (string) $this->getValue(); + } + + /* ArrayAccess interface {{{ */ + + /** + * Checks if an array element exists. + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset): bool + { + if (is_int($offset)) { + return parent::offsetExists($offset); + } + + $offset = strtoupper($offset); + + foreach ($this->parameters as $parameter) { + if ($parameter->name == $offset) { + return true; + } + } + + return false; + } + + /** + * Returns a parameter. + * + * If the parameter does not exist, null is returned. + * + * @param string|int $offset + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset): ?Node + { + if (is_int($offset)) { + return parent::offsetGet($offset); + } + $offset = strtoupper($offset); + + if (!isset($this->parameters[$offset])) { + return null; + } + + return $this->parameters[$offset]; + } + + /** + * Creates a new parameter. + * + * @param string|int $offset + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value): void + { + if (is_int($offset)) { + parent::offsetSet($offset, $value); + + // @codeCoverageIgnoreStart + // This will never be reached, because an exception is always + // thrown. + return; + // @codeCoverageIgnoreEnd + } + + $param = new Parameter($this->root, $offset, $value); + $this->parameters[$param->name] = $param; + } + + /** + * Removes one or more parameters with the specified name. + * + * @param string|int $offset + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset): void + { + if (is_int($offset)) { + parent::offsetUnset($offset); + + // @codeCoverageIgnoreStart + // This will never be reached, because an exception is always + // thrown. + return; + // @codeCoverageIgnoreEnd + } + + unset($this->parameters[strtoupper($offset)]); + } + + /* }}} */ + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + */ + public function __clone() + { + foreach ($this->parameters as $key => $child) { + $this->parameters[$key] = clone $child; + $this->parameters[$key]->parent = $this; + } + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + */ + public function validate(int $options = 0): array + { + $warnings = []; + + // Checking if our value is UTF-8 + if (!StringUtil::isUTF8($this->getRawMimeDirValue())) { + $oldValue = $this->getRawMimeDirValue(); + $level = 3; + if ($options & self::REPAIR) { + $newValue = StringUtil::convertToUTF8($oldValue); + $this->setRawMimeDirValue($newValue); + $level = 1; + } + + if (preg_match('%([\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', $oldValue, $matches)) { + $message = 'Property contained a control character (0x'.bin2hex($matches[1]).')'; + } else { + $message = 'Property is not valid UTF-8! '.$oldValue; + } + + $warnings[] = [ + 'level' => $level, + 'message' => $message, + 'node' => $this, + ]; + } + + // Checking if the property name does not contain any invalid bytes. + if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { + $warnings[] = [ + 'level' => $options & self::REPAIR ? 1 : 3, + 'message' => 'The property name: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed', + 'node' => $this, + ]; + if ($options & self::REPAIR) { + // Uppercasing and converting underscores to dashes. + $this->name = strtoupper( + str_replace('_', '-', $this->name) + ); + // Removing every other invalid character + $this->name = \preg_replace('/([^A-Z0-9-])/u', '', $this->name); + } + } + + if ($encoding = $this->offsetGet('ENCODING')) { + if (Document::VCARD40 === $this->root->getDocumentType()) { + $warnings[] = [ + 'level' => 3, + 'message' => 'ENCODING parameter is not valid in vCard 4.', + 'node' => $this, + ]; + } else { + /** @var Property $encoding */ + $encoding = (string) $encoding; + + $allowedEncoding = []; + + switch ($this->root->getDocumentType()) { + case Document::ICALENDAR20: + $allowedEncoding = ['8BIT', 'BASE64']; + break; + case Document::VCARD21: + $allowedEncoding = ['QUOTED-PRINTABLE', 'BASE64', '8BIT']; + break; + case Document::VCARD30: + $allowedEncoding = ['B']; + // Repair vCard30 that use BASE64 encoding + if ($options & self::REPAIR) { + if ('BASE64' === strtoupper($encoding)) { + $encoding = 'B'; + $this['ENCODING'] = $encoding; + $warnings[] = [ + 'level' => 1, + 'message' => 'ENCODING=BASE64 has been transformed to ENCODING=B.', + 'node' => $this, + ]; + } + } + break; + } + if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.', + 'node' => $this, + ]; + } + } + } + + // Validating inner parameters + foreach ($this->parameters as $param) { + $warnings = array_merge($warnings, $param->validate($options)); + } + + return $warnings; + } + + /** + * Call this method on a document if you're done using it. + * + * It's intended to remove all circular references, so PHP can easily clean + * it up. + */ + public function destroy(): void + { + parent::destroy(); + foreach ($this->parameters as $param) { + $param->destroy(); + } + $this->parameters = []; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Binary.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Binary.php new file mode 100644 index 0000000000..25cda8d25f --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Binary.php @@ -0,0 +1,99 @@ +value = $value[0]; + } else { + throw new \InvalidArgumentException('The argument must either be a string or an array with only one child'); + } + } else { + $this->value = $value; + } + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + */ + public function setRawMimeDirValue(string $val): void + { + $this->value = base64_decode($val); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + return base64_encode($this->value); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return 'BINARY'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + */ + public function getJsonValue(): array + { + return [base64_encode($this->getValue())]; + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value): void + { + $value = array_map('base64_decode', $value); + parent::setJsonValue($value); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Boolean.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Boolean.php new file mode 100644 index 0000000000..cdc3408aa2 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Boolean.php @@ -0,0 +1,66 @@ +setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + return $this->value ? 'TRUE' : 'FALSE'; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return 'BOOLEAN'; + } + + /** + * Hydrate data from an XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value): void + { + $value = array_map( + function ($value) { + return 'true' === $value; + }, + $value + ); + parent::setXmlValue($value); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/FlatText.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/FlatText.php new file mode 100644 index 0000000000..eee1316e2b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/FlatText.php @@ -0,0 +1,46 @@ +setValue($val); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/FloatValue.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/FloatValue.php new file mode 100644 index 0000000000..7a36e0976d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/FloatValue.php @@ -0,0 +1,112 @@ +delimiter, $val); + foreach ($val as &$item) { + $item = (float) $item; + } + $this->setParts($val); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + return implode( + $this->delimiter, + $this->getParts() + ); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return 'FLOAT'; + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + */ + public function getJsonValue(): array + { + $val = array_map('floatval', $this->getParts()); + + // Special-casing the GEO property. + // + // See: + // http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-04#section-3.4.1.2 + if ('GEO' === $this->name) { + return [$val]; + } + + return $val; + } + + /** + * Hydrate data from an XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value): void + { + $value = array_map('floatval', $value); + parent::setXmlValue($value); + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + */ + protected function xmlSerializeValue(Xml\Writer $writer): void + { + // Special-casing the GEO property. + // + // See: + // http://tools.ietf.org/html/rfc6321#section-3.4.1.2 + if ('GEO' === $this->name) { + $value = array_map('floatval', $this->getParts()); + + $writer->writeElement('latitude', $value[0]); + $writer->writeElement('longitude', $value[1]); + } else { + parent::xmlSerializeValue($writer); + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/CalAddress.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/CalAddress.php new file mode 100644 index 0000000000..01969e87d9 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/CalAddress.php @@ -0,0 +1,57 @@ +getValue(); + if (!strpos($input, ':')) { + return $input; + } + list($schema, $everythingElse) = explode(':', $input, 2); + $schema = strtolower($schema); + if ('mailto' === $schema) { + $everythingElse = strtolower($everythingElse); + } + + return $schema.':'.$everythingElse; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Date.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Date.php new file mode 100644 index 0000000000..d8e86d13e7 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Date.php @@ -0,0 +1,18 @@ +setDateTimes($parts); + } else { + parent::setParts($parts); + } + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * Instead of strings, you may also use DateTime here. + * + * @param string|array|\DateTimeInterface $value + * + * @throws InvalidDataException + */ + public function setValue($value): void + { + if (is_array($value) && isset($value[0]) && $value[0] instanceof \DateTimeInterface) { + $this->setDateTimes($value); + } elseif ($value instanceof \DateTimeInterface) { + $this->setDateTimes([$value]); + } else { + parent::setValue($value); + } + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @throws InvalidDataException + */ + public function setRawMimeDirValue(string $val): void + { + $this->setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns true if this is a DATE-TIME value, false if it's a DATE. + */ + public function hasTime(): bool + { + return 'DATE' !== strtoupper((string) $this['VALUE']); + } + + /** + * Returns true if this is a floating DATE or DATE-TIME. + * + * Note that DATE is always floating. + */ + public function isFloating(): bool + { + return + !$this->hasTime() + || ( + !isset($this['TZID']) + && false === strpos($this->getValue(), 'Z') + ); + } + + /** + * Returns a date-time value. + * + * Note that if this property contained more than 1 date-time, only the + * first will be returned. To get an array with multiple values, call + * getDateTimes. + * + * If no timezone information is known, because it's either an all-day + * property or floating time, we will use the DateTimeZone argument to + * figure out the exact date. + * + * @throws InvalidDataException + */ + public function getDateTime(?\DateTimeZone $timeZone = null): ?\DateTimeImmutable + { + $dt = $this->getDateTimes($timeZone); + if (!$dt) { + return null; + } + + return $dt[0]; + } + + /** + * Returns multiple date-time values. + * + * If no timezone information is known, because it's either an all-day + * property or floating time, we will use the DateTimeZone argument to + * figure out the exact date. + * + * @return \DateInterval[]|\DateTimeImmutable[] + * + * @throws InvalidDataException + */ + public function getDateTimes(?\DateTimeZone $timeZone = null): array + { + // Does the property have a TZID? + /** @var Property\FlatText $tzid */ + $tzid = $this['TZID']; + + if ($tzid) { + $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root); + } + + $dts = []; + foreach ($this->getParts() as $part) { + $dts[] = DateTimeParser::parse($part, $timeZone); + } + + return $dts; + } + + /** + * Sets the property as a DateTime object. + * + * @param bool isFloating If set to true, timezones will be ignored + * + * @throws InvalidDataException + */ + public function setDateTime(\DateTimeInterface $dt, $isFloating = false): void + { + $this->setDateTimes([$dt], $isFloating); + } + + /** + * Sets the property as multiple date-time objects. + * + * The first value will be used as a reference for the timezones, and all + * the other values will be adjusted for that timezone + * + * @param \DateTimeInterface[] $dt + * @param bool isFloating If set to true, timezones will be ignored + * + * @throws InvalidDataException + */ + public function setDateTimes(array $dt, $isFloating = false): void + { + $values = []; + + if ($this->hasTime()) { + $tz = null; + $isUtc = false; + + foreach ($dt as $d) { + if ($isFloating) { + $values[] = $d->format('Ymd\\THis'); + continue; + } + if (is_null($tz)) { + $tz = $d->getTimeZone(); + $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00']); + if (!$isUtc) { + $this->offsetSet('TZID', $tz->getName()); + } + } else { + $d = $d->setTimeZone($tz); + } + + if ($isUtc) { + $values[] = $d->format('Ymd\\THis\\Z'); + } else { + $values[] = $d->format('Ymd\\THis'); + } + } + if ($isUtc || $isFloating) { + $this->offsetUnset('TZID'); + } + } else { + foreach ($dt as $d) { + $values[] = $d->format('Ymd'); + } + $this->offsetUnset('TZID'); + } + + $this->value = $values; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return $this->hasTime() ? 'DATE-TIME' : 'DATE'; + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + * + * @throws InvalidDataException + */ + public function getJsonValue(): array + { + $dts = $this->getDateTimes(); + $hasTime = $this->hasTime(); + $isFloating = $this->isFloating(); + + $tz = $dts[0]->getTimeZone(); + $isUtc = !$isFloating && in_array($tz->getName(), ['UTC', 'GMT', 'Z']); + + return array_map( + function (\DateTimeInterface $dt) use ($hasTime, $isUtc) { + if ($hasTime) { + return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : ''); + } else { + return $dt->format('Y-m-d'); + } + }, + $dts + ); + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + * + * @throws InvalidDataException + */ + public function setJsonValue(array $value): void + { + // dates and times in jCal have one difference to dates and times in + // iCalendar. In jCal date-parts are separated by dashes, and + // time-parts are separated by colons. It makes sense to just remove + // those. + $this->setValue( + array_map( + function ($item) { + return strtr($item, [':' => '', '-' => '']); + }, + $value + ) + ); + } + + /** + * We need to intercept offsetSet, because it may be used to alter the + * VALUE from DATE-TIME to DATE or vice-versa. + * + * @param string|int $offset + * + * @throws InvalidDataException + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value): void + { + parent::offsetSet($offset, $value); + if ('VALUE' !== strtoupper($offset)) { + return; + } + + // This will ensure that dates are correctly encoded. + $this->setDateTimes($this->getDateTimes()); + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + */ + public function validate(int $options = 0): array + { + $messages = parent::validate($options); + $valueType = $this->getValueType(); + $values = $this->getParts(); + foreach ($values as $value) { + try { + switch ($valueType) { + case 'DATE': + DateTimeParser::parseDate($value); + break; + case 'DATE-TIME': + DateTimeParser::parseDateTime($value); + break; + } + } catch (InvalidDataException $e) { + $messages[] = [ + 'level' => 3, + 'message' => 'The supplied value ('.$value.') is not a correct '.$valueType, + 'node' => $this, + ]; + break; + } + } + + return $messages; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Duration.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Duration.php new file mode 100644 index 0000000000..e95f355f25 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Duration.php @@ -0,0 +1,73 @@ +setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return 'DURATION'; + } + + /** + * Returns a DateInterval representation of the Duration property. + * + * If the property has more than one value, only the first is returned. + * + * @throws InvalidDataException + */ + public function getDateInterval(): \DateInterval + { + $parts = $this->getParts(); + $value = $parts[0]; + + return DateTimeParser::parseDuration($value); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Period.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Period.php new file mode 100644 index 0000000000..7632cb4d15 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Period.php @@ -0,0 +1,128 @@ +setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return 'PERIOD'; + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value): void + { + $value = array_map( + function ($item) { + return strtr(implode('/', $item), [':' => '', '-' => '']); + }, + $value + ); + parent::setJsonValue($value); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @throws InvalidDataException + */ + public function getJsonValue(): array + { + $return = []; + foreach ($this->getParts() as $item) { + list($start, $end) = explode('/', $item, 2); + + $start = DateTimeParser::parseDateTime($start); + + // This is a duration value. + if ('P' === $end[0]) { + $return[] = [ + $start->format('Y-m-d\\TH:i:s'), + $end, + ]; + } else { + $end = DateTimeParser::parseDateTime($end); + $return[] = [ + $start->format('Y-m-d\\TH:i:s'), + $end->format('Y-m-d\\TH:i:s'), + ]; + } + } + + return $return; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @throws InvalidDataException + */ + protected function xmlSerializeValue(Xml\Writer $writer): void + { + $writer->startElement(strtolower($this->getValueType())); + $value = $this->getJsonValue(); + $writer->writeElement('start', $value[0][0]); + + if ('P' === $value[0][1][0]) { + $writer->writeElement('duration', $value[0][1]); + } else { + $writer->writeElement('end', $value[0][1]); + } + + $writer->endElement(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Recur.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Recur.php new file mode 100644 index 0000000000..10081673e3 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/ICalendar/Recur.php @@ -0,0 +1,330 @@ +value array that is accessible using + * getParts, and may be set using setParts. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Recur extends Property +{ + /** + * Reference to the parent object, if this is not the top object. + */ + public ?Node $parent; + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + */ + public function setValue($value): void + { + // If we're getting the data from json, we'll be receiving an object + if ($value instanceof \stdClass) { + $value = (array) $value; + } + + if (is_array($value)) { + $newVal = []; + foreach ($value as $k => $v) { + if (is_string($v)) { + $v = strtoupper($v); + + // The value had multiple sub-values + if (false !== strpos($v, ',')) { + $v = explode(',', $v); + } + if (0 === strcmp($k, 'until')) { + $v = strtr($v, [':' => '', '-' => '']); + } + } elseif (is_array($v)) { + $v = array_map('strtoupper', $v); + } + + $newVal[strtoupper($k)] = $v; + } + $this->value = $newVal; + } elseif (is_string($value)) { + $this->value = self::stringToArray($value); + } else { + throw new \InvalidArgumentException('You must either pass a string, or a key=>value array'); + } + } + + /** + * Returns the current value. + * + * This method will always return a singular value. If this was a + * multi-value object, some decision will be made first on how to represent + * it as a string. + * + * To get the correct multi-value version, use getParts. + */ + public function getValue(): string + { + $out = []; + foreach ($this->value as $key => $value) { + $out[] = $key.'='.(is_array($value) ? implode(',', $value) : $value); + } + + return strtoupper(implode(';', $out)); + } + + /** + * Sets a multi-valued property. + */ + public function setParts(array $parts): void + { + $this->setValue($parts); + } + + /** + * Returns a multi-valued property. + * + * This method always returns an array, if there was only a single value, + * it will still be wrapped in an array. + */ + public function getParts(): array + { + return $this->value; + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + */ + public function setRawMimeDirValue(string $val): void + { + $this->setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + return $this->getValue(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return 'RECUR'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @throws InvalidDataException + */ + public function getJsonValue(): array + { + $values = []; + foreach ($this->getParts() as $k => $v) { + if (0 === strcmp($k, 'UNTIL')) { + $date = new DateTime($this->root, null, $v); + $values[strtolower($k)] = $date->getJsonValue()[0]; + } elseif (0 === strcmp($k, 'COUNT')) { + $values[strtolower($k)] = intval($v); + } else { + $values[strtolower($k)] = $v; + } + } + + return [$values]; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + */ + protected function xmlSerializeValue(Xml\Writer $writer): void + { + $valueType = strtolower($this->getValueType()); + + foreach ($this->getJsonValue() as $value) { + $writer->writeElement($valueType, $value); + } + } + + /** + * Parses an RRULE value string, and turns it into a struct-ish array. + */ + public static function stringToArray(string $value): array + { + $value = strtoupper($value); + $newValue = []; + foreach (explode(';', $value) as $part) { + // Skipping empty parts. + if (empty($part)) { + continue; + } + + $parts = explode('=', $part); + + if (2 !== count($parts)) { + throw new InvalidDataException('The supplied iCalendar RRULE part is incorrect: '.$part); + } + + list($partName, $partValue) = $parts; + + // The value itself had multiple values.. + if (false !== strpos($partValue, ',')) { + $partValue = explode(',', $partValue); + } + $newValue[$partName] = $partValue; + } + + return $newValue; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + */ + public function validate(int $options = 0): array + { + $repair = ($options & self::REPAIR); + + $warnings = parent::validate($options); + $values = $this->getParts(); + + foreach ($values as $key => $value) { + if ('' === $value) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'Invalid value for '.$key.' in '.$this->name, + 'node' => $this, + ]; + if ($repair) { + unset($values[$key]); + } + } elseif ('BYMONTH' == $key) { + $byMonth = (array) $value; + foreach ($byMonth as $i => $v) { + if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYMONTH in RRULE must have value(s) between 1 and 12!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } elseif ('BYWEEKNO' == $key) { + $byWeekNo = (array) $value; + foreach ($byWeekNo as $i => $v) { + if (!is_numeric($v) || (int) $v < -53 || 0 == (int) $v || (int) $v > 53) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } elseif ('BYYEARDAY' == $key) { + $byYearDay = (array) $value; + foreach ($byYearDay as $i => $v) { + if (!is_numeric($v) || (int) $v < -366 || 0 == (int) $v || (int) $v > 366) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } + } + if (!isset($values['FREQ'])) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'FREQ is required in '.$this->name, + 'node' => $this, + ]; + if ($repair) { + $this->parent->remove($this); + } + } + if ($repair) { + $this->setValue($values); + } + + return $warnings; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/IntegerValue.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/IntegerValue.php new file mode 100644 index 0000000000..f1ae55d173 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/IntegerValue.php @@ -0,0 +1,68 @@ +setValue((int) $val); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + return $this->value; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return 'INTEGER'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + */ + public function getJsonValue(): array + { + return [(int) $this->getValue()]; + } + + /** + * Hydrate data from an XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value): void + { + $value = array_map('intval', $value); + parent::setXmlValue($value); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Text.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Text.php new file mode 100644 index 0000000000..c59029059e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Text.php @@ -0,0 +1,370 @@ + 5, + 'ADR' => 7, + ]; + + /** + * Creates the property. + * + * You can specify the parameters either in key=>value syntax, in which case + * parameters will automatically be created, or you can just pass a list of + * Parameter objects. + * + * @param Component $root The root document + * @param string|array|null $value + * @param array $parameters List of parameters + * @param string|null $group The vcard property group + */ + public function __construct(Component $root, string $name, $value = null, array $parameters = [], ?string $group = null) + { + // There's two types of multi-valued text properties: + // 1. multivalue properties. + // 2. structured value properties + // + // The former is always separated by a comma, the latter by semicolon. + if (in_array($name, $this->structuredValues)) { + $this->delimiter = ';'; + } + + parent::__construct($root, $name, $value, $parameters, $group); + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @throws InvalidDataException + */ + public function setRawMimeDirValue(string $val): void + { + $this->setValue(MimeDir::unescapeValue($val, $this->delimiter)); + } + + /** + * Sets the value as a quoted-printable encoded string. + */ + public function setQuotedPrintableValue(string $val): void + { + $val = quoted_printable_decode($val); + + // Quoted printable only appears in vCard 2.1, and the only character + // that may be escaped there is ;. So we are simply splitting on just + // that. + // + // We also don't have to unescape \\, so all we need to look for is a ; + // that's not preceded with a \. + $regex = '# (?setValue($matches); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + $val = $this->getParts(); + + if (isset($this->minimumPropertyValues[$this->name])) { + $val = \array_pad($val, $this->minimumPropertyValues[$this->name], ''); + } + + foreach ($val as &$item) { + if (!is_array($item)) { + $item = [$item]; + } + + foreach ($item as &$subItem) { + if (!\is_null($subItem)) { + $subItem = strtr( + $subItem, + [ + '\\' => '\\\\', + ';' => '\;', + ',' => '\,', + "\n" => '\n', + "\r" => '', + ] + ); + } + } + $item = \implode(',', $item); + } + + return \implode($this->delimiter, $val); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + */ + public function getJsonValue(): array + { + // Structured text values should always be returned as a single + // array-item. Multi-value text should be returned as multiple items in + // the top-array. + if (in_array($this->name, $this->structuredValues)) { + return [$this->getParts()]; + } + + return $this->getParts(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return 'TEXT'; + } + + /** + * Turns the object back into a serialized blob. + */ + public function serialize(): string + { + // We need to kick in a special type of encoding, if it's a 2.1 vcard. + if (Document::VCARD21 !== $this->root->getDocumentType()) { + return parent::serialize(); + } + + $val = $this->getParts(); + + if (isset($this->minimumPropertyValues[$this->name])) { + $val = \array_pad($val, $this->minimumPropertyValues[$this->name], ''); + } + + // Imploding multiple parts into a single value, and splitting the + // values with ;. + if (\count($val) > 1) { + foreach ($val as $k => $v) { + $val[$k] = \str_replace(';', '\;', $v); + } + $val = \implode(';', $val); + } else { + $val = $val[0]; + } + + $str = $this->name; + if ($this->group) { + $str = $this->group.'.'.$this->name; + } + foreach ($this->parameters as $param) { + if ('QUOTED-PRINTABLE' === $param->getValue()) { + continue; + } + $str .= ';'.$param->serialize(); + } + + // If the resulting value contains a \n, we must encode it as + // quoted-printable. + if (false !== \strpos($val, "\n")) { + $str .= ';ENCODING=QUOTED-PRINTABLE:'; + $lastLine = $str; + $out = ''; + + // The PHP built-in quoted-printable-encode does not correctly + // encode newlines for us. Specifically, the \r\n sequence must in + // vcards be encoded as =0D=OA and we must insert soft-newlines + // every 75 bytes. + for ($ii = 0; $ii < \strlen($val); ++$ii) { + $ord = \ord($val[$ii]); + // These characters are encoded as themselves. + if ($ord >= 32 && $ord <= 126) { + $lastLine .= $val[$ii]; + } else { + $lastLine .= '='.\strtoupper(\bin2hex($val[$ii])); + } + if (\strlen($lastLine) >= 75) { + // Soft line break + $out .= $lastLine."=\r\n "; + $lastLine = null; + } + } + if (!\is_null($lastLine)) { + $out .= $lastLine."\r\n"; + } + + return $out; + } else { + $str .= ':'.$val; + + $str = \preg_replace( + '/( + (?:^.)? # 1 additional byte in first line because of missing single space (see next line) + .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) + (?![\x80-\xbf]) # prevent splitting multibyte characters + )/x', + "$1\r\n ", + $str + ); + + // remove single space after last CRLF + return \substr($str, 0, -1); + } + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + */ + protected function xmlSerializeValue(Xml\Writer $writer): void + { + $values = $this->getParts(); + + $map = function (array $items) use ($values, $writer): void { + foreach ($items as $i => $item) { + $writer->writeElement( + $item, + !empty($values[$i]) ? $values[$i] : null + ); + } + }; + + switch ($this->name) { + // Special-casing the REQUEST-STATUS property. + // + // See: + // http://tools.ietf.org/html/rfc6321#section-3.4.1.3 + case 'REQUEST-STATUS': + $writer->writeElement('code', $values[0]); + $writer->writeElement('description', $values[1]); + + if (isset($values[2])) { + $writer->writeElement('data', $values[2]); + } + break; + + case 'N': + $map([ + 'surname', + 'given', + 'additional', + 'prefix', + 'suffix', + ]); + break; + + case 'GENDER': + $map([ + 'sex', + 'text', + ]); + break; + + case 'ADR': + $map([ + 'pobox', + 'ext', + 'street', + 'locality', + 'region', + 'code', + 'country', + ]); + break; + + case 'CLIENTPIDMAP': + $map([ + 'sourceid', + 'uri', + ]); + break; + + default: + parent::xmlSerializeValue($writer); + } + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + */ + public function validate(int $options = 0): array + { + $warnings = parent::validate($options); + + if (isset($this->minimumPropertyValues[$this->name])) { + $minimum = $this->minimumPropertyValues[$this->name]; + $parts = $this->getParts(); + if (\count($parts) < $minimum) { + $warnings[] = [ + 'level' => $options & self::REPAIR ? 1 : 3, + 'message' => 'The '.$this->name.' property must have at least '.$minimum.' values. It only has '.\count($parts), + 'node' => $this, + ]; + if ($options & self::REPAIR) { + $parts = \array_pad($parts, $minimum, ''); + $this->setParts($parts); + } + } + } + + return $warnings; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Time.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Time.php new file mode 100644 index 0000000000..ed314fc0d0 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Time.php @@ -0,0 +1,128 @@ +setValue(reset($value)); + } else { + $this->setValue($value); + } + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @throws InvalidDataException + */ + public function getJsonValue(): array + { + $parts = DateTimeParser::parseVCardTime($this->getValue()); + $timeStr = ''; + + // Hour + if (!is_null($parts['hour'])) { + $timeStr .= $parts['hour']; + + if (!is_null($parts['minute'])) { + $timeStr .= ':'; + } + } else { + // We know either minute or second _must_ be set, so we insert a + // dash for an empty value. + $timeStr .= '-'; + } + + // Minute + if (!is_null($parts['minute'])) { + $timeStr .= $parts['minute']; + + if (!is_null($parts['second'])) { + $timeStr .= ':'; + } + } else { + if (isset($parts['second'])) { + // Dash for empty minute + $timeStr .= '-'; + } + } + + // Second + if (!is_null($parts['second'])) { + $timeStr .= $parts['second']; + } + + // Timezone + if (!is_null($parts['timezone'])) { + if ('Z' === $parts['timezone']) { + $timeStr .= 'Z'; + } else { + $timeStr .= + preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', $parts['timezone']); + } + } + + return [$timeStr]; + } + + /** + * Hydrate data from an XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value): void + { + $value = array_map( + function ($value) { + return str_replace(':', '', $value); + }, + $value + ); + parent::setXmlValue($value); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Unknown.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Unknown.php new file mode 100644 index 0000000000..3bcf61351a --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Unknown.php @@ -0,0 +1,37 @@ +getRawMimeDirValue()]; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return 'UNKNOWN'; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Uri.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Uri.php new file mode 100644 index 0000000000..228b7090ff --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/Uri.php @@ -0,0 +1,105 @@ +name, ['URL', 'PHOTO'])) { + // If we are encoding a URI value, and this URI value has no + // VALUE=URI parameter, we add it anyway. + // + // This is not required by any spec, but both Apple iCal and Apple + // AddressBook (at least in version 10.8) will trip over this if + // this is not set, and so it improves compatibility. + // + // See Issue #227 and #235 + $parameters['VALUE'] = new Parameter($this->root, 'VALUE', 'URI'); + } + + return $parameters; + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + */ + public function setRawMimeDirValue(string $val): void + { + // Normally we don't need to do any type of unescaping for these + // properties, however, we've noticed that Google Contacts + // specifically escapes the colon (:) with a backslash. While I have + // no clue why they thought that was a good idea, I'm unescaping it + // anyway. + // + // Good thing backslashes are not allowed in urls. Makes it easy to + // assume that a backslash is always intended as an escape character. + if ('URL' === $this->name) { + $regex = '# (?: (\\\\ (?: \\\\ | : ) ) ) #x'; + $matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $newVal = ''; + foreach ($matches as $match) { + switch ($match) { + case '\:': + $newVal .= ':'; + break; + default: + $newVal .= $match; + break; + } + } + $this->value = $newVal; + } else { + $this->value = strtr($val, ['\,' => ',']); + } + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + if (is_array($this->value)) { + $value = $this->value[0]; + } else { + $value = $this->value; + } + + return strtr($value, [',' => '\,']); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/UtcOffset.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/UtcOffset.php new file mode 100644 index 0000000000..09aab01512 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/UtcOffset.php @@ -0,0 +1,68 @@ +value = $dt->format('Ymd'); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/DateAndOrTime.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/DateAndOrTime.php new file mode 100644 index 0000000000..e87bf1b3ab --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/DateAndOrTime.php @@ -0,0 +1,354 @@ + 1) { + throw new \InvalidArgumentException('Only one value allowed'); + } + if (isset($parts[0]) && $parts[0] instanceof \DateTimeInterface) { + $this->setDateTime($parts[0]); + } else { + parent::setParts($parts); + } + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * Instead of strings, you may also use DateTimeInterface here. + * + * @param string|array|\DateTimeInterface $value + */ + public function setValue($value): void + { + if ($value instanceof \DateTimeInterface) { + $this->setDateTime($value); + } else { + parent::setValue($value); + } + } + + /** + * Sets the property as a DateTime object. + */ + public function setDateTime(\DateTimeInterface $dt): void + { + $tz = $dt->getTimeZone(); + $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z']); + + if ($isUtc) { + $value = $dt->format('Ymd\\THis\\Z'); + } else { + // Calculating the offset. + $value = $dt->format('Ymd\\THisO'); + } + + $this->value = $value; + } + + /** + * Returns a date-time value. + * + * Note that if this property contained more than 1 date-time, only the + * first will be returned. To get an array with multiple values, call + * getDateTimes. + * + * If no time was specified, we will always use midnight (in the default + * timezone) as the time. + * + * If parts of the date were omitted, such as the year, we will grab the + * current values for those. So at the time of writing, if the year was + * omitted, we would have filled in 2014. + * + * @throws InvalidDataException + */ + public function getDateTime(): \DateTimeImmutable + { + $now = new \DateTime(); + + $tzFormat = 0 === $now->getTimezone()->getOffset($now) ? '\\Z' : 'O'; + $nowParts = DateTimeParser::parseVCardDateTime($now->format('Ymd\\This'.$tzFormat)); + + $dateParts = DateTimeParser::parseVCardDateTime($this->getValue()); + + // This sets all the missing parts to the current date/time. + // So if the year was missing for a birthday, we're making it 'this + // year'. + foreach ($dateParts as $k => $v) { + if (is_null($v)) { + $dateParts[$k] = $nowParts[$k]; + } + } + + return new \DateTimeImmutable("$dateParts[year]-$dateParts[month]-$dateParts[date] $dateParts[hour]:$dateParts[minute]:$dateParts[second] $dateParts[timezone]"); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @throws InvalidDataException + */ + public function getJsonValue(): array + { + $parts = DateTimeParser::parseVCardDateTime($this->getValue()); + + $dateStr = ''; + + // Year + if (!is_null($parts['year'])) { + $dateStr .= $parts['year']; + + if (!is_null($parts['month'])) { + // If a year and a month is set, we need to insert a separator + // dash. + $dateStr .= '-'; + } + } else { + if (!is_null($parts['month']) || !is_null($parts['date'])) { + // Inserting two dashes + $dateStr .= '--'; + } + } + + // Month + if (!is_null($parts['month'])) { + $dateStr .= $parts['month']; + + if (isset($parts['date'])) { + // If month and date are set, we need the separator dash. + $dateStr .= '-'; + } + } elseif (isset($parts['date'])) { + // If the month is empty, and a date is set, we need a 'empty + // dash' + $dateStr .= '-'; + } + + // Date + if (!is_null($parts['date'])) { + $dateStr .= $parts['date']; + } + + // Early exit if we don't have a time string. + if (is_null($parts['hour']) && is_null($parts['minute']) && is_null($parts['second'])) { + return [$dateStr]; + } + + $dateStr .= 'T'; + + // Hour + if (!is_null($parts['hour'])) { + $dateStr .= $parts['hour']; + + if (!is_null($parts['minute'])) { + $dateStr .= ':'; + } + } else { + // We know either minute or second _must_ be set, so we insert a + // dash for an empty value. + $dateStr .= '-'; + } + + // Minute + if (!is_null($parts['minute'])) { + $dateStr .= $parts['minute']; + + if (!is_null($parts['second'])) { + $dateStr .= ':'; + } + } elseif (isset($parts['second'])) { + // Dash for empty minute + $dateStr .= '-'; + } + + // Second + if (!is_null($parts['second'])) { + $dateStr .= $parts['second']; + } + + // Timezone + if (!is_null($parts['timezone'])) { + $dateStr .= $parts['timezone']; + } + + return [$dateStr]; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @throws InvalidDataException + */ + protected function xmlSerializeValue(Xml\Writer $writer): void + { + $valueType = strtolower($this->getValueType()); + $parts = DateTimeParser::parseVCardDateAndOrTime($this->getValue()); + $value = ''; + + // $d = defined + $d = function ($part) use ($parts): bool { + return !is_null($parts[$part]); + }; + + // $r = read + $r = function ($part) use ($parts) { + return $parts[$part]; + }; + + // From the Relax NG Schema. + // + // # 4.3.1 + // value-date = element date { + // xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" } + // } + if (($d('year') || $d('month') || $d('date')) + && (!$d('hour') && !$d('minute') && !$d('second') && !$d('timezone'))) { + if ($d('year') && $d('month') && $d('date')) { + $value .= $r('year').$r('month').$r('date'); + } elseif ($d('year') && $d('month') && !$d('date')) { + $value .= $r('year').'-'.$r('month'); + } elseif (!$d('year') && $d('month')) { + $value .= '--'.$r('month').$r('date'); + } elseif (!$d('year') && !$d('month') && $d('date')) { + $value .= '---'.$r('date'); + } + + // # 4.3.2 + // value-time = element time { + // xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d?)|--\d\d)" + // ~ "(Z|[+\-]\d\d(\d\d)?)?" } + // } + } elseif ((!$d('year') && !$d('month') && !$d('date')) + && ($d('hour') || $d('minute') || $d('second'))) { + if ($d('hour')) { + $value .= $r('hour').$r('minute').$r('second'); + } elseif ($d('minute')) { + $value .= '-'.$r('minute').$r('second'); + } elseif ($d('second')) { + $value .= '--'.$r('second'); + } + + $value .= $r('timezone'); + + // # 4.3.3 + // value-date-time = element date-time { + // xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?" + // ~ "(Z|[+\-]\d\d(\d\d)?)?" } + // } + } elseif ($d('date') && $d('hour')) { + if ($d('year') && $d('month') && $d('date')) { + $value .= $r('year').$r('month').$r('date'); + } elseif (!$d('year') && $d('month') && $d('date')) { + $value .= '--'.$r('month').$r('date'); + } elseif (!$d('year') && !$d('month') && $d('date')) { + $value .= '---'.$r('date'); + } + + $value .= 'T'.$r('hour').$r('minute').$r('second'). + $r('timezone'); + } + + $writer->writeElement($valueType, $value); + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + */ + public function setRawMimeDirValue(string $val): void + { + $this->setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + */ + public function validate(int $options = 0): array + { + $messages = parent::validate($options); + $value = $this->getValue(); + + try { + DateTimeParser::parseVCardDateTime($value); + } catch (InvalidDataException $e) { + $messages[] = [ + 'level' => 3, + 'message' => 'The supplied value ('.$value.') is not a correct DATE-AND-OR-TIME property', + 'node' => $this, + ]; + } + + return $messages; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/DateTime.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/DateTime.php new file mode 100644 index 0000000000..c71071f1a9 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/DateTime.php @@ -0,0 +1,26 @@ +setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + return $this->getValue(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return 'LANGUAGE-TAG'; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/PhoneNumber.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/PhoneNumber.php new file mode 100644 index 0000000000..f007be3dfd --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/PhoneNumber.php @@ -0,0 +1,28 @@ + + */ +class PhoneNumber extends Property\Text +{ + protected array $structuredValues = []; + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + */ + public function getValueType(): string + { + return 'PHONE-NUMBER'; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/TimeStamp.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/TimeStamp.php new file mode 100644 index 0000000000..80739237e3 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/VCard/TimeStamp.php @@ -0,0 +1,76 @@ +getValue()); + + $dateStr = + $parts['year'].'-'. + $parts['month'].'-'. + $parts['date'].'T'. + $parts['hour'].':'. + $parts['minute'].':'. + $parts['second']; + + // Timezone + if (!is_null($parts['timezone'])) { + $dateStr .= $parts['timezone']; + } + + return [$dateStr]; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + */ + protected function xmlSerializeValue(Xml\Writer $writer): void + { + // xCard is the only XML and JSON format that has the same date and time + // format than vCard. + $valueType = strtolower($this->getValueType()); + $writer->writeElement($valueType, $this->getValue()); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/XCrypto.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/XCrypto.php new file mode 100644 index 0000000000..00cea12317 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Property/XCrypto.php @@ -0,0 +1,68 @@ +setValue(MimeDir::unescapeValue($val, $this->delimiter)); + } + + /** + * Returns a raw mime-dir representation of the value. + */ + public function getRawMimeDirValue(): string + { + $result = []; + foreach ($this->parameters as $parameter) { + $result[] = strtolower($parameter->name) . '=' . $parameter->getValue(); + } + return implode(',', $result); + } + + public function xmlSerialize(\Sabre\Xml\Writer $writer): void + { + $writer->startElement(strtolower($this->name)); + foreach ($this->parameters as $parameter) { + $writer->startElement(strtolower($parameter->name)); + $writer->write($parameter); + $writer->endElement(); + } + $writer->endElement(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Reader.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Reader.php new file mode 100644 index 0000000000..353186920b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Reader.php @@ -0,0 +1,94 @@ +setCharset($charset); + + return $parser->parse($data, $options); + } + + /** + * Parses a jCard or jCal object, and returns the top component. + * + * The options argument is a bitfield. Pass any of the OPTIONS constant to + * alter the parsers' behaviour. + * + * You can either a string, a readable stream, or an array for its input. + * Specifying the array is useful if json_decode was already called on the + * input. + * + * @param string|resource|array $data + * + * @throws EofException + * @throws ParseException|InvalidDataException + */ + public static function readJson($data, int $options = 0): ?Document + { + $parser = new Parser\Json(); + + return $parser->parse($data, $options); + } + + /** + * Parses a xCard or xCal object, and returns the top component. + * + * The options argument is a bitfield. Pass any of the OPTIONS constant to + * alter the parsers' behaviour. + * + * You can either supply a string, or a readable stream for input. + * + * @param string|resource $data + * + * @throws EofException + * @throws InvalidDataException + * @throws ParseException + * @throws LibXMLException + */ + public static function readXML($data, int $options = 0): ?Document + { + $parser = new Parser\XML(); + + return $parser->parse($data, $options); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/EventIterator.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/EventIterator.php new file mode 100644 index 0000000000..2c93bcf4e4 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/EventIterator.php @@ -0,0 +1,476 @@ +timeZone = $timeZone; + + if (is_array($input)) { + $events = $input; + } elseif ($input instanceof VEvent) { + // Single instance mode. + $events = [$input]; + } else { + // Calendar + UID mode. + if (!$uid) { + throw new \InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor'); + } + if (!isset($input->VEVENT)) { + throw new \InvalidArgumentException('No events found in this calendar'); + } + $events = $input->getByUID($uid); + } + + /** @var VEvent[] $events */ + foreach ($events as $vevent) { + if (!isset($vevent->{'RECURRENCE-ID'})) { + $this->masterEvent = $vevent; + } else { + $this->exceptions[ + $vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp() + ] = true; + $this->overriddenEvents[] = $vevent; + } + } + + if (!$this->masterEvent) { + // No base event was found. CalDAV does allow cases where only + // overridden instances are stored. + // + // In this particular case, we're just going to grab the first + // event and use that instead. This may not always give the + // desired result. + if (!count($this->overriddenEvents)) { + throw new \InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid); + } + $this->masterEvent = array_shift($this->overriddenEvents); + } + + $this->startDate = $this->masterEvent->DTSTART->getDateTime($this->timeZone); + $this->allDay = !$this->masterEvent->DTSTART->hasTime(); + + if (isset($this->masterEvent->EXDATE)) { + foreach ($this->masterEvent->EXDATE as $exDate) { + foreach ($exDate->getDateTimes($this->timeZone) as $dt) { + $this->exceptions[$dt->getTimeStamp()] = true; + } + } + } + + if (isset($this->masterEvent->DTEND)) { + $this->eventDuration = + $this->masterEvent->DTEND->getDateTime($this->timeZone)->getTimeStamp() - + $this->startDate->getTimeStamp(); + } elseif (isset($this->masterEvent->DURATION)) { + $duration = $this->masterEvent->DURATION->getDateInterval(); + $end = clone $this->startDate; + $end = $end->add($duration); + $this->eventDuration = $end->getTimeStamp() - $this->startDate->getTimeStamp(); + } elseif ($this->allDay) { + $this->eventDuration = 3600 * 24; + } else { + $this->eventDuration = 0; + } + + if (isset($this->masterEvent->RDATE)) { + $this->recurIterator = new RDateIterator( + $this->masterEvent->RDATE->getParts(), + $this->startDate + ); + } elseif (isset($this->masterEvent->RRULE)) { + $this->recurIterator = new RRuleIterator( + $this->masterEvent->RRULE->getParts(), + $this->startDate + ); + } else { + $this->recurIterator = new RRuleIterator( + [ + 'FREQ' => 'DAILY', + 'COUNT' => 1, + ], + $this->startDate + ); + } + + $this->rewind(); + if (!$this->valid()) { + throw new NoInstancesException('This recurrence rule does not generate any valid instances'); + } + } + + /** + * Returns the date for the current position of the iterator. + */ + #[\ReturnTypeWillChange] + public function current(): ?\DateTimeImmutable + { + if ($this->currentDate) { + return clone $this->currentDate; + } + + return null; + } + + /** + * This method returns the start date for the current iteration of the + * event. + */ + public function getDtStart(): ?\DateTimeImmutable + { + if ($this->currentDate) { + return clone $this->currentDate; + } + + return null; + } + + /** + * This method returns the end date for the current iteration of the + * event. + * + * @throws MaxInstancesExceededException|InvalidDataException + */ + public function getDtEnd(): ?\DateTimeImmutable + { + if (!$this->valid()) { + return null; + } + if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) { + return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone); + } else { + $end = clone $this->currentDate; + + return $end->modify('+'.$this->eventDuration.' seconds'); + } + } + + /** + * Returns a VEVENT for the current iterations of the event. + * + * This VEVENT will have a recurrence id, and its DTSTART and DTEND + * altered. + * + * @throws MaxInstancesExceededException + * @throws InvalidDataException + */ + public function getEventObject(): VEvent + { + if ($this->currentOverriddenEvent) { + return $this->currentOverriddenEvent; + } + + /** @var VEvent $event */ + $event = clone $this->masterEvent; + + // Ignoring the following block, because PHPUnit's code coverage + // ignores most of these lines, and this messes with our stats. + // + // @codeCoverageIgnoreStart + unset( + $event->RRULE, + $event->EXDATE, + $event->RDATE, + $event->EXRULE, + $event->{'RECURRENCE-ID'} + ); + // @codeCoverageIgnoreEnd + + $event->DTSTART->setDateTime($this->getDtStart(), $event->DTSTART->isFloating()); + if (isset($event->DTEND)) { + $event->DTEND->setDateTime($this->getDtEnd(), $event->DTEND->isFloating()); + } + $recurid = clone $event->DTSTART; + $recurid->name = 'RECURRENCE-ID'; + $event->add($recurid); + + return $event; + } + + /** + * Returns the current position of the iterator. + * + * This is for us simply a 0-based index. + */ + #[\ReturnTypeWillChange] + public function key(): int + { + // The counter is always 1 ahead. + return $this->counter - 1; + } + + /** + * This is called after next, to see if the iterator is still at a valid + * position, or if it's at the end. + * + * @throws MaxInstancesExceededException + */ + #[\ReturnTypeWillChange] + public function valid(): bool + { + if ($this->counter > Settings::$maxRecurrences && -1 !== Settings::$maxRecurrences) { + throw new MaxInstancesExceededException('Recurring events are only allowed to generate '.Settings::$maxRecurrences); + } + + return (bool) $this->currentDate; + } + + /** + * Sets the iterator back to the starting point. + * + * @throws InvalidDataException + */ + #[\ReturnTypeWillChange] + public function rewind(): void + { + $this->recurIterator->rewind(); + // re-creating overridden event index. + $index = []; + foreach ($this->overriddenEvents as $key => $event) { + /** @var VEvent $event */ + $stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp(); + $index[$stamp][] = $key; + } + krsort($index); + $this->counter = 0; + $this->overriddenEventsIndex = $index; + $this->currentOverriddenEvent = null; + + $this->nextDate = null; + $this->currentDate = clone $this->startDate; + + $this->next(); + } + + /** + * Advances the iterator with one step. + * + * @throws InvalidDataException + */ + #[\ReturnTypeWillChange] + public function next(): void + { + $this->currentOverriddenEvent = null; + ++$this->counter; + if ($this->nextDate) { + // We had a stored value. + $nextDate = $this->nextDate; + $this->nextDate = null; + } else { + // We need to ask rruleparser for the next date. + // We need to do this until we find a date that's not in the + // exception list. + do { + if (!$this->recurIterator->valid()) { + $nextDate = null; + break; + } + $nextDate = $this->recurIterator->current(); + $this->recurIterator->next(); + } while (isset($this->exceptions[$nextDate->getTimeStamp()])); + } + + // $nextDate now contains what rrule thinks is the next one, but an + // overridden event may cut ahead. + if ($this->overriddenEventsIndex) { + $offsets = end($this->overriddenEventsIndex); + $timestamp = key($this->overriddenEventsIndex); + $offset = end($offsets); + if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) { + // Overridden event comes first. + $this->currentOverriddenEvent = $this->overriddenEvents[$offset]; + + // Putting the rrule next date aside. + $this->nextDate = $nextDate; + $this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone); + + // Ensuring that this item will only be used once. + array_pop($this->overriddenEventsIndex[$timestamp]); + if (!$this->overriddenEventsIndex[$timestamp]) { + array_pop($this->overriddenEventsIndex); + } + + // Exit point! + return; + } + } + + $this->currentDate = $nextDate; + } + + /** + * Quickly jump to a date in the future. + * + * @throws MaxInstancesExceededException|InvalidDataException + */ + public function fastForward(\DateTimeInterface $dateTime): void + { + while ($this->valid() && $this->getDtEnd() <= $dateTime) { + $this->next(); + } + } + + /** + * Returns true if this recurring event never ends. + */ + public function isInfinite(): bool + { + return $this->recurIterator->isInfinite(); + } + + /** + * RRULE parser. + * + * @var RRuleIterator|RDateIterator + */ + protected \Iterator $recurIterator; + + /** + * The duration, in seconds, of the master event. + * + * We use this to calculate the DTEND for subsequent events. + */ + protected $eventDuration; + + /** + * A reference to the main (master) event. + */ + protected ?VEvent $masterEvent = null; + + /** + * List of overridden events. + */ + protected array $overriddenEvents = []; + + /** + * Overridden event index. + * + * Key is timestamp, value is the list of indexes of the item in the $overriddenEvent + * property. + */ + protected array $overriddenEventsIndex; + + /** + * A list of recurrence-id's that are either part of EXDATE, or are + * overridden. + */ + protected array $exceptions = []; + + /** + * Internal event counter. + */ + protected int $counter = 0; + + /** + * The very start of the iteration process. + */ + protected ?\DateTimeImmutable $startDate; + + /** + * Where we are currently in the iteration process. + */ + protected ?\DateTimeImmutable $currentDate = null; + + /** + * The next date from the rrule parser. + * + * Sometimes we need to temporary store the next date, because an + * overridden event came before. + */ + protected ?\DateTimeImmutable $nextDate = null; + + /** + * The event that overwrites the current iteration. + */ + protected ?VEvent $currentOverriddenEvent = null; +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/MaxInstancesExceededException.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/MaxInstancesExceededException.php new file mode 100644 index 0000000000..38d62b46bd --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/MaxInstancesExceededException.php @@ -0,0 +1,15 @@ +startDate = $start; + $this->parseRDate($rrule); + $this->currentDate = clone $this->startDate; + } + + /* Implementation of the Iterator interface {{{ */ + + #[\ReturnTypeWillChange] + public function current(): ?\DateTimeInterface + { + if (!$this->valid()) { + return null; + } + + return clone $this->currentDate; + } + + /** + * Returns the current item number. + */ + #[\ReturnTypeWillChange] + public function key(): int + { + return $this->counter; + } + + /** + * Returns whether the current item is a valid item for the recurrence + * iterator. + */ + #[\ReturnTypeWillChange] + public function valid(): bool + { + return $this->counter <= count($this->dates); + } + + /** + * Resets the iterator. + */ + #[\ReturnTypeWillChange] + public function rewind(): void + { + $this->currentDate = clone $this->startDate; + $this->counter = 0; + } + + /** + * Goes on to the next iteration. + * + * @throws InvalidDataException + */ + #[\ReturnTypeWillChange] + public function next(): void + { + ++$this->counter; + if (!$this->valid()) { + return; + } + + $this->currentDate = + DateTimeParser::parse( + $this->dates[$this->counter - 1], + $this->startDate->getTimezone() + ); + } + + /* End of Iterator implementation }}} */ + + /** + * Returns true if this recurring event never ends. + */ + public function isInfinite(): bool + { + return false; + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + * + * @throws InvalidDataException + */ + public function fastForward(\DateTimeInterface $dt): void + { + while ($this->valid() && $this->currentDate < $dt) { + $this->next(); + } + } + + /** + * The reference start date/time for the rrule. + * + * All calculations are based on this initial date. + */ + protected \DateTimeInterface $startDate; + + /** + * The date of the current iteration. You can get this by calling + * ->current(). + */ + protected \DateTimeInterface $currentDate; + + /** + * The current item in the list. + * + * You can get this number with the key() method. + */ + protected int $counter = 0; + + /* }}} */ + + /** + * This method receives a string from an RRULE property, and populates this + * class with all the values. + * + * @param string|array $rdate + */ + protected function parseRDate($rdate): void + { + if (is_string($rdate)) { + $rdate = explode(',', $rdate); + } + + $this->dates = $rdate; + } + + /** + * Array with the RRULE dates. + */ + protected array $dates = []; +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/RRuleIterator.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/RRuleIterator.php new file mode 100644 index 0000000000..ff276a6971 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Recur/RRuleIterator.php @@ -0,0 +1,1036 @@ +startDate = $start; + $this->parseRRule($rrule); + $this->currentDate = clone $this->startDate; + } + + /* Implementation of the Iterator interface {{{ */ + + #[\ReturnTypeWillChange] + public function current(): ?\DateTimeInterface + { + if (!$this->valid()) { + return null; + } + + return clone $this->currentDate; + } + + /** + * Returns the current item number. + */ + #[\ReturnTypeWillChange] + public function key(): int + { + return $this->counter; + } + + /** + * Returns whether the current item is a valid item for the recurrence + * iterator. This will return false if we've gone beyond the UNTIL or COUNT + * statements. + */ + #[\ReturnTypeWillChange] + public function valid(): bool + { + if (null === $this->currentDate) { + return false; + } + if (!is_null($this->count)) { + return $this->counter < $this->count; + } + + return is_null($this->until) || $this->currentDate <= $this->until; + } + + /** + * Resets the iterator. + */ + #[\ReturnTypeWillChange] + public function rewind(): void + { + $this->currentDate = clone $this->startDate; + $this->counter = 0; + } + + /** + * Goes on to the next iteration. + */ + #[\ReturnTypeWillChange] + public function next(): void + { + // Otherwise, we find the next event in the normal RRULE + // sequence. + switch ($this->frequency) { + case 'hourly': + $this->nextHourly(); + break; + + case 'daily': + $this->nextDaily(); + break; + + case 'weekly': + $this->nextWeekly(); + break; + + case 'monthly': + $this->nextMonthly(); + break; + + case 'yearly': + $this->nextYearly(); + break; + } + ++$this->counter; + } + + /* End of Iterator implementation }}} */ + + /** + * Returns true if this recurring event never ends. + */ + public function isInfinite(): bool + { + return !$this->count && !$this->until; + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + */ + public function fastForward(\DateTimeInterface $dt): void + { + while ($this->valid() && $this->currentDate < $dt) { + $this->next(); + } + } + + /** + * The reference start date/time for the rrule. + * + * All calculations are based on this initial date. + */ + protected \DateTimeInterface $startDate; + + /** + * The date of the current iteration. You can get this by calling + * ->current(). + */ + protected ?\DateTimeInterface $currentDate; + + /** + * The number of hours that the next occurrence of an event + * jumped forward, usually because summer time started and + * the requested time-of-day like 0230 did not exist on that + * day. And so the event was scheduled 1 hour later at 0330. + */ + protected int $hourJump = 0; + + /** + * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly, + * yearly. + */ + protected string $frequency; + + /** + * The number of recurrences, or 'null' if infinitely recurring. + */ + protected ?int $count = null; + + /** + * The interval. + * + * If for example frequency is set to daily, interval = 2 would mean every + * 2 days. + */ + protected int $interval = 1; + + /** + * The last instance of this recurrence, inclusively. + */ + protected ?\DateTimeInterface $until = null; + + /** + * Which seconds to recur. + * + * This is an array of integers (between 0 and 60) + */ + protected ?array $bySecond = null; + + /** + * Which minutes to recur. + * + * This is an array of integers (between 0 and 59) + */ + protected ?array $byMinute = null; + + /** + * Which hours to recur. + * + * This is an array of integers (between 0 and 23) + */ + protected ?array $byHour = null; + + /** + * The current item in the list. + * + * You can get this number with the key() method. + */ + protected int $counter = 0; + + /** + * Which weekdays to recur. + * + * This is an array of weekdays + * + * This may also be preceded by a positive or negative integer. If present, + * this indicates the nth occurrence of a specific day within the monthly or + * yearly rrule. For instance, -2TU indicates the second-last tuesday of + * the month, or year. + */ + protected ?array $byDay = null; + + /** + * Which days of the month to recur. + * + * This is an array of days of the months (1-31). The value can also be + * negative. -5 for instance means the 5th last day of the month. + */ + protected ?array $byMonthDay = null; + + /** + * Which days of the year to recur. + * + * This is an array with days of the year (1 to 366). The values can also + * be negative. For instance, -1 will always represent the last day of the + * year. (December 31st). + */ + protected ?array $byYearDay = null; + + /** + * Which week numbers to recur. + * + * This is an array of integers from 1 to 53. The values can also be + * negative. -1 will always refer to the last week of the year. + */ + protected ?array $byWeekNo = null; + + /** + * Which months to recur. + * + * This is an array of integers from 1 to 12. + */ + protected ?array $byMonth = null; + + /** + * Which items in an existing st to recur. + * + * These numbers work together with an existing by* rule. It specifies + * exactly which items of the existing by-rule to filter. + * + * Valid values are 1 to 366 and -1 to -366. As an example, this can be + * used to recur the last workday of the month. + * + * This would be done by setting frequency to 'monthly', byDay to + * 'MO,TU,WE,TH,FR' and bySetPos to -1. + */ + protected ?array $bySetPos = null; + + /** + * When the week starts. + */ + protected string $weekStart = 'MO'; + + /* Functions that advance the iterator {{{ */ + + /** + * Gets the original start time of the RRULE. + * + * The value is formatted as a string with 24-hour:minute:second + */ + protected function startTime(): string + { + return $this->startDate->format('H:i:s'); + } + + /** + * Advances currentDate by the interval. + * The time is set from the original startDate. + * If the recurrence is on a day when summer time started, then the + * time on that day may have jumped forward, for example, from 0230 to 0330. + * Using the original time means that the next recurrence will be calculated + * based on the original start time and the day/week/month/year interval. + * So the start time of the next occurrence can correctly revert to 0230. + */ + protected function advanceTheDate(string $interval): void + { + $this->currentDate = $this->currentDate->modify($interval.' '.$this->startTime()); + } + + /** + * Does the processing for adjusting the time of multi-hourly events when summer time starts. + */ + protected function adjustForTimeJumpsOfHourlyEvent(\DateTimeInterface $previousEventDateTime): void + { + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the next occurrence. + // That happens if the next event time is on a day when summer time starts + // and the event time is in the non-existent hour of the day. + // For example, an event that normally starts at 02:30 will + // have to start at 03:30 on that day. + // If the interval is just 1 hour, then there is no "jumping back" to do. + // The events that day will happen, for example, at 0030 0130 0330 0430 0530... + if ($this->interval > 1) { + $expectedHourOfNextDate = ((int) $previousEventDateTime->format('G') + $this->interval) % 24; + $actualHourOfNextDate = (int) $this->currentDate->format('G'); + $this->hourJump = $actualHourOfNextDate - $expectedHourOfNextDate; + } + } else { + // The hour "jumped" for the previous occurrence, to avoid the non-existent time. + // currentDate got set ahead by (usually) 1 hour on that day. + // Adjust it back for this next occurrence. + $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); + $this->hourJump = 0; + } + } + + /** + * Does the processing for advancing the iterator for hourly frequency. + */ + protected function nextHourly(): void + { + $previousEventDateTime = clone $this->currentDate; + $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); + $this->adjustForTimeJumpsOfHourlyEvent($previousEventDateTime); + } + + /** + * Does the processing for advancing the iterator for daily frequency. + */ + protected function nextDaily(): void + { + if (!$this->byHour && !$this->byDay) { + $this->advanceTheDate('+'.$this->interval.' days'); + + return; + } + + $recurrenceHours = []; + if (!empty($this->byHour)) { + $recurrenceHours = $this->getHours(); + } + + $recurrenceDays = []; + if (!empty($this->byDay)) { + $recurrenceDays = $this->getDays(); + } + + $recurrenceMonths = []; + if (!empty($this->byMonth)) { + $recurrenceMonths = $this->getMonths(); + } + + do { + if ($this->byHour) { + if ('23' == $this->currentDate->format('G')) { + // to obey the interval rule + $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days'); + } + + $this->currentDate = $this->currentDate->modify('+1 hours'); + } else { + $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + } + + // Current month of the year + $currentMonth = $this->currentDate->format('n'); + + // Current day of the week + $currentDay = $this->currentDate->format('w'); + + // Current hour of the day + $currentHour = $this->currentDate->format('G'); + + if ($this->currentDate->getTimestamp() > self::dateUpperLimit) { + $this->currentDate = null; + + return; + } + } while ( + ($this->byDay && !in_array($currentDay, $recurrenceDays)) + || ($this->byHour && !in_array($currentHour, $recurrenceHours)) + || ($this->byMonth && !in_array($currentMonth, $recurrenceMonths)) + ); + } + + /** + * Does the processing for advancing the iterator for weekly frequency. + */ + protected function nextWeekly(): void + { + if (!$this->byHour && !$this->byDay) { + $this->advanceTheDate('+'.$this->interval.' weeks'); + + return; + } + + $recurrenceHours = []; + if ($this->byHour) { + $recurrenceHours = $this->getHours(); + } + + $recurrenceDays = []; + if ($this->byDay) { + $recurrenceDays = $this->getDays(); + } + + // First day of the week: + $firstDay = $this->dayMap[$this->weekStart]; + + do { + if ($this->byHour) { + $this->currentDate = $this->currentDate->modify('+1 hours'); + } else { + $this->advanceTheDate('+1 days'); + } + + // Current day of the week + $currentDay = (int) $this->currentDate->format('w'); + + // Current hour of the day + $currentHour = (int) $this->currentDate->format('G'); + + // We need to roll over to the next week + if ($currentDay === $firstDay && (!$this->byHour || '0' == $currentHour)) { + $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks'); + + // We need to go to the first day of this week, but only if we + // are not already on this first day of this week. + if ($this->currentDate->format('w') != $firstDay) { + $this->currentDate = $this->currentDate->modify('last '.$this->dayNames[$this->dayMap[$this->weekStart]]); + } + } + + // We have a match + } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); + } + + /** + * Does the processing for advancing the iterator for monthly frequency. + * + * @throws \Exception + */ + protected function nextMonthly(): void + { + $currentDayOfMonth = $this->currentDate->format('j'); + if (!$this->byMonthDay && !$this->byDay) { + // If the current day is higher than the 28th, rollover can + // occur to the next month. We Must skip these invalid + // entries. + if ($currentDayOfMonth < 29) { + $this->advanceTheDate('+'.$this->interval.' months'); + } else { + $increase = 0; + do { + ++$increase; + $tempDate = clone $this->currentDate; + $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime()); + } while ($tempDate->format('j') != $currentDayOfMonth); + $this->currentDate = $tempDate; + } + + return; + } + + $occurrence = -1; + while (true) { + $occurrences = $this->getMonthlyOccurrences(); + + foreach ($occurrences as $occurrence) { + // The first occurrence that's higher than the current + // day of the month wins. + if ($occurrence > $currentDayOfMonth) { + break 2; + } + } + + // If we made it all the way here, it means there were no + // valid occurrences, and we need to advance to the next + // month. + // + // This line does not currently work in hhvm. Temporary workaround + // follows: + // $this->currentDate->modify('first day of this month'); + $this->currentDate = new \DateTimeImmutable($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone()); + // end of workaround + $this->currentDate = $this->currentDate->modify('+ '.$this->interval.' months'); + + // This goes to 0 because we need to start counting at the + // beginning. + $currentDayOfMonth = 0; + + // For some reason the "until" parameter was not being used here, + // that's why the workaround of the 10000-year bug was needed at all + // let's stop it before the "until" parameter date + if ($this->until && $this->currentDate->getTimestamp() >= $this->until->getTimestamp()) { + return; + } + + // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply + // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php .... + if ($this->currentDate->getTimestamp() > self::dateUpperLimit) { + $this->currentDate = null; + + return; + } + } + + // Set the currentDate to the year and month that we are in, and the day of the month that we have selected. + // That day could be a day when summer time starts, and if the time of the event is, for example, 0230, + // then 0230 will not be a valid time on that day. So always apply the start time from the original startDate. + // The "modify" method will set the time forward to 0330, for example, if needed. + $this->currentDate = $this->currentDate->setDate( + (int) $this->currentDate->format('Y'), + (int) $this->currentDate->format('n'), + (int) $occurrence + )->modify($this->startTime()); + } + + /** + * Does the processing for advancing the iterator for yearly frequency. + */ + protected function nextYearly(): void + { + $currentMonth = $this->currentDate->format('n'); + $currentYear = $this->currentDate->format('Y'); + $currentDayOfMonth = $this->currentDate->format('j'); + + // No sub-rules, so we just advance by year + if (empty($this->byMonth)) { + // Unless it was a leap day! + if (2 == $currentMonth && 29 == $currentDayOfMonth) { + $counter = 0; + do { + ++$counter; + // Here we increase the year count by the interval, until + // we hit a date that's also in a leap year. + // + // We could just find the next interval that's dividable by + // 4, but that would ignore the rule that there's no leap + // year every year that's dividable by a 100, but not by + // 400. (1800, 1900, 2100). So we just rely on the datetime + // functions instead. + $nextDate = clone $this->currentDate; + $nextDate = $nextDate->modify('+ '.($this->interval * $counter).' years'); + } while (2 != $nextDate->format('n')); + + $this->currentDate = $nextDate; + + return; + } + + if (null !== $this->byWeekNo) { // byWeekNo is an array with values from -53 to -1, or 1 to 53 + $dayOffsets = []; + if ($this->byDay) { + foreach ($this->byDay as $byDay) { + $dayOffsets[] = $this->dayMap[$byDay]; + } + } else { // default is Monday + $dayOffsets[] = 1; + } + + $currentYear = $this->currentDate->format('Y'); + + while (true) { + $checkDates = []; + + // loop through all WeekNo and Days to check all the combinations + foreach ($this->byWeekNo as $byWeekNo) { + foreach ($dayOffsets as $dayOffset) { + $date = clone $this->currentDate; + $date = $date->setISODate($currentYear, $byWeekNo, $dayOffset); + + if ($date > $this->currentDate) { + $checkDates[] = $date; + } + } + } + + if (count($checkDates) > 0) { + $this->currentDate = min($checkDates); + + return; + } + + // if there is no date found, check the next year + $currentYear += $this->interval; + } + } + + if (null !== $this->byYearDay) { // byYearDay is an array with values from -366 to -1, or 1 to 366 + $dayOffsets = []; + if ($this->byDay) { + foreach ($this->byDay as $byDay) { + $dayOffsets[] = $this->dayMap[$byDay]; + } + } else { // default is Monday-Sunday + $dayOffsets = [1, 2, 3, 4, 5, 6, 7]; + } + + $currentYear = $this->currentDate->format('Y'); + + while (true) { + $checkDates = []; + + // loop through all YearDay and Days to check all the combinations + foreach ($this->byYearDay as $byYearDay) { + $date = clone $this->currentDate; + if ($byYearDay > 0) { + $date = $date->setDate($currentYear, 1, 1); + $date = $date->add(new \DateInterval('P'.($byYearDay - 1).'D')); + } else { + $date = $date->setDate($currentYear, 12, 31); + $date = $date->sub(new \DateInterval('P'.abs($byYearDay + 1).'D')); + } + + if ($date > $this->currentDate && in_array($date->format('N'), $dayOffsets)) { + $checkDates[] = $date; + } + } + + if (count($checkDates) > 0) { + $this->currentDate = min($checkDates); + + return; + } + + // if there is no date found, check the next year + $currentYear += $this->interval; + } + } + + // The easiest form + $this->advanceTheDate('+'.$this->interval.' years'); + + return; + } + + $currentMonth = $this->currentDate->format('n'); + $currentYear = $this->currentDate->format('Y'); + $currentDayOfMonth = $this->currentDate->format('j'); + + $advancedToNewMonth = false; + + // If we got a byDay or getMonthDay filter, we must first expand + // further. + if ($this->byDay || $this->byMonthDay) { + $occurrence = -1; + while (true) { + $occurrences = $this->getMonthlyOccurrences(); + + foreach ($occurrences as $occurrence) { + // The first occurrence that's higher than the current + // day of the month wins. + // If we advanced to the next month or year, the first + // occurrence is always correct. + if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { + // only consider byMonth matches, + // otherwise, we don't follow RRule correctly + if (in_array($currentMonth, $this->byMonth)) { + break 2; + } + } + } + + // If we made it here, it means we need to advance to + // the next month or year. + $currentDayOfMonth = 1; + $advancedToNewMonth = true; + do { + ++$currentMonth; + if ($currentMonth > 12) { + $currentYear += $this->interval; + $currentMonth = 1; + } + } while (!in_array($currentMonth, $this->byMonth)); + + $this->currentDate = $this->currentDate->setDate( + (int) $currentYear, + (int) $currentMonth, + (int) $currentDayOfMonth + ); + + // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply + // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php .... + if ($this->currentDate->getTimestamp() > self::dateUpperLimit) { + $this->currentDate = null; + + return; + } + } + + // If we made it here, it means we got a valid occurrence + $this->currentDate = $this->currentDate->setDate( + (int) $currentYear, + (int) $currentMonth, + (int) $occurrence + )->modify($this->startTime()); + + return; + } else { + // These are the 'byMonth' rules, if there are no byDay or + // byMonthDay sub-rules. + do { + ++$currentMonth; + if ($currentMonth > 12) { + $currentYear += $this->interval; + $currentMonth = 1; + } + } while (!in_array($currentMonth, $this->byMonth)); + $this->currentDate = $this->currentDate->setDate( + (int) $currentYear, + (int) $currentMonth, + (int) $currentDayOfMonth + )->modify($this->startTime()); + + return; + } + } + + /* }}} */ + + /** + * This method receives a string from an RRULE property, and populates this + * class with all the values. + * + * @param string|array $rrule + * + * @throws InvalidDataException + */ + protected function parseRRule($rrule): void + { + if (is_string($rrule)) { + $rrule = Property\ICalendar\Recur::stringToArray($rrule); + } + + foreach ($rrule as $key => $value) { + $key = strtoupper($key); + switch ($key) { + case 'FREQ': + $value = strtolower($value); + if (!in_array( + $value, + ['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'] + )) { + throw new InvalidDataException('Unknown value for FREQ='.strtoupper($value)); + } + $this->frequency = $value; + break; + + case 'UNTIL': + $this->until = DateTimeParser::parse($value, $this->startDate->getTimezone()); + + // In some cases events are generated with an UNTIL= + // parameter before the actual start of the event. + // + // Not sure why this is happening. We assume that the + // intention was that the event only recurs once. + // + // So we are modifying the parameter so our code doesn't + // break. + if ($this->until < $this->startDate) { + $this->until = $this->startDate; + } + break; + + case 'INTERVAL': + case 'COUNT': + $val = (int) $value; + if ($val < 1) { + throw new InvalidDataException(strtoupper($key).' in RRULE must be a positive integer!'); + } + $key = strtolower($key); + $this->$key = $val; + break; + + case 'BYSECOND': + $this->bySecond = (array) $value; + break; + + case 'BYMINUTE': + $this->byMinute = (array) $value; + break; + + case 'BYHOUR': + $this->byHour = (array) $value; + break; + + case 'BYDAY': + $value = (array) $value; + foreach ($value as $part) { + if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) { + throw new InvalidDataException('Invalid part in BYDAY clause: '.$part); + } + } + $this->byDay = $value; + break; + + case 'BYMONTHDAY': + $this->byMonthDay = (array) $value; + break; + + case 'BYYEARDAY': + $this->byYearDay = (array) $value; + foreach ($this->byYearDay as $byYearDay) { + if (!is_numeric($byYearDay) || (int) $byYearDay < -366 || 0 == (int) $byYearDay || (int) $byYearDay > 366) { + throw new InvalidDataException('BYYEARDAY in RRULE must have value(s) from 1 to 366, or -366 to -1!'); + } + } + break; + + case 'BYWEEKNO': + $this->byWeekNo = (array) $value; + foreach ($this->byWeekNo as $byWeekNo) { + if (!is_numeric($byWeekNo) || (int) $byWeekNo < -53 || 0 == (int) $byWeekNo || (int) $byWeekNo > 53) { + throw new InvalidDataException('BYWEEKNO in RRULE must have value(s) from 1 to 53, or -53 to -1!'); + } + } + break; + + case 'BYMONTH': + $this->byMonth = (array) $value; + foreach ($this->byMonth as $byMonth) { + if (!is_numeric($byMonth) || (int) $byMonth < 1 || (int) $byMonth > 12) { + throw new InvalidDataException('BYMONTH in RRULE must have value(s) between 1 and 12!'); + } + } + break; + + case 'BYSETPOS': + $this->bySetPos = (array) $value; + break; + + case 'WKST': + $this->weekStart = strtoupper($value); + break; + + default: + throw new InvalidDataException('Not supported: '.strtoupper($key)); + } + } + } + + /** + * Mappings between the day number and english day name. + */ + protected array $dayNames = [ + 0 => 'Sunday', + 1 => 'Monday', + 2 => 'Tuesday', + 3 => 'Wednesday', + 4 => 'Thursday', + 5 => 'Friday', + 6 => 'Saturday', + ]; + + /** + * Returns all the occurrences for a monthly frequency with a 'byDay' or + * 'byMonthDay' expansion for the current month. + * + * The returned list is an array of integers with the day of month (1-31). + * + * @throws \Exception + */ + protected function getMonthlyOccurrences(): array + { + $startDate = clone $this->currentDate; + + $byDayResults = []; + + // Our strategy is to simply go through the byDays, advance the date to + // that point and add it to the results. + if ($this->byDay) { + foreach ($this->byDay as $day) { + $dayName = $this->dayNames[$this->dayMap[substr($day, -2)]]; + + // Dayname will be something like 'wednesday'. Now we need to find + // all wednesdays in this month. + $dayHits = []; + + // workaround for missing 'first day of the month' support in hhvm + $checkDate = new \DateTime($startDate->format('Y-m-1')); + // workaround modify always advancing the date even if the current day is a $dayName in hhvm + if ($checkDate->format('l') !== $dayName) { + $checkDate = $checkDate->modify($dayName); + } + + do { + $dayHits[] = $checkDate->format('j'); + $checkDate = $checkDate->modify('next '.$dayName); + } while ($checkDate->format('n') === $startDate->format('n')); + + // So now we have 'all wednesdays' for month. It is however + // possible that the user only really wanted the 1st, 2nd or last + // wednesday. + if (strlen($day) > 2) { + $offset = (int) substr($day, 0, -2); + + if ($offset > 0) { + // It is possible that the day does not exist, such as a + // 5th or 6th wednesday of the month. + if (isset($dayHits[$offset - 1])) { + $byDayResults[] = $dayHits[$offset - 1]; + } + } else { + // if it was negative we count from the end of the array + // might not exist, fx. -5th tuesday + if (isset($dayHits[count($dayHits) + $offset])) { + $byDayResults[] = $dayHits[count($dayHits) + $offset]; + } + } + } else { + // There was no counter (first, second, last wednesdays), so we + // just need to add the all to the list. + $byDayResults = array_merge($byDayResults, $dayHits); + } + } + } + + $byMonthDayResults = []; + if ($this->byMonthDay) { + foreach ($this->byMonthDay as $monthDay) { + // Removing values that are out of range for this month + if ($monthDay > $startDate->format('t') + || $monthDay < 0 - $startDate->format('t')) { + continue; + } + if ($monthDay > 0) { + $byMonthDayResults[] = $monthDay; + } else { + // Negative values + $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay; + } + } + } + + // If there was just byDay or just byMonthDay, they just specify our + // (almost) final list. If both were provided, then byDay limits the + // list. + if ($this->byMonthDay && $this->byDay) { + $result = array_intersect($byMonthDayResults, $byDayResults); + } elseif ($this->byMonthDay) { + $result = $byMonthDayResults; + } else { + $result = $byDayResults; + } + $result = array_unique($result); + sort($result, SORT_NUMERIC); + + // The last thing that needs checking is the BYSETPOS. If it's set, it + // means only certain items in the set survive the filter. + if (!$this->bySetPos) { + return $result; + } + + $filteredResult = []; + foreach ($this->bySetPos as $setPos) { + if ($setPos < 0) { + $setPos = count($result) + ($setPos + 1); + } + if (isset($result[$setPos - 1])) { + $filteredResult[] = $result[$setPos - 1]; + } + } + + sort($filteredResult, SORT_NUMERIC); + + return $filteredResult; + } + + /** + * Simple mapping from iCalendar day names to day numbers. + */ + protected array $dayMap = [ + 'SU' => 0, + 'MO' => 1, + 'TU' => 2, + 'WE' => 3, + 'TH' => 4, + 'FR' => 5, + 'SA' => 6, + ]; + + protected function getHours(): array + { + $recurrenceHours = []; + foreach ($this->byHour as $byHour) { + $recurrenceHours[] = $byHour; + } + + return $recurrenceHours; + } + + protected function getDays(): array + { + $recurrenceDays = []; + foreach ($this->byDay as $byDay) { + // The day may be preceded with a positive (+n) or + // negative (-n) integer. However, this does not make + // sense in 'weekly' so we ignore it here. + $recurrenceDays[] = $this->dayMap[substr($byDay, -2)]; + } + + return $recurrenceDays; + } + + protected function getMonths(): array + { + $recurrenceMonths = []; + foreach ($this->byMonth as $byMonth) { + $recurrenceMonths[] = $byMonth; + } + + return $recurrenceMonths; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Settings.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Settings.php new file mode 100644 index 0000000000..5e50f914f2 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Settings.php @@ -0,0 +1,55 @@ +children() as $component) { + if (!$component instanceof Component) { + continue; + } + + // Get all timezones + if ('VTIMEZONE' === $component->name) { + $this->vtimezones[(string) $component->TZID] = $component; + continue; + } + + // Get component UID for recurring Events search + if (!$component->UID) { + $component->UID = sha1(microtime()).'-vobjectimport'; + } + $uid = (string) $component->UID; + + // Take care of recurring events + if (!array_key_exists($uid, $this->objects)) { + $this->objects[$uid] = new VCalendar(); + } + + $this->objects[$uid]->add(clone $component); + } + } + + /** + * Every time getNext() is called, a new object will be parsed, until we + * hit the end of the stream. + * + * When the end is reached, null will be returned. + */ + public function getNext(): ?Component + { + if ($object = array_shift($this->objects)) { + // create our baseobject + $object->version = '2.0'; + $object->prodid = '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN'; + $object->calscale = 'GREGORIAN'; + + // add vtimezone information to obj (if we have it) + foreach ($this->vtimezones as $vtimezone) { + $object->add($vtimezone); + } + + return $object; + } else { + return null; + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Splitter/SplitterInterface.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Splitter/SplitterInterface.php new file mode 100644 index 0000000000..c4014abf3b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Splitter/SplitterInterface.php @@ -0,0 +1,38 @@ +input = $input; + $this->parser = new MimeDir($input, $options); + } + + /** + * Every time getNext() is called, a new object will be parsed, until we + * hit the end of the stream. + * + * When the end is reached, null will be returned. + * + * @throws VObject\ParseException + */ + public function getNext(): ?Component + { + try { + $object = $this->parser->parse(); + + if (!$object instanceof Component\VCard) { + throw new VObject\ParseException('The supplied input contained non-VCARD data.'); + } + } catch (VObject\EofException $e) { + return null; + } + + return $object; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/StringUtil.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/StringUtil.php new file mode 100644 index 0000000000..8fe5f9f0a6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/StringUtil.php @@ -0,0 +1,42 @@ +addGuesser('lic', new GuessFromLicEntry()); + $this->addGuesser('msTzId', new GuessFromMsTzId()); + $this->addFinder('tzid', new FindFromTimezoneIdentifier()); + $this->addFinder('tzmap', new FindFromTimezoneMap()); + $this->addFinder('offset', new FindFromOffset()); + } + + private static function getInstance(): self + { + if (null === self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + private function addGuesser(string $key, TimezoneGuesser $guesser): void + { + $this->timezoneGuessers[$key] = $guesser; + } + + private function addFinder(string $key, TimezoneFinder $finder): void + { + $this->timezoneFinders[$key] = $finder; + } + + /** + * This method will try to find out the correct timezone for an iCalendar + * date-time value. + * + * You must pass the contents of the TZID parameter, as well as the full + * calendar. + * + * If the lookup fails, this method will return the default PHP timezone + * (as configured using date_default_timezone_set, or the date.timezone ini + * setting). + * + * Alternatively, if $failIfUncertain is set to true, it will throw an + * exception if we cannot accurately determine the timezone. + */ + private function findTimeZone(string $tzid, ?Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone + { + foreach ($this->timezoneFinders as $timezoneFinder) { + $timezone = $timezoneFinder->find($tzid, $failIfUncertain); + if (!$timezone instanceof \DateTimeZone) { + continue; + } + + return $timezone; + } + + if ($vcalendar) { + // If that didn't work, we will scan VTIMEZONE objects + foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) { + if ((string) $vtimezone->TZID === $tzid) { + foreach ($this->timezoneGuessers as $timezoneGuesser) { + $timezone = $timezoneGuesser->guess($vtimezone, $failIfUncertain); + if (!$timezone instanceof \DateTimeZone) { + continue; + } + + return $timezone; + } + } + } + } + + if ($failIfUncertain) { + throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: '.$tzid); + } + + // If we got all the way here, we default to whatever has been set as the PHP default timezone. + return new \DateTimeZone(date_default_timezone_get()); + } + + public static function addTimezoneGuesser(string $key, TimezoneGuesser $guesser): void + { + self::getInstance()->addGuesser($key, $guesser); + } + + public static function addTimezoneFinder(string $key, TimezoneFinder $finder): void + { + self::getInstance()->addFinder($key, $finder); + } + + public static function getTimeZone(string $tzid, ?Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone + { + return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain); + } + + public static function clean(): void + { + self::$instance = null; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/FindFromOffset.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/FindFromOffset.php new file mode 100644 index 0000000000..d50cb969c9 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/FindFromOffset.php @@ -0,0 +1,29 @@ +getIdentifiersBC()) + ) { + return new \DateTimeZone($tzid); + } + } catch (\Exception $e) { + } + + return null; + } + + /** + * This method returns an array of timezone identifiers, that are supported + * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). + * + * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: + * - It's not supported by some PHP versions as well as HHVM. + * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. + * (See timezonedata/php-bc.php and timezonedata php-workaround.php) + */ + private function getIdentifiersBC(): array + { + return include __DIR__.'/../timezonedata/php-bc.php'; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/FindFromTimezoneMap.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/FindFromTimezoneMap.php new file mode 100644 index 0000000000..83466b28e3 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/FindFromTimezoneMap.php @@ -0,0 +1,76 @@ +hasTzInMap($tzid)) { + return new \DateTimeZone($this->getTzFromMap($tzid)); + } + + // Some Microsoft products prefix the offset first, so let's strip that off + // and see if it is our tzid map. We don't want to check for this first just + // in case there are overrides in our tzid map. + foreach ($this->patterns as $pattern) { + if (!preg_match($pattern, $tzid, $matches)) { + continue; + } + $tzidAlternate = $matches[3]; + if ($this->hasTzInMap($tzidAlternate)) { + return new \DateTimeZone($this->getTzFromMap($tzidAlternate)); + } + } + + return null; + } + + /** + * This method returns an array of timezone identifiers, that are supported + * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). + * + * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: + * - It's not supported by some PHP versions as well as HHVM. + * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. + * (See timezonedata/php-bc.php and timezonedata php-workaround.php) + */ + private function getTzMaps(): array + { + if ([] === $this->map) { + $this->map = array_merge( + include __DIR__.'/../timezonedata/windowszones.php', + include __DIR__.'/../timezonedata/lotuszones.php', + include __DIR__.'/../timezonedata/exchangezones.php', + include __DIR__.'/../timezonedata/php-workaround.php' + ); + } + + return $this->map; + } + + private function getTzFromMap(string $tzid): string + { + return $this->getTzMaps()[$tzid]; + } + + private function hasTzInMap(string $tzid): bool + { + return isset($this->getTzMaps()[$tzid]); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/GuessFromLicEntry.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/GuessFromLicEntry.php new file mode 100644 index 0000000000..486b337961 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/GuessFromLicEntry.php @@ -0,0 +1,32 @@ +{'X-LIC-LOCATION'})) { + return null; + } + + $lic = (string) $vtimezone->{'X-LIC-LOCATION'}; + + // Libical generators may specify strings like + // "SystemV/EST5EDT". For those we must remove the + // SystemV part. + if ('SystemV/' === substr($lic, 0, 8)) { + $lic = substr($lic, 8); + } + + return TimeZoneUtil::getTimeZone($lic, null, $failIfUncertain); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/GuessFromMsTzId.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/GuessFromMsTzId.php new file mode 100644 index 0000000000..392333cb08 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/GuessFromMsTzId.php @@ -0,0 +1,118 @@ + 'UTC', + 31 => 'Africa/Casablanca', + + // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. + // I'm not even kidding. We handle this special case in the + // getTimeZone method. + 2 => 'Europe/Lisbon', + 1 => 'Europe/London', + 4 => 'Europe/Berlin', + 6 => 'Europe/Prague', + 3 => 'Europe/Paris', + 69 => 'Africa/Luanda', // This was a best guess + 7 => 'Europe/Athens', + 5 => 'Europe/Bucharest', + 49 => 'Africa/Cairo', + 50 => 'Africa/Harare', + 59 => 'Europe/Helsinki', + 27 => 'Asia/Jerusalem', + 26 => 'Asia/Baghdad', + 74 => 'Asia/Kuwait', + 51 => 'Europe/Moscow', + 56 => 'Africa/Nairobi', + 25 => 'Asia/Tehran', + 24 => 'Asia/Muscat', // Best guess + 54 => 'Asia/Baku', + 48 => 'Asia/Kabul', + 58 => 'Asia/Yekaterinburg', + 47 => 'Asia/Karachi', + 23 => 'Asia/Calcutta', + 62 => 'Asia/Kathmandu', + 46 => 'Asia/Almaty', + 71 => 'Asia/Dhaka', + 66 => 'Asia/Colombo', + 61 => 'Asia/Rangoon', + 22 => 'Asia/Bangkok', + 64 => 'Asia/Krasnoyarsk', + 45 => 'Asia/Shanghai', + 63 => 'Asia/Irkutsk', + 21 => 'Asia/Singapore', + 73 => 'Australia/Perth', + 75 => 'Asia/Taipei', + 20 => 'Asia/Tokyo', + 72 => 'Asia/Seoul', + 70 => 'Asia/Yakutsk', + 19 => 'Australia/Adelaide', + 44 => 'Australia/Darwin', + 18 => 'Australia/Brisbane', + 76 => 'Australia/Sydney', + 43 => 'Pacific/Guam', + 42 => 'Australia/Hobart', + 68 => 'Asia/Vladivostok', + 41 => 'Asia/Magadan', + 17 => 'Pacific/Auckland', + 40 => 'Pacific/Fiji', + 67 => 'Pacific/Tongatapu', + 29 => 'Atlantic/Azores', + 53 => 'Atlantic/Cape_Verde', + 30 => 'America/Noronha', + 8 => 'America/Sao_Paulo', // Best guess + 32 => 'America/Argentina/Buenos_Aires', + 60 => 'America/Godthab', + 28 => 'America/St_Johns', + 9 => 'America/Halifax', + 33 => 'America/Caracas', + 65 => 'America/Santiago', + 35 => 'America/Bogota', + 10 => 'America/New_York', + 34 => 'America/Indiana/Indianapolis', + 55 => 'America/Guatemala', + 11 => 'America/Chicago', + 37 => 'America/Mexico_City', + 36 => 'America/Edmonton', + 38 => 'America/Phoenix', + 12 => 'America/Denver', // Best guess + 13 => 'America/Los_Angeles', // Best guess + 14 => 'America/Anchorage', + 15 => 'Pacific/Honolulu', + 16 => 'Pacific/Midway', + 39 => 'Pacific/Kwajalein', + ]; + + public function guess(VTimeZone $vtimezone, ?bool $failIfUncertain = false): ?\DateTimeZone + { + // Microsoft may add a magic number, which we also have an + // answer for. + if (!isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) { + return null; + } + $cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue(); + + // 2 can mean both Europe/Lisbon and Europe/Sarajevo. + if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) { + return new \DateTimeZone('Europe/Sarajevo'); + } + + if (isset(self::$microsoftExchangeMap[$cdoId])) { + return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]); + } + + return null; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/TimezoneFinder.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/TimezoneFinder.php new file mode 100644 index 0000000000..220459527e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/TimezoneGuesser/TimezoneFinder.php @@ -0,0 +1,8 @@ +getDocumentType(); + if ($inputVersion === $targetVersion) { + return clone $input; + } + + if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40])) { + throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data'); + } + if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40])) { + throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version'); + } + + $newVersion = Document::VCARD40 === $targetVersion ? '4.0' : '3.0'; + + $output = new Component\VCard([ + 'VERSION' => $newVersion, + ]); + + // We might have generated a default UID. Remove it! + unset($output->UID); + + foreach ($input->children() as $property) { + $this->convertProperty($input, $output, $property, $targetVersion); + } + + return $output; + } + + /** + * Handles conversion of a single property. + * + * @throws InvalidDataException + */ + protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, int $targetVersion): void + { + // Skipping these, those are automatically added. + if (in_array($property->name, ['VERSION', 'PRODID'])) { + return; + } + + $parameters = $property->parameters(); + $valueType = null; + if (isset($parameters['VALUE'])) { + $valueType = $parameters['VALUE']->getValue(); + unset($parameters['VALUE']); + } + if (!$valueType) { + $valueType = $property->getValueType(); + } + if (Document::VCARD30 !== $targetVersion && 'PHONE-NUMBER' === $valueType) { + $valueType = null; + } + $newProperty = $output->createProperty( + $property->name, + $property->getParts(), + [], // parameters will get added a bit later. + $valueType + ); + + if (Document::VCARD30 === $targetVersion) { + if ($property instanceof Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { + /** @var Uri $newProperty */ + $newProperty = $this->convertUriToBinary($output, $newProperty); + } elseif ($property instanceof Property\VCard\DateAndOrTime) { + // In vCard 4, the birth year may be optional. This is not the + // case for vCard 3. Apple has a workaround for this that + // allows applications that support Apple's extension still + // omit birth years in vCard 3, but applications that do not + // support this, will just use a random birth year. We're + // choosing 1604 for the birth year, because that's what apple + // uses. + $parts = DateTimeParser::parseVCardDateTime($property->getValue()); + if (is_null($parts['year'])) { + $newValue = '1604-'.$parts['month'].'-'.$parts['date']; + $newProperty->setValue($newValue); + $newProperty['X-APPLE-OMIT-YEAR'] = '1604'; + } + + if ('ANNIVERSARY' == $newProperty->name) { + // Microsoft non-standard anniversary + $newProperty->name = 'X-ANNIVERSARY'; + + // We also need to add a new apple property for the same + // purpose. This apple property needs a 'label' in the same + // group, so we first need to find a groupname that doesn't + // exist yet. + $x = 1; + while ($output->select('ITEM'.$x.'.')) { + ++$x; + } + $output->add('ITEM'.$x.'.X-ABDATE', $newProperty->getValue(), ['VALUE' => 'DATE-AND-OR-TIME']); + $output->add('ITEM'.$x.'.X-ABLABEL', '_$!!$_'); + } + } elseif ('KIND' === $property->name) { + switch (strtolower($property->getValue())) { + case 'org': + // vCard 3.0 does not have an equivalent to KIND:ORG, + // but apple has an extension that means the same + // thing. + $newProperty = $output->createProperty('X-ABSHOWAS', 'COMPANY'); + break; + + case 'individual': + // Individual is implicit, so we skip it. + return; + + case 'group': + // OS X addressbook property + $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND', 'GROUP'); + break; + } + } elseif ('MEMBER' === $property->name) { + $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-MEMBER', $property->getValue()); + } + } elseif (Document::VCARD40 === $targetVersion) { + // These properties were removed in vCard 4.0 + if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) { + return; + } + + if ($property instanceof Binary) { + /** @var Binary $newProperty */ + $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters); + } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) { + // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR', + // then we're stripping the year from the vcard 4 value. + $parts = DateTimeParser::parseVCardDateTime($property->getValue()); + if ($parts['year'] === $property['X-APPLE-OMIT-YEAR']->getValue()) { + $newValue = '--'.$parts['month'].'-'.$parts['date']; + $newProperty->setValue($newValue); + } + + // Regardless if the year matched or not, we do need to strip + // X-APPLE-OMIT-YEAR. + unset($parameters['X-APPLE-OMIT-YEAR']); + } + switch ($property->name) { + case 'X-ABSHOWAS': + if ('COMPANY' === strtoupper($property->getValue())) { + $newProperty = $output->createProperty('KIND', 'ORG'); + } + break; + case 'X-ADDRESSBOOKSERVER-KIND': + if ('GROUP' === strtoupper($property->getValue())) { + $newProperty = $output->createProperty('KIND', 'GROUP'); + } + break; + case 'X-ADDRESSBOOKSERVER-MEMBER': + $newProperty = $output->createProperty('MEMBER', $property->getValue()); + break; + case 'X-ANNIVERSARY': + $newProperty->name = 'ANNIVERSARY'; + // If we already have an anniversary property with the same + // value, ignore. + foreach ($output->select('ANNIVERSARY') as $anniversary) { + if ($anniversary->getValue() === $newProperty->getValue()) { + return; + } + } + break; + case 'X-ABDATE': + // Find out what the label was, if it exists. + if (!$property->group) { + break; + } + $label = $input->{$property->group.'.X-ABLABEL'}; + + // We only support converting anniversaries. + if (!$label || '_$!!$_' !== $label->getValue()) { + break; + } + + // If we already have an anniversary property with the same + // value, ignore. + foreach ($output->select('ANNIVERSARY') as $anniversary) { + if ($anniversary->getValue() === $newProperty->getValue()) { + return; + } + } + $newProperty->name = 'ANNIVERSARY'; + break; + // Apple's per-property label system. + case 'X-ABLABEL': + if ('_$!!$_' === $newProperty->getValue()) { + // We can safely remove these, as they are converted to + // ANNIVERSARY properties. + return; + } + break; + } + } + + // set property group + $newProperty->group = $property->group; + + if (Document::VCARD40 === $targetVersion) { + $this->convertParameters40($newProperty, $parameters); + } else { + $this->convertParameters30($newProperty, $parameters); + } + + // Lastly, we need to see if there's a need for a VALUE parameter. + // + // We can do that by instantiating an empty property with that name, and + // seeing if the default valueType is identical to the current one. + $tempProperty = $output->createProperty($newProperty->name); + if ($tempProperty->getValueType() !== $newProperty->getValueType()) { + $newProperty['VALUE'] = $newProperty->getValueType(); + } + + $output->add($newProperty); + } + + /** + * Converts a BINARY property to a URI property. + * + * vCard 4.0 no longer supports BINARY properties. + * + * @param array $parameters list of parameters that will eventually be added to + * the new property + * + * @throws InvalidDataException + */ + protected function convertBinaryToUri(Component\VCard $output, Binary $newProperty, array &$parameters): Uri + { + $value = $newProperty->getValue(); + /** @var Uri $newProperty */ + $newProperty = $output->createProperty( + $newProperty->name, + null, // no value + [], // no parameters yet + 'URI' // Forcing the BINARY type + ); + + $mimeType = 'application/octet-stream'; + + // See if we can find a better mimetype. + if (isset($parameters['TYPE'])) { + $newTypes = []; + foreach ($parameters['TYPE']->getParts() as $typePart) { + if (in_array( + strtoupper($typePart), + ['JPEG', 'PNG', 'GIF'] + )) { + $mimeType = 'image/'.strtolower($typePart); + } else { + $newTypes[] = $typePart; + } + } + + // If there were any parameters we're not converting to a + // mime-type, we need to keep them. + if ($newTypes) { + $parameters['TYPE']->setParts($newTypes); + } else { + unset($parameters['TYPE']); + } + } + + $newProperty->setValue('data:'.$mimeType.';base64,'.base64_encode($value)); + + return $newProperty; + } + + /** + * Converts a URI property to a BINARY property. + * + * In vCard 4.0 attachments are encoded as data: uri. Even though these may + * be valid in vCard 3.0 as well, we should convert those to BINARY if + * possible, to improve compatibility. + * + * @return Binary|Uri|null + * + * @throws InvalidDataException + */ + protected function convertUriToBinary(Component\VCard $output, Uri $newProperty): Property + { + $value = $newProperty->getValue(); + + // Only converting data: uris + if ('data:' !== substr($value, 0, 5)) { + return $newProperty; + } + + /** @var Binary $newProperty */ + $newProperty = $output->createProperty( + $newProperty->name, + null, // no value + [], // no parameters yet + 'BINARY' + ); + + $mimeType = substr($value, 5, strpos($value, ',') - 5); + if (strpos($mimeType, ';')) { + $mimeType = substr($mimeType, 0, strpos($mimeType, ';')); + $newProperty->setValue(base64_decode(substr($value, strpos($value, ',') + 1))); + } else { + $newProperty->setValue(substr($value, strpos($value, ',') + 1)); + } + unset($value); + + $newProperty['ENCODING'] = 'b'; + switch ($mimeType) { + case 'image/jpeg': + $newProperty['TYPE'] = 'JPEG'; + break; + case 'image/png': + $newProperty['TYPE'] = 'PNG'; + break; + case 'image/gif': + $newProperty['TYPE'] = 'GIF'; + break; + } + + return $newProperty; + } + + /** + * Adds parameters to a new property for vCard 4.0. + */ + protected function convertParameters40(Property $newProperty, array $parameters): void + { + // Adding all parameters. + foreach ($parameters as $param) { + // vCard 2.1 allowed parameters with no name + if ($param->noName) { + $param->noName = false; + } + + switch ($param->name) { + // We need to see if there's any TYPE=PREF, because in vCard 4 + // that's now PREF=1. + case 'TYPE': + foreach ($param->getParts() as $paramPart) { + if ('PREF' === strtoupper($paramPart)) { + $newProperty->add('PREF', '1'); + } else { + $newProperty->add($param->name, $paramPart); + } + } + break; + // These no longer exist in vCard 4 + case 'ENCODING': + case 'CHARSET': + break; + + default: + $newProperty->add($param->name, $param->getParts()); + break; + } + } + } + + /** + * Adds parameters to a new property for vCard 3.0. + */ + protected function convertParameters30(Property $newProperty, array $parameters): void + { + // Adding all parameters. + foreach ($parameters as $param) { + // vCard 2.1 allowed parameters with no name + if ($param->noName) { + $param->noName = false; + } + + switch ($param->name) { + case 'ENCODING': + // This value only existed in vCard 2.1, and should be + // removed for anything else. + if ('QUOTED-PRINTABLE' !== strtoupper($param->getValue())) { + $newProperty->add($param->name, $param->getParts()); + } + break; + + /* + * Converting PREF=1 to TYPE=PREF. + * + * Any other PREF numbers we'll drop. + */ + case 'PREF': + if ('1' == $param->getValue()) { + $newProperty->add('TYPE', 'PREF'); + } + break; + + default: + $newProperty->add($param->name, $param->getParts()); + break; + } + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Version.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Version.php new file mode 100644 index 0000000000..893f272d32 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/Version.php @@ -0,0 +1,18 @@ +serialize(); + } + + /** + * Serializes a jCal or jCard object. + */ + public static function writeJson(Component $component, int $options = 0): string + { + return json_encode($component, $options); + } + + /** + * Serializes a xCal or xCard object. + */ + public static function writeXml(Component $component): string + { + $writer = new Xml\Writer(); + $writer->openMemory(); + $writer->setIndent(true); + + $writer->startDocument('1.0', 'utf-8'); + + if ($component instanceof Component\VCalendar) { + $writer->startElement('icalendar'); + $writer->writeAttribute('xmlns', Parser\XML::XCAL_NAMESPACE); + } else { + $writer->startElement('vcards'); + $writer->writeAttribute('xmlns', Parser\XML::XCARD_NAMESPACE); + } + + $component->xmlSerialize($writer); + + $writer->endElement(); + + return $writer->outputMemory(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/exchangezones.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/exchangezones.php new file mode 100644 index 0000000000..89bddc27c5 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/exchangezones.php @@ -0,0 +1,94 @@ + 'UTC', + 'Casablanca, Monrovia' => 'Africa/Casablanca', + 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon', + 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London', + 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', + 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague', + 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', + 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris', + 'Prague, Central Europe' => 'Europe/Prague', + 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo', + 'West Central Africa' => 'Africa/Luanda', // This was a best guess + 'Athens, Istanbul, Minsk' => 'Europe/Athens', + 'Bucharest' => 'Europe/Bucharest', + 'Cairo' => 'Africa/Cairo', + 'Harare, Pretoria' => 'Africa/Harare', + 'Helsinki, Riga, Tallinn' => 'Europe/Helsinki', + 'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem', + 'Baghdad' => 'Asia/Baghdad', + 'Arab, Kuwait, Riyadh' => 'Asia/Kuwait', + 'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow', + 'East Africa, Nairobi' => 'Africa/Nairobi', + 'Tehran' => 'Asia/Tehran', + 'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess + 'Baku, Tbilisi, Yerevan' => 'Asia/Baku', + 'Kabul' => 'Asia/Kabul', + 'Ekaterinburg' => 'Asia/Yekaterinburg', + 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi', + 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta', + 'Kathmandu, Nepal' => 'Asia/Kathmandu', + 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty', + 'Astana, Dhaka' => 'Asia/Dhaka', + 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo', + 'Rangoon' => 'Asia/Rangoon', + 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', + 'Krasnoyarsk' => 'Asia/Krasnoyarsk', + 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai', + 'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk', + 'Kuala Lumpur, Singapore' => 'Asia/Singapore', + 'Perth, Western Australia' => 'Australia/Perth', + 'Taipei' => 'Asia/Taipei', + 'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo', + 'Seoul, Korea Standard time' => 'Asia/Seoul', + 'Yakutsk' => 'Asia/Yakutsk', + 'Adelaide, Central Australia' => 'Australia/Adelaide', + 'Darwin' => 'Australia/Darwin', + 'Brisbane, East Australia' => 'Australia/Brisbane', + 'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney', + 'Guam, Port Moresby' => 'Pacific/Guam', + 'Hobart, Tasmania' => 'Australia/Hobart', + 'Vladivostok' => 'Asia/Vladivostok', + 'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan', + 'Auckland, Wellington' => 'Pacific/Auckland', + 'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji', + 'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu', + 'Azores' => 'Atlantic/Azores', + 'Cape Verde Is.' => 'Atlantic/Cape_Verde', + 'Mid-Atlantic' => 'America/Noronha', + 'Brasilia' => 'America/Sao_Paulo', // Best guess + 'Buenos Aires' => 'America/Argentina/Buenos_Aires', + 'Greenland' => 'America/Godthab', + 'Newfoundland' => 'America/St_Johns', + 'Atlantic Time (Canada)' => 'America/Halifax', + 'Caracas, La Paz' => 'America/Caracas', + 'Santiago' => 'America/Santiago', + 'Bogota, Lima, Quito' => 'America/Bogota', + 'Eastern Time (US & Canada)' => 'America/New_York', + 'Indiana (East)' => 'America/Indiana/Indianapolis', + 'Central America' => 'America/Guatemala', + 'Central Time (US & Canada)' => 'America/Chicago', + 'Mexico City, Tegucigalpa' => 'America/Mexico_City', + 'Saskatchewan' => 'America/Edmonton', + 'Arizona' => 'America/Phoenix', + 'Mountain Time (US & Canada)' => 'America/Denver', // Best guess + 'Pacific Time (US & Canada)' => 'America/Los_Angeles', // Best guess + 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess + 'Alaska' => 'America/Anchorage', + 'Hawaii' => 'Pacific/Honolulu', + 'Midway Island, Samoa' => 'Pacific/Midway', + 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein', +]; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/lotuszones.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/lotuszones.php new file mode 100644 index 0000000000..9115ac743d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/lotuszones.php @@ -0,0 +1,101 @@ + 'Etc/GMT-12', + 'Samoa' => 'Pacific/Apia', + 'Hawaiian' => 'Pacific/Honolulu', + 'Alaskan' => 'America/Anchorage', + 'Pacific' => 'America/Los_Angeles', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Mexico Standard Time 2' => 'America/Chihuahua', + 'Mountain' => 'America/Denver', + // 'Mountain Standard Time' => 'America/Chihuahua', // conflict with windows timezones. + 'US Mountain' => 'America/Phoenix', + 'Canada Central' => 'America/Edmonton', + 'Central America' => 'America/Guatemala', + 'Central' => 'America/Chicago', + // 'Central Standard Time' => 'America/Mexico_City', // conflict with windows timezones. + 'Mexico' => 'America/Mexico_City', + 'Eastern' => 'America/New_York', + 'SA Pacific' => 'America/Bogota', + 'US Eastern' => 'America/Indiana/Indianapolis', + 'Venezuela' => 'America/Caracas', + 'Atlantic' => 'America/Halifax', + 'Central Brazilian' => 'America/Manaus', + 'Pacific SA' => 'America/Santiago', + 'SA Western' => 'America/La_Paz', + 'Newfoundland' => 'America/St_Johns', + 'Argentina' => 'America/Argentina/Buenos_Aires', + 'E. South America' => 'America/Belem', + 'Greenland' => 'America/Godthab', + 'Montevideo' => 'America/Montevideo', + 'SA Eastern' => 'America/Belem', + // 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones. + 'Azores' => 'Atlantic/Azores', + 'Cape Verde' => 'Atlantic/Cape_Verde', + 'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious. Greenwich is not GMT. + 'Morocco' => 'Africa/Casablanca', + 'Central Europe' => 'Europe/Prague', + 'Central European' => 'Europe/Sarajevo', + 'Romance' => 'Europe/Paris', + 'W. Central Africa' => 'Africa/Lagos', // Best guess + 'W. Europe' => 'Europe/Amsterdam', + 'E. Europe' => 'Europe/Minsk', + 'Egypt' => 'Africa/Cairo', + 'FLE' => 'Europe/Helsinki', + 'GTB' => 'Europe/Athens', + 'Israel' => 'Asia/Jerusalem', + 'Jordan' => 'Asia/Amman', + 'Middle East' => 'Asia/Beirut', + 'Namibia' => 'Africa/Windhoek', + 'South Africa' => 'Africa/Harare', + 'Arab' => 'Asia/Kuwait', + 'Arabic' => 'Asia/Baghdad', + 'E. Africa' => 'Africa/Nairobi', + 'Georgian' => 'Asia/Tbilisi', + 'Russian' => 'Europe/Moscow', + 'Iran' => 'Asia/Tehran', + 'Arabian' => 'Asia/Muscat', + 'Armenian' => 'Asia/Yerevan', + 'Azerbijan' => 'Asia/Baku', + 'Caucasus' => 'Asia/Yerevan', + 'Mauritius' => 'Indian/Mauritius', + 'Afghanistan' => 'Asia/Kabul', + 'Ekaterinburg' => 'Asia/Yekaterinburg', + 'Pakistan' => 'Asia/Karachi', + 'West Asia' => 'Asia/Tashkent', + 'India' => 'Asia/Calcutta', + 'Sri Lanka' => 'Asia/Colombo', + 'Nepal' => 'Asia/Kathmandu', + 'Central Asia' => 'Asia/Dhaka', + 'N. Central Asia' => 'Asia/Almaty', + 'Myanmar' => 'Asia/Rangoon', + 'North Asia' => 'Asia/Krasnoyarsk', + 'SE Asia' => 'Asia/Bangkok', + 'China' => 'Asia/Shanghai', + 'North Asia East' => 'Asia/Irkutsk', + 'Singapore' => 'Asia/Singapore', + 'Taipei' => 'Asia/Taipei', + 'W. Australia' => 'Australia/Perth', + 'Korea' => 'Asia/Seoul', + 'Tokyo' => 'Asia/Tokyo', + 'Yakutsk' => 'Asia/Yakutsk', + 'AUS Central' => 'Australia/Darwin', + 'Cen. Australia' => 'Australia/Adelaide', + 'AUS Eastern' => 'Australia/Sydney', + 'E. Australia' => 'Australia/Brisbane', + 'Tasmania' => 'Australia/Hobart', + 'Vladivostok' => 'Asia/Vladivostok', + 'West Pacific' => 'Pacific/Guam', + 'Central Pacific' => 'Asia/Magadan', + 'Fiji' => 'Pacific/Fiji', + 'New Zealand' => 'Pacific/Auckland', + 'Tonga' => 'Pacific/Tongatapu', +]; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/php-bc.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/php-bc.php new file mode 100644 index 0000000000..3116c6868e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/php-bc.php @@ -0,0 +1,152 @@ + 'America/Chicago', + 'Cuba' => 'America/Havana', + 'Egypt' => 'Africa/Cairo', + 'Eire' => 'Europe/Dublin', + 'EST5EDT' => 'America/New_York', + 'Factory' => 'UTC', + 'GB-Eire' => 'Europe/London', + 'GMT0' => 'UTC', + 'Greenwich' => 'UTC', + 'Hongkong' => 'Asia/Hong_Kong', + 'Iceland' => 'Atlantic/Reykjavik', + 'Iran' => 'Asia/Tehran', + 'Israel' => 'Asia/Jerusalem', + 'Jamaica' => 'America/Jamaica', + 'Japan' => 'Asia/Tokyo', + 'Kwajalein' => 'Pacific/Kwajalein', + 'Libya' => 'Africa/Tripoli', + 'MST7MDT' => 'America/Denver', + 'Navajo' => 'America/Denver', + 'NZ-CHAT' => 'Pacific/Chatham', + 'Poland' => 'Europe/Warsaw', + 'Portugal' => 'Europe/Lisbon', + 'PST8PDT' => 'America/Los_Angeles', + 'Singapore' => 'Asia/Singapore', + 'Turkey' => 'Europe/Istanbul', + 'Universal' => 'UTC', + 'W-SU' => 'Europe/Moscow', + 'Zulu' => 'UTC', +]; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/windowszones.php b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/windowszones.php new file mode 100644 index 0000000000..335007983a --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/VObject/timezonedata/windowszones.php @@ -0,0 +1,152 @@ + 'Australia/Darwin', + 'AUS Eastern Standard Time' => 'Australia/Sydney', + 'Afghanistan Standard Time' => 'Asia/Kabul', + 'Alaskan Standard Time' => 'America/Anchorage', + 'Aleutian Standard Time' => 'America/Adak', + 'Altai Standard Time' => 'Asia/Barnaul', + 'Arab Standard Time' => 'Asia/Riyadh', + 'Arabian Standard Time' => 'Asia/Dubai', + 'Arabic Standard Time' => 'Asia/Baghdad', + 'Argentina Standard Time' => 'America/Buenos_Aires', + 'Astrakhan Standard Time' => 'Europe/Astrakhan', + 'Atlantic Standard Time' => 'America/Halifax', + 'Aus Central W. Standard Time' => 'Australia/Eucla', + 'Azerbaijan Standard Time' => 'Asia/Baku', + 'Azores Standard Time' => 'Atlantic/Azores', + 'Bahia Standard Time' => 'America/Bahia', + 'Bangladesh Standard Time' => 'Asia/Dhaka', + 'Belarus Standard Time' => 'Europe/Minsk', + 'Bougainville Standard Time' => 'Pacific/Bougainville', + 'Canada Central Standard Time' => 'America/Regina', + 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', + 'Caucasus Standard Time' => 'Asia/Yerevan', + 'Cen. Australia Standard Time' => 'Australia/Adelaide', + 'Central America Standard Time' => 'America/Guatemala', + 'Central Asia Standard Time' => 'Asia/Almaty', + 'Central Brazilian Standard Time' => 'America/Cuiaba', + 'Central Europe Standard Time' => 'Europe/Budapest', + 'Central European Standard Time' => 'Europe/Warsaw', + 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', + 'Central Standard Time' => 'America/Chicago', + 'Central Standard Time (Mexico)' => 'America/Mexico_City', + 'Chatham Islands Standard Time' => 'Pacific/Chatham', + 'China Standard Time' => 'Asia/Shanghai', + 'Cuba Standard Time' => 'America/Havana', + 'Dateline Standard Time' => 'Etc/GMT+12', + 'E. Africa Standard Time' => 'Africa/Nairobi', + 'E. Australia Standard Time' => 'Australia/Brisbane', + 'E. Europe Standard Time' => 'Europe/Chisinau', + 'E. South America Standard Time' => 'America/Sao_Paulo', + 'Easter Island Standard Time' => 'Pacific/Easter', + 'Eastern Standard Time' => 'America/New_York', + 'Eastern Standard Time (Mexico)' => 'America/Cancun', + 'Egypt Standard Time' => 'Africa/Cairo', + 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', + 'FLE Standard Time' => 'Europe/Kiev', + 'Fiji Standard Time' => 'Pacific/Fiji', + 'GMT Standard Time' => 'Europe/London', + 'GTB Standard Time' => 'Europe/Bucharest', + 'Georgian Standard Time' => 'Asia/Tbilisi', + 'Greenland Standard Time' => 'America/Godthab', + 'Greenwich Standard Time' => 'Atlantic/Reykjavik', + 'Haiti Standard Time' => 'America/Port-au-Prince', + 'Hawaiian Standard Time' => 'Pacific/Honolulu', + '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', + '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', + 'Middle East Standard Time' => 'Asia/Beirut', + 'Montevideo Standard Time' => 'America/Montevideo', + 'Morocco Standard Time' => 'Africa/Casablanca', + 'Mountain Standard Time' => 'America/Denver', + 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', + 'Myanmar Standard Time' => 'Asia/Rangoon', + 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', + 'Namibia Standard Time' => 'Africa/Windhoek', + 'Nepal Standard Time' => 'Asia/Katmandu', + 'New Zealand Standard Time' => 'Pacific/Auckland', + 'Newfoundland Standard Time' => 'America/St_Johns', + 'Norfolk Standard Time' => 'Pacific/Norfolk', + 'North Asia East Standard Time' => 'Asia/Irkutsk', + 'North Asia Standard Time' => 'Asia/Krasnoyarsk', + 'North Korea Standard Time' => 'Asia/Pyongyang', + 'Omsk Standard Time' => 'Asia/Omsk', + 'Pacific SA Standard Time' => 'America/Santiago', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Pacific Standard Time (Mexico)' => 'America/Tijuana', + 'Pakistan Standard Time' => 'Asia/Karachi', + 'Paraguay Standard Time' => 'America/Asuncion', + 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', + 'Romance Standard Time' => 'Europe/Paris', + 'Russia Time Zone 10' => 'Asia/Srednekolymsk', + 'Russia Time Zone 11' => 'Asia/Kamchatka', + 'Russia Time Zone 3' => 'Europe/Samara', + 'Russian Standard Time' => 'Europe/Moscow', + 'SA Eastern Standard Time' => 'America/Cayenne', + 'SA Pacific Standard Time' => 'America/Bogota', + 'SA Western Standard Time' => 'America/La_Paz', + 'SE Asia Standard Time' => 'Asia/Bangkok', + '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', + 'Singapore Standard Time' => 'Asia/Singapore', + 'South Africa Standard Time' => 'Africa/Johannesburg', + '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/Hobart', + 'Tocantins Standard Time' => 'America/Araguaina', + 'Tokyo Standard Time' => 'Asia/Tokyo', + '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', + 'US Eastern Standard Time' => 'America/Indianapolis', + 'US Mountain Standard Time' => 'America/Phoenix', + 'UTC' => 'Etc/GMT', + 'UTC+12' => 'Etc/GMT-12', + 'UTC+13' => 'Etc/GMT-13', + 'UTC-02' => 'Etc/GMT+2', + 'UTC-08' => 'Etc/GMT+8', + 'UTC-09' => 'Etc/GMT+9', + 'UTC-11' => 'Etc/GMT+11', + 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', + 'Venezuela Standard Time' => 'America/Caracas', + 'Vladivostok Standard Time' => 'Asia/Vladivostok', + 'Volgograd Standard Time' => 'Europe/Volgograd', + 'W. Australia Standard Time' => 'Australia/Perth', + 'W. Central Africa Standard Time' => 'Africa/Lagos', + 'W. Europe Standard Time' => 'Europe/Berlin', + 'W. Mongolia Standard Time' => 'Asia/Hovd', + 'West Asia Standard Time' => 'Asia/Tashkent', + 'West Bank Standard Time' => 'Asia/Hebron', + 'West Pacific Standard Time' => 'Pacific/Port_Moresby', + 'Yakutsk Standard Time' => 'Asia/Yakutsk', + 'Yukon Standard Time' => 'America/Whitehorse', +]; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/ContextStackTrait.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/ContextStackTrait.php new file mode 100644 index 0000000000..7c082f5fbe --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/ContextStackTrait.php @@ -0,0 +1,116 @@ + + */ + public array $elementMap = []; + + /** + * A contextUri pointing to the document being parsed / written. + * This uri may be used to resolve relative urls that may appear in the + * document. + * + * The reader and writer don't use this property, but as it's an extremely + * common use-case for parsing XML documents, it's added here as a + * convenience. + */ + public ?string $contextUri = null; + + /** + * This is a list of namespaces that you want to give default prefixes. + * + * You must make sure you create this entire list before starting to write. + * They should be registered on the root element. + * + * @phpstan-var array + */ + public array $namespaceMap = []; + + /** + * This is a list of custom serializers for specific classes. + * + * The writer may use this if you attempt to serialize an object with a + * class that does not implement XmlSerializable. + * + * Instead, it will look at this classmap to see if there is a custom + * serializer here. This is useful if you don't want your value objects + * to be responsible for serializing themselves. + * + * The keys in this classmap need to be fully qualified PHP class names, + * the values must be callbacks. The callbacks take two arguments. The + * writer class, and the value that must be written. + * + * function (Writer $writer, object $value) + * + * @phpstan-var array + */ + public array $classMap = []; + + /** + * Backups of previous contexts. + * + * @var list + */ + protected array $contextStack = []; + + /** + * Create a new "context". + * + * This allows you to safely modify the elementMap, contextUri or + * namespaceMap. After you're done, you can restore the old data again + * with popContext. + */ + public function pushContext(): void + { + $this->contextStack[] = [ + $this->elementMap, + $this->contextUri, + $this->namespaceMap, + $this->classMap, + ]; + } + + /** + * Restore the previous "context". + */ + public function popContext(): void + { + list( + $this->elementMap, + $this->contextUri, + $this->namespaceMap, + $this->classMap, + ) = array_pop($this->contextStack); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Deserializer/functions.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Deserializer/functions.php new file mode 100644 index 0000000000..949aa720e8 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Deserializer/functions.php @@ -0,0 +1,239 @@ +value" array. + * + * For example, keyvalue will parse: + * + * + * + * value1 + * value2 + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1" => "value1", + * "{http://sabredav.org/ns}elem2" => "value2", + * "{http://sabredav.org/ns}elem3" => null, + * ]; + * + * If you specify the 'namespace' argument, the deserializer will remove + * the namespaces of the keys that match that namespace. + * + * For example, if you call keyValue like this: + * + * keyValue($reader, 'http://sabredav.org/ns') + * + * it's output will instead be: + * + * [ + * "elem1" => "value1", + * "elem2" => "value2", + * "elem3" => null, + * ]; + * + * Attributes will be removed from the top-level elements. If elements with + * the same name appear twice in the list, only the last one will be kept. + * + * @phpstan-return array + */ +function keyValue(Reader $reader, ?string $namespace = null): array +{ + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + + $values = []; + + do { + if (Reader::ELEMENT === $reader->nodeType) { + if (null !== $namespace && $reader->namespaceURI === $namespace) { + $values[$reader->localName] = $reader->parseCurrentElement()['value']; + } else { + $clark = $reader->getClark(); + $values[$clark] = $reader->parseCurrentElement()['value']; + } + } else { + if (!$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; +} + +/** + * The 'enum' deserializer parses elements into a simple list + * without values or attributes. + * + * For example, Elements will parse: + * + * + * + * + * + * + * content + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1", + * "{http://sabredav.org/ns}elem2", + * "{http://sabredav.org/ns}elem3", + * "{http://sabredav.org/ns}elem4", + * "{http://sabredav.org/ns}elem5", + * ]; + * + * This is useful for 'enum'-like structures. + * + * If the $namespace argument is specified, it will strip the namespace + * for all elements that match that. + * + * For example, + * + * enum($reader, 'http://sabredav.org/ns') + * + * would return: + * + * [ + * "elem1", + * "elem2", + * "elem3", + * "elem4", + * "elem5", + * ]; + * + * @return string[] + * + * @phpstan-return list + */ +function enum(Reader $reader, ?string $namespace = null): array +{ + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + $currentDepth = $reader->depth; + + $values = []; + do { + if (Reader::ELEMENT !== $reader->nodeType) { + continue; + } + if (!is_null($namespace) && $namespace === $reader->namespaceURI) { + $values[] = $reader->localName; + } else { + $values[] = (string) $reader->getClark(); + } + } while ($reader->depth >= $currentDepth && $reader->next()); + + $reader->next(); + + return $values; +} + +/** + * The valueObject deserializer turns an XML element into a PHP object of + * a specific class. + * + * This is primarily used by the mapValueObject function from the Service + * class, but it can also easily be used for more specific situations. + * + * @template C of object + * + * @param class-string $className + * + * @phpstan-return C + */ +function valueObject(Reader $reader, string $className, string $namespace): object +{ + $valueObject = new $className(); + if ($reader->isEmptyElement) { + $reader->next(); + + return $valueObject; + } + + $defaultProperties = get_class_vars($className); + + $reader->read(); + do { + if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) { + if (property_exists($valueObject, $reader->localName)) { + if (is_array($defaultProperties[$reader->localName])) { + $valueObject->{$reader->localName}[] = $reader->parseCurrentElement()['value']; + } else { + $valueObject->{$reader->localName} = $reader->parseCurrentElement()['value']; + } + } else { + // Ignore property + $reader->next(); + } + } elseif (Reader::ELEMENT === $reader->nodeType) { + // Skipping element from different namespace + $reader->next(); + } else { + if (Reader::END_ELEMENT !== $reader->nodeType && !$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $valueObject; +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element.php new file mode 100644 index 0000000000..559eb54ea3 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element.php @@ -0,0 +1,22 @@ +value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $writer->write($this->value); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return array>|string|null + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + $subTree = $reader->parseInnerTree(); + + return $subTree; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/Cdata.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/Cdata.php new file mode 100644 index 0000000000..8e16afbae9 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/Cdata.php @@ -0,0 +1,57 @@ +value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $writer->writeCData($this->value); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/Elements.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/Elements.php new file mode 100644 index 0000000000..b62a2f3ea4 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/Elements.php @@ -0,0 +1,104 @@ + + * + * + * + * + * content + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1", + * "{http://sabredav.org/ns}elem2", + * "{http://sabredav.org/ns}elem3", + * "{http://sabredav.org/ns}elem4", + * "{http://sabredav.org/ns}elem5", + * ]; + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Elements implements Xml\Element +{ + /** + * Value to serialize. + * + * @var array + */ + protected array $value; + + /** + * Constructor. + * + * @param array $value + */ + public function __construct(array $value = []) + { + $this->value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer): void + { + require_once __DIR__ . '/../Serializer/functions.php'; + Serializer\enum($writer, $this->value); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseSubTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return string[] + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + require_once __DIR__ . '/../Deserializer/functions.php'; + return Deserializer\enum($reader); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/KeyValue.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/KeyValue.php new file mode 100644 index 0000000000..54c31eb260 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/KeyValue.php @@ -0,0 +1,104 @@ +value struct. + * + * Attributes will be removed, and duplicate child elements are discarded. + * Complex values within the elements will be parsed by the 'standard' parser. + * + * For example, KeyValue will parse: + * + * + * + * value1 + * value2 + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1" => "value1", + * "{http://sabredav.org/ns}elem2" => "value2", + * "{http://sabredav.org/ns}elem3" => null, + * ]; + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class KeyValue implements Xml\Element +{ + /** + * Value to serialize. + * + * @var array + */ + protected array $value; + + /** + * Constructor. + * + * @param array $value + */ + public function __construct(array $value = []) + { + $this->value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer): void + { + require_once __DIR__ . '/Serializer/functions.php'; + $writer->write($this->value); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return array + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + require_once __DIR__ . '/../Deserializer/functions.php'; + return Deserializer\keyValue($reader); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/Uri.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/Uri.php new file mode 100644 index 0000000000..e54d706a9d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/Uri.php @@ -0,0 +1,95 @@ +/foo/bar + * http://example.org/hi + * + * If the uri is relative, it will be automatically expanded to an absolute + * url during writing and reading, if the contextUri property is set on the + * reader and/or writer. + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Uri implements Xml\Element +{ + /** + * Uri element value. + */ + protected string $value; + + /** + * Constructor. + */ + public function __construct(string $value) + { + $this->value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $writer->text( + resolve( + $writer->contextUri ?? '', + $this->value + ) + ); + } + + /** + * This method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseSubTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + return new self( + resolve( + (string) $reader->contextUri, + $reader->readText() + ) + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/XmlFragment.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/XmlFragment.php new file mode 100644 index 0000000000..94f745b4cc --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Element/XmlFragment.php @@ -0,0 +1,144 @@ +xml = $xml; + } + + /** + * Returns the inner XML document. + */ + public function getXml(): string + { + return $this->xml; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer): void + { + $reader = new Reader(); + + // Wrapping the xml in a container, so root-less values can still be + // parsed. + $xml = << +{$this->getXml()} +XML; + + $reader->xml($xml); + + while ($reader->read()) { + if ($reader->depth < 1) { + // Skipping the root node. + continue; + } + + switch ($reader->nodeType) { + case Reader::ELEMENT: + $writer->startElement( + (string) $reader->getClark() + ); + $empty = $reader->isEmptyElement; + while ($reader->moveToNextAttribute()) { + switch ($reader->namespaceURI) { + case '': + $writer->writeAttribute($reader->localName, $reader->value); + break; + case 'http://www.w3.org/2000/xmlns/': + // Skip namespace declarations + break; + default: + $writer->writeAttribute((string) $reader->getClark(), $reader->value); + break; + } + } + if ($empty) { + $writer->endElement(); + } + break; + case Reader::CDATA: + case Reader::TEXT: + $writer->text( + $reader->value + ); + break; + case Reader::END_ELEMENT: + $writer->endElement(); + break; + } + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Reader $reader) + { + $result = new self($reader->readInnerXml()); + $reader->next(); + + return $result; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/LibXMLException.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/LibXMLException.php new file mode 100644 index 0000000000..5c642a3bf4 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/LibXMLException.php @@ -0,0 +1,49 @@ +errors = $errors; + parent::__construct($errors[0]->message.' on line '.$errors[0]->line.', column '.$errors[0]->column, $code, $previousException); + } + + /** + * Returns the LibXML errors. + * + * @return \LibXMLError[] + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/ParseException.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/ParseException.php new file mode 100644 index 0000000000..ff6656b236 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/ParseException.php @@ -0,0 +1,16 @@ +localName) { + return null; + } + + return '{'.$this->namespaceURI.'}'.$this->localName; + } + + /** + * Reads the entire document. + * + * This function returns an array with the following three elements: + * * name - The root element name. + * * value - The value for the root element. + * * attributes - An array of attributes. + * + * This function will also disable the standard libxml error handler (which + * usually just results in PHP errors), and throw exceptions instead. + * + * @return array + */ + public function parse(): array + { + $previousEntityState = null; + $shouldCallLibxmlDisableEntityLoader = (\LIBXML_VERSION < 20900); + if ($shouldCallLibxmlDisableEntityLoader) { + $previousEntityState = libxml_disable_entity_loader(true); + } + $previousSetting = libxml_use_internal_errors(true); + + try { + while (self::ELEMENT !== $this->nodeType) { + if (!$this->read()) { + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + throw new LibXMLException($errors); + } + } + } + $result = $this->parseCurrentElement(); + + // last line of defense in case errors did occur above + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + throw new LibXMLException($errors); + } + } finally { + libxml_use_internal_errors($previousSetting); + if ($shouldCallLibxmlDisableEntityLoader) { + libxml_disable_entity_loader($previousEntityState); + } + } + + return $result; + } + + /** + * parseGetElements parses everything in the current sub-tree, + * and returns an array of elements. + * + * Each element has a 'name', 'value' and 'attributes' key. + * + * If the element didn't contain sub-elements, an empty array is always + * returned. If there was any text inside the element, it will be + * discarded. + * + * If the $elementMap argument is specified, the existing elementMap will + * be overridden while parsing the tree, and restored after this process. + * + * @param array|null $elementMap + * + * @return array> + */ + public function parseGetElements(?array $elementMap = null): array + { + $result = $this->parseInnerTree($elementMap); + if (!is_array($result)) { + return []; + } + + return $result; + } + + /** + * Parses all elements below the current element. + * + * This method will return a string if this was a text-node, or an array if + * there were sub-elements. + * + * If there's both text and sub-elements, the text will be discarded. + * + * If the $elementMap argument is specified, the existing elementMap will + * be overridden while parsing the tree, and restored after this process. + * + * @param array|null $elementMap + * + * @return array>|string|null + */ + public function parseInnerTree(?array $elementMap = null) + { + $text = null; + $elements = []; + + if (self::ELEMENT === $this->nodeType && $this->isEmptyElement) { + // Easy! + $this->next(); + + return null; + } + + if (!is_null($elementMap)) { + $this->pushContext(); + $this->elementMap = $elementMap; + } + + try { + if (!$this->read()) { + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + throw new LibXMLException($errors); + } + throw new ParseException('This should never happen (famous last words)'); + } + + $keepOnParsing = true; + + while ($keepOnParsing) { + if (!$this->isValid()) { + $errors = libxml_get_errors(); + + if ($errors) { + libxml_clear_errors(); + throw new LibXMLException($errors); + } + } + + switch ($this->nodeType) { + case self::ELEMENT: + $elements[] = $this->parseCurrentElement(); + break; + case self::TEXT: + case self::CDATA: + $text .= $this->value; + $this->read(); + break; + case self::END_ELEMENT: + // Ensuring we are moving the cursor after the end element. + $this->read(); + $keepOnParsing = false; + break; + case self::NONE: + throw new ParseException('We hit the end of the document prematurely. This likely means that some parser "eats" too many elements. Do not attempt to continue parsing.'); + default: + // Advance to the next element + $this->read(); + break; + } + } + } finally { + if (!is_null($elementMap)) { + $this->popContext(); + } + } + + return $elements ?: $text; + } + + /** + * Reads all text below the current element, and returns this as a string. + */ + public function readText(): string + { + $result = ''; + $previousDepth = $this->depth; + + while ($this->read() && $this->depth != $previousDepth) { + if (in_array($this->nodeType, [\XMLReader::TEXT, \XMLReader::CDATA, \XMLReader::WHITESPACE])) { + $result .= $this->value; + } + } + + return $result; + } + + /** + * Parses the current XML element. + * + * This method returns arn array with 3 properties: + * * name - A clark-notation XML element name. + * * value - The parsed value. + * * attributes - A key-value list of attributes. + * + * @return array + */ + public function parseCurrentElement(): array + { + $name = $this->getClark(); + + $attributes = []; + + if ($this->hasAttributes) { + $attributes = $this->parseAttributes(); + } + + $value = call_user_func( + $this->getDeserializerForElementName((string) $name), + $this + ); + + return [ + 'name' => $name, + 'value' => $value, + 'attributes' => $attributes, + ]; + } + + /** + * Grabs all the attributes from the current element, and returns them as a + * key-value array. + * + * If the attributes are part of the same namespace, they will simply be + * short keys. If they are defined on a different namespace, the attribute + * name will be returned in clark-notation. + * + * @return array + */ + public function parseAttributes(): array + { + $attributes = []; + + while ($this->moveToNextAttribute()) { + if ($this->namespaceURI) { + // Ignoring 'xmlns', it doesn't make any sense. + if ('http://www.w3.org/2000/xmlns/' === $this->namespaceURI) { + continue; + } + + $name = $this->getClark(); + $attributes[$name] = $this->value; + } else { + $attributes[$this->localName] = $this->value; + } + } + $this->moveToElement(); + + return $attributes; + } + + /** + * Returns the function that should be used to parse the element identified + * by its clark-notation name. + */ + public function getDeserializerForElementName(string $name): callable + { + if (!array_key_exists($name, $this->elementMap)) { + if ('{}' == substr($name, 0, 2) && array_key_exists(substr($name, 2), $this->elementMap)) { + $name = substr($name, 2); + } else { + return ['Sabre\\Xml\\Element\\Base', 'xmlDeserialize']; + } + } + + $deserializer = $this->elementMap[$name]; + if (is_callable($deserializer)) { + return $deserializer; + } + + if (is_subclass_of($deserializer, 'Sabre\\Xml\\XmlDeserializable')) { + return [$deserializer, 'xmlDeserialize']; + } + + throw new \LogicException('Could not use this type as a deserializer: '.get_debug_type($deserializer).' for element: '.$name); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Serializer/functions.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Serializer/functions.php new file mode 100644 index 0000000000..4e0856a0cc --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Serializer/functions.php @@ -0,0 +1,183 @@ + + * + * + * content + * + * + * @param string[] $values + */ +function enum(Writer $writer, array $values): void +{ + foreach ($values as $value) { + $writer->writeElement($value); + } +} + +/** + * The valueObject serializer turns a simple PHP object into a classname. + * + * Every public property will be encoded as an XML element with the same + * name, in the XML namespace as specified. + * + * Values that are set to null or an empty array are not serialized. To + * serialize empty properties, you must specify them as an empty string. + */ +function valueObject(Writer $writer, object $valueObject, string $namespace): void +{ + foreach (get_object_vars($valueObject) as $key => $val) { + if (is_array($val)) { + // If $val is an array, it has a special meaning. We need to + // generate one child element for each item in $val + foreach ($val as $child) { + $writer->writeElement('{'.$namespace.'}'.$key, $child); + } + } elseif (null !== $val) { + $writer->writeElement('{'.$namespace.'}'.$key, $val); + } + } +} + +/** + * This function is the 'default' serializer that is able to serialize most + * things, and delegates to other serializers if needed. + * + * The standardSerializer supports a wide-array of values. + * + * $value may be a string or integer, it will just write out the string as text. + * $value may be an instance of XmlSerializable or Element, in which case it + * calls it's xmlSerialize() method. + * $value may be a PHP callback/function/closure, in case we call the callback + * and give it the Writer as an argument. + * $value may be an object, and if it's in the classMap we automatically call + * the correct serializer for it. + * $value may be null, in which case we do nothing. + * + * If $value is an array, the array must look like this: + * + * [ + * [ + * 'name' => '{namespaceUri}element-name', + * 'value' => '...', + * 'attributes' => [ 'attName' => 'attValue' ] + * ] + * [, + * 'name' => '{namespaceUri}element-name2', + * 'value' => '...', + * ] + * ] + * + * This would result in xml like: + * + * + * ... + * + * + * ... + * + * + * The value property may be any value standardSerializer supports, so you can + * nest data-structures this way. Both value and attributes are optional. + * + * Alternatively, you can also specify the array using this syntax: + * + * [ + * [ + * '{namespaceUri}element-name' => '...', + * '{namespaceUri}element-name2' => '...', + * ] + * ] + * + * This is excellent for simple key->value structures, and here you can also + * specify anything for the value. + * + * You can even mix the two array syntaxes. + * + * @param string|int|float|bool|array|object $value + */ +function standardSerializer(Writer $writer, $value): void +{ + if (is_scalar($value)) { + // String, integer, float, boolean + $writer->text((string) $value); + } elseif ($value instanceof XmlSerializable) { + // XmlSerializable classes or Element classes. + $value->xmlSerialize($writer); + } elseif (is_object($value) && isset($writer->classMap[get_class($value)])) { + // It's an object which class appears in the classmap. + $writer->classMap[get_class($value)]($writer, $value); + } elseif (is_callable($value)) { + // A callback + $value($writer); + } elseif (is_array($value) && array_key_exists('name', $value)) { + // if the array had a 'name' element, we assume that this array + // describes a 'name' and optionally 'attributes' and 'value'. + + $name = $value['name']; + $attributes = isset($value['attributes']) ? $value['attributes'] : []; + $value = isset($value['value']) ? $value['value'] : null; + + $writer->startElement($name); + $writer->writeAttributes($attributes); + $writer->write($value); + $writer->endElement(); + } elseif (is_array($value)) { + foreach ($value as $name => $item) { + if (is_int($name)) { + // This item has a numeric index. We just loop through the + // array and throw it back in the writer. + standardSerializer($writer, $item); + } elseif (is_string($name) && is_array($item) && isset($item['attributes'])) { + // The key is used for a name, but $item has 'attributes' and + // possibly 'value' + $writer->startElement($name); + $writer->writeAttributes($item['attributes']); + if (isset($item['value'])) { + $writer->write($item['value']); + } + $writer->endElement(); + } elseif (is_string($name)) { + // This was a plain key-value array. + $writer->startElement($name); + $writer->write($item); + $writer->endElement(); + } else { + throw new \InvalidArgumentException('The writer does not know how to serialize arrays with keys of type: '.get_debug_type($name)); + } + } + } elseif (is_object($value)) { + throw new \InvalidArgumentException('The writer cannot serialize objects of class: '.get_class($value)); + } elseif (!is_null($value)) { + throw new \InvalidArgumentException('The writer cannot serialize values of type: '.get_debug_type($value)); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Service.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Service.php new file mode 100644 index 0000000000..6e225bdfdd --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Service.php @@ -0,0 +1,328 @@ + + */ + public array $elementMap = []; + + /** + * This is a list of namespaces that you want to give default prefixes. + * + * You must make sure you create this entire list before starting to write. + * They should be registered on the root element. + * + * @phpstan-var array + */ + public array $namespaceMap = []; + + /** + * This is a list of custom serializers for specific classes. + * + * The writer may use this if you attempt to serialize an object with a + * class that does not implement XmlSerializable. + * + * Instead, it will look at this classmap to see if there is a custom + * serializer here. This is useful if you don't want your value objects + * to be responsible for serializing themselves. + * + * The keys in this classmap need to be fully qualified PHP class names, + * the values must be callbacks. The callbacks take two arguments. The + * writer class, and the value that must be written. + * + * function (Writer $writer, object $value) + * + * @phpstan-var array + */ + public array $classMap = []; + + /** + * A bitmask of the LIBXML_* constants. + */ + public int $options = 0; + + /** + * Returns a fresh XML Reader. + */ + public function getReader(): Reader + { + $r = new Reader(); + $r->elementMap = $this->elementMap; + + return $r; + } + + /** + * Returns a fresh xml writer. + */ + public function getWriter(): Writer + { + $w = new Writer(); + $w->namespaceMap = $this->namespaceMap; + $w->classMap = $this->classMap; + + return $w; + } + + /** + * Parses a document in full. + * + * Input may be specified as a string or readable stream resource. + * The returned value is the value of the root document. + * + * Specifying the $contextUri allows the parser to figure out what the URI + * of the document was. This allows relative URIs within the document to be + * expanded easily. + * + * The $rootElementName is specified by reference and will be populated + * with the root element name of the document. + * + * @param string|resource $input + * + * @return array|object|string + * + * @throws ParseException + */ + public function parse($input, ?string $contextUri = null, ?string &$rootElementName = null) + { + if (!is_string($input)) { + // Unfortunately the XMLReader doesn't support streams. When it + // does, we can optimize this. + if (is_resource($input)) { + $input = (string) stream_get_contents($input); + } else { + // Input is not a string and not a resource. + // Therefore, it has to be a closed resource. + // Effectively empty input has been passed in. + $input = ''; + } + } + + // If input is empty, then it's safe to throw an exception + if (empty($input)) { + throw new ParseException('The input element to parse is empty. Do not attempt to parse'); + } + + $r = $this->getReader(); + $r->contextUri = $contextUri; + $r->XML($input, null, $this->options); + + $result = $r->parse(); + $rootElementName = $result['name']; + + return $result['value']; + } + + /** + * Parses a document in full, and specify what the expected root element + * name is. + * + * This function works similar to parse, but the difference is that the + * user can specify what the expected name of the root element should be, + * in clark notation. + * + * This is useful in cases where you expected a specific document to be + * passed, and reduces the amount of if statements. + * + * It's also possible to pass an array of expected rootElements if your + * code may expect more than one document type. + * + * @param string|string[] $rootElementName + * @param string|resource $input + * + * @return array|object|string + * + * @throws ParseException + */ + public function expect($rootElementName, $input, ?string $contextUri = null) + { + if (!is_string($input)) { + // Unfortunately the XMLReader doesn't support streams. When it + // does, we can optimize this. + if (is_resource($input)) { + $input = (string) stream_get_contents($input); + } else { + // Input is not a string and not a resource. + // Therefore, it has to be a closed resource. + // Effectively empty input has been passed in. + $input = ''; + } + } + + // If input is empty, then it's safe to throw an exception + if (empty($input)) { + throw new ParseException('The input element to parse is empty. Do not attempt to parse'); + } + + $r = $this->getReader(); + $r->contextUri = $contextUri; + $r->XML($input, null, $this->options); + + $rootElementName = (array) $rootElementName; + + foreach ($rootElementName as &$rEl) { + if ('{' !== $rEl[0]) { + $rEl = '{}'.$rEl; + } + } + + $result = $r->parse(); + if (!in_array($result['name'], $rootElementName, true)) { + throw new ParseException('Expected '.implode(' or ', $rootElementName).' but received '.$result['name'].' as the root element'); + } + + return $result['value']; + } + + /** + * Generates an XML document in one go. + * + * The $rootElement must be specified in clark notation. + * The value must be a string, an array or an object implementing + * XmlSerializable. Basically, anything that's supported by the Writer + * object. + * + * $contextUri can be used to specify a sort of 'root' of the PHP application, + * in case the xml document is used as a http response. + * + * This allows an implementor to easily create URI's relative to the root + * of the domain. + * + * @param string|array|object|XmlSerializable $value + */ + public function write(string $rootElementName, $value, ?string $contextUri = null): string + { + $w = $this->getWriter(); + $w->openMemory(); + $w->contextUri = $contextUri; + $w->setIndent(true); + $w->startDocument(); + $w->writeElement($rootElementName, $value); + + return $w->outputMemory(); + } + + /** + * Map an XML element to a PHP class. + * + * Calling this function will automatically set up the Reader and Writer + * classes to turn a specific XML element to a PHP class. + * + * For example, given a class such as : + * + * class Author { + * public $firstName; + * public $lastName; + * } + * + * and an XML element such as: + * + * + * ... + * ... + * + * + * These can easily be mapped by calling: + * + * $service->mapValueObject('{http://example.org}author', 'Author'); + * + * @param class-string $className + */ + public function mapValueObject(string $elementName, string $className): void + { + list($namespace) = self::parseClarkNotation($elementName); + + require_once __DIR__ . '/Deserializer/functions.php'; + $this->elementMap[$elementName] = function (Reader $reader) use ($className, $namespace) { + return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace); + }; + require_once __DIR__ . '/Serializer/functions.php'; + $this->classMap[$className] = function (Writer $writer, $valueObject) use ($namespace) { + \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace); + }; + $this->valueObjectMap[$className] = $elementName; + } + + /** + * Writes a value object. + * + * This function largely behaves similar to write(), except that it's + * intended specifically to serialize a Value Object into an XML document. + * + * The ValueObject must have been previously registered using + * mapValueObject(). + * + * @throws \InvalidArgumentException + */ + public function writeValueObject(object $object, ?string $contextUri = null): string + { + if (!isset($this->valueObjectMap[get_class($object)])) { + throw new \InvalidArgumentException('"'.get_class($object).'" is not a registered value object class. Register your class with mapValueObject.'); + } + + return $this->write( + $this->valueObjectMap[get_class($object)], + $object, + $contextUri + ); + } + + /** + * Parses a clark-notation string, and returns the namespace and element + * name components. + * + * If the string was invalid, it will throw an InvalidArgumentException. + * + * @return array{string, string} + * + * @throws \InvalidArgumentException + */ + public static function parseClarkNotation(string $str): array + { + static $cache = []; + + if (!isset($cache[$str])) { + if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) { + throw new \InvalidArgumentException('\''.$str.'\' is not a valid clark-notation formatted string'); + } + + $cache[$str] = [ + $matches[1], + $matches[2], + ]; + } + + return $cache[$str]; + } + + /** + * A list of classes and which XML elements they map to. + * + * @var array + */ + protected array $valueObjectMap = []; +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Version.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Version.php new file mode 100644 index 0000000000..3403931697 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Version.php @@ -0,0 +1,20 @@ + + */ + protected array $adhocNamespaces = []; + + /** + * When the first element is written, this flag is set to true. + * + * This ensures that the namespaces in the namespaces map are only written + * once. + */ + protected bool $namespacesWritten = false; + + /** + * Writes a value to the output stream. + * + * The following values are supported: + * 1. Scalar values will be written as-is, as text. + * 2. Null values will be skipped (resulting in a short xml tag). + * 3. If a value is an instance of an Element class, writing will be + * delegated to the object. + * 4. If a value is an array, two formats are supported. + * + * Array format 1: + * [ + * "{namespace}name1" => "..", + * "{namespace}name2" => "..", + * ] + * + * One element will be created for each key in this array. The values of + * this array support any format this method supports (this method is + * called recursively). + * + * Array format 2: + * + * [ + * [ + * "name" => "{namespace}name1" + * "value" => "..", + * "attributes" => [ + * "attr" => "attribute value", + * ] + * ], + * [ + * "name" => "{namespace}name1" + * "value" => "..", + * "attributes" => [ + * "attr" => "attribute value", + * ] + * ] + * ] + * + * @param mixed $value PHP value to be written + */ + public function write($value): void + { + require_once __DIR__ . '/Serializer/functions.php'; + Serializer\standardSerializer($this, $value); + } + + /** + * Opens a new element. + * + * You can either just use a local element name, or you can use clark- + * notation to start a new element. + * + * Example: + * + * $writer->startElement('{http://www.w3.org/2005/Atom}entry'); + * + * Would result in something like: + * + * + * + * Note: this function doesn't have the string typehint, because PHP's + * XMLWriter::startElement doesn't either. + * From PHP 8.0 the typehint exists, so it can be added here after PHP 7.4 is dropped. + * + * @param string $name + */ + public function startElement($name): bool + { + if ('{' === $name[0]) { + list($namespace, $localName) = + Service::parseClarkNotation($name); + + if (array_key_exists($namespace, $this->namespaceMap)) { + $result = $this->startElementNS( + '' === $this->namespaceMap[$namespace] ? null : $this->namespaceMap[$namespace], + $localName, + null + ); + } else { + // An empty namespace means it's the global namespace. This is + // allowed, but it mustn't get a prefix. + if ('' === $namespace) { + $result = $this->startElement($localName); + $this->writeAttribute('xmlns', ''); + } else { + if (!isset($this->adhocNamespaces[$namespace])) { + $this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1); + } + $result = $this->startElementNS($this->adhocNamespaces[$namespace], $localName, $namespace); + } + } + } else { + $result = parent::startElement($name); + } + + if (!$this->namespacesWritten) { + foreach ($this->namespaceMap as $namespace => $prefix) { + $this->writeAttribute($prefix ? 'xmlns:'.$prefix : 'xmlns', $namespace); + } + $this->namespacesWritten = true; + } + + return $result; + } + + /** + * Write a full element tag and it's contents. + * + * This method automatically closes the element as well. + * + * The element name may be specified in clark-notation. + * + * Examples: + * + * $writer->writeElement('{http://www.w3.org/2005/Atom}author',null); + * becomes: + * + * + * $writer->writeElement('{http://www.w3.org/2005/Atom}author', [ + * '{http://www.w3.org/2005/Atom}name' => 'Evert Pot', + * ]); + * becomes: + * Evert Pot + * + * Note: this function doesn't have the string typehint, because PHP's + * XMLWriter::startElement doesn't either. + * From PHP 8.0 the typehint exists, so it can be added here after PHP 7.4 is dropped. + * + * @param string $name + * @param array|string|object|null $content + */ + public function writeElement($name, $content = null): bool + { + $this->startElement($name); + if (!is_null($content)) { + $this->write($content); + } + $this->endElement(); + + return true; + } + + /** + * Writes a list of attributes. + * + * Attributes are specified as a key->value array. + * + * The key is an attribute name. If the key is a 'localName', the current + * xml namespace is assumed. If it's a 'clark notation key', this namespace + * will be used instead. + * + * @param array $attributes + */ + public function writeAttributes(array $attributes): void + { + foreach ($attributes as $name => $value) { + $this->writeAttribute($name, $value); + } + } + + /** + * Writes a new attribute. + * + * The name may be specified in clark-notation. + * + * Returns true when successful. + * + * Note: this function doesn't have typehints, because for some reason + * PHP's XMLWriter::writeAttribute doesn't either. + * From PHP 8.0 the typehint exists, so it can be added here after PHP 7.4 is dropped. + * + * @param string $name + * @param string $value + */ + public function writeAttribute($name, $value): bool + { + if ('{' !== $name[0]) { + return parent::writeAttribute($name, $value); + } + + list( + $namespace, + $localName, + ) = Service::parseClarkNotation($name); + + if (array_key_exists($namespace, $this->namespaceMap)) { + // It's an attribute with a namespace we know + return $this->writeAttribute( + $this->namespaceMap[$namespace].':'.$localName, + $value + ); + } + + // We don't know the namespace, we must add it in-line + if (!isset($this->adhocNamespaces[$namespace])) { + $this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1); + } + + return $this->writeAttributeNS( + $this->adhocNamespaces[$namespace], + $localName, + $namespace, + $value + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/XmlDeserializable.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/XmlDeserializable.php new file mode 100644 index 0000000000..56448d17df --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/XmlDeserializable.php @@ -0,0 +1,38 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed see comments above + */ + public static function xmlDeserialize(Reader $reader); +} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/XmlSerializable.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/XmlSerializable.php new file mode 100644 index 0000000000..4cd4e7f203 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/XmlSerializable.php @@ -0,0 +1,34 @@ + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFAttachment.php b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFAttachment.php new file mode 100644 index 0000000000..973854786d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFAttachment.php @@ -0,0 +1,334 @@ + + * Copyright (c) 2003 Bernd Wiegmann + * Copyright (c) 2002 Graham Norburys + * + * Licensed under the GNU GPL. For full terms see the file COPYING. + * + * @package plugins + * @subpackage tnef_decoder + * + */ + +function tnef_log($string) +{ + echo $string . "\n"; +// \error_log($string . "\n", 3, '/tmp/squirrelmail_tnef_decoder.log'); +// \SnappyMail\Log::debug('TNEF', $string); +} + +class TNEFAttachment +{ + + public bool $debug; + public bool $validateChecksum; + public TNEFMailinfo $mailinfo; + public array + $files = [], + $attachments = [], + $body = []; + public ?array $files_nested = null; + public ?TNEFFile $current_receiver = null; + + public function __construct(bool $debug = false, bool $validateChecksum = false) + { + $this->debug = $debug; + $this->validateChecksum = $validateChecksum; + $this->mailinfo = new TNEFMailinfo(); + } + + /** + * @return TNEFFileBase[] + */ + public function &getFiles(): array + { + return $this->files; + } + + /** + * @return TNEFFileBase[] + */ + public function &getFilesNested(): array + { + if (null === $this->files_nested) { + $this->files_nested = array(); + $this->addFilesNested($this->files); + foreach ($this->attachments as $attachment) { + $this->addFilesNested($attachment->getFilesNested()); + } + } + return $this->files_nested; + } + + private function addFilesNested(array &$add): void + { + foreach ($add as $file) { + $this->files_nested[] = &$file; + } + } + + public function getAttachments(): array + { + return $this->attachments; + } + + public function getMailinfo(): TNEFMailinfo + { + return $this->mailinfo; + } + + public function getBodyElements(): array + { + return $this->body; + } + + public function decodeTnef($data): void + { + $buffer = new TNEFBuffer($data); + + $tnef_signature = $buffer->geti32(); + if (TNEF_SIGNATURE == $tnef_signature) { + $tnef_key = $buffer->geti16(); + $this->debug && tnef_log(\sprintf("Signature: 0x%08x\nKey: 0x%04x\n", $tnef_signature, $tnef_key)); + + while ($buffer->getRemainingBytes() > 0) { + $lvl_type = $buffer->geti8(); + + switch ($lvl_type) { + case TNEF_LVL_MESSAGE: + case TNEF_LVL_ATTACHMENT: + $this->tnef_decode_attribute($buffer); + break; + + default: + if ($this->debug) { + $len = $buffer->getRemainingBytes(); + if ($len) + tnef_log("Invalid file format! Unknown Level {$lvl_type}. Rest={$len}"); + } + break; + } + } + } else { + throw new \RuntimeException("TNEF: Invalid file format! Wrong signature."); + } + + // propagate parent message's code page to child files if given + // + $code_page = $this->mailinfo->getCodePage(); + if (!empty($code_page)) + foreach ($this->files as $i => $file) + $this->files[$i]->setMessageCodePage($code_page); + } + + private function tnef_decode_attribute(TNEFBuffer $buffer) + { + $attribute = $buffer->geti32(); // attribute if + $length = $buffer->geti32(); // length + $value = $buffer->getBytes($length); // data + $checksumAtt = $buffer->geti16(); // checksum + if ($value !== null && $this->validateChecksum) { + $checksum = \array_sum(\unpack('C*', $value)) & 0xFFFF; + if ($checksum !== $checksumAtt) { + throw new \Exception('Checksums do not match'); + } + } + + switch ($attribute) + { + case TNEF_ARENDDATA: // marks start of new attachment + $this->debug && tnef_log("Creating new File for Attachment"); + $this->current_receiver = new TNEFFile($this->debug); + $this->files[] = $this->current_receiver; + break; + + case TNEF_AMAPIATTRS: + $this->debug && tnef_log("mapi attrs"); + $this->extract_mapi_attrs(new TNEFBuffer($value)); + break; + + case TNEF_AMAPIPROPS: + $this->debug && tnef_log("mapi props"); + $this->extract_mapi_attrs(new TNEFBuffer($value)); + break; + + case TNEF_AMCLASS: + $value = substr($value, 0, $length - 1); + if ($value == 'IPM.Contact') { + $this->debug && tnef_log("Creating vCard Attachment"); + $this->current_receiver = new TNEFvCard($this->debug); + $this->files[] = $this->current_receiver; + } + break; + + default: + $this->mailinfo->receiveTnefAttribute($attribute, $value, $length); + if ($this->current_receiver) + $this->current_receiver->receiveTnefAttribute($attribute, $value, $length); + break; + } + } + + private function extract_mapi_attrs(TNEFBuffer $buffer): void + { + + $number = $buffer->geti32(); // number of attributes + $props = 0; + $ended = 0; + + while (($buffer->getRemainingBytes() > 0) && ($props++ < $number) && !$ended) { + $value = ''; + unset($named_id); + $length = 0; + $have_multivalue = false; + $num_multivalues = 1; + $attr_type = $buffer->geti16(); + $attr_name = $buffer->geti16(); + + if (($attr_type & TNEF_MAPI_MV_FLAG) != 0) { + $this->debug && tnef_log("Multivalue Attribute found."); + $have_multivalue = true; + $attr_type = $attr_type & ~TNEF_MAPI_MV_FLAG; + } + + // Named Attribute + if (($attr_name >= 0x8000) && ($attr_name < 0xFFFE)) { + $guid = $buffer->getBytes(16); + $named_type = $buffer->geti32(); + switch ($named_type) + { + case TNEF_MAPI_NAMED_TYPE_ID: + $named_id = $buffer->geti32(); + $attr_name = $named_id; + $this->debug && tnef_log(sprintf("Named Id='0x%04x'", $named_id)); + break; + + case TNEF_MAPI_NAMED_TYPE_STRING: + $attr_name = 0x9999; // dummy to identify strings + $idlen = $buffer->geti32(); + $this->debug && tnef_log("idlen={$idlen}"); + $buflen = $idlen + ((4 - ($idlen % 4)) % 4); // pad to next 4 byte boundary + $this->debug && tnef_log("buflen={$buflen}"); + $named_id = substr($buffer->getBytes($buflen), 0, $idlen ); // read and truncate to length + $this->debug && tnef_log("Named Id='{$named_id}'"); + break; + + default: + $this->debug && tnef_log(\sprintf("Unknown Named Type 0x%04x found", $named_type)); + break; + } + } + + if ($have_multivalue) { + $num_multivalues = $buffer->geti32(); + $this->debug && tnef_log("Number of multivalues={$num_multivalues}"); + } + + switch ($attr_type) + { + case TNEF_MAPI_NULL: + break; + + case TNEF_MAPI_SHORT: + $value = $buffer->geti16(); + break; + + case TNEF_MAPI_INT: + case TNEF_MAPI_BOOLEAN: + for ($cnt = 0; $cnt < $num_multivalues; $cnt++) + $value = $buffer->geti32(); + break; + + case TNEF_MAPI_FLOAT: + case TNEF_MAPI_ERROR: + $value = $buffer->getBytes(4); + break; + + case TNEF_MAPI_DOUBLE: + case TNEF_MAPI_APPTIME: + case TNEF_MAPI_CURRENCY: + case TNEF_MAPI_INT8BYTE: + case TNEF_MAPI_SYSTIME: + $value = $buffer->getBytes(8); + break; + + case TNEF_MAPI_CLSID: + $this->debug && tnef_log("What is a MAPI CLSID ????"); + break; + + case TNEF_MAPI_STRING: + case TNEF_MAPI_UNICODE_STRING: + case TNEF_MAPI_BINARY: + case TNEF_MAPI_OBJECT: + $num_vals = $have_multivalue ? $num_multivalues : $buffer->geti32(); + + if ($num_vals > 20) { + // A Sanity check. + $ended = 1; + $this->debug && tnef_log("Number of entries in String Attributes={$num_vals}. Aborting Mapi parsing."); + } else { + for ($cnt = 0; $cnt < $num_vals; ++$cnt) { + $length = $buffer->geti32(); + $buflen = $length + ((4 - ($length % 4)) % 4); // pad to next 4 byte boundary + if ($attr_type == TNEF_MAPI_STRING) + $length -= 1; + $value = \substr($buffer->getBytes($buflen), 0, $length); // read and truncate to length + } + } + break; + + default: + $this->debug && tnef_log("Unknown mapi attribute! {$attr_type}"); + break; + } + + switch ($attr_name) + { + case TNEF_MAPI_ATTACH_DATA: + $this->debug && tnef_log("MAPI Found nested attachment. Processing new one."); + $value = substr($value, 16); // skip the next 16 bytes (unknown data) + $att = new TNEFAttachment($this->debug, $this->validateChecksum); + $att->decodeTnef($value); + $this->attachments[] = $att; + $this->debug && tnef_log("MAPI Finished nested attachment. Continuing old one."); + break; + + case TNEF_MAPI_RTF_COMPRESSED: + $this->debug && tnef_log("MAPI Found Compressed RTF Attachment."); + $this->files[] = new TNEFFileRTF($this->debug, $value); + break; + case TNEF_MAPI_BODY: + case TNEF_MAPI_BODY_HTML: + $result = []; + $result['type'] = 'text'; + $result['subtype'] = $attr_name == TNEF_MAPI_BODY ? 'plain' : 'html'; + $result['name'] = ('Untitled') . ($attr_name == TNEF_MAPI_BODY ? '.txt' : '.html'); + $result['stream'] = $value; + $result['size'] = strlen($value); + $this->body[] = $result; + break; + default: + $this->mailinfo->receiveMapiAttribute($attr_type, $attr_name, $value, $length); + if ($this->current_receiver) + $this->current_receiver->receiveMapiAttribute($attr_type, $attr_name, $value, $length); + break; + } + } + if ($this->debug && $ended) { + $len = $buffer->getRemainingBytes(); + for ($cnt = 0; $cnt < $len; ++$cnt) { + $ord = $buffer->geti8(); + $char = $ord ? \chr($ord) : ''; + tnef_log(\sprintf("Char Nr. %6d = 0x%02x = '%s'", $cnt, $ord, $char)); + } + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFBuffer.php b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFBuffer.php new file mode 100644 index 0000000000..d4dded488a --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFBuffer.php @@ -0,0 +1,66 @@ + + * Copyright (c) 2003 Bernd Wiegmann + * Copyright (c) 2002 Graham Norburys + * + * Licensed under the GNU GPL. For full terms see the file COPYING. + * + * @package plugins + * @subpackage tnef_decoder + * + */ + +class TNEFBuffer +{ + private string $data; + private int $offset = 0; + + public function __construct(string $data) + { + $this->data = $data; + } + + public function getBytes(int $numBytes): ?string + { + if ($this->getRemainingBytes() < $numBytes) { + $this->offset = \strlen($this->data); + return null; + } + + $this->offset += $numBytes; + return \substr($this->data, $this->offset - $numBytes, $numBytes); + } + + public function getRemainingBytes(): int + { + return \strlen($this->data) - $this->offset; + } + + public function geti8(): ?int + { + $bytes = $this->getBytes(1); + return (null === $bytes) ? null : \ord($bytes[0]); + } + + public function geti16(): ?int + { + $bytes = $this->getBytes(2); + return (null === $bytes) ? null : \ord($bytes[0]) + (\ord($bytes[1]) << 8); + } + + public function geti32(): ?int + { + $bytes = $this->getBytes(4); + return (null === $bytes) ? null + : \ord($bytes[0]) + + (\ord($bytes[1]) << 8) + + (\ord($bytes[2]) << 16) + + (\ord($bytes[3]) << 24); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFDate.php b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFDate.php new file mode 100644 index 0000000000..369f88a4c6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFDate.php @@ -0,0 +1,37 @@ + + * Copyright (c) 2003 Bernd Wiegmann + * Copyright (c) 2002 Graham Norburys + * + * Licensed under the GNU GPL. For full terms see the file COPYING. + * + * @package plugins + * @subpackage tnef_decoder + * + */ + +class TNEFDate extends \DateTime +{ + public function __construct(string $datetime = "now", ?\DateTimeZone $timezone = null) + { + parent::__construct($datetime, new \DateTimeZone('UTC')); + } + + public function setTnefBuffer(TNEFBuffer $buffer) + { + $this->setDate( + $buffer->geti16(), // year + $buffer->geti16(), // month + $buffer->geti16() // day + ); + $this->setTime( + $buffer->geti16(), // hour + $buffer->geti16(), // minute + $buffer->geti16() // second + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFFile.php b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFFile.php new file mode 100644 index 0000000000..2b96c56d0e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFFile.php @@ -0,0 +1,120 @@ + + * Copyright (c) 2003 Bernd Wiegmann + * Copyright (c) 2002 Graham Norburys + * + * Licensed under the GNU GPL. For full terms see the file COPYING. + * + * @package plugins + * @subpackage tnef_decoder + * + */ + +class TNEFFile extends TNEFFileBase +{ + + public string $metafile; + + public function getMetafile() + { + return $this->metafile; + } + + public function receiveTnefAttribute(int $attribute, string $value, int $length): void + { + switch ($attribute) + { + + // filename + // + case TNEF_AFILENAME: + // strip path + // + if (($pos = \strrpos($value, '/')) !== FALSE) + $this->name = \substr($value, $pos + 1); + else + $this->name = $value; + + // Strip trailing null bytes if present + $this->name = \trim($this->name); + break; + // code page + // + case TNEF_AOEMCODEPAGE: + $this->code_page = (new TNEFBuffer($value))->geti16(); + break; + + // the attachment itself + // + case TNEF_ATTACHDATA: + $this->content = $value; + break; + + // a metafile + // + case TNEF_ATTACHMETAFILE: + $this->metafile = $value; + break; + + case TNEF_AATTACHCREATEDATE: + $this->created = new TNEFDate(); + $this->created->setTnefBuffer(new TNEFBuffer($value)); + + case TNEF_AATTACHMODDATE: + $this->modified = new TNEFDate(); + $this->modified->setTnefBuffer(new TNEFBuffer($value)); + break; + } + } + + public function receiveMapiAttribute(int $attr_type, int $attr_name, string $value, int $length): void + { + switch ($attr_name) + { + + // used in preference to AFILENAME value + // + case TNEF_MAPI_ATTACH_LONG_FILENAME: + // strip path + // + if (($pos = \strrpos($value, '/')) !== FALSE) + $this->name = \substr($value, $pos + 1); + else + $this->name = $value; + + $this->name_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type; + break; + + // Is this ever set, and what is format? + // + case TNEF_MAPI_ATTACH_MIME_TAG: + $type0 = $type1 = ''; + $mime_type = \explode('/', $value, 2); + if (!empty($mime_type[0])) + $type0 = $mime_type[0]; + if (!empty($mime_type[1])) + $type1 = $mime_type[1]; + $this->type = "{$type0}/{$type1}"; + if (TNEF_MAPI_UNICODE_STRING === $attr_type) { + $this->type = \substr(\mb_convert_encoding($this->type, "UTF-8" , "UTF-16LE"), 0, -1); + } + break; + + case TNEF_MAPI_ATTACH_EXTENSION: + $type = \SnappyMail\File\MimeType::fromFilename($value); + if ($type) + $this->type = $type; + break; + } + } + +} + + + diff --git a/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFFileBase.php b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFFileBase.php new file mode 100644 index 0000000000..6d36dede2e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFFileBase.php @@ -0,0 +1,82 @@ + + * Copyright (c) 2003 Bernd Wiegmann + * Copyright (c) 2002 Graham Norburys + * + * Licensed under the GNU GPL. For full terms see the file COPYING. + * + * @package plugins + * @subpackage tnef_decoder + * + */ + +class TNEFFileBase +{ + public bool $name_is_unicode = FALSE; + public string + $name = 'Untitled', + $code_page = '', + $message_code_page = '', // parent message's code page (the whole TNEF file) + $type = 'application/octet-stream', + $content = ''; + public ?TNEFDate + $created = null, + $modified = null; + public bool $debug; + + public function __construct(bool $debug) + { + $this->debug = $debug; + } + + public function setMessageCodePage(string $code_page): void + { + $this->message_code_page = $code_page; + } + + public function getCodePage(): string + { + return empty($this->code_page) + ? $this->message_code_page + : $this->code_page; + } + + public function getName(): string + { + return $this->name_is_unicode + ? \substr(\mb_convert_encoding($this->name, "UTF-8" , "UTF-16LE"), 0, -1) + : $this->name; + } + + public function getType(): string + { + return $this->type; + } + + public function getSize(): int + { + return \strlen($this->content); + } + + public function getCreated(): ?TNEFDate + { + return $this->created; + } + + public function getModified(): ?TNEFDate + { + return $this->modified; + } + + public function getContent(): string + { + return $this->content; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFFileRTF.php b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFFileRTF.php new file mode 100644 index 0000000000..e477390b5e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFFileRTF.php @@ -0,0 +1,110 @@ + + * Copyright (c) 2003 Bernd Wiegmann + * Copyright (c) 2002 Graham Norburys + * + * Licensed under the GNU GPL. For full terms see the file COPYING. + * + * @package plugins + * @subpackage tnef_decoder + * + */ +class TNEFFileRTF extends TNEFFileBase +{ + public string $name = 'EmbeddedRTF.rtf'; + public string $type = 'application/rtf'; + protected int $size = 0; + const MAX_DICT_SIZE = 4096; + const INIT_DICT_SIZE = 207; + + public function __construct($debug, $data) + { + parent::__construct($debug); + $this->decode_crtf(new TNEFBuffer($data)); + } + + public function getSize(): int + { + return $this->size; + } + + public function decode_crtf(TNEFBuffer $buffer) + { + $size_compressed = $buffer->geti32(); + $this->size = $buffer->geti32(); + $magic = $buffer->geti32(); + $crc32 = $buffer->geti32(); + + $this->debug && tnef_log("CRTF: size comp={$size_compressed}, size={$this->size}"); + + $data = $buffer->getBytes($buffer->getRemainingBytes()); + + switch ($magic) { + case CRTF_COMPRESSED: + $this->uncompress($data); + break; + + case CRTF_UNCOMPRESSED: + $this->content = $data; + break; + + default: + $this->debug && tnef_log("Unknown Compressed RTF Format"); + break; + } + } + + public function uncompress($data) + { + $preload = "{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New RomanCourier{\\colortbl\\red0\\green0\\blue0\n\r\\par \\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx"; + $length_preload = \strlen($preload); + $init_dict = []; + for ($cnt = 0; $cnt < $length_preload; ++$cnt) { + $init_dict[$cnt] = $preload[$cnt]; + } + $init_dict = \array_merge($init_dict, \array_fill(\count($init_dict), self::MAX_DICT_SIZE - $length_preload, ' ')); + $write_offset = self::INIT_DICT_SIZE; + $this->content = ''; + $end = false; + $in = 0; + $l = \strlen($data); + while (!$end) { + if ($in >= $l) { + break; + } + $control = \strrev(\str_pad(\decbin(\ord($data[$in++])), 8, 0, STR_PAD_LEFT)); + for ($i = 0; $i < 8; ++$i) { + if ($control[$i] == '1') { + $token = \unpack("n", $data[$in++] . $data[$in++])[1]; + $offset = ($token >> 4) & 0b111111111111; + $length = $token & 0b1111; + if ($write_offset == $offset) { + $end = true; + break; + } + $actual_length = $length + 2; + for ($step = 0; $step < $actual_length; ++$step) { + $read_offset = ($offset + $step) % self::MAX_DICT_SIZE; + $char = $init_dict[$read_offset]; + $this->content .= $char; + $init_dict[$write_offset] = $char; + $write_offset = ($write_offset + 1) % self::MAX_DICT_SIZE; + } + } else { + if ($in >= $l) { + break; + } + $val = $data[$in++]; + $this->content .= $val; + $init_dict[$write_offset] = $val; + $write_offset = ($write_offset + 1) % self::MAX_DICT_SIZE; + } + } + } + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFMailinfo.php b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFMailinfo.php new file mode 100644 index 0000000000..4b2e2537c3 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFMailinfo.php @@ -0,0 +1,107 @@ + + * Copyright (c) 2003 Bernd Wiegmann + * Copyright (c) 2002 Graham Norburys + * + * Licensed under the GNU GPL. For full terms see the file COPYING. + * + * @package plugins + * @subpackage tnef_decoder + * + */ + +class TNEFMailinfo +{ + + public string + $subject = '', + $topic = '', + $from = '', + $from_name = '', + $code_page = ''; + public ?TNEFDate $date_sent = null; + public bool + $topic_is_unicode = FALSE, + $from_is_unicode = FALSE, + $from_name_is_unicode = FALSE; + + public function getTopic(): string + { + return $this->topic; + } + + public function getSubject(): string + { + return $this->subject; + } + + public function getFrom(): string + { + return $this->from; + } + + public function getCodePage(): string + { + return $this->code_page; + } + + public function getFromName(): string + { + return $this->from_name; + } + + public function getDateSent(): TNEFDate + { + return $this->date_sent; + } + + public function receiveTnefAttribute(int $attribute, string $value, int $length): void + { + $value = new TNEFBuffer($value); + + switch ($attribute) + { + case TNEF_AOEMCODEPAGE: + $this->code_page = $value->geti16(); + break; + + case TNEF_ASUBJECT: + $this->subject = $value->getBytes($length - 1); + break; + + case TNEF_ADATERECEIVED: + if ($this->date_sent) { + break; + } + case TNEF_ADATESENT: + $this->date_sent = new TNEFDate(); + $this->date_sent->setTnefBuffer($value); + } + } + + public function receiveMapiAttribute(int $attr_type, int $attr_name, string $value, int $length): void + { + switch ($attr_name) + { + case TNEF_MAPI_CONVERSATION_TOPIC: + $this->topic = $value; + $this->topic_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type; + break; + + case TNEF_MAPI_SENT_REP_EMAIL_ADDR: + $this->from = $value; + $this->from_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type; + break; + + case TNEF_MAPI_SENT_REP_NAME: + $this->from_name = $value; + $this->from_name_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type; + break; + } + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFvCard.php b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFvCard.php new file mode 100644 index 0000000000..546c837107 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/TNEFvCard.php @@ -0,0 +1,249 @@ + + * Copyright (c) 2003 Bernd Wiegmann + * Copyright (c) 2002 Graham Norburys + * + * Licensed under the GNU GPL. For full terms see the file COPYING. + * + * @package plugins + * @subpackage tnef_decoder + * + */ + +class TNEFvCard extends TNEFFileBase +{ + public string $type = 'text/x-vcard'; + + public bool + $surname_is_unicode = FALSE, + $given_name_is_unicode = FALSE, + $middle_name_is_unicode = FALSE, + $nickname_is_unicode = FALSE, + $company_is_unicode = FALSE; + public string + $surname, + $given_name, + $middle_name, + $nickname, + $company, + $metafile; + public array + $homepages = [], + $addresses = [], + $emails = [], + $telefones = []; + + private static + $address_mapping = array ( + TNEF_MAPI_LOCALTY => array ("Address", ADDRESS_CITY), + TNEF_MAPI_COUNTRY => array ("Address", ADDRESS_COUNTRY), + TNEF_MAPI_POSTAL_CODE => array ("Address", ADDRESS_ZIP), + TNEF_MAPI_STATE_OR_PROVINCE => array ("Address", ADDRESS_STATE), + TNEF_MAPI_STREET_ADDRESS => array ("Address", ADDRESS_STREET), + TNEF_MAPI_POST_OFFICE_BOX => array ("Address", ADDRESS_PO_BOX), + TNEF_MAPI_HOME_ADDR_CITY => array ("Home Address", ADDRESS_CITY), + TNEF_MAPI_HOME_ADDR_COUNTRY => array ("Home Address", ADDRESS_COUNTRY), + TNEF_MAPI_HOME_ADDR_ZIP => array ("Home Address", ADDRESS_ZIP), + TNEF_MAPI_HOME_ADDR_STATE => array ("Home Address", ADDRESS_STATE), + TNEF_MAPI_HOME_ADDR_STREET => array ("Home Address", ADDRESS_STREET), + TNEF_MAPI_HOME_ADDR_PO_BOX => array ("Home Address", ADDRESS_PO_BOX), + TNEF_MAPI_OTHER_ADDR_CITY => array ("Other Address", ADDRESS_CITY), + TNEF_MAPI_OTHER_ADDR_COUNTRY => array ("Other Address", ADDRESS_COUNTRY), + TNEF_MAPI_OTHER_ADDR_ZIP => array ("Other Address", ADDRESS_ZIP), + TNEF_MAPI_OTHER_ADDR_STATE => array ("Other Address", ADDRESS_STATE), + TNEF_MAPI_OTHER_ADDR_STREET => array ("Other Address", ADDRESS_STREET), + TNEF_MAPI_OTHER_ADDR_PO_BOX => array ("Other Address", ADDRESS_PO_BOX), + ), + $email_mapping = array ( + TNEF_MAPI_EMAIL1_DISPLAY => array ("Email 1", EMAIL_DISPLAY), + TNEF_MAPI_EMAIL1_TRANSPORT => array ("Email 1", EMAIL_TRANSPORT), + TNEF_MAPI_EMAIL1_EMAIL => array ("Email 1", EMAIL_EMAIL), + TNEF_MAPI_EMAIL1_EMAIL2 => array ("Email 1", EMAIL_EMAIL2), + TNEF_MAPI_EMAIL2_DISPLAY => array ("Email 2", EMAIL_DISPLAY), + TNEF_MAPI_EMAIL2_TRANSPORT => array ("Email 2", EMAIL_TRANSPORT), + TNEF_MAPI_EMAIL2_EMAIL => array ("Email 2", EMAIL_EMAIL), + TNEF_MAPI_EMAIL2_EMAIL2 => array ("Email 2", EMAIL_EMAIL2), + TNEF_MAPI_EMAIL3_DISPLAY => array ("Email 3", EMAIL_DISPLAY), + TNEF_MAPI_EMAIL3_TRANSPORT => array ("Email 3", EMAIL_TRANSPORT), + TNEF_MAPI_EMAIL3_EMAIL => array ("Email 3", EMAIL_EMAIL), + TNEF_MAPI_EMAIL3_EMAIL2 => array ("Email 3", EMAIL_EMAIL2), + ), + $homepage_mapping = array ( + TNEF_MAPI_PERSONAL_HOME_PAGE => "Personal Homepage", + TNEF_MAPI_BUSINESS_HOME_PAGE => "Business Homepage", + TNEF_MAPI_OTHER_HOME_PAGE => "Other Homepage", + ), + $telefone_mapping = array ( + TNEF_MAPI_PRIMARY_TEL_NUMBER => "Primary Telefone", + TNEF_MAPI_HOME_TEL_NUMBER => "Home Telefone", + TNEF_MAPI_HOME2_TEL_NUMBER => "Home2 Telefone", + TNEF_MAPI_BUSINESS_TEL_NUMBER => "Business Telefone", + TNEF_MAPI_BUSINESS2_TEL_NUMBER => "Business2 Telefone", + TNEF_MAPI_MOBILE_TEL_NUMBER => "Mobile Telefone", + TNEF_MAPI_RADIO_TEL_NUMBER => "Radio Telefone", + TNEF_MAPI_CAR_TEL_NUMBER => "Car Telefone", + TNEF_MAPI_OTHER_TEL_NUMBER => "Other Telefone", + TNEF_MAPI_PAGER_TEL_NUMBER => "Pager Telefone", + TNEF_MAPI_PRIMARY_FAX_NUMBER => "Primary Fax", + TNEF_MAPI_BUSINESS_FAX_NUMBER => "Business Fax", + TNEF_MAPI_HOME_FAX_NUMBER => "Home Fax", + ); + + public function getSurname(): string + { + return $this->surname; + } + + public function getGivenName(): string + { + return $this->given_name; + } + + public function getMiddleName(): string + { + return $this->middle_name; + } + + public function getNickname(): string + { + return $this->nickname; + } + + public function getCompany(): string + { + return $this->company; + } + + public function getAddresses(): array + { + return $this->addresses; + } + + public function getMetafile() + { + return $this->metafile; + } + + public function getTelefones(): array + { + return $this->telefones; + } + + public function getHomepages(): array + { + return $this->homepages; + } + + public function getEmails(): array + { + return $this->emails; + } + + public function receiveTnefAttribute(int $attribute, string $value, int $length): void + { + switch ($attribute) + { + // code page + // + case TNEF_AOEMCODEPAGE: + $this->code_page = (new TNEFBuffer($value))->geti16(); + break; + } + } + + public function receiveMapiAttribute(int $attr_type, int $attr_name, string $value, int $length) + { + switch ($attr_name) + { + case TNEF_MAPI_DISPLAY_NAME: + $this->name = $value; + $this->name_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type; + break; + + case TNEF_MAPI_SURNAME: + $this->surname = $value; + $this->surname_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type; + break; + + case TNEF_MAPI_GIVEN_NAME: + $this->given_name = $value; + $this->given_name_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type; + break; + + case TNEF_MAPI_MIDDLE_NAME: + $this->middle_name = $value; + $this->middle_name_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type; + break; + + case TNEF_MAPI_NICKNAME: + $this->nickname = $value; + $this->nickname_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type; + break; + + case TNEF_MAPI_COMPANY_NAME: + $this->company = $value; + $this->company_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type; + break; + + default: + $this->evaluateTelefoneAttribute($attr_type, $attr_name, $value, $length) + || $this->evaluateEmailAttribute($attr_type, $attr_name, $value, $length) + || $this->evaluateAddressAttribute($attr_type, $attr_name, $value, $length) + || $this->evaluateHomepageAttribute($attr_type, $attr_name, $value, $length); + break; + } + } + + private function evaluateTelefoneAttribute(int $attr_type, int $attr_name, string $value, int $length): bool + { + if ($length && \array_key_exists($attr_name, static::$telefone_mapping)) { + $telefone_key = static::$telefone_mapping[$attr_name]; + $this->telefones[$telefone_key] = $value; + $this->debug && tnef_log("Setting telefone '{$telefone_key}' to value '{$value}'"); + return true; + } + return false; + } + + private function evaluateEmailAttribute(int $attr_type, int $attr_name, string $value, int $length): bool + { + if ($length && \array_key_exists($attr_name, static::$email_mapping)) { + $email_key = static::$email_mapping[$attr_name]; + if (!\array_key_exists($email_key[0], $this->emails)) + $this->emails[$email_key[0]] = array(EMAIL_DISPLAY => "", EMAIL_TRANSPORT => "", EMAIL_EMAIL => "", EMAIL_EMAIL2 => ""); + $this->emails[$email_key[0]][$email_key[1]] = $value; + return true; + } + return false; + } + + private function evaluateAddressAttribute(int $attr_type, int $attr_name, string $value, int $length): bool + { + if ($length && \array_key_exists($attr_name, static::$address_mapping)) { + $address_key = static::$address_mapping[$attr_name]; + if (!\array_key_exists($address_key[0], $this->addresses)) + $this->addresses[$address_key[0]] = array(); + $this->addresses[$address_key[0]][$address_key[1]] = $value; + return true; + } + return false; + } + + private function evaluateHomepageAttribute(int $attr_type, int $attr_name, string $value, int $length): bool + { + if ($length && \array_key_exists($attr_name, static::$homepage_mapping)) { + $homepage_key = static::$homepage_mapping[$attr_name]; + $this->homepages[$homepage_key] = $value; + $this->debug && tnef_log("Setting homepage '{$homepage_key}' to value '{$value}'"); + return true; + } + return false; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/TNEFDecoder/constants.php b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/constants.php new file mode 100644 index 0000000000..13f73f284c --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/TNEFDecoder/constants.php @@ -0,0 +1,202 @@ + + * Copyright (c) 2003 Bernd Wiegmann + * Copyright (c) 2002 Graham Norburys + * + * Licensed under the GNU GPL. For full terms see the file COPYING. + * + * @package plugins + * @subpackage tnef_decoder + * + */ + +define("TNEF_SIGNATURE", 0x223e9f78); +define("TNEF_LVL_MESSAGE", 0x01); +define("TNEF_LVL_ATTACHMENT", 0x02); + +define("TNEF_TRIPLES", 0x00000000); +define("TNEF_STRING", 0x00010000); +define("TNEF_TEXT", 0x00020000); +define("TNEF_DATE", 0x00030000); +define("TNEF_SHORT", 0x00040000); +define("TNEF_LONG", 0x00050000); +define("TNEF_BYTE", 0x00060000); +define("TNEF_WORD", 0x00070000); +define("TNEF_DWORD", 0x00080000); +define("TNEF_MAX", 0x00090000); + +define("TNEF_AIDOWNER", TNEF_LONG | 0x0008); +define("TNEF_AREQUESTRES", TNEF_SHORT | 0x0009); +define("TNEF_AFROM", TNEF_TRIPLES | 0x8000); +define("TNEF_ASUBJECT", TNEF_STRING | 0x8004); +define("TNEF_ADATESENT", TNEF_DATE | 0x8005); +define("TNEF_ADATERECEIVED", TNEF_DATE | 0x8006); +define("TNEF_ASTATUS", TNEF_BYTE | 0x8007); +define("TNEF_AMCLASS", TNEF_WORD | 0x8008); +define("TNEF_AMESSAGEID", TNEF_STRING | 0x8009); +define("TNEF_ABODYTEXT", TNEF_TEXT | 0x800c); +define("TNEF_APRIORITY", TNEF_SHORT | 0x800d); +define("TNEF_ATTACHDATA", TNEF_BYTE | 0x800f); +define("TNEF_AFILENAME", TNEF_STRING | 0x8010); +define("TNEF_ATTACHMETAFILE", TNEF_BYTE | 0x8011); +define("TNEF_AATTACHCREATEDATE", TNEF_DATE | 0x8012); +define("TNEF_AATTACHMODDATE", TNEF_DATE | 0x8013); +define("TNEF_ADATEMODIFIED", TNEF_DATE | 0x8020); +define("TNEF_ARENDDATA", TNEF_BYTE | 0x9002); +define("TNEF_AMAPIPROPS", TNEF_BYTE | 0x9003); +define("TNEF_AMAPIATTRS", TNEF_BYTE | 0x9005); +define("TNEF_AVERSION", TNEF_DWORD | 0x9006); +define("TNEF_AOEMCODEPAGE", TNEF_BYTE | 0x9007); + +define("TNEF_MAPI_NULL", 0x0001); +define("TNEF_MAPI_SHORT", 0x0002); +define("TNEF_MAPI_INT", 0x0003); +define("TNEF_MAPI_FLOAT", 0x0004); +define("TNEF_MAPI_DOUBLE", 0x0005); +define("TNEF_MAPI_CURRENCY", 0x0006); +define("TNEF_MAPI_APPTIME", 0x0007); +define("TNEF_MAPI_ERROR", 0x000a); +define("TNEF_MAPI_BOOLEAN", 0x000b); +define("TNEF_MAPI_OBJECT", 0x000d); +define("TNEF_MAPI_INT8BYTE", 0x0014); +define("TNEF_MAPI_STRING", 0x001e); +define("TNEF_MAPI_UNICODE_STRING", 0x001f); +define("TNEF_MAPI_SYSTIME", 0x0040); +define("TNEF_MAPI_CLSID", 0x0048); +define("TNEF_MAPI_BINARY", 0x0102); + +define("TNEF_MAPI_MV_FLAG", 0x1000); +define("TNEF_MAPI_NAMED_TYPE_ID", 0x0000); +define("TNEF_MAPI_NAMED_TYPE_STRING", 0x0001); + +define("TNEF_MAPI_SUBJECT_PREFIX", 0x003D); +define("TNEF_MAPI_SENT_REP_NAME", 0x0042); +define("TNEF_MAPI_ORIGINAL_AUTHOR", 0x004D); +define("TNEF_MAPI_SENT_REP_ADDRTYPE", 0x0064); +define("TNEF_MAPI_SENT_REP_EMAIL_ADDR", 0x0065); +define("TNEF_MAPI_CONVERSATION_TOPIC", 0x0070); +define("TNEF_MAPI_SENDER_NAME", 0x0c1A); +define("TNEF_MAPI_SENDER_ADDRTYPE", 0x0c1E); +define("TNEF_MAPI_SENDER_EMAIL_ADDRESS", 0x0c1F); +define("TNEF_MAPI_NORMALIZED_SUBJECT", 0x0E1D); +define("TNEF_MAPI_ATTACH_SIZE", 0x0E20); +define("TNEF_MAPI_ATTACH_NUM", 0x0E21); +define("TNEF_MAPI_ACCESS_LEVEL", 0x0FF7); +define("TNEF_MAPI_MAPPING_SIGNATURE", 0x0FF8); +define("TNEF_MAPI_RECORD_KEY", 0x0FF9); +define("TNEF_MAPI_STORE_RECORD_KEY", 0x0FFA); +define("TNEF_MAPI_STORE_ENTRY_ID", 0x0FFB); +define("TNEF_MAPI_OBJECT_TYPE", 0x0FFE); +define("TNEF_MAPI_BODY", 0x1000); +define("TNEF_MAPI_RTF_SYNC_BODY_TAG", 0x1008); +define("TNEF_MAPI_RTF_COMPRESSED", 0x1009); +define("TNEF_MAPI_BODY_HTML", 0x1013); +define("TNEF_MAPI_NATIVE_BODY", 0x1016); +define("TNEF_MAPI_DISPLAY_NAME", 0x3001); +define("TNEF_MAPI_CREATION_TIME", 0x3007); +define("TNEF_MAPI_MODIFICATION_TIME", 0x3008); +define("TNEF_MAPI_ATTACH_DATA", 0x3701); +define("TNEF_MAPI_ATTACH_ENCODING", 0x3702); +define("TNEF_MAPI_ATTACH_EXTENSION", 0x3703); +define("TNEF_MAPI_ATTACH_METHOD", 0x3705); +define("TNEF_MAPI_ATTACH_LONG_FILENAME", 0x3707); +define("TNEF_MAPI_RENDERING_POSITION", 0x370B); +define("TNEF_MAPI_ATTACH_MIME_TAG", 0x370E); +define("TNEF_MAPI_ACCOUNT", 0x3A00); +define("TNEF_MAPI_GENERATION", 0x3A05); +define("TNEF_MAPI_GIVEN_NAME", 0x3A06); +define("TNEF_MAPI_BUSINESS_TEL_NUMBER", 0x3A08); +define("TNEF_MAPI_HOME_TEL_NUMBER", 0x3A09); +define("TNEF_MAPI_INITIALS", 0x3A0A); +define("TNEF_MAPI_KEYWORDS", 0x3A0B); +define("TNEF_MAPI_LANGUAGE", 0x3A0C); +define("TNEF_MAPI_LOCATION", 0x3A0D); +define("TNEF_MAPI_SURNAME", 0x3A11); +define("TNEF_MAPI_POSTAL_ADDRESS", 0x3A15); +define("TNEF_MAPI_COMPANY_NAME", 0x3A16); +define("TNEF_MAPI_TITLE", 0x3A17); +define("TNEF_MAPI_DEPARTMENT_NAME", 0x3A18); +define("TNEF_MAPI_OFFICE_LOCATION", 0x3A19); +define("TNEF_MAPI_PRIMARY_TEL_NUMBER", 0x3A1A); +define("TNEF_MAPI_BUSINESS2_TEL_NUMBER", 0x3A1B); +define("TNEF_MAPI_MOBILE_TEL_NUMBER", 0x3A1C); +define("TNEF_MAPI_RADIO_TEL_NUMBER", 0x3A1D); +define("TNEF_MAPI_CAR_TEL_NUMBER", 0x3A1E); +define("TNEF_MAPI_OTHER_TEL_NUMBER", 0x3A1F); +define("TNEF_MAPI_PAGER_TEL_NUMBER", 0x3A21); +define("TNEF_MAPI_PRIMARY_FAX_NUMBER", 0x3A23); +define("TNEF_MAPI_BUSINESS_FAX_NUMBER", 0x3A24); +define("TNEF_MAPI_HOME_FAX_NUMBER", 0x3A25); +define("TNEF_MAPI_COUNTRY", 0x3A26); +define("TNEF_MAPI_LOCALTY", 0x3A27); +define("TNEF_MAPI_STATE_OR_PROVINCE", 0x3A28); +define("TNEF_MAPI_STREET_ADDRESS", 0x3A29); +define("TNEF_MAPI_POSTAL_CODE", 0x3A2A); +define("TNEF_MAPI_POST_OFFICE_BOX", 0x3A2B); +define("TNEF_MAPI_TELEX_NUMBER", 0x3A2C); +define("TNEF_MAPI_ISDN_NUMBER", 0x3A2D); +define("TNEF_MAPI_ASSISTANT_TEL_NUMBER", 0x3A2E); +define("TNEF_MAPI_HOME2_TEL_NUMBER", 0x3A2F); +define("TNEF_MAPI_ASSISTANT", 0x3A30); +define("TNEF_MAPI_MIDDLE_NAME", 0x3A44); +define("TNEF_MAPI_DISPLAYNAME_PREFIX", 0x3A45); +define("TNEF_MAPI_PROFESSION", 0x3A46); +define("TNEF_MAPI_SPOUSE_NAME", 0x3A48); +define("TNEF_MAPI_MANAGER_NAME", 0x3A4E); +define("TNEF_MAPI_NICKNAME", 0x3A4F); +define("TNEF_MAPI_PERSONAL_HOME_PAGE", 0x3A50); +define("TNEF_MAPI_BUSINESS_HOME_PAGE", 0x3A51); +define("TNEF_MAPI_CONTACT_EMAIL_ADDR", 0x3A56); +define("TNEF_MAPI_HOME_ADDR_CITY", 0x3A59); +define("TNEF_MAPI_HOME_ADDR_COUNTRY", 0x3A5A); +define("TNEF_MAPI_HOME_ADDR_ZIP", 0x3A5B); +define("TNEF_MAPI_HOME_ADDR_STATE", 0x3A5C); +define("TNEF_MAPI_HOME_ADDR_STREET", 0x3A5D); +define("TNEF_MAPI_HOME_ADDR_PO_BOX", 0x3A5E); +define("TNEF_MAPI_OTHER_ADDR_CITY", 0x3A5F); +define("TNEF_MAPI_OTHER_ADDR_COUNTRY", 0x3A60); +define("TNEF_MAPI_OTHER_ADDR_ZIP", 0x3A61); +define("TNEF_MAPI_OTHER_ADDR_STATE", 0x3A62); +define("TNEF_MAPI_OTHER_ADDR_STREET", 0x3A63); +define("TNEF_MAPI_OTHER_ADDR_PO_BOX", 0x3A64); + +define("TNEF_MAPI_OTHER_HOME_PAGE", 0x804F); +define("TNEF_MAPI_EMAIL1_DISPLAY", 0x8080); +define("TNEF_MAPI_EMAIL1_TRANSPORT", 0x8082); +define("TNEF_MAPI_EMAIL1_EMAIL", 0x8083); +define("TNEF_MAPI_EMAIL1_EMAIL2", 0x8084); +define("TNEF_MAPI_EMAIL2_DISPLAY", 0x8090); +define("TNEF_MAPI_EMAIL2_TRANSPORT", 0x8092); +define("TNEF_MAPI_EMAIL2_EMAIL", 0x8093); +define("TNEF_MAPI_EMAIL2_EMAIL2", 0x8094); +define("TNEF_MAPI_EMAIL3_DISPLAY", 0x80A0); +define("TNEF_MAPI_EMAIL3_TRANSPORT", 0x80A2); +define("TNEF_MAPI_EMAIL3_EMAIL", 0x80A3); +define("TNEF_MAPI_EMAIL3_EMAIL2", 0x80A4); + + + +// used in RTF +// +define("CRTF_UNCOMPRESSED", 0x414c454d); +define("CRTF_COMPRESSED", 0x75465a4c); + + +// used in VCARD +// +define ("EMAIL_DISPLAY", 1); +define ("EMAIL_TRANSPORT", 2); +define ("EMAIL_EMAIL", 3); +define ("EMAIL_EMAIL2", 4); +define ("ADDRESS_STREET", "Street"); +define ("ADDRESS_ZIP", "Zip"); +define ("ADDRESS_CITY", "City"); +define ("ADDRESS_COUNTRY", "Country"); +define ("ADDRESS_STATE", "State"); +define ("ADDRESS_PO_BOX", "PO Box"); diff --git a/rainloop/v/0.0.0/app/libraries/lessphp/LICENSE b/snappymail/v/0.0.0/app/libraries/lessphp/LICENSE similarity index 100% rename from rainloop/v/0.0.0/app/libraries/lessphp/LICENSE rename to snappymail/v/0.0.0/app/libraries/lessphp/LICENSE diff --git a/rainloop/v/0.0.0/app/libraries/lessphp/README.md b/snappymail/v/0.0.0/app/libraries/lessphp/README.md similarity index 100% rename from rainloop/v/0.0.0/app/libraries/lessphp/README.md rename to snappymail/v/0.0.0/app/libraries/lessphp/README.md diff --git a/snappymail/v/0.0.0/app/libraries/lessphp/lessc.php b/snappymail/v/0.0.0/app/libraries/lessphp/lessc.php new file mode 100644 index 0000000000..4b27bc70bd --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/lessphp/lessc.php @@ -0,0 +1,3036 @@ + + * Licensed under MIT or GPLv3, see LICENSE + */ + +trait lessc_lib +{ + protected function assertNumber($value) { + if ($value[0] == "number") return $value[1]; + $this->throwError('expecting number'); + } + + /** + * Helper function to get arguments for color manipulation functions. + * takes a list that contains a color like thing and a percentage + */ + protected function colorArgs($args) { + if ($args[0] != 'list' || count($args[2]) < 2) { + return array(array('color', 0, 0, 0), 0); + } + list($color, $delta) = $args[2]; + $color = $this->assertColor($color); + $delta = floatval($delta[1]); + + return array($color, $delta); + } + + protected function toHSL($color) { + if ($color[0] == 'hsl') return $color; + + $r = $color[1] / 255; + $g = $color[2] / 255; + $b = $color[3] / 255; + + $min = min($r, $g, $b); + $max = max($r, $g, $b); + + $L = ($min + $max) / 2; + if ($min == $max) { + $S = $H = 0; + } else { + if ($L < 0.5) { + $S = ($max - $min)/($max + $min); + } else { + $S = ($max - $min)/(2.0 - $max - $min); + } + if ($r == $max) { + $H = ($g - $b)/($max - $min); + } elseif ($g == $max) { + $H = 2.0 + ($b - $r)/($max - $min); + } elseif ($b == $max) { + $H = 4.0 + ($r - $g)/($max - $min); + } + + } + + $out = array('hsl', + ($H < 0 ? $H + 6 : $H)*60, + $S * 100, + $L * 100, + ); + + if (count($color) > 4) { + // copy alpha + $out[] = $color[4]; + } + return $out; + } + + protected function lib_isnumber($value) { + return $this->toBool($value[0] == "number"); + } + + protected function lib_isstring($value) { + return $this->toBool($value[0] == "string"); + } + + protected function lib_iscolor($value) { + return $this->toBool($this->coerceColor($value)); + } + + protected function lib_iskeyword($value) { + return $this->toBool($value[0] == "keyword"); + } + + protected function lib_ispixel($value) { + return $this->toBool($value[0] == "number" && $value[2] == "px"); + } + + protected function lib_ispercentage($value) { + return $this->toBool($value[0] == "number" && $value[2] == "%"); + } + + protected function lib_isem($value) { + return $this->toBool($value[0] == "number" && $value[2] == "em"); + } + + protected function lib_isrem($value) { + return $this->toBool($value[0] == "number" && $value[2] == "rem"); + } + + protected function lib_rgbahex($color) { + $color = $this->coerceColor($color); + if (is_null($color)) { + $this->throwError("color expected for rgbahex"); + } + + return sprintf("#%02x%02x%02x%02x", + isset($color[4]) ? $color[4]*255 : 255, + $color[1], + $color[2], + $color[3] + ); + } + + protected function lib_argb($color){ + return $this->lib_rgbahex($color); + } + + protected function lib_floor($arg) { + $value = $this->assertNumber($arg); + return array("number", floor($value), $arg[2]); + } + + protected function lib_ceil($arg) { + $value = $this->assertNumber($arg); + return array("number", ceil($value), $arg[2]); + } + + protected function lib_round($arg) { + $value = $this->assertNumber($arg); + return array("number", round($value), $arg[2]); + } + + protected function lib_unit($arg) { + if ($arg[0] == "list") { + list($number, $newUnit) = $arg[2]; + return array("number", $this->assertNumber($number), + $this->compileValue($this->lib_e($newUnit))); + } + return array("number", $this->assertNumber($arg), ""); + } + + protected function lib_darken($args) { + list($color, $delta) = $this->colorArgs($args); + + $hsl = $this->toHSL($color); + $hsl[3] = $this->clamp($hsl[3] - $delta, 100); + return $this->toRGB($hsl); + } + + protected function lib_lighten($args) { + list($color, $delta) = $this->colorArgs($args); + + $hsl = $this->toHSL($color); + $hsl[3] = $this->clamp($hsl[3] + $delta, 100); + return $this->toRGB($hsl); + } + + protected function lib_saturate($args) { + list($color, $delta) = $this->colorArgs($args); + + $hsl = $this->toHSL($color); + $hsl[2] = $this->clamp($hsl[2] + $delta, 100); + return $this->toRGB($hsl); + } + + protected function lib_desaturate($args) { + list($color, $delta) = $this->colorArgs($args); + + $hsl = $this->toHSL($color); + $hsl[2] = $this->clamp($hsl[2] - $delta, 100); + return $this->toRGB($hsl); + } + + protected function lib_spin($args) { + list($color, $delta) = $this->colorArgs($args); + + $hsl = $this->toHSL($color); + + $hsl[1] = $hsl[1] + $delta % 360; + if ($hsl[1] < 0) { + $hsl[1] += 360; + } + + return $this->toRGB($hsl); + } + + protected function lib_fadeout($args) { + list($color, $delta) = $this->colorArgs($args); + $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100); + return $color; + } + + protected function lib_fadein($args) { + list($color, $delta) = $this->colorArgs($args); + $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100); + return $color; + } + + protected function lib_hue($color) { + $hsl = $this->toHSL($this->assertColor($color)); + return round($hsl[1]); + } + + protected function lib_saturation($color) { + $hsl = $this->toHSL($this->assertColor($color)); + return round($hsl[2]); + } + + protected function lib_lightness($color) { + $hsl = $this->toHSL($this->assertColor($color)); + return round($hsl[3]); + } + + // get the alpha of a color + // defaults to 1 for non-colors or colors without an alpha + protected function lib_alpha($value) { + if (!is_null($color = $this->coerceColor($value))) { + return isset($color[4]) ? $color[4] : 1; + } + } + + // set the alpha of the color + protected function lib_fade($args) { + list($color, $alpha) = $this->colorArgs($args); + $color[4] = $this->clamp($alpha / 100.0); + return $color; + } + + protected function lib_percentage($arg) { + $num = $this->assertNumber($arg); + return array("number", $num*100, "%"); + } + + // mixes two colors by weight + // mix(@color1, @color2, @weight); + // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method + protected function lib_mix($args) { + if ($args[0] != "list" || count($args[2]) < 3) + $this->throwError("mix expects (color1, color2, weight)"); + + list($first, $second, $weight) = $args[2]; + $first = $this->assertColor($first); + $second = $this->assertColor($second); + + $first_a = $this->lib_alpha($first); + $second_a = $this->lib_alpha($second); + $weight = $weight[1] / 100.0; + + $w = $weight * 2 - 1; + $a = $first_a - $second_a; + + $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; + $w2 = 1.0 - $w1; + + $new = array('color', + $w1 * $first[1] + $w2 * $second[1], + $w1 * $first[2] + $w2 * $second[2], + $w1 * $first[3] + $w2 * $second[3], + ); + + if ($first_a != 1.0 || $second_a != 1.0) { + $new[] = $first_a * $weight + $second_a * ($weight - 1); + } + + return $this->fixColor($new); + } + + protected function lib_contrast($args) { + if ($args[0] != 'list' || count($args[2]) < 3) { + return array(array('color', 0, 0, 0), 0); + } + + list($inputColor, $darkColor, $lightColor) = $args[2]; + + $inputColor = $this->assertColor($inputColor); + $darkColor = $this->assertColor($darkColor); + $lightColor = $this->assertColor($lightColor); + $hsl = $this->toHSL($inputColor); + + if ($hsl[3] > 50) { + return $darkColor; + } + + return $lightColor; + } + + function lib_red($color){ + $color = $this->coerceColor($color); + if (is_null($color)) { + $this->throwError('color expected for red()'); + } + + return $color[1]; + } + + function lib_green($color){ + $color = $this->coerceColor($color); + if (is_null($color)) { + $this->throwError('color expected for green()'); + } + + return $color[2]; + } + + function lib_blue($color){ + $color = $this->coerceColor($color); + if (is_null($color)) { + $this->throwError('color expected for blue()'); + } + + return $color[3]; + } +} + +/** + * The LESS compiler and parser. + * + * Converting LESS to CSS is a three stage process. The incoming file is parsed + * by `lessc_parser` into a syntax tree, then it is compiled into another tree + * representing the CSS structure by `lessc`. The CSS tree is fed into a + * formatter, like `lessc_formatter` which then outputs CSS as a string. + * + * During the first compile, all values are *reduced*, which means that their + * types are brought to the lowest form before being dump as strings. This + * handles math equations, variable dereferences, and the like. + * + * The `parse` function of `lessc` is the entry point. + * + * In summary: + * + * The `lessc` class creates an intstance of the parser, feeds it LESS code, + * then transforms the resulting tree to a CSS tree. This class also holds the + * evaluation context, such as all available mixins and variables at any given + * time. + * + * The `lessc_parser` class is only concerned with parsing its input. + * + * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string, + * handling things like indentation. + */ +class lessc { + + use lessc_lib; + + static public $VERSION = "v0.3.9-sm"; + + static protected $TRUE = array("keyword", "true"); + static protected $FALSE = array("keyword", "false"); + + const vPrefix = '@'; // prefix of abstract properties + const mPrefix = '$'; // prefix of abstract blocks + const parentSelector = '&'; + + // set to the parser that generated the current line when compiling + // so we know how to create error messages + protected $sourceParser = null; + protected $sourceLoc = null; + + static public $defaultValue = array("keyword", ""); + + public static function compressList($items, $delim) { + if (!isset($items[1]) && isset($items[0])) return $items[0]; + return array('list', $delim, $items); + } + + public static function preg_quote($what) { + return preg_quote($what, '/'); + } + + /** + * Recursively compiles a block. + * + * A block is analogous to a CSS block in most cases. A single LESS document + * is encapsulated in a block when parsed, but it does not have parent tags + * so all of it's children appear on the root level when compiled. + * + * Blocks are made up of props and children. + * + * Props are property instructions, array tuples which describe an action + * to be taken, eg. write a property, set a variable, mixin a block. + * + * The children of a block are just all the blocks that are defined within. + * This is used to look up mixins when performing a mixin. + * + * Compiling the block involves pushing a fresh environment on the stack, + * and iterating through the props, compiling each one. + * + * See lessc::compileProp() + * + */ + protected function compileBlock($block) { + switch ($block->type) { + case "root": + $this->compileRoot($block); + break; + case null: + $this->compileCSSBlock($block); + break; + case "media": + $this->compileMedia($block); + break; + case "directive": + $name = "@" . $block->name; + if (!empty($block->value)) { + $name .= " " . $this->compileValue($this->reduce($block->value)); + } + + $this->compileNestedBlock($block, array($name)); + break; + default: + $this->throwError("unknown block type: $block->type\n"); + } + } + + protected function compileCSSBlock($block) { + $env = $this->pushEnv(); + + $selectors = $this->compileSelectors($block->tags); + $env->selectors = $this->multiplySelectors($selectors); + $out = $this->makeOutputBlock(null, $env->selectors); + + $this->scope->children[] = $out; + $this->compileProps($block, $out); + + $block->scope = $env; // mixins carry scope with them! + $this->popEnv(); + } + + protected function compileMedia($media) { + $env = $this->pushEnv($media); + $parentScope = $this->mediaParent($this->scope); + + $query = $this->compileMediaQuery($this->multiplyMedia($env)); + + $this->scope = $this->makeOutputBlock($media->type, array($query)); + $parentScope->children[] = $this->scope; + + $this->compileProps($media, $this->scope); + + if (count($this->scope->lines) > 0) { + $orphanSelelectors = $this->findClosestSelectors(); + if (!is_null($orphanSelelectors)) { + $orphan = $this->makeOutputBlock(null, $orphanSelelectors); + $orphan->lines = $this->scope->lines; + array_unshift($this->scope->children, $orphan); + $this->scope->lines = array(); + } + } + + $this->scope = $this->scope->parent; + $this->popEnv(); + } + + protected function mediaParent($scope) { + while (!empty($scope->parent)) { + if (!empty($scope->type) && $scope->type != "media") { + break; + } + $scope = $scope->parent; + } + + return $scope; + } + + protected function compileNestedBlock($block, $selectors) { + $this->pushEnv($block); + $this->scope = $this->makeOutputBlock($block->type, $selectors); + $this->scope->parent->children[] = $this->scope; + + $this->compileProps($block, $this->scope); + + $this->scope = $this->scope->parent; + $this->popEnv(); + } + + protected function compileRoot($root) { + $this->pushEnv(); + $this->scope = $this->makeOutputBlock($root->type); + $this->compileProps($root, $this->scope); + $this->popEnv(); + } + + protected function compileProps($block, $out) { + foreach ($this->sortProps($block->props) as $prop) { + $this->compileProp($prop, $block, $out); + } + } + + protected function sortProps($props, $split = false) { + $vars = array(); + $other = array(); + + foreach ($props as $prop) { + switch ($prop[0]) { + case "assign": + if (isset($prop[1][0]) && $prop[1][0] == self::vPrefix) { + $vars[] = $prop; + } else { + $other[] = $prop; + } + break; + case "import": + break; + default: + $other[] = $prop; + } + } + + return $split ? array($vars, $other) : array_merge($vars, $other); + } + + protected function compileMediaQuery($queries) { + $compiledQueries = array(); + foreach ($queries as $query) { + $parts = array(); + foreach ($query as $q) { + switch ($q[0]) { + case "mediaType": + $parts[] = implode(" ", array_slice($q, 1)); + break; + case "mediaExp": + if (isset($q[2])) { + $parts[] = "($q[1]: " . + $this->compileValue($this->reduce($q[2])) . ")"; + } else { + $parts[] = "($q[1])"; + } + break; + case "variable": + $parts[] = $this->compileValue($this->reduce($q)); + break; + } + } + + if (count($parts) > 0) { + $compiledQueries[] = implode(" and ", $parts); + } + } + + $out = "@media"; + if (!empty($parts)) { + $out .= " " . + implode(',', $compiledQueries); + } + return $out; + } + + protected function multiplyMedia($env, $childQueries = null) { + if (is_null($env) || + !empty($env->block->type) && $env->block->type != "media" + ) { + return $childQueries; + } + + // plain old block, skip + if (empty($env->block->type)) { + return $this->multiplyMedia($env->parent, $childQueries); + } + + $out = array(); + $queries = $env->block->queries; + if (is_null($childQueries)) { + $out = $queries; + } else { + foreach ($queries as $parent) { + foreach ($childQueries as $child) { + $out[] = array_merge($parent, $child); + } + } + } + + return $this->multiplyMedia($env->parent, $out); + } + + protected function expandParentSelectors(&$tag, $replace) { + $parts = explode("$&$", $tag); + $count = 0; + foreach ($parts as &$part) { + $part = str_replace(self::parentSelector, $replace, $part, $c); + $count += $c; + } + $tag = implode(self::parentSelector, $parts); + return $count; + } + + protected function findClosestSelectors() { + $env = $this->env; + $selectors = null; + while ($env !== null) { + if (isset($env->selectors)) { + $selectors = $env->selectors; + break; + } + $env = $env->parent; + } + + return $selectors; + } + + + // multiply $selectors against the nearest selectors in env + protected function multiplySelectors($selectors) { + // find parent selectors + + $parentSelectors = $this->findClosestSelectors(); + if (is_null($parentSelectors)) { + // kill parent reference in top level selector + foreach ($selectors as &$s) { + $this->expandParentSelectors($s, ""); + } + + return $selectors; + } + + $out = array(); + foreach ($parentSelectors as $parent) { + foreach ($selectors as $child) { + $count = $this->expandParentSelectors($child, $parent); + + // don't prepend the parent tag if & was used + if ($count > 0) { + $out[] = trim($child); + } else { + $out[] = trim($parent . ' ' . $child); + } + } + } + + return $out; + } + + // reduces selector expressions + protected function compileSelectors($selectors) { + $out = array(); + + foreach ($selectors as $s) { + if (is_array($s)) { + list(, $value) = $s; + $out[] = trim($this->compileValue($this->reduce($value))); + } else { + $out[] = $s; + } + } + + return $out; + } + + protected function eq($left, $right) { + return $left == $right; + } + + protected function patternMatch($block, $callingArgs) { + // match the guards if it has them + // any one of the groups must have all its guards pass for a match + if (!empty($block->guards)) { + $groupPassed = false; + foreach ($block->guards as $guardGroup) { + foreach ($guardGroup as $guard) { + $this->pushEnv(); + $this->zipSetArgs($block->args, $callingArgs); + + $negate = false; + if ($guard[0] == "negate") { + $guard = $guard[1]; + $negate = true; + } + + $passed = $this->reduce($guard) == self::$TRUE; + if ($negate) $passed = !$passed; + + $this->popEnv(); + + if ($passed) { + $groupPassed = true; + } else { + $groupPassed = false; + break; + } + } + + if ($groupPassed) break; + } + + if (!$groupPassed) { + return false; + } + } + + $numCalling = count($callingArgs); + + if (empty($block->args)) { + return $block->isVararg || $numCalling == 0; + } + + $i = -1; // no args + // try to match by arity or by argument literal + foreach ($block->args as $i => $arg) { + switch ($arg[0]) { + case "lit": + if (empty($callingArgs[$i]) || !$this->eq($arg[1], $callingArgs[$i])) { + return false; + } + break; + case "arg": + // no arg and no default value + if (!isset($callingArgs[$i]) && !isset($arg[2])) { + return false; + } + break; + case "rest": + --$i; // rest can be empty + break 2; + } + } + + if ($block->isVararg) { + return true; // not having enough is handled above + } + // greater than becuase default values always match + return $i + 1 >= $numCalling; + } + + protected function patternMatchAll($blocks, $callingArgs) { + $matches = null; + foreach ($blocks as $block) { + if ($this->patternMatch($block, $callingArgs)) { + $matches[] = $block; + } + } + + return $matches; + } + + // attempt to find blocks matched by path and args + protected function findBlocks($searchIn, $path, $args, $seen=array()) { + if ($searchIn == null) return null; + if (isset($seen[$searchIn->id])) return null; + $seen[$searchIn->id] = true; + + $name = $path[0]; + + if (isset($searchIn->children[$name])) { + $blocks = $searchIn->children[$name]; + if (count($path) == 1) { + $matches = $this->patternMatchAll($blocks, $args); + if (!empty($matches)) { + // This will return all blocks that match in the closest + // scope that has any matching block, like lessjs + return $matches; + } + } else { + $matches = array(); + foreach ($blocks as $subBlock) { + $subMatches = $this->findBlocks($subBlock, + array_slice($path, 1), $args, $seen); + + if (!is_null($subMatches)) { + foreach ($subMatches as $sm) { + $matches[] = $sm; + } + } + } + + return count($matches) > 0 ? $matches : null; + } + } + + if ($searchIn->parent === $searchIn) return null; + return $this->findBlocks($searchIn->parent, $path, $args, $seen); + } + + // sets all argument names in $args to either the default value + // or the one passed in through $values + protected function zipSetArgs($args, $values) { + $i = 0; + $assignedValues = array(); + foreach ($args as $a) { + if ($a[0] == "arg") { + if ($i < count($values) && !is_null($values[$i])) { + $value = $values[$i]; + } elseif (isset($a[2])) { + $value = $a[2]; + } else { + $value = null; + } + + $value = $this->reduce($value); + $this->set($a[1], $value); + $assignedValues[] = $value; + } + ++$i; + } + + // check for a rest + $last = end($args); + if ($last[0] == "rest") { + $rest = array_slice($values, count($args) - 1); + $this->set($last[1], $this->reduce(array("list", " ", $rest))); + } + + $this->env->arguments = $assignedValues; + } + + // compile a prop and update $lines or $blocks appropriately + protected function compileProp($prop, $block, $out) { + // set error position context + $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1; + + switch ($prop[0]) { + case 'assign': + list(, $name, $value) = $prop; + if ($name[0] == self::vPrefix) { + $this->set($name, $value); + } else { + $out->lines[] = "{$name}:{$this->compileValue($this->reduce($value))};"; + } + break; + case 'block': + list(, $child) = $prop; + $this->compileBlock($child); + break; + case 'mixin': + list(, $path, $args, $suffix) = $prop; + + $args = array_map(array($this, "reduce"), (array)$args); + $mixins = $this->findBlocks($block, $path, $args); + + if ($mixins === null) { + // fwrite(STDERR,"failed to find block: ".implode(" > ", $path)."\n"); + break; // throw error here?? + } + + foreach ($mixins as $mixin) { + $haveScope = false; + if (isset($mixin->parent->scope)) { + $haveScope = true; + $mixinParentEnv = $this->pushEnv(); + $mixinParentEnv->storeParent = $mixin->parent->scope; + } + + $haveArgs = false; + if (isset($mixin->args)) { + $haveArgs = true; + $this->pushEnv(); + $this->zipSetArgs($mixin->args, $args); + } + + $oldParent = $mixin->parent; + if ($mixin != $block) $mixin->parent = $block; + + foreach ($this->sortProps($mixin->props) as $subProp) { + if ($suffix !== null && + $subProp[0] == "assign" && + is_string($subProp[1]) && + $subProp[1][0] != self::vPrefix + ) { + $subProp[2] = array( + 'list', ' ', + array($subProp[2], array('keyword', $suffix)) + ); + } + + $this->compileProp($subProp, $mixin, $out); + } + + $mixin->parent = $oldParent; + + if ($haveArgs) $this->popEnv(); + if ($haveScope) $this->popEnv(); + } + + break; + case 'raw': + $out->lines[] = $prop[1]; + break; + case "directive": + list(, $name, $value) = $prop; + $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';'; + break; + case "comment": + $out->lines[] = $prop[1]; + break; + case "import": + break; + case "import_mixin": + break; + default: + $this->throwError("unknown op: {$prop[0]}\n"); + } + } + + + /** + * Compiles a primitive value into a CSS property value. + * + * Values in lessphp are typed by being wrapped in arrays, their format is + * typically: + * + * array(type, contents [, additional_contents]*) + * + * The input is expected to be reduced. This function will not work on + * things like expressions and variables. + */ + protected function compileValue($value) { + switch ($value[0]) { + case 'list': + // [1] - delimiter + // [2] - array of values + return implode($value[1], array_map(array($this, 'compileValue'), $value[2])); + case 'raw_color': + return $this->compileValue($this->coerceColor($value)); + case 'keyword': + // [1] - the keyword + return $value[1]; + case 'number': + list(, $num, $unit) = $value; + // [1] - the number + // [2] - the unit + return $num . $unit; + case 'string': + // [1] - contents of string (includes quotes) + list(, $delim, $content) = $value; + foreach ($content as &$part) { + if (is_array($part)) { + $part = $this->compileValue($part); + } + } + return $delim . implode($content) . $delim; + case 'color': + // [1] - red component (either number or a %) + // [2] - green component + // [3] - blue component + // [4] - optional alpha component + list(, $r, $g, $b) = $value; + $r = round($r); + $g = round($g); + $b = round($b); + + if (count($value) == 5 && $value[4] != 1) { // rgba + return 'rgba('.$r.','.$g.','.$b.','.$value[4].')'; + } + + $h = sprintf("#%02x%02x%02x", $r, $g, $b); + + // Converting hex color to short notation (e.g. #003399 to #039) + if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { + $h = '#' . $h[1] . $h[3] . $h[5]; + } + + return $h; + + case 'function': + list(, $name, $args) = $value; + return $name.'('.$this->compileValue($args).')'; + default: // assumed to be unit + $this->throwError("unknown value type: $value[0]"); + } + } + + // utility func to unquote a string + protected function lib_e($arg) { + switch ($arg[0]) { + case "list": + $items = $arg[2]; + if (isset($items[0])) { + return $this->lib_e($items[0]); + } + return self::$defaultValue; + case "string": + $arg[1] = ""; + return $arg; + case "keyword": + return $arg; + default: + return array("keyword", $this->compileValue($arg)); + } + } + + protected function lib__sprintf($args) { + if ($args[0] != "list") return $args; + $values = $args[2]; + $string = array_shift($values); + $template = $this->compileValue($this->lib_e($string)); + + $i = 0; + if (preg_match_all('/%[dsa]/', $template, $m)) { + foreach ($m[0] as $match) { + $val = isset($values[$i]) ? + $this->reduce($values[$i]) : array('keyword', ''); + + // lessjs compat, renders fully expanded color, not raw color + if ($color = $this->coerceColor($val)) { + $val = $color; + } + + ++$i; + $rep = $this->compileValue($this->lib_e($val)); + $template = preg_replace('/'.self::preg_quote($match).'/', + $rep, $template, 1); + } + } + + $d = $string[0] == "string" ? $string[1] : '"'; + return array("string", $d, array($template)); + } + + protected function assertColor($value, $error = "expected color value") { + $color = $this->coerceColor($value); + if (is_null($color)) $this->throwError($error); + return $color; + } + + protected function toRGB_helper($comp, $temp1, $temp2) { + if ($comp < 0) { + $comp += 1.0; + } elseif ($comp > 1) { + $comp -= 1.0; + } + + if (6 * $comp < 1) { + return $temp1 + ($temp2 - $temp1) * 6 * $comp; + } + if (2 * $comp < 1) { + return $temp2; + } + if (3 * $comp < 2) { + return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; + } + + return $temp1; + } + + /** + * Converts a hsl array into a color value in rgb. + * Expects H to be in range of 0 to 360, S and L in 0 to 100 + */ + protected function toRGB($color) { + if ($color[0] === 'color') { + return $color; + } + + $H = $color[1] / 360; + $S = $color[2] / 100; + $L = $color[3] / 100; + + if ($S == 0) { + $r = $g = $b = $L; + } else { + $temp2 = $L < 0.5 ? + $L * (1.0 + $S) : + $L + $S - $L * $S; + + $temp1 = 2.0 * $L - $temp2; + + $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2); + $g = $this->toRGB_helper($H, $temp1, $temp2); + $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2); + } + + // $out = array('color', round($r*255), round($g*255), round($b*255)); + $out = array('color', $r*255, $g*255, $b*255); + if (count($color) > 4) { + // copy alpha + $out[] = $color[4]; + } + return $out; + } + + protected function clamp($v, $max = 1, $min = 0) { + return min($max, max($min, $v)); + } + + /** + * Convert the rgb, rgba, hsl color literals of function type + * as returned by the parser into values of color type. + */ + protected function funcToColor($func) { + $fname = $func[1]; + if ($func[2][0] != 'list') { + // need a list of arguments + return false; + } + $rawComponents = $func[2][2]; + + if ($fname == 'hsl' || $fname == 'hsla') { + $hsl = array('hsl'); + $i = 0; + foreach ($rawComponents as $c) { + $val = $this->reduce($c); + $val = isset($val[1]) ? floatval($val[1]) : 0; + + if ($i == 0) { + $clamp = 360; + } elseif ($i < 3) { + $clamp = 100; + } else { + $clamp = 1; + } + + $hsl[] = $this->clamp($val, $clamp); + ++$i; + } + + while (count($hsl) < 4) { + $hsl[] = 0; + } + return $this->toRGB($hsl); + + } elseif ($fname == 'rgb' || $fname == 'rgba') { + $components = array(); + $i = 1; + foreach ($rawComponents as $c) { + $c = $this->reduce($c); + if ($i < 4) { + if ($c[0] == "number" && $c[2] == "%") { + $components[] = 255 * ($c[1] / 100); + } else { + $components[] = floatval($c[1]); + } + } elseif ($i == 4) { + if ($c[0] == "number" && $c[2] == "%") { + $components[] = 1.0 * ($c[1] / 100); + } else { + $components[] = floatval($c[1]); + } + } else break; + + ++$i; + } + while (count($components) < 3) { + $components[] = 0; + } + array_unshift($components, 'color'); + return $this->fixColor($components); + } + + return false; + } + + protected function reduce($value, $forExpression = false) { + switch ($value[0]) { + case "interpolate": + $reduced = $this->reduce($value[1]); + $var = $this->compileValue($reduced); + $res = $this->reduce(array("variable", self::vPrefix . $var)); + + if (empty($value[2])) $res = $this->lib_e($res); + + return $res; + case "variable": + $key = $value[1]; + if (is_array($key)) { + $key = $this->reduce($key); + $key = self::vPrefix . $this->compileValue($this->lib_e($key)); + } + + $seen =& $this->env->seenNames; + + if (!empty($seen[$key])) { + $this->throwError("infinite loop detected: $key"); + } + + $seen[$key] = true; + $out = $this->reduce($this->get($key, self::$defaultValue)); + $seen[$key] = false; + return $out; + case "list": + foreach ($value[2] as &$item) { + $item = $this->reduce($item, $forExpression); + } + return $value; + case "expression": + return $this->evaluate($value); + case "string": + foreach ($value[2] as &$part) { + if (is_array($part)) { + $strip = $part[0] == "variable"; + $part = $this->reduce($part); + if ($strip) $part = $this->lib_e($part); + } + } + return $value; + case "escape": + list(,$inner) = $value; + return $this->lib_e($this->reduce($inner)); + case "function": + $color = $this->funcToColor($value); + if ($color) return $color; + + list(, $name, $args) = $value; + if ($name == "%") $name = "_sprintf"; + + $f = array($this, 'lib_'.$name); + + if (is_callable($f)) { + if ($args[0] == 'list') + $args = self::compressList($args[2], $args[1]); + + $ret = $f($this->reduce($args, true), $this); + + if (is_null($ret)) { + return array("string", "", array( + $name, "(", $args, ")" + )); + } + + // convert to a typed value if the result is a php primitive + if (is_numeric($ret)) { + $ret = array('number', $ret, ""); + } elseif (!is_array($ret)) { + $ret = array('keyword', $ret); + } + + return $ret; + } + + // plain function, reduce args + $value[2] = $this->reduce($value[2]); + return $value; + case "unary": + list(, $op, $exp) = $value; + $exp = $this->reduce($exp); + + if ($exp[0] == "number") { + switch ($op) { + case "+": + return $exp; + case "-": + $exp[1] *= -1; + return $exp; + } + } + return array("string", "", array($op, $exp)); + } + + if ($forExpression) { + switch ($value[0]) { + case "keyword": + if ($color = $this->coerceColor($value)) { + return $color; + } + break; + case "raw_color": + return $this->coerceColor($value); + } + } + + return $value; + } + + + // coerce a value for use in color operation + protected function coerceColor($value) { + switch ($value[0]) { + case 'color': return $value; + case 'raw_color': + $c = array("color", 0, 0, 0); + $colorStr = substr($value[1], 1); + $num = hexdec($colorStr); + $width = strlen($colorStr) == 3 ? 16 : 256; + + for ($i = 3; $i > 0; --$i) { // 3 2 1 + $t = $num % $width; + $num /= $width; + + $c[$i] = $t * (256/$width) + $t * floor(16/$width); + } + + return $c; + case 'keyword': + $name = $value[1]; + if (isset(self::$cssColors[$name])) { + $rgba = explode(',', self::$cssColors[$name]); + + if (isset($rgba[3])) { + return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]); + } + return array('color', $rgba[0], $rgba[1], $rgba[2]); + } + return null; + } + } + + // make something string like into a string + protected function coerceString($value) { + switch ($value[0]) { + case "string": + return $value; + case "keyword": + return array("string", "", array($value[1])); + } + return null; + } + + // turn list of length 1 into value type + protected function flattenList($value) { + if ($value[0] == "list" && count($value[2]) == 1) { + return $this->flattenList($value[2][0]); + } + return $value; + } + + protected function toBool($a) { + return $a ? self::$TRUE : self::$FALSE; + } + + // evaluate an expression + protected function evaluate($exp) { + list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; + + $left = $this->reduce($left, true); + $right = $this->reduce($right, true); + + if ($leftColor = $this->coerceColor($left)) { + $left = $leftColor; + } + + if ($rightColor = $this->coerceColor($right)) { + $right = $rightColor; + } + + $ltype = $left[0]; + $rtype = $right[0]; + + // operators that work on all types + if ($op == "and") { + return $this->toBool($left == self::$TRUE && $right == self::$TRUE); + } + + if ($op == "=") { + return $this->toBool($this->eq($left, $right) ); + } + + if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) { + return $str; + } + + // type based operators + $fname = "op_${ltype}_${rtype}"; + if (is_callable(array($this, $fname))) { + $out = $this->$fname($op, $left, $right); + if (!is_null($out)) return $out; + } + + // make the expression look it did before being parsed + $paddedOp = $op; + if ($whiteBefore) { + $paddedOp = " " . $paddedOp; + } + if ($whiteAfter) { + $paddedOp .= " "; + } + + return array("string", "", array($left, $paddedOp, $right)); + } + + protected function stringConcatenate($left, $right) { + if ($strLeft = $this->coerceString($left)) { + if ($right[0] == "string") { + $right[1] = ""; + } + $strLeft[2][] = $right; + return $strLeft; + } + + if ($strRight = $this->coerceString($right)) { + array_unshift($strRight[2], $left); + return $strRight; + } + } + + + // make sure a color's components don't go out of bounds + protected function fixColor($c) { + foreach (range(1, 3) as $i) { + if ($c[$i] < 0) $c[$i] = 0; + if ($c[$i] > 255) $c[$i] = 255; + } + + return $c; + } + + protected function op_number_color($op, $lft, $rgt) { + if ($op == '+' || $op == '*') { + return $this->op_color_number($op, $rgt, $lft); + } + } + + protected function op_color_number($op, $lft, $rgt) { + if ($rgt[0] == '%') $rgt[1] /= 100; + + return $this->op_color_color($op, $lft, + array_fill(1, count($lft) - 1, $rgt[1])); + } + + protected function op_color_color($op, $left, $right) { + $out = array('color'); + $max = count($left) > count($right) ? count($left) : count($right); + foreach (range(1, $max - 1) as $i) { + $lval = isset($left[$i]) ? $left[$i] : 0; + $rval = isset($right[$i]) ? $right[$i] : 0; + switch ($op) { + case '+': + $out[] = $lval + $rval; + break; + case '-': + $out[] = $lval - $rval; + break; + case '*': + $out[] = $lval * $rval; + break; + case '%': + $out[] = $lval % $rval; + break; + case '/': + if ($rval == 0) { + $this->throwError("evaluate error: can't divide by zero"); + } + $out[] = $lval / $rval; + break; + default: + $this->throwError('evaluate error: color op number failed on op '.$op); + } + } + return $this->fixColor($out); + } + + // operator on two numbers + protected function op_number_number($op, $left, $right) { + $unit = empty($left[2]) ? $right[2] : $left[2]; + + $value = 0; + switch ($op) { + case '+': + $value = $left[1] + $right[1]; + break; + case '*': + $value = $left[1] * $right[1]; + break; + case '-': + $value = $left[1] - $right[1]; + break; + case '%': + $value = $left[1] % $right[1]; + break; + case '/': + if ($right[1] == 0) $this->throwError('parse error: divide by zero'); + $value = $left[1] / $right[1]; + break; + case '<': + return $this->toBool($left[1] < $right[1]); + case '>': + return $this->toBool($left[1] > $right[1]); + case '>=': + return $this->toBool($left[1] >= $right[1]); + case '=<': + return $this->toBool($left[1] <= $right[1]); + default: + $this->throwError('parse error: unknown number operator: '.$op); + } + + return array("number", $value, $unit); + } + + + /* environment functions */ + + protected function makeOutputBlock($type, $selectors = null) { + $b = new \stdClass; + $b->lines = array(); + $b->children = array(); + $b->selectors = $selectors; + $b->type = $type; + $b->parent = $this->scope; + return $b; + } + + // the state of execution + protected function pushEnv($block = null) { + $e = new \stdClass; + $e->parent = $this->env; + $e->store = array(); + $e->block = $block; + + $this->env = $e; + return $e; + } + + // pop something off the stack + protected function popEnv() { + $old = $this->env; + $this->env = $this->env->parent; + return $old; + } + + // set something in the current env + protected function set($name, $value) { + $this->env->store[$name] = $value; + } + + // get the highest occurrence entry for a name + protected function get($name, $default=null) { + $current = $this->env; + + $isArguments = $name == self::vPrefix . 'arguments'; + while ($current) { + if ($isArguments && isset($current->arguments)) { + return array('list', ' ', $current->arguments); + } + + if (isset($current->store[$name])) { + return $current->store[$name]; + } + + $current = isset($current->storeParent) ? + $current->storeParent : + $current->parent; + } + + return $default; + } + + protected function isEmpty($block) { + if (empty($block->lines)) { + foreach ($block->children as $child) { + if (!$this->isEmpty($child)) return false; + } + + return true; + } + return false; + } + + protected function block($block) : string { + if ($this->isEmpty($block)) + return ''; + + $result = ''; + + if (!empty($block->selectors)) { + $result .= implode(',', $block->selectors) . '{'; + } + + if (!empty($block->lines)) { + $result .= implode('', $block->lines); + } + + foreach ($block->children as $child) { + $result .= $this->block($child); + } + + if (!empty($block->selectors)) { + $result .= '}'; + } + + return $result; + } + + public function compile($string, $name = null) { + $locale = setlocale(LC_NUMERIC, 0); + setlocale(LC_NUMERIC, "C"); + + $this->parser = new lessc_parser($name); + $root = $this->parser->parse($string); + + $this->env = null; + $this->scope = null; + + $this->sourceParser = $this->parser; // used for error messages + $this->compileBlock($root); + + $out = $this->block($this->scope); + setlocale(LC_NUMERIC, $locale); + return $out; + } + + /** + * Uses the current value of $this->count to show line and line number + */ + protected function throwError($msg = null) { + if ($this->sourceLoc >= 0) { + $this->sourceParser->throwError($msg, $this->sourceLoc); + } + throw new \Exception($msg); + } + + static protected $cssColors = array( + 'aliceblue' => '240,248,255', + 'antiquewhite' => '250,235,215', + 'aqua' => '0,255,255', + 'aquamarine' => '127,255,212', + 'azure' => '240,255,255', + 'beige' => '245,245,220', + 'bisque' => '255,228,196', + 'black' => '0,0,0', + 'blanchedalmond' => '255,235,205', + 'blue' => '0,0,255', + 'blueviolet' => '138,43,226', + 'brown' => '165,42,42', + 'burlywood' => '222,184,135', + 'cadetblue' => '95,158,160', + 'chartreuse' => '127,255,0', + 'chocolate' => '210,105,30', + 'coral' => '255,127,80', + 'cornflowerblue' => '100,149,237', + 'cornsilk' => '255,248,220', + 'crimson' => '220,20,60', + 'cyan' => '0,255,255', + 'darkblue' => '0,0,139', + 'darkcyan' => '0,139,139', + 'darkgoldenrod' => '184,134,11', + 'darkgray' => '169,169,169', + 'darkgreen' => '0,100,0', + 'darkgrey' => '169,169,169', + 'darkkhaki' => '189,183,107', + 'darkmagenta' => '139,0,139', + 'darkolivegreen' => '85,107,47', + 'darkorange' => '255,140,0', + 'darkorchid' => '153,50,204', + 'darkred' => '139,0,0', + 'darksalmon' => '233,150,122', + 'darkseagreen' => '143,188,143', + 'darkslateblue' => '72,61,139', + 'darkslategray' => '47,79,79', + 'darkslategrey' => '47,79,79', + 'darkturquoise' => '0,206,209', + 'darkviolet' => '148,0,211', + 'deeppink' => '255,20,147', + 'deepskyblue' => '0,191,255', + 'dimgray' => '105,105,105', + 'dimgrey' => '105,105,105', + 'dodgerblue' => '30,144,255', + 'firebrick' => '178,34,34', + 'floralwhite' => '255,250,240', + 'forestgreen' => '34,139,34', + 'fuchsia' => '255,0,255', + 'gainsboro' => '220,220,220', + 'ghostwhite' => '248,248,255', + 'gold' => '255,215,0', + 'goldenrod' => '218,165,32', + 'gray' => '128,128,128', + 'green' => '0,128,0', + 'greenyellow' => '173,255,47', + 'grey' => '128,128,128', + 'honeydew' => '240,255,240', + 'hotpink' => '255,105,180', + 'indianred' => '205,92,92', + 'indigo' => '75,0,130', + 'ivory' => '255,255,240', + 'khaki' => '240,230,140', + 'lavender' => '230,230,250', + 'lavenderblush' => '255,240,245', + 'lawngreen' => '124,252,0', + 'lemonchiffon' => '255,250,205', + 'lightblue' => '173,216,230', + 'lightcoral' => '240,128,128', + 'lightcyan' => '224,255,255', + 'lightgoldenrodyellow' => '250,250,210', + 'lightgray' => '211,211,211', + 'lightgreen' => '144,238,144', + 'lightgrey' => '211,211,211', + 'lightpink' => '255,182,193', + 'lightsalmon' => '255,160,122', + 'lightseagreen' => '32,178,170', + 'lightskyblue' => '135,206,250', + 'lightslategray' => '119,136,153', + 'lightslategrey' => '119,136,153', + 'lightsteelblue' => '176,196,222', + 'lightyellow' => '255,255,224', + 'lime' => '0,255,0', + 'limegreen' => '50,205,50', + 'linen' => '250,240,230', + 'magenta' => '255,0,255', + 'maroon' => '128,0,0', + 'mediumaquamarine' => '102,205,170', + 'mediumblue' => '0,0,205', + 'mediumorchid' => '186,85,211', + 'mediumpurple' => '147,112,219', + 'mediumseagreen' => '60,179,113', + 'mediumslateblue' => '123,104,238', + 'mediumspringgreen' => '0,250,154', + 'mediumturquoise' => '72,209,204', + 'mediumvioletred' => '199,21,133', + 'midnightblue' => '25,25,112', + 'mintcream' => '245,255,250', + 'mistyrose' => '255,228,225', + 'moccasin' => '255,228,181', + 'navajowhite' => '255,222,173', + 'navy' => '0,0,128', + 'oldlace' => '253,245,230', + 'olive' => '128,128,0', + 'olivedrab' => '107,142,35', + 'orange' => '255,165,0', + 'orangered' => '255,69,0', + 'orchid' => '218,112,214', + 'palegoldenrod' => '238,232,170', + 'palegreen' => '152,251,152', + 'paleturquoise' => '175,238,238', + 'palevioletred' => '219,112,147', + 'papayawhip' => '255,239,213', + 'peachpuff' => '255,218,185', + 'peru' => '205,133,63', + 'pink' => '255,192,203', + 'plum' => '221,160,221', + 'powderblue' => '176,224,230', + 'purple' => '128,0,128', + 'red' => '255,0,0', + 'rosybrown' => '188,143,143', + 'royalblue' => '65,105,225', + 'saddlebrown' => '139,69,19', + 'salmon' => '250,128,114', + 'sandybrown' => '244,164,96', + 'seagreen' => '46,139,87', + 'seashell' => '255,245,238', + 'sienna' => '160,82,45', + 'silver' => '192,192,192', + 'skyblue' => '135,206,235', + 'slateblue' => '106,90,205', + 'slategray' => '112,128,144', + 'slategrey' => '112,128,144', + 'snow' => '255,250,250', + 'springgreen' => '0,255,127', + 'steelblue' => '70,130,180', + 'tan' => '210,180,140', + 'teal' => '0,128,128', + 'thistle' => '216,191,216', + 'tomato' => '255,99,71', + 'transparent' => '0,0,0,0', + 'turquoise' => '64,224,208', + 'violet' => '238,130,238', + 'wheat' => '245,222,179', + 'white' => '255,255,255', + 'whitesmoke' => '245,245,245', + 'yellow' => '255,255,0', + 'yellowgreen' => '154,205,50' + ); +} + +// responsible for taking a string of LESS code and converting it into a +// syntax tree +class lessc_parser { + static protected $nextBlockId = 0; // used to uniquely identify blocks + + static protected $precedence = array( + '=<' => 0, + '>=' => 0, + '=' => 0, + '<' => 0, + '>' => 0, + + '+' => 1, + '-' => 1, + '*' => 2, + '/' => 2, + '%' => 2, + ); + + // regex string to match any of the operators + static protected $operatorString; + + // these properties will supress division unless it's inside parenthases + static protected $supressDivisionProps = + array('/border-radius$/i', '/^font$/i'); + + protected $blockDirectives = array("font-face", "keyframes", "page"); + protected $lineDirectives = array("charset"); + + /** + * if we are in parens we can be more liberal with whitespace around + * operators because it must evaluate to a single value and thus is less + * ambiguous. + * + * Consider: + * property1: 10 -5; // is two numbers, 10 and -5 + * property2: (10 -5); // should evaluate to 5 + */ + protected $inParens = false; + + // caches preg escaped literals + static protected $literalCache = array(); + + public function __construct($sourceName = null) { + $this->eatWhiteDefault = true; + + $this->sourceName = $sourceName; // name used for error messages + + if (!self::$operatorString) { + self::$operatorString = + '('.implode('|', array_map(array('LessPHP\\lessc', 'preg_quote'), + array_keys(self::$precedence))).')'; + } + } + + public function parse($buffer) { + $this->count = 0; + $this->line = 1; + + $this->env = null; // block stack + $this->buffer = $this->removeComments($buffer); + $this->pushSpecialBlock("root"); + $this->eatWhiteDefault = true; + $this->seenComments = array(); + + // trim whitespace on head + // if (preg_match('/^\s+/', $this->buffer, $m)) { + // $this->line += substr_count($m[0], "\n"); + // $this->buffer = ltrim($this->buffer); + // } + $this->whitespace(); + + // parse the entire file + $lastCount = $this->count; + while (false !== $this->parseChunk()); + + if ($this->count != strlen($this->buffer)) + $this->throwError(); + + // TODO report where the block was opened + if (!is_null($this->env->parent)) + throw new \Exception('parse error: unclosed block'); + + return $this->env; + } + + /** + * Parse a single chunk off the head of the buffer and append it to the + * current parse environment. + * Returns false when the buffer is empty, or when there is an error. + * + * This function is called repeatedly until the entire document is + * parsed. + * + * This parser is most similar to a recursive descent parser. Single + * functions represent discrete grammatical rules for the language, and + * they are able to capture the text that represents those rules. + * + * Consider the function lessc::keyword(). (all parse functions are + * structured the same) + * + * The function takes a single reference argument. When calling the + * function it will attempt to match a keyword on the head of the buffer. + * If it is successful, it will place the keyword in the referenced + * argument, advance the position in the buffer, and return true. If it + * fails then it won't advance the buffer and it will return false. + * + * All of these parse functions are powered by lessc::match(), which behaves + * the same way, but takes a literal regular expression. Sometimes it is + * more convenient to use match instead of creating a new function. + * + * Because of the format of the functions, to parse an entire string of + * grammatical rules, you can chain them together using &&. + * + * But, if some of the rules in the chain succeed before one fails, then + * the buffer position will be left at an invalid state. In order to + * avoid this, lessc::seek() is used to remember and set buffer positions. + * + * Before parsing a chain, use $s = $this->seek() to remember the current + * position into $s. Then if a chain fails, use $this->seek($s) to + * go back where we started. + */ + protected function parseChunk() { + if (empty($this->buffer)) return false; + $s = $this->seek(); + + // setting a property + if ($this->keyword($key) && $this->assign() && + $this->propertyValue($value, $key) && $this->end() + ) { + $this->append(array('assign', $key, $value), $s); + return true; + } + $this->seek($s); + + // look for special css blocks + if ($this->literal('@', false)) { + --$this->count; + + // media + if ($this->literal('@media')) { + if (($this->mediaQueryList($mediaQueries) || true) + && $this->literal('{') + ) { + $media = $this->pushSpecialBlock("media"); + $media->queries = is_null($mediaQueries) ? array() : $mediaQueries; + return true; + } + $this->seek($s); + return false; + } + + if ($this->literal("@", false) && $this->keyword($dirName)) { + if ($this->isDirective($dirName, $this->blockDirectives)) { + if (($this->openString("{", $dirValue, null, array(";")) || true) && + $this->literal("{") + ) { + $dir = $this->pushSpecialBlock("directive"); + $dir->name = $dirName; + if (isset($dirValue)) $dir->value = $dirValue; + return true; + } + } elseif ($this->isDirective($dirName, $this->lineDirectives)) { + if ($this->propertyValue($dirValue) && $this->end()) { + $this->append(array("directive", $dirName, $dirValue)); + return true; + } + } + } + + $this->seek($s); + } + + // setting a variable + if ($this->variable($var) && $this->assign() && + $this->propertyValue($value) && $this->end() + ) { + $this->append(array('assign', $var, $value), $s); + return true; + } + $this->seek($s); + + if ($this->import($importValue)) { + $this->append($importValue, $s); + return true; + } + + // opening parametric mixin + if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && + ($this->guards($guards) || true) && + $this->literal('{') + ) { + $block = $this->pushBlock($this->fixTags(array($tag))); + $block->args = $args; + $block->isVararg = $isVararg; + if (!empty($guards)) $block->guards = $guards; + return true; + } + $this->seek($s); + + // opening a simple block + if ($this->tags($tags) && $this->literal('{')) { + $tags = $this->fixTags($tags); + $this->pushBlock($tags); + return true; + } + $this->seek($s); + + // closing a block + if ($this->literal('}', false)) { + try { + $block = $this->pop(); + } catch (\Exception $e) { + $this->seek($s); + $this->throwError($e->getMessage()); + } + + $hidden = false; + if (is_null($block->type)) { + $hidden = true; + if (!isset($block->args)) { + foreach ($block->tags as $tag) { + if (!is_string($tag) || $tag[0] != lessc::mPrefix) { + $hidden = false; + break; + } + } + } + + foreach ($block->tags as $tag) { + if (is_string($tag)) { + $this->env->children[$tag][] = $block; + } + } + } + + if (!$hidden) { + $this->append(array('block', $block), $s); + } + + // this is done here so comments aren't bundled into he block that + // was just closed + $this->whitespace(); + return true; + } + + // mixin + if ($this->mixinTags($tags) && + ($this->argumentValues($argv) || true) && + ($this->keyword($suffix) || true) && $this->end() + ) { + $tags = $this->fixTags($tags); + $this->append(array('mixin', $tags, $argv, $suffix), $s); + return true; + } + $this->seek($s); + + // spare ; + if ($this->literal(';')) return true; + + return false; // got nothing, throw error + } + + protected function isDirective($dirname, $directives) { + // TODO: cache pattern in parser + $pattern = implode('|', + array_map(array('LessPHP\\lessc', 'preg_quote'), $directives)); + $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; + + return preg_match($pattern, $dirname); + } + + protected function fixTags($tags) { + // move @ tags out of variable namespace + foreach ($tags as &$tag) { + if ($tag[0] == lessc::vPrefix) + $tag[0] = lessc::mPrefix; + } + return $tags; + } + + // a list of expressions + protected function expressionList(&$exps) { + $values = array(); + + while ($this->expression($exp)) { + $values[] = $exp; + } + + if (count($values) == 0) return false; + + $exps = lessc::compressList($values, ' '); + return true; + } + + /** + * Attempt to consume an expression. + * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code + */ + protected function expression(&$out) { + if ($this->value($lhs)) { + $out = $this->expHelper($lhs, 0); + + // look for / shorthand + if (!empty($this->env->supressedDivision)) { + unset($this->env->supressedDivision); + $s = $this->seek(); + if ($this->literal("/") && $this->value($rhs)) { + $out = array("list", "", + array($out, array("keyword", "/"), $rhs)); + } else { + $this->seek($s); + } + } + + return true; + } + return false; + } + + /** + * recursively parse infix equation with $lhs at precedence $minP + */ + protected function expHelper($lhs, $minP) { + $this->inExp = true; + $ss = $this->seek(); + + while (true) { + $whiteBefore = isset($this->buffer[$this->count - 1]) && + ctype_space($this->buffer[$this->count - 1]); + + // If there is whitespace before the operator, then we require + // whitespace after the operator for it to be an expression + $needWhite = $whiteBefore && !$this->inParens; + + if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) { + if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) { + foreach (self::$supressDivisionProps as $pattern) { + if (preg_match($pattern, $this->env->currentProperty)) { + $this->env->supressedDivision = true; + break 2; + } + } + } + + + $whiteAfter = isset($this->buffer[$this->count - 1]) && + ctype_space($this->buffer[$this->count - 1]); + + if (!$this->value($rhs)) break; + + // peek for next operator to see what to do with rhs + if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) { + $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); + } + + $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter); + $ss = $this->seek(); + + continue; + } + + break; + } + + $this->seek($ss); + + return $lhs; + } + + // consume a list of values for a property + public function propertyValue(&$value, $keyName = null) { + $values = array(); + + if ($keyName !== null) $this->env->currentProperty = $keyName; + + $s = null; + while ($this->expressionList($v)) { + $values[] = $v; + $s = $this->seek(); + if (!$this->literal(',')) break; + } + + if ($s) $this->seek($s); + + if ($keyName !== null) unset($this->env->currentProperty); + + if (count($values) == 0) return false; + + $value = lessc::compressList($values, ', '); + return true; + } + + protected function parenValue(&$out) { + $s = $this->seek(); + + // speed shortcut + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") { + return false; + } + + $inParens = $this->inParens; + if ($this->literal("(") && + ($this->inParens = true) && $this->expression($exp) && + $this->literal(")") + ) { + $out = $exp; + $this->inParens = $inParens; + return true; + } else { + $this->inParens = $inParens; + $this->seek($s); + } + + return false; + } + + // a single value + protected function value(&$value) { + $s = $this->seek(); + + // speed shortcut + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") { + // negation + if ($this->literal("-", false) && + (($this->variable($inner) && $inner = array("variable", $inner)) || + $this->unit($inner) || + $this->parenValue($inner)) + ) { + $value = array("unary", "-", $inner); + return true; + } + $this->seek($s); + } + + if ($this->parenValue($value)) return true; + if ($this->unit($value)) return true; + if ($this->color($value)) return true; + if ($this->func($value)) return true; + if ($this->string($value)) return true; + + if ($this->keyword($word)) { + $value = array('keyword', $word); + return true; + } + + // try a variable + if ($this->variable($var)) { + $value = array('variable', $var); + return true; + } + + // unquote string (should this work on any type? + if ($this->literal("~") && $this->string($str)) { + $value = array("escape", $str); + return true; + } + $this->seek($s); + + // css hack: \0 + if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { + $value = array('keyword', '\\'.$m[1]); + return true; + } + $this->seek($s); + + return false; + } + + // an import statement + protected function import(&$out) { + $s = $this->seek(); + if (!$this->literal('@import')) return false; + + // @import "something.css" media; + // @import url("something.css") media; + // @import url(something.css) media; + + if ($this->propertyValue($value)) { + $out = array("import", $value); + return true; + } + } + + protected function mediaQueryList(&$out) { + if ($this->genericList($list, "mediaQuery", ",", false)) { + $out = $list[2]; + return true; + } + return false; + } + + protected function mediaQuery(&$out) { + $s = $this->seek(); + + $expressions = null; + $parts = array(); + + if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) { + $prop = array("mediaType"); + if (isset($only)) $prop[] = "only"; + if (isset($not)) $prop[] = "not"; + $prop[] = $mediaType; + $parts[] = $prop; + } else { + $this->seek($s); + } + + + if (!empty($mediaType) && !$this->literal("and")) { + // ~ + } else { + $this->genericList($expressions, "mediaExpression", "and", false); + if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); + } + + if (count($parts) == 0) { + $this->seek($s); + return false; + } + + $out = $parts; + return true; + } + + protected function mediaExpression(&$out) { + $s = $this->seek(); + $value = null; + if ($this->literal("(") && + $this->keyword($feature) && + ($this->literal(":") && $this->expression($value) || true) && + $this->literal(")") + ) { + $out = array("mediaExp", $feature); + if ($value) $out[] = $value; + return true; + } + if ($this->variable($variable)) { + $out = array('variable', $variable); + return true; + } + + $this->seek($s); + return false; + } + + // an unbounded string stopped by $end + protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) { + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + $stop = array("'", '"', "@{", $end); + $stop = array_map(array('LessPHP\\lessc', 'preg_quote'), $stop); + + if (!is_null($rejectStrs)) { + $stop = array_merge($stop, $rejectStrs); + } + + $patt = '(.*?)('.implode("|", $stop).')'; + + $nestingLevel = 0; + + $content = array(); + while ($this->match($patt, $m, false)) { + if (!empty($m[1])) { + $content[] = $m[1]; + if ($nestingOpen) { + $nestingLevel += substr_count($m[1], $nestingOpen); + } + } + + $tok = $m[2]; + + $this->count-= strlen($tok); + if ($tok == $end) { + if ($nestingLevel == 0) { + break; + } else { + --$nestingLevel; + } + } + + if (($tok == "'" || $tok == '"') && $this->string($str)) { + $content[] = $str; + continue; + } + + if ($tok == "@{" && $this->interpolation($inter)) { + $content[] = $inter; + continue; + } + + if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { + $ount = null; + break; + } + + $content[] = $tok; + $this->count+= strlen($tok); + } + + $this->eatWhiteDefault = $oldWhite; + + if (count($content) == 0) return false; + + // trim the end + if (is_string(end($content))) { + $content[count($content) - 1] = rtrim(end($content)); + } + + $out = array("string", "", $content); + return true; + } + + protected function string(&$out) { + $s = $this->seek(); + if ($this->literal('"', false)) { + $delim = '"'; + } elseif ($this->literal("'", false)) { + $delim = "'"; + } else { + return false; + } + + $content = array(); + + // look for either ending delim , escape, or string interpolation + $patt = '([^\n]*?)(@\{|\\\\|' . + lessc::preg_quote($delim).')'; + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + while ($this->match($patt, $m, false)) { + $content[] = $m[1]; + if ($m[2] == "@{") { + $this->count -= strlen($m[2]); + if ($this->interpolation($inter, false)) { + $content[] = $inter; + } else { + $this->count += strlen($m[2]); + $content[] = "@{"; // ignore it + } + } elseif ($m[2] == '\\') { + $content[] = $m[2]; + if ($this->literal($delim, false)) { + $content[] = $delim; + } + } else { + $this->count -= strlen($delim); + break; // delim + } + } + + $this->eatWhiteDefault = $oldWhite; + + if ($this->literal($delim)) { + $out = array("string", $delim, $content); + return true; + } + + $this->seek($s); + return false; + } + + protected function interpolation(&$out) { + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = true; + + $s = $this->seek(); + if ($this->literal("@{") && + $this->openString("}", $interp, null, array("'", '"', ";")) && + $this->literal("}", false) + ) { + $out = array("interpolate", $interp); + $this->eatWhiteDefault = $oldWhite; + if ($this->eatWhiteDefault) $this->whitespace(); + return true; + } + + $this->eatWhiteDefault = $oldWhite; + $this->seek($s); + return false; + } + + protected function unit(&$unit) { + // speed shortcut + if (isset($this->buffer[$this->count])) { + $char = $this->buffer[$this->count]; + if (!ctype_digit($char) && $char != ".") return false; + } + + if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { + $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]); + return true; + } + return false; + } + + // a # color + protected function color(&$out) { + if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { + if (strlen($m[1]) > 7) { + $out = array("string", "", array($m[1])); + } else { + $out = array("raw_color", $m[1]); + } + return true; + } + + return false; + } + + // consume a list of property values delimited by ; and wrapped in () + protected function argumentValues(&$args, $delim = ',') { + $s = $this->seek(); + if (!$this->literal('(')) return false; + + $values = array(); + while (true) { + if ($this->expressionList($value)) $values[] = $value; + if (!$this->literal($delim)) break; + else { + if ($value == null) $values[] = null; + $value = null; + } + } + + if (!$this->literal(')')) { + $this->seek($s); + return false; + } + + $args = $values; + return true; + } + + // consume an argument definition list surrounded by () + // each argument is a variable name with optional value + // or at the end a ... or a variable named followed by ... + protected function argumentDef(&$args, &$isVararg, $delim = ',') { + $s = $this->seek(); + if (!$this->literal('(')) { + return false; + } + + $values = array(); + + $isVararg = false; + while (true) { + if ($this->literal("...")) { + $isVararg = true; + break; + } + + if ($this->variable($vname)) { + $arg = array("arg", $vname); + $ss = $this->seek(); + if ($this->assign() && $this->expressionList($value)) { + $arg[] = $value; + } else { + $this->seek($ss); + if ($this->literal("...")) { + $arg[0] = "rest"; + $isVararg = true; + } + } + $values[] = $arg; + if ($isVararg) { + break; + } + continue; + } + + if ($this->value($literal)) { + $values[] = array("lit", $literal); + } + + if (!$this->literal($delim)) break; + } + + if (!$this->literal(')')) { + $this->seek($s); + return false; + } + + $args = $values; + + return true; + } + + // consume a list of tags + // this accepts a hanging delimiter + protected function tags(&$tags, $simple = false, $delim = ',') { + $tags = array(); + while ($this->tag($tt, $simple)) { + $tags[] = $tt; + if (!$this->literal($delim)) break; + } + if (count($tags) == 0) return false; + + return true; + } + + // list of tags of specifying mixin path + // optionally separated by > (lazy, accepts extra >) + protected function mixinTags(&$tags) { + $s = $this->seek(); + $tags = array(); + while ($this->tag($tt, true)) { + $tags[] = $tt; + $this->literal(">"); + } + + if (!$tags) { + return false; + } + + return true; + } + + // a bracketed value (contained within in a tag definition) + protected function tagBracket(&$value) { + // speed shortcut + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") { + return false; + } + + $s = $this->seek(); + if ($this->literal('[') && $this->to(']', $c, true) && $this->literal(']', false)) { + $value = '['.$c.']'; + // whitespace? + if ($this->whitespace()) $value .= " "; + + // escape parent selector, (yuck) + $value = str_replace(lessc::parentSelector, "$&$", $value); + return true; + } + + $this->seek($s); + return false; + } + + protected function tagExpression(&$value) { + $s = $this->seek(); + if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) { + $value = array('exp', $exp); + return true; + } + + $this->seek($s); + return false; + } + + // a space separated list of selectors + protected function tag(&$tag, $simple = false) { + if ($simple) { + $chars = '^@,:;{}\][>\(\) "\''; + } else { + $chars = '^@,;{}["\''; + } + + $s = $this->seek(); + + if (!$simple && $this->tagExpression($tag)) { + return true; + } + + $hasExpression = false; + $parts = array(); + while ($this->tagBracket($first)) { + $parts[] = $first; + } + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + while (true) { + if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) { + $parts[] = $m[1]; + if ($simple) break; + + while ($this->tagBracket($brack)) { + $parts[] = $brack; + } + continue; + } + + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { + if ($this->interpolation($interp)) { + $hasExpression = true; + $interp[2] = true; // don't unescape + $parts[] = $interp; + continue; + } + + if ($this->literal("@")) { + $parts[] = "@"; + continue; + } + } + + if ($this->unit($unit)) { // for keyframes + $parts[] = $unit[1]; + $parts[] = $unit[2]; + continue; + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + if (!$parts) { + $this->seek($s); + return false; + } + + if ($hasExpression) { + $tag = array("exp", array("string", "", $parts)); + } else { + $tag = trim(implode($parts)); + } + + $this->whitespace(); + return true; + } + + // a css function + protected function func(&$func) { + $s = $this->seek(); + + if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) { + $fname = $m[1]; + + $sPreArgs = $this->seek(); + + $args = array(); + while (true) { + $ss = $this->seek(); + // this ugly nonsense is for ie filter properties + if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { + $args[] = array("string", "", array($name, "=", $value)); + } else { + $this->seek($ss); + if ($this->expressionList($value)) { + $args[] = $value; + } + } + + if (!$this->literal(',')) break; + } + $args = array('list', ',', $args); + + if ($this->literal(')')) { + $func = array('function', $fname, $args); + return true; + } elseif ($fname == 'url') { + // couldn't parse and in url? treat as string + $this->seek($sPreArgs); + if ($this->openString(")", $string) && $this->literal(")")) { + $func = array('function', $fname, $string); + return true; + } + } + } + + $this->seek($s); + return false; + } + + // consume a less variable + protected function variable(&$name) { + $s = $this->seek(); + if ($this->literal(lessc::vPrefix, false) && + ($this->variable($sub) || $this->keyword($name)) + ) { + if (!empty($sub)) { + $name = array('variable', $sub); + } else { + $name = lessc::vPrefix.$name; + } + return true; + } + + $name = null; + $this->seek($s); + return false; + } + + /** + * Consume an assignment operator + * Can optionally take a name that will be set to the current property name + */ + protected function assign($name = null) { + if ($name) $this->currentProperty = $name; + return $this->literal(':') || $this->literal('='); + } + + // consume a keyword + protected function keyword(&$word) { + if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { + $word = $m[1]; + return true; + } + return false; + } + + // consume an end of statement delimiter + protected function end() { + if ($this->literal(';')) { + return true; + } + if ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { + // if there is end of file or a closing block next then we don't need a ; + return true; + } + return false; + } + + protected function guards(&$guards) { + $s = $this->seek(); + + if (!$this->literal("when")) { + $this->seek($s); + return false; + } + + $guards = array(); + + while ($this->guardGroup($g)) { + $guards[] = $g; + if (!$this->literal(",")) break; + } + + if (count($guards) == 0) { + $guards = null; + $this->seek($s); + return false; + } + + return true; + } + + // a bunch of guards that are and'd together + // TODO rename to guardGroup + protected function guardGroup(&$guardGroup) { + $s = $this->seek(); + $guardGroup = array(); + while ($this->guard($guard)) { + $guardGroup[] = $guard; + if (!$this->literal("and")) break; + } + + if (count($guardGroup) == 0) { + $guardGroup = null; + $this->seek($s); + return false; + } + + return true; + } + + protected function guard(&$guard) { + $s = $this->seek(); + $negate = $this->literal("not"); + + if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) { + $guard = $exp; + if ($negate) $guard = array("negate", $guard); + return true; + } + + $this->seek($s); + return false; + } + + /* raw parsing functions */ + + protected function literal($what, $eatWhitespace = null) { + if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; + + // shortcut on single letter + if (!isset($what[1]) && isset($this->buffer[$this->count])) { + if ($this->buffer[$this->count] != $what) { + return false; + } + if (!$eatWhitespace) { + ++$this->count; + return true; + } + } + + if (!isset(self::$literalCache[$what])) { + self::$literalCache[$what] = lessc::preg_quote($what); + } + + return $this->match(self::$literalCache[$what], $m, $eatWhitespace); + } + + protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { + $s = $this->seek(); + $items = array(); + while ($this->$parseItem($value)) { + $items[] = $value; + if ($delim) { + if (!$this->literal($delim)) break; + } + } + + if (count($items) == 0) { + $this->seek($s); + return false; + } + + if ($flatten && count($items) == 1) { + $out = $items[0]; + } else { + $out = array("list", $delim, $items); + } + + return true; + } + + + // advance counter to next occurrence of $what + // $until - don't include $what in advance + // $allowNewline, if string, will be used as valid char set + protected function to($what, &$out, $until = false, $allowNewline = false) { + if (is_string($allowNewline)) { + $validChars = $allowNewline; + } else { + $validChars = $allowNewline ? "." : "[^\n]"; + } + if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false; + if ($until) $this->count -= strlen($what); // give back $what + $out = $m[1]; + return true; + } + + // try to match something on head of buffer + protected function match($regex, &$out, $eatWhitespace = null) { + if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; + + $r = '/'.$regex.($eatWhitespace ? '\s*' : '').'/Ais'; + if (preg_match($r, $this->buffer, $out, null, $this->count)) { + $this->count += strlen($out[0]); + return true; + } + return false; + } + + // match some whitespace + protected function whitespace() { + $this->match("", $m); + return strlen($m[0]) > 0; + } + + // match something without consuming it + protected function peek($regex, &$out = null, $from=null) { + if (is_null($from)) $from = $this->count; + $r = '/'.$regex.'/Ais'; + $result = preg_match($r, $this->buffer, $out, null, $from); + + return $result; + } + + // seek to a spot in the buffer or return where we are on no argument + protected function seek($where = null) { + if ($where === null) return $this->count; + $this->count = $where; + return true; + } + + /* misc functions */ + + public function throwError($msg = "parse error", $count = null) { + $count = is_null($count) ? $this->count : $count; + + $line = $this->line + + substr_count(substr($this->buffer, 0, $count), "\n"); + + if (!empty($this->sourceName)) { + $loc = "{$this->sourceName} on line {$line}"; + } else { + $loc = "line: {$line}"; + } + + // TODO this depends on $this->count + if ($this->peek("(.*?)(\n|$)", $m, $count)) { + throw new \Exception("$msg: failed at `$m[1]` $loc"); + } + throw new \Exception("$msg: $loc"); + } + + protected function pushBlock($selectors=null, $type=null) { + $b = new \stdClass; + $b->parent = $this->env; + + $b->type = $type; + $b->id = self::$nextBlockId++; + + $b->isVararg = false; // TODO: kill me from here + $b->tags = $selectors; + + $b->props = array(); + $b->children = array(); + + $this->env = $b; + return $b; + } + + // push a block that doesn't multiply tags + protected function pushSpecialBlock($type) { + return $this->pushBlock(null, $type); + } + + // append a property to the current block + protected function append($prop, $pos = null) { + if ($pos !== null) $prop[-1] = $pos; + $this->env->props[] = $prop; + } + + // pop something off the stack + protected function pop() { + $old = $this->env; + $this->env = $this->env->parent; + return $old; + } + + // remove comments from $text + // todo: make it work for all functions, not just url + protected function removeComments($text) { + $look = array( + 'url(', '//', '/*', '"', "'" + ); + + $out = ''; + $min = null; + while (true) { + // find the next item + foreach ($look as $token) { + $pos = strpos($text, $token); + if ($pos !== false) { + if (!isset($min) || $pos < $min[1]) $min = array($token, $pos); + } + } + + if (is_null($min)) break; + + $count = $min[1]; + $skip = 0; + $newlines = 0; + switch ($min[0]) { + case 'url(': + if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) + $count += strlen($m[0]) - strlen($min[0]); + break; + case '"': + case "'": + if (preg_match('/'.$min[0].'.*?'.$min[0].'/', $text, $m, 0, $count)) + $count += strlen($m[0]) - 1; + break; + case '//': + $skip = strpos($text, "\n", $count); + if ($skip === false) $skip = strlen($text) - $count; + else $skip -= $count; + break; + case '/*': + if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) { + $skip = strlen($m[0]); + $newlines = substr_count($m[0], "\n"); + } + break; + } + + if ($skip == 0) $count += strlen($min[0]); + + $out .= substr($text, 0, $count).str_repeat("\n", $newlines); + $text = substr($text, $count + $skip); + + $min = null; + } + + return $out.$text; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/polyfill/ctype.php b/snappymail/v/0.0.0/app/libraries/polyfill/ctype.php new file mode 100644 index 0000000000..7f913e728e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/polyfill/ctype.php @@ -0,0 +1,17 @@ +Get('labs', 'cookie_default_path', ''); + static::$SameSite = $oConfig->Get('security', 'cookie_samesite', 'Strict'); + static::$Secure = isset($_SERVER['HTTPS']) + || 'None' == static::$SameSite + || !!$oConfig->Get('labs', 'cookie_default_secure', false); + $bOne = true; + } + return $bOne; + } + + public static function get(string $sName) : ?string + { + if (isset($_COOKIE[$sName])) { + $aParts = []; + foreach (\array_keys($_COOKIE) as $sCookieName) { + if (\strtok($sCookieName, '~') === $sName) { + $aParts[$sCookieName] = $_COOKIE[$sCookieName]; + } + } + \ksort($aParts); + return \implode('', $aParts); + } + return null; + } + + public static function getSecure(string $sName) + { + return isset($_COOKIE[$sName]) + ? Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode(static::get($sName))) + : null; + } + + public static function setSecure(string $sName, $data): void + { + if (\is_null($data)) { + static::clear($sName); + } else { + static::set( + $sName, + \MailSo\Base\Utils::UrlSafeBase64Encode(Crypt::EncryptToJSON($data)) + ); + } + } + + private static function _set(string $sName, string $sValue, int $iExpire, bool $httponly = true) : bool + { + $sPath = static::$DefaultPath; + $sPath = $sPath && \strlen($sPath) ? $sPath : '/'; +/* + if (\strlen($sValue) > 4000 - \strlen($sPath . $sName)) { + throw new \Exception("Cookie '{$sName}' value too long"); + } +*/ + if (\strlen($sValue)) { + $_COOKIE[$sName] = $sValue; + } else { + if (!isset($_COOKIE[$sName])) { + return true; + } + unset($_COOKIE[$sName]); + $iExpire = \time() - 3600 * 24 * 30; + } + + // Cookie "$sName" has been rejected because it is already expired. + // Happens when \setcookie() sends multiple with the same name (and one is deleted) + // So when previously set, we must delete all 'Set-Cookie' headers and start over + $cookies = []; + $cookie_remove = false; + foreach (\headers_list() as $header) { + if (\preg_match("/Set-Cookie:([^=]+)=/i", $header, $match)) { + if (\trim($match[1]) == $sName) { + $cookie_remove = true; + } else { + $cookies[] = $header; + } + } + } + if ($cookie_remove) { + \header_remove('Set-Cookie'); + foreach ($cookies as $cookie) { + \header($cookie,false); + } + } + + return \setcookie($sName, $sValue, array( + 'expires' => $iExpire, + 'path' => $sPath, +// 'domain' => null, + 'secure' => static::$Secure, + 'httponly' => $httponly, + 'samesite' => static::$SameSite + )); + } + + /** + * Firefox: Cookie "$sName" has been rejected because it is already expired. + * \header_remove("set-cookie: {$sName}"); + */ + public static function set(string $sName, string $sValue, int $iExpire = 0, bool $httponly = true) : void + { + static::init(); + $sPath = static::$DefaultPath; + $sPath = $sPath && \strlen($sPath) ? $sPath : '/'; + // https://github.com/the-djmaze/snappymail/issues/451 + // The 4K browser limit is for the entire cookie, including name, value, expiry date etc. + $iMaxSize = 4000 - \strlen($sPath . $sName); +/* + if ($iMaxSize < \strlen($sValue)) { + throw new \Exception("Cookie '{$sName}' value too long"); + } +*/ + // Set the new 4K split cookie + foreach (\str_split($sValue, $iMaxSize) as $i => $sPart) { + $sCookieName = $i ? "{$sName}~{$i}" : $sName; + Log::debug('COOKIE', "set {$sCookieName}"); + static::_set($sCookieName, $sPart, $iExpire, $httponly); + } + // Delete unused old 4K split cookie parts + foreach (\array_keys($_COOKIE) as $sCookieName) { + $aSplit = \explode('~', $sCookieName); + if (isset($aSplit[1]) && $aSplit[0] == $sName && $aSplit[1] > $i) { + Log::debug('COOKIE', "unset {$sCookieName}"); + static::_set($sCookieName, '', 0, $httponly); + } + } + } + + public static function clear(string $sName) : void + { + static::init(); + static::_set($sName, '', 0); + // Delete 4K split cookie parts + foreach (\array_keys($_COOKIE) as $sCookieName) { + if (\strtok($sCookieName, '~') === $sName) { + static::_set($sCookieName, '', 0); + } + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php b/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php new file mode 100644 index 0000000000..77ea6f29a9 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php @@ -0,0 +1,306 @@ +getMessage()}\n{$e->getTraceAsString()}"); + } + } + } else { + Log::warning('Crypt', 'Decrypt() invalid $data'); + } + } + + public static function DecryptFromJSON(string $data, + #[\SensitiveParameter] + ?string $key = null + ) /* : mixed */ + { + $data = static::jsonDecode($data); + if (!\is_array($data)) { + Log::notice('Crypt', 'DecryptFromJSON() invalid $data'); + return null; + } + return static::Decrypt(\array_map('base64_decode', $data), $key); + } + + public static function DecryptUrlSafe(string $data, + #[\SensitiveParameter] + ?string $key = null + ) /* : mixed */ + { + $data = \explode('.', $data); + if (!\is_array($data)) { + Log::notice('Crypt', 'DecryptUrlSafe() invalid $data'); + return null; + } + return static::Decrypt(\array_map('MailSo\\Base\\Utils::UrlSafeBase64Decode', $data), $key); + } + + public static function Encrypt( + #[\SensitiveParameter] + $data, + #[\SensitiveParameter] + ?string $key = null + ) : array + { + $data = \json_encode($data); + + if (\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) { + try { + $nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES); + return ['sodium', $nonce, static::SodiumEncrypt($data, $nonce, $key)]; + } catch (\Throwable $e) { + Log::error('Crypt', 'Sodium ' . $e->getMessage()); + } + } + + // Too much OpenSSL v3 issues ? +// if (\is_callable('openssl_encrypt') && OPENSSL_VERSION_NUMBER < 805306368) { + if (\is_callable('openssl_encrypt')) { + try { + $iv = \random_bytes(\openssl_cipher_iv_length(static::$cipher)); + return ['openssl', $iv, static::OpenSSLEncrypt($data, $iv, $key)]; + } catch (\Throwable $e) { + Log::error('Crypt', 'OpenSSL ' . $e->getMessage()); + } + } + + $salt = \random_bytes(16); + return ['xxtea', $salt, static::XxteaEncrypt($data, $salt, $key)]; +/* + if (static::{"{$result[0]}Decrypt"}($result[2], $result[1], $key) !== $data) { + throw new \RuntimeException('Encrypt/Decrypt mismatch'); + } +*/ + } + + public static function EncryptToJSON( + #[\SensitiveParameter] + $data, + #[\SensitiveParameter] + ?string $key = null + ) : string + { + return \json_encode(\array_map('base64_encode', static::Encrypt($data, $key))); + } + + public static function EncryptUrlSafe( + #[\SensitiveParameter] + $data, + #[\SensitiveParameter] + ?string $key = null + ) : string + { + return \implode('.', \array_map('MailSo\\Base\\Utils::UrlSafeBase64Encode', static::Encrypt($data, $key))); + } + + public static function SodiumDecrypt(string $data, string $nonce, + #[\SensitiveParameter] + ?string $key = null + ) /* : string|false */ + { + if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) { + throw new \Exception('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt not callable'); + } + return \sodium_crypto_aead_xchacha20poly1305_ietf_decrypt( + $data, + APP_SALT, + $nonce, + \str_pad('', \SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, static::Passphrase($key)) + ); + } + + public static function SodiumEncrypt( + #[\SensitiveParameter] + string $data, + string $nonce, + #[\SensitiveParameter] + ?string $key = null + ) : string + { + if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) { + throw new \Exception('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt not callable'); + } + $result = \sodium_crypto_aead_xchacha20poly1305_ietf_encrypt( + $data, + APP_SALT, + $nonce, + \str_pad('', \SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, static::Passphrase($key)) + ); + if (!$result) { + throw new \RuntimeException('Sodium encryption failed'); + } + return $result; + } + + public static function OpenSSLDecrypt(string $data, string $iv, + #[\SensitiveParameter] + ?string $key = null + ) /* : string|false */ + { + if (!$data || !$iv) { + throw new \ValueError('$data or $iv is empty string'); + } + if (!\is_callable('openssl_decrypt')) { + throw new \Exception('openssl_decrypt not callable'); + } + if (!static::$cipher) { + throw new \RuntimeException('openssl $cipher not set'); + } + Log::debug('Crypt', 'openssl_decrypt() with cipher ' . static::$cipher); + return \openssl_decrypt( + $data, + static::$cipher, + static::Passphrase($key), + OPENSSL_RAW_DATA, + $iv + ); + } + + public static function OpenSSLEncrypt( + #[\SensitiveParameter] + string $data, + string $iv, + #[\SensitiveParameter] + ?string $key = null + ) : string + { + if (!$data || !$iv) { + throw new \ValueError('$data or $iv is empty string'); + } + if (!\is_callable('openssl_encrypt')) { + throw new \Exception('openssl_encrypt not callable'); + } + if (!static::$cipher) { + throw new \RuntimeException('openssl $cipher not set'); + } + Log::debug('Crypt', 'openssl_encrypt() with cipher ' . static::$cipher); + $result = \openssl_encrypt( + $data, + static::$cipher, + static::Passphrase($key), + OPENSSL_RAW_DATA, + $iv + ); + if (!$result) { + throw new \RuntimeException('OpenSSL encryption with ' . static::$cipher . ' failed'); + } + return $result; + } + + public static function XxteaDecrypt(string $data, string $salt, + #[\SensitiveParameter] + ?string $key = null + ) /* : mixed */ + { + if (!$data || !$salt) { + throw new \ValueError('$data or $salt is empty string'); + } + $key = $salt . static::Passphrase($key); + return \is_callable('xxtea_decrypt') + ? \xxtea_decrypt($data, $key) + : \MailSo\Base\Xxtea::decrypt($data, $key); + } + + public static function XxteaEncrypt( + #[\SensitiveParameter] + string $data, + string $salt, + #[\SensitiveParameter] + ?string $key = null + ) : string + { + if (!$data || !$salt) { + throw new \ValueError('$data or $salt is empty string'); + } + $key = $salt . static::Passphrase($key); + $result = \is_callable('xxtea_encrypt') + ? \xxtea_encrypt($data, $key) + : \MailSo\Base\Xxtea::encrypt($data, $key); + if (!$result) { + throw new \RuntimeException('Xxtea encryption failed'); + } + return $result; + } + + private static function jsonDecode(string $data) /*: mixed*/ + { + return \json_decode($data, true, 512, JSON_THROW_ON_ERROR); + } + +} + +\SnappyMail\Crypt::setCipher(\RainLoop\Api::Config()->Get('security', 'encrypt_cipher', 'aes-256-cbc-hmac-sha1')); diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php b/snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php new file mode 100644 index 0000000000..8bc717c151 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php @@ -0,0 +1,223 @@ +baseUri = $settings['baseUri']; + + $this->HTTP = \SnappyMail\HTTP\Request::factory(/*'socket'*/); + $this->HTTP->proxy = $settings['proxy'] ?? null; + if (!empty($settings['userName']) && !empty($settings['password'])) { + $this->HTTP->setAuth(3, $settings['userName'], $settings['password']); + } else { + \SnappyMail\Log::warning('DAV', 'No user credentials set'); + } + $this->HTTP->max_response_kb = 0; + $this->HTTP->timeout = 15; // timeout in seconds. + $this->HTTP->max_redirects = 0; + } + + /** + * Enable/disable SSL peer verification + */ + public function setVerifyPeer(bool $value) : void + { + $this->HTTP->verify_peer = $value; + } + + /** + * Performs an actual HTTP request, and returns the result. + * + * If the specified url is relative, it will be expanded based on the base url. + */ + public function request(string $method, string $url = '', ?string $body = null, array $headers = array()) : \SnappyMail\HTTP\Response + { + if (!\preg_match('@^(https?:)?//@', $url)) { + // If the url starts with a slash, we must calculate the url based off + // the root of the base url. + if (\str_starts_with($url, '/')) { + $parts = \parse_url($this->baseUri); + $url = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url; + } else { + $url = $this->baseUri . $url; + } + } + \SnappyMail\Log::debug('DAV', "{$method} {$url}" . ($body ? "\n\t" . \str_replace("\n", "\n\t", $body) : '')); + $response = $this->HTTP->doRequest($method, $url, $body, $headers); + if (301 == $response->status) { + // Like: RewriteRule ^\.well-known/carddav /nextcloud/remote.php/dav [R=301,L] + $location = $response->getRedirectLocation(); + \SnappyMail\Log::info('DAV', "301 Redirect {$url} to {$location}"); + $url = \preg_replace('@^(https?:)?//[^/]+[/$]@', '/', $location); + $parts = \parse_url($this->baseUri); + $url = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url; + $response = $this->HTTP->doRequest($method, $url, $body, $headers); + } + if (300 <= $response->status) { + throw new \SnappyMail\HTTP\Exception("{$method} {$url}", $response->status, $response); + } + \SnappyMail\Log::debug('DAV', "{$response->status}: {$response->body}"); + + return $response; + } + + /** + * Does a PROPFIND request + * + * The list of requested properties must be specified as an array, in clark + * notation. + * + * The returned array will contain a list of filenames as keys, and + * properties as values. + * + * The properties array will contain the list of properties. Only properties + * that are actually returned from the server (without error) will be + * returned, anything else is discarded. + * + * Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + */ + public function propFind(string $url, array $properties, int $depth = 0) : array + { + $body = '' . "\n" . ''; + + foreach ($properties as $property) { + if (!\preg_match('/^{([^}]*)}(.*)$/', $property, $match)) { + throw new \ValueError('\'' . $property . '\' is not a valid clark-notation formatted string'); + } + if ('DAV:' === $match[1]) { + $body .= ""; + } else { + $body .= ""; + } + } + + $body .= ''; + + $response = $this->request('PROPFIND', $url, $body, array( + "Depth: {$depth}", + 'Content-Type: application/xml' + )); + + /** + * Parse the WebDAV multistatus response body + */ + $responseXML = \simplexml_load_string( + /** + * Convert all instances of the DAV: namespace to urn:DAV + * + * This is unfortunately needed, because the DAV: namespace violates the xml namespaces + * spec, and causes the DOM to throw errors + * + * This is used to map the DAV: namespace to urn:DAV. This is needed, because the DAV: + * namespace is actually a violation of the XML namespaces specification, and will cause errors + */ + \preg_replace("/xmlns(:[A-Za-z0-9_]*)?=(\"|\')DAV:(\\2)/", "xmlns\\1=\\2urn:DAV\\2", $response->body), + null, LIBXML_NOBLANKS | LIBXML_NOCDATA); + + if (false === $responseXML) { + throw new \UnexpectedValueException("The passed data is not valid XML\n{$response->body}"); + } + + $ns = \array_search('urn:DAV', $responseXML->getNamespaces(true)) ?: 'd'; +// $ns_card = \array_search('urn:ietf:params:xml:ns:carddav', $responseXML->getNamespaces(true)); + + $result = array(); + + $responseXML->registerXPathNamespace($ns, 'urn:DAV'); + foreach ($responseXML->xpath("{$ns}:response") as $response) { + $properties = array(); + $response->registerXPathNamespace($ns, 'urn:DAV'); + foreach ($response->xpath("{$ns}:propstat") as $propStat) { + // Parse all WebDAV properties + $propList = array(); + $propStat->registerXPathNamespace($ns, 'urn:DAV'); + foreach ($propStat->xpath("{$ns}:prop") as $prop) { + foreach ($prop->xpath("*") as $element) { + $propertyName = self::toClarkNotation($element); + $propList[$propertyName] = []; + if ('{DAV:}resourcetype' === $propertyName) { + foreach ($element->xpath("*") as $resourcetype) { + $propList[$propertyName][] = self::toClarkNotation($resourcetype); + } + } else { + foreach ($element->xpath("*") as $child) { + $propList[$propertyName][self::toClarkNotation($child)] = (string) $child; + } + if (!$propList[$propertyName]) { + $propList[$propertyName] = (string) $element; + } + } + } + } + list($httpVersion, $statusCode, $message) = \explode(' ', $propStat->children('urn:DAV')->status, 3); + $properties[$statusCode] = $propList; + } + + $result[(string) $response->children('urn:DAV')->href] = $properties; + } + + if (0 === $depth) { + \reset($result); + return \current($result)[200] ?? array(); + } + + return \array_map(function($statusList){ + return $statusList[200] ?? array(); + }, $result); + } + + /** + * Returns the 'clark notation' for an element. + * + * For example, and element encoded as: + * + * will be returned as: + * {http://www.example.org}myelem + * + * This format is used throughout the SabreDAV sourcecode. + * Elements encoded with the urn:DAV namespace will + * be returned as if they were in the DAV: namespace. This is to avoid + * compatibility problems. + */ + public static function toClarkNotation(\SimpleXMLElement $element) : string + { + // Mapping back to the real namespace, in case it was dav + // Mapping to clark notation + $ns = \array_values($element->getNamespaces())[0]; + return '{' . ('urn:DAV' == $ns ? 'DAV:' : $ns) . '}' . $element->getName(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/dns.php b/snappymail/v/0.0.0/app/libraries/snappymail/dns.php new file mode 100644 index 0000000000..ee0ca75172 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/dns.php @@ -0,0 +1,80 @@ +Cacher(); + $sCacheKey = "dns-bimi-{$domain}-{$selector}"; + $BIMI = $oCache->Get($sCacheKey); + if ($BIMI) { + $BIMI = \json_decode($BIMI); + if ($BIMI[1] < \time()) { + $BIMI = null; + } else { + $BIMI = $BIMI[0]; + } + } + if (null === $BIMI) { + $BIMI = ''; + $values = \dns_get_record("{$selector}._bimi.{$domain}", \DNS_TXT); + if ($values) { + foreach ($values as $value) { + if (\str_starts_with($value['txt'], 'v=BIMI1')) { + $BIMI = \preg_replace('/^.+l=([^;]+)(;.*)?$/D', '$1', $value['txt']); + $oCache->Set($sCacheKey, \json_encode([ + $BIMI, + time() + $value['ttl'] + ])); + break; + } + } + } + if (!$BIMI) { + // Don't lookup for 24 hours + $oCache->Set($sCacheKey, \json_encode([ + $BIMI, + time() + 86400 + ])); + } + } + return $BIMI; + } + + public static function MX(string $domain) : array + { + $mxhosts = array(); + $values = \dns_get_record($domain, \DNS_MX); + if ($values) { + foreach ($values as $record) { + $mxhosts[$record['pri']] = $record['target']; + } + } + if (!$mxhosts) { + \getmxrr($domain, $mxhosts); + } + \ksort($mxhosts); + return \array_values($mxhosts); + } + + public static function SRV(string $domain) : array + { + $result = array(); + $values = \dns_get_record($domain, \DNS_SRV); + if ($values) { + foreach ($values as $record) { + $result[$record['pri']] = $record; + } + } + \ksort($result); + return \array_values($result); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/file/magic.mime.php b/snappymail/v/0.0.0/app/libraries/snappymail/file/magic.mime.php new file mode 100644 index 0000000000..2b79d0069b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/file/magic.mime.php @@ -0,0 +1,34 @@ + 'application/x-7z-compressed', # unknown by magic.mime + '#^BZh.*#s' => 'application/x-bzip2', + '#^\x1f\x8b.*#s' => 'application/x-gzip', + '#^.{257}ustar \x00.*#s' => 'application/x-gtar', + '#^.{257}ustar\x00.*#s' => 'application/x-tar', + '#^Rar!.*#s' => 'application/x-rar-compressed', + '#^PK\x03\x04.*#s' => 'application/zip', + + '#^%PDF-.*#s' => 'application/pdf', + '#^[CF]WS.*#s' => 'application/x-shockwave-flash', + + '#^(.*\n)?(INSERT|CREATE|DROP|DELETE|ALTER|UPDATE)\ .*#is' => 'text/x-sql', + '#^BEGIN:VCARD#s' => 'text/x-vcard', + + '#^GIF8.*#s' => 'image/gif', + '#^.{8}heic#s' => 'image/heic', // https://nokiatech.github.io/heif/technical.html + '#^\xFF\xD8.*#s' => 'image/jpeg', + '#^\x89PNG.*#s' => 'image/png', + '#^.PNG.*#s' => 'image/png', + '#^ 'image/svg+xml', # wild guess, unknown by magic.mime + '#^RIFF.{4}WEBP#s' => 'image/webp', + + '#^FLV.*#s' => 'video/x-flv', + '#^OggS.+\x80theora.*#s' => 'video/ogg', + + '#^ID3.*#s' => 'audio/mpeg', # wild guess? + '#^\xff\xfa.*#s' => 'audio/mpeg', # wild guess? + '#^OggS.+\x01vorbis.*#s' => 'audio/ogg', + + '#^\xD0\xCF\x11\xE0\xA1.*#s' => 'application/msword', + '#^OggS.*#s' => 'application/ogg', +); diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/file/mimetype.php b/snappymail/v/0.0.0/app/libraries/snappymail/file/mimetype.php new file mode 100644 index 0000000000..6156145dae --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/file/mimetype.php @@ -0,0 +1,286 @@ +file($filename)); + } + if (!$mime && \is_callable('mime_content_type')) { + $mime = \mime_content_type($filename); + } + if (!$mime && $fp = \fopen($filename, 'rb')) { + $mime = self::fromStream($fp); + \fclose($fp); + } + if ('application/zip' === \str_replace('/x-', '/', $mime)) { + $zip = new \ZipArchive(); + if ($zip->open($filename, \ZIPARCHIVE::RDONLY)) { + if (false !== $zip->locateName('word/_rels/document.xml.rels')) { + return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + } + if (false !== $zip->locateName('xl/_rels/workbook.xml.rels')) { + return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + } + } + } + } + return $mime ? static::detectDeeper($mime, $name ?: $filename) : null; + } + + public static function fromStream($stream, string $name = '') : ?string + { + if (\is_resource($stream) && \stream_get_meta_data($stream)['seekable']) { + $pos = \ftell($stream); +// if (\is_int($pos) && \rewind($stream)) { + if (\is_int($pos) && 0 === \fseek($stream, 0)) { +// $str = \fread($stream, 265); + $str = \stream_get_contents($stream, 265, 0); + \fseek($stream, $pos); + if ($str) { + return static::fromString($str, $name); + } + } + } + return null; + } + + public static function fromString(string &$str, string $name = '') : ?string + { + static::initFInfo(); + $mime = self::$finfo + ? \preg_replace('#[,;].*#', '', self::$finfo->buffer($str)) + : self::getFromData($str); + return $mime ? static::detectDeeper($mime, $name) : null; + } + + protected static function getFromData(string $str) : ?string + { + if (\str_contains($str, '-----BEGIN PGP SIGNATURE-----')) { + return 'application/pgp-signature'; + } + if (\preg_match('/-----BEGIN PGP (PUBLIC|PRIVATE) KEY BLOCK-----/', $str)) { + return 'application/pgp-keys'; + } + static $magic; + if (!$magic) { + require __DIR__ . '/magic.mime.php'; + } + $str = \preg_replace(\array_keys($magic), \array_values($magic), $str, 1, $c); + return $c ? $str : null; + } + + public static function fromFilename(string $filename) : ?string + { + $filename = \strtolower($filename); + if ('winmail.dat' === $filename) { + return static::$types['tnef']; + } + $extension = \explode('.', $filename); + $extension = \array_pop($extension); + return isset(static::$types[$extension]) ? static::$types[$extension] : null; + } + + /** + * Issue with 'text/plain' + */ + public static function toExtension(string $mime, bool $include_dot = true) : ?string + { + $mime = \strtolower($mime); + if ('application/pgp-signature' === $mime || 'application/pgp-keys' === $mime) { + $ext = 'asc'; + } else { + $mime = \str_replace('application/x-tar', 'application/gtar', $mime); + $ext = \array_search($mime, static::$types) + ?: \array_search(\str_replace('/x-', '/', $mime), static::$types) + ?: \array_search(\str_replace('/', '/x-', $mime), static::$types) + ?: 'bin'; + } + return ($include_dot ? '.' : '') . $ext; + } + + protected static $types = [ + '7z' => 'application/x-7z-compressed', + 'ai' => 'application/postscript', +// 'asc' => 'application/pgp-signature', +// 'asc' => 'application/pgp-keys', + 'bat' => 'application/x-msdownload', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', + 'cab' => 'application/vnd.ms-cab-compressed', + 'chm' => 'application/vnd.ms-htmlhelp', + 'com' => 'application/x-msdownload', + 'deb' => 'application/x-debian-package', + 'dll' => 'application/x-msdownload', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dot' => 'application/msword', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'eps' => 'application/postscript', + 'epub' => 'application/epub', + 'exe' => 'application/x-msdownload', + 'gz' => 'application/gzip', + 'hlp' => 'application/winhlp', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'msi' => 'application/x-msdownload', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ogx' => 'application/ogg', + 'p10' => 'application/pkcs10', + 'p7c' => 'application/pkcs7-mime', + 'p7m' => 'application/pkcs7-mime', + 'p7s' => 'application/pkcs7-signature', + 'pdf' => 'application/pdf', + 'php' => 'application/x-httpd-php', + 'php' => 'application/x-php', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ps' => 'application/postscript', + 'psd' => 'image/vnd.adobe.photoshop', + 'rar' => 'application/rar-compressed', + 'rar' => 'application/x-rar-compressed', + 'rtf' => 'application/rtf', + 'scr' => 'application/x-msdownload', + 'sql' => 'application/sql', + 'swf' => 'application/shockwave-flash', + 'swf' => 'application/x-shockwave-flash', + 'tar' => 'application/gtar', +// 'tar' => 'application/x-tar', +// 'tgz' => 'application/gzip', + 'tnef' => 'application/vnd.ms-tnef', +// 'tnef' => 'application/ms-tnef', // not IANA official + 'torrent' => 'application/x-bittorrent', + 'wgt' => 'application/widget', + 'xls' => 'application/vnd.ms-excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'zip' => 'application/zip', + + 'aac' => 'audio/aac', + 'aif' => 'audio/aiff', + 'aifc' => 'audio/aiff', + 'aiff' => 'audio/aiff', + 'flac' => 'audio/flac', + 'm3u' => 'audio/x-mpegurl', + 'midi' => 'audio/midi', + 'mp3' => 'audio/mpeg', + 'mp4a' => 'audio/mp4', + 'ogg' => 'audio/ogg', + 'wav' => 'audio/wav', + 'weba' => 'audio/webm', + + 'ttf' => 'font/ttf', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', + + 'bmp' => 'image/bmp', + 'cgm' => 'image/cgm', + 'djv' => 'image/vnd.djvu', + 'djvu' => 'image/vnd.djvu', + 'gif' => 'image/gif', +// 'heic' => 'image/heic', + 'ico' => 'image/vnd.microsoft.icon', +// 'ico' => 'image/x-icon', + 'ief' => 'image/ief', + 'jpeg' => 'image/jpeg', + 'jfif' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'png' => 'image/png', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'webp' => 'image/webp', + + 'eml' => 'message/rfc822', + 'mime' => 'message/rfc822', + // RFC 3464 +// 'u8dsn' => 'message/delivery-status', + // RFC 5337 + 'u8dsn' => 'message/global-delivery-status', + + 'txt' => 'text/plain', + 'asp' => 'text/asp', + 'cfg' => 'text/plain', + 'conf' => 'text/plain', + 'css' => 'text/css', + 'csv' => 'text/csv', + 'def' => 'text/plain', + 'html' => 'text/html', + 'htm' => 'text/html', + 'ics' => 'text/calendar', + 'ifb' => 'text/calendar', + 'in' => 'text/plain', + 'ini' => 'text/plain', + 'list' => 'text/plain', + 'log' => 'text/plain', + 'pl' => 'text/perl', + 'rtx' => 'text/richtext', + 'text' => 'text/plain', + 'vcf' => 'text/vcard', + 'vcard' => 'text/vcard', + 'xml' => 'text/xml', + + '3g2' => 'video/3gpp2', + '3gp' => 'video/3gpp', + 'asf' => 'video/x-ms-asf', + 'asx' => 'video/x-ms-asf', + 'avi' => 'video/x-msvideo', + 'flv' => 'video/flv', + 'h261' => 'video/h261', + 'h263' => 'video/h263', + 'h264' => 'video/h264', + 'jpgv' => 'video/jpgv', + 'm4v' => 'video/x-m4v', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp4' => 'video/mp4', + 'mp4v' => 'video/mp4', + 'mpeg' => 'video/mpeg', + 'm1v' => 'video/mpeg', + 'm2v' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpg4' => 'video/mp4', + 'ogv' => 'video/ogg', + 'qt' => 'video/quicktime', + 'webm' => 'video/webm', + 'wm' => 'video/x-ms-wm', + 'wmv' => 'video/x-ms-wmv', + 'wmx' => 'video/x-ms-wmx', + 'wvx' => 'video/x-ms-wvx', + ]; + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/file/temporary.php b/snappymail/v/0.0.0/app/libraries/snappymail/file/temporary.php new file mode 100644 index 0000000000..f466c2474b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/file/temporary.php @@ -0,0 +1,63 @@ +filename = @\tempnam($tmpdir, $name); + } else { + $this->filename = $tmpdir . '/' . $name; + } + } + + function __destruct() + { + $this->filename && \unlink($this->filename); + } + + function __toString() : string + { + return $this->filename; + } + + public function filename() : string + { + return $this->filename; + } + + private $fp = null; + public function fopen()/* : resource|false*/ + { + if (!$this->fp) { + $this->fp = \fopen($this->filename, 'r+b'); + } + return $this->fp; + } + + public function writeFromStream(/*resource*/ $from)/* : int|false*/ + { + $fp = $this->fopen(); +// return \stream_copy_to_stream($from, $fp); // Fails + $bytes = 0; + while (!\feof($from)) $bytes += \fwrite($fp, \fread($from, 8192)); + return $bytes; + } + + public function putContents($data, int $flags = 0)/* : int|false*/ + { + return \file_put_contents($this->filename, $data /*, $flags, $context*/); + } + + public function getContents()/* : string|false*/ + { + return \file_get_contents($this->filename); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/gpg/base.php b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/base.php new file mode 100644 index 0000000000..122d51ae63 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/base.php @@ -0,0 +1,509 @@ +doc/DETAILS in the + * {@link http://www.gnupg.org/download/ GPG package} for a detailed + * description of GPG's status output. + */ + FD_STATUS = 3, + + // This is used for methods requiring passphrases. + FD_COMMAND = 4, + + // This is used for passing signed data when verifying a detached signature. + FD_MESSAGE = 5; + + public + $strict = false; + + protected + $binary, + $version = '2.0', + $pinentries = [], + $encryptKeys = [], + // Create PEM encoded output + $armor = true, + + $_input, + $_output, + + $proc_resource, + $_openPipes, // ProcPipes + + // https://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html + $options = [ + 'homedir' => '', + 'keyring' => '', + 'digest-algo' => '', + 'cipher-algo' => '', + ]; + + function __construct(string $homedir) + { + $homedir = \rtrim($homedir, '/\\'); + // BSD 4.4 max length + if (104 <= \strlen($homedir . '/S.gpg-agent.extra')) { + throw new \Exception('Socket name for S.gpg-agent.extra is too long'); + } + $this->options['homedir'] = $homedir; +// \putenv("GNUPGHOME={$homedir}"); + } + + function __destruct() + { + $this->proc_close(); + $gpgconf = static::findBinary('gpgconf'); + if ($gpgconf) { + $cmd = $gpgconf . ' --kill gpg-agent'; + // https://github.com/the-djmaze/snappymail/issues/1560#issuecomment-2144817883 +// if (\version_compare($this->version, '2.4.0', '<')) { + $cmd .= ' ' . \escapeshellarg($this->options['homedir']); +// } + $env = ['GNUPGHOME' => $this->options['homedir']]; + $pipes = []; + if ($process = \proc_open($cmd, [], $pipes, null, $env)) { + \proc_close($process); + } + } + } + + public static function isSupported() : bool + { + return \is_callable('shell_exec') && \is_callable('proc_open'); + } + + protected function listDecryptKeys(/*string|resource*/ $input, /*string|resource*/ $output = null) + { + return false; + } + + /** + * Decrypts a given text + */ + public function decrypt(string $text) /*: string|false */ + { + return false; + } + + /** + * Decrypts a given file + */ + public function decryptFile(string $filename) /*: string|false */ + { + return false; + } + + /** + * Decrypts a given stream + */ + public function decryptStream($fp, /*string|resource*/ $output = null) /*: string|false*/ + { + return false; + } + + /** + * Decrypts and verifies a given text + */ + public function decryptVerify(string $text, string &$plaintext) /*: array|false*/ + { + return false; + } + + /** + * Decrypts and verifies a given file + */ + public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/ + { + return false; + } + + /** + * Encrypts a given text + */ + public function encrypt(string $plaintext, /*string|resource*/ $output = null) /*: string|false*/ + { + return false; + } + + /** + * Encrypts a given text + */ + public function encryptFile(string $filename, /*string|resource*/ $output = null) /*: string|false*/ + { + return false; + } + + public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/ + { + return false; + } + + /** + * Encrypts and signs a given text + */ + public function encryptSign(string $plaintext) /*: string|false*/ + { + return false; + } + + /** + * Encrypts and signs a given text + */ + public function encryptSignFile(string $filename) /*: string|false*/ + { + return false; + } + + /** + * Exports a public or private key + */ + public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/ + { + return false; + } + + /** + * Imports a key + */ + public function import(string $keydata) /*: array|false*/ + { + return false; + } + + /** + * Imports a key + */ + public function importFile(string $filename) /*: array|false*/ + { + return false; + } + + public function deleteKey(string $keyId, bool $private) + { + return false; + } + + /** + * Returns an array with information about all keys that matches the given pattern + */ + public function keyInfo(string $pattern, bool $private = false) : array + { + return []; + } + + /** + * Sets the mode for error_reporting + * GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT. + * By default GNUPG_ERROR_SILENT is used. + */ + public function setErrorMode(int $errormode) : void + { + } + + /** + * Signs a given text + */ + public function sign(string $plaintext, /*string|resource*/ $output = null) /*: string|false*/ + { + return false; + } + + /** + * Signs a given file + */ + public function signFile(string $filename, /*string|resource*/ $output = null) /*: string|false*/ + { + return false; + } + + /** + * Signs a given file + */ + public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/ + { + return false; + } + + /** + * Verifies a signed text + */ + public function verify(string $signed_text, string $signature, ?string &$plaintext = null) /*: array|false*/ + { + return false; + } + + /** + * Verifies a signed file + */ + public function verifyFile(string $filename, string $signature, ?string &$plaintext = null) /*: array|false*/ + { + return false; + } + + /** + * Verifies a signed file + */ + public function verifyStream($fp, string $signature, ?string &$plaintext = null) /*: array|false*/ + { + return false; + } + + public function getEncryptedMessageKeys(/*string|resource*/ $data) : array + { + return []; + } + + /****************************************************************** + * Defined methods + ******************************************************************/ + + /** + * Add a key for decryption + */ + public function addDecryptKey(string $fingerprint, SensitiveString $passphrase) : bool + { + $this->addPinentry($fingerprint, $passphrase); + return true; + } + + /** + * Add a key for encryption + */ + public function addEncryptKey(string $fingerprint) : bool + { + $this->encryptKeys[$fingerprint] = 1; + return true; + } + + /** + * Add a key for signing + */ + public function addSignKey(string $fingerprint, SensitiveString $passphrase) : bool + { + $this->addPinentry($fingerprint, $passphrase); + return true; + } + + public function clear() : bool + { + $this->clearPinentries(); + $this->clearEncryptKeys(); + return true; + } + + /** + * Removes all keys which were set for decryption before + */ + public function clearDecryptKeys() : bool + { + return $this->clearPinentries(); + } + + /** + * Removes all keys which were set for encryption before + */ + public function clearEncryptKeys() : bool + { + $this->encryptKeys = []; + return true; + } + + /** + * Removes all keys which were set for signing before + */ + public function clearSignKeys() : bool + { + return $this->clearPinentries(); + } + + /** + * Returns the engine info + */ + public function getEngineInfo() : array + { + return [ + 'protocol' => null, + 'file_name' => $this->binary, + 'home_dir' => $this->options['homedir'], + 'version' => $this->version + ]; + } + + /** + * Add private key passphrase for decrypt, sign or export + * $keyId or fingerprint + */ + public function addPinentry(string $keyId, SensitiveString $passphrase) + { + /** + * Test first? + * gpg --dry-run --passwd + $_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', [$keyId => $passphrase])); + $result = $this->exec([ + '--dry-run', + '--passwd', + $passphrase + ]); + */ +// $this->export($keyId, $passphrase); + $this->pinentries[$keyId] = $passphrase; +// $this->pinentries[\substr($keyId, -16)] = $passphrase; + return $this; + } + + /** + * Removes all keys which were set for decryption, signing and export + */ + public function clearPinentries() : bool + { + $this->pinentries = []; + return true; + } + + /** + * Toggle the armored output + * When true the output is ASCII + */ + public function setArmor(bool $armor = true) : bool + { + $this->armor = $armor; + return true; + } + + protected function _debug(string $msg) : void + { + \SnappyMail\Log::debug('GPG', $msg); + } + + protected function setInput(&$input) : void + { + if (\is_resource($input)) { + // https://github.com/the-djmaze/snappymail/issues/331 + // $meta['stream_type'] == MEMORY or $meta['wrapper_data'] == MailSo\Base\StreamWrappers\Literal + $meta = \stream_get_meta_data($input); + if (!\in_array($meta['stream_type'], ['STDIO', 'TEMP'])) { +/* + $fp = \fopen('php://temp'); + \stream_copy_to_stream($input, $fp); + $input = $fp; +*/ + $input = \stream_get_contents($input); + } + } + $this->_input =& $input; + } + + protected function setOutput($output)/* : resource|false*/ + { + $fclose = false; + if ($output && !\is_resource($output)) { + $output = \fopen($output, 'wb'); + if (!$output) { + throw new \Exception("Could not open file '{$filename}'"); + } + $fclose = $output; + } + $this->_output = $output; + return $fclose; + } + + public function agent() + { +// $home = \escapeshellarg($this->options['homedir']); +// echo `gpg-agent --daemon --homedir $home 2>&1`; + } + + protected function getPassphrase($key) + { + $passphrase = ''; + $keyIdLength = \strlen($key); + if ($keyIdLength && !empty($_ENV['PINENTRY_USER_DATA'])) { + $passphrases = \json_decode($_ENV['PINENTRY_USER_DATA'], true); + foreach ($passphrases as $keyId => $pass) { + $length = \min($keyIdLength, \strlen($keyId)); + if (\substr($keyId, -$length) === \substr($key, -$length)) { + return $pass; + } + } + } +// throw new \Exception("Passphrase not found for {$key}"); + return ''; + } + + protected function proc_close() : int + { + $exitCode = 0; + + // clear PINs from environment if they were set + $_ENV['PINENTRY_USER_DATA'] = null; + + if (\is_resource($this->proc_resource)) { + $this->_debug('CLOSING SUBPROCESS'); + + // close remaining open pipes + $this->_openPipes->closeAll(); + + $status = \proc_get_status($this->proc_resource); + $exitCode = \proc_close($this->proc_resource); + $this->proc_resource = null; + + // proc_close() can return -1 in some cases, + // get the real exit code from the process status + if ($exitCode < 0 && $status && !$status['running']) { + $exitCode = $status['exitcode']; + } + + if ($exitCode > 0) { + $this->_debug('=> subprocess returned an unexpected exit code: ' . $exitCode); + } + } + + return $exitCode; + } + + protected static function findBinary($name) : ?string + { + $binary = \function_exists('shell_exec') ? \trim((string) `which $name`) : ''; + if ($binary && \RainLoop\Utils::inOpenBasedir($binary) && \is_executable($binary)) { + return $binary; + } + $locations = \array_filter([ + '/sw/bin/', + '/usr/bin/', + '/usr/local/bin/', + '/opt/local/bin/', + '/run/current-system/sw/bin/' + ], '\RainLoop\Utils::inOpenBasedir'); + foreach ($locations as $location) { + if (\is_executable($location . $name)) { + return $location . $name; + } + } + return null; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/gpg/pgp.php b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/pgp.php new file mode 100644 index 0000000000..f3921112f8 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/pgp.php @@ -0,0 +1,1189 @@ + '', + 'keyring' => '', + 'digest-algo' => '', + 'cipher-algo' => '', + 'secret-keyring' => '', + /* + 2 = ZLIB (GnuPG, default) + 1 = ZIP (PGP) + 0 = Uncompressed + */ + 'compress-algo' => 2, + ]; + + function __construct(string $homedir) + { + parent::__construct($homedir); + + // the random seed file makes subsequent actions faster so only disable it if we have to. + if ($this->options['homedir'] && !\is_writable($this->options['homedir'])) { + $this->options['no-random-seed-file'] = true; + } + + // How to use gpgme-json ? + $this->binary = static::findBinary('gpg'); + + $info = \preg_replace('/\R +/', ' ', `$this->binary --with-colons --list-config`); + if (\preg_match('/cfg:version:([0-9]+\\.[0-9]+\\.[0-9]+)/', $info, $match)) { + $this->version = $match[1]; + } + if (\preg_match('/cfg:cipher:(.+)/', $info, $match) && \preg_match('/cfg:ciphername:(.+)/', $info, $match1)) { + $this->ciphers = \array_combine(\explode(';', $match[1]), \explode(';', $match1[1])); + } + if (\preg_match('/cfg:digest:(.+)/', $info, $match) && \preg_match('/cfg:digestname:(.+)/', $info, $match1)) { + $this->digests = \array_combine(\explode(';', $match[1]), \explode(';', $match1[1])); + } + if (\preg_match('/cfg:pubkey:(.+)/', $info, $match) && \preg_match('/cfg:pubkeyname:(.+)/', $info, $match1)) { + $this->pubkey_types = \array_combine(\explode(';', $match[1]), \explode(';', $match1[1])); + } + if (\preg_match('/cfg:compress:(.+)/', $info, $match) && \preg_match('/cfg:compressname:(.+)/', $info, $match1)) { + $this->compressions = \array_combine(\explode(';', $match[1]), \explode(';', $match1[1])); + } + if (\preg_match('/cfg:curve:(.+)/', $info, $match)) { + $this->curves = \explode(';', $match[1]); + } + } + + public static function isSupported() : bool + { + return parent::isSupported() && static::findBinary('gpg'); + } + + /** + * TODO: parse result + * https://github.com/the-djmaze/snappymail/issues/89 + */ + protected function listDecryptKeys(/*string|resource*/ $input, /*string|resource*/ $output = null) + { + $this->setInput($input); + $_ENV['PINENTRY_USER_DATA'] = ''; + return $this->execOutput(['--list-packets'], $output); + } + + protected function _decrypt(/*string|resource*/ $input, /*string|resource*/ $output = null) + { + $this->setInput($input); + if ($this->pinentries) { + $_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->pinentries)); + } + return $this->execOutput(['--decrypt','--skip-verify'], $output); + } + + /** + * Decrypts a given text + */ + public function decrypt(string $text) /*: string|false */ + { + return $this->_decrypt($text); + } + + /** + * Decrypts a given file + */ + public function decryptFile(string $filename) /*: string|false */ + { + $fp = \fopen($filename, 'rb'); + try { + if (!$fp) { + throw new \Exception("Could not open file '{$filename}'"); + } + return $this->_decrypt($fp, $output); + } finally { + $fp && \fclose($fp); + } + } + + /** + * Decrypts a given stream + */ + public function decryptStream($fp, /*string|resource*/ $output = null) /*: string|false*/ + { + if (!$fp || !\is_resource($fp)) { + throw new \Exception('Invalid stream resource'); + } +// \rewind($fp); + return $this->_decrypt($fp, $output); + } + + /** + * Decrypts and verifies a given text + */ + public function decryptVerify(string $text, string &$plaintext) /*: array|false*/ + { + // TODO: https://github.com/the-djmaze/snappymail/issues/89 + return false; + } + + /** + * Decrypts and verifies a given file + */ + public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/ + { + // TODO: https://github.com/the-djmaze/snappymail/issues/89 + return false; + } + + protected function _encrypt(/*string|resource*/ $input, /*string|resource*/ $output = null) + { + if (!$this->encryptKeys) { + throw new \Exception('No encryption keys specified.'); + } + + $this->setInput($input); + + $arguments = [ + '--encrypt' + ]; + if ($this->armor) { + $arguments[] = '--armor'; + } + + foreach ($this->encryptKeys as $fingerprint => $dummy) { + $arguments[] = '--recipient ' . \escapeshellarg($fingerprint); + } + + return $this->execOutput($arguments, $output); + } + + /** + * Encrypts a given text + */ + public function encrypt(string $plaintext, /*string|resource*/ $output = null) /*: string|false*/ + { + return $this->_encrypt($plaintext, $output); + } + + /** + * Encrypts a given text + */ + public function encryptFile(string $filename, /*string|resource*/ $output = null) /*: string|false*/ + { + $fp = \fopen($filename, 'rb'); + try { + if (!$fp) { + throw new \Exception("Could not open file '{$filename}'"); + } + return $this->_encrypt($filename, $output); + } finally { + $fp && \fclose($fp); + } + } + + public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/ + { + if (!$fp || !\is_resource($fp)) { + throw new \Exception('Invalid stream resource'); + } + \rewind($fp); + return $this->_encrypt($fp, $output); + } + + /** + * Encrypts and signs a given text + */ + public function encryptSign(string $plaintext) /*: string|false*/ + { + return false; + } + + /** + * Encrypts and signs a given text + */ + public function encryptSignFile(string $filename) /*: string|false*/ + { + return false; + } + + /** + * Exports a public or private key + */ + public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/ + { +// \SnappyMail\Log::debug('GnuPG', "export({$fingerprint}, {$passphrase})"); + $private = null !== $passphrase; + $keys = $this->keyInfo($fingerprint, $private); + if (!$keys) { + throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $fingerprint); + } + if ($private) { + $_ENV['PINENTRY_USER_DATA'] = \json_encode([$fingerprint => \strval($passphrase)]); + } + $result = $this->exec([ + $private ? '--export-secret-keys' : '--export', + '--armor', + \escapeshellarg($keys[0]['subkeys'][0]['fingerprint']), + ]); + return $result ? $result['output'] : false; + } + + /** + * Returns the errortext, if a function fails + */ + public function getError() /*: string|false*/ + { + return false; + } + + /** + * Returns the error info + */ + public function getErrorInfo() : array + { + return []; + } + + /** + * Returns the currently active protocol for all operations + */ + public function getProtocol() : int + { + return 0; // GPGME_PROTOCOL_OpenPGP + } + + /** + * Generates a key + * Also saves revocation certificate in {homedir}/openpgp-revocs.d/ + * https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Key-Management.html + */ + public function generateKey(string $uid, SensitiveString $passphrase) /*: string|false*/ + { + $settings = new PGPKeySettings; + $settings->name = $uid; + $settings->email = $uid; + $settings->passphrase = $passphrase; + + $arguments = [ + '--batch', + '--yes', + '--passphrase', \escapeshellarg($settings->passphrase) + ]; + + /** + * https://www.gnupg.org/documentation/manuals/gnupg/Unattended-GPG-key-generation.html + * But it can't generate multiple subkeys + * Somehow generating first subkey is also broken in v2.3.4 + $this->_input = $settings->asUnattendedData(); + $result = $this->exec(['--batch', '--yes', '--full-gen-key']); + */ + $result = $this->exec(\array_merge($arguments, [ + '--quick-gen-key', + \escapeshellarg($settings->uid()), + $settings->algo(), + $settings->usage + ])); + if (!$result) { + return false; + } + + $fingerprint = ''; + foreach ($result['status'] as $line) { + $tokens = \explode(' ', $line); + if ('KEY_CREATED' === $tokens[0]/* && 'P' === $tokens[1]*/) { + $fingerprint = $tokens[2]; + } + } + if (!$fingerprint) { + return false; + } + + $arguments[] = '--quick-add-key'; + $arguments[] = $fingerprint; + + foreach ($settings->subkeys as $i => $key) { + $algo = 'default'; + if (!empty($key['curve'])) { + $algo = $key['curve']; + } + if (!empty($key['type'])) { + $algo = $key['type'] . ($key['length'] ?? ''); + } + $this->exec(\array_merge($arguments, [$algo, $key['usage'], '0'])); + } + +/* + [status][0] => KEY_NOT_CREATED + [errors][0] => gpg: -:3: specified Key-Usage not allowed for algo 22 + [errors][0] => gpg: key generation failed: Unknown elliptic curve + + [status][0] => KEY_CONSIDERED B2FD2BCADCC6A9E4B2C90DBBE776CADFF94D327F 0 + [status][1] => KEY_CREATED P B2FD2BCADCC6A9E4B2C90DBBE776CADFF94D327F +*/ + return $fingerprint; + } + + protected function _importKey($input) /*: array|false*/ + { + $arguments = ['--import']; + + if ($this->pinentries) { + $_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->pinentries)); + } else { + $arguments[] = '--batch'; + } + + $this->setInput($input); + $result = $this->exec($arguments); + if ($result) { + foreach ($result['status'] as $line) { + if (false !== \strpos($line, 'IMPORT_RES')) { + $line = \explode(' ', \explode('IMPORT_RES ', $line)[1]); + return [ + 'imported' => (int) $line[2], + 'unchanged' => (int) $line[4], + 'newuserids' => (int) $line[5], + 'newsubkeys' => (int) $line[6], + 'secretimported' => (int) $line[10], + 'secretunchanged' => (int) $line[11], + 'newsignatures' => (int) $line[7], + 'skippedkeys' => (int) $line[12], + 'fingerprint' => '' + ]; + } + } + } + return false; + } + + /** + * Imports a key + */ + public function import(string $keydata) /*: array|false*/ + { + if (!$keydata) { + throw new \Exception('No valid input data found.'); + } + return $this->_importKey($keydata); + } + + /** + * Imports a key + */ + public function importFile(string $filename) /*: array|false*/ + { + $fp = \fopen($filename, 'rb'); + try { + if (!$fp) { + throw new \Exception("Could not open file '{$filename}'"); + } + return $this->_importKey($fp); + } finally { + $fp && \fclose($fp); + } + } + + public function deleteKey(string $keyId, bool $private) : bool + { + $key = $this->keyInfo($keyId, $private); + if (!$key) { + throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId); + } +// if (!$private && $this->keyInfo($keyId, true)) { +// throw new \Exception('Delete private key first: ' . $keyId); +// } + + $result = $this->exec([ + '--batch', + '--yes', + $private ? '--delete-secret-key' : '--delete-key', + \escapeshellarg($key[0]['subkeys'][0]['fingerprint']) + ]); + +// $result['status'][0] = '[GNUPG:] ERROR keylist.getkey 17' +// $result['errors'][0] = 'gpg: error reading key: No public key' + +// print_r($result); + return !!$result; + } + + /** + * Returns an array with information about all keys that matches the given pattern + */ + public function keyInfo(string $pattern, bool $private = false) : array + { + // According to The file 'doc/DETAILS' in the GnuPG distribution, using + // double '--with-fingerprint' also prints the fingerprint for subkeys. + $arguments = [ + '--with-colons', + '--with-fingerprint', + '--with-fingerprint', + '--fixed-list-mode', + $private ? '--list-secret-keys' : '--list-public-keys' + ]; + if ($pattern) { + $arguments[] = '--utf8-strings'; + $arguments[] = \escapeshellarg($pattern); + } + + $result = $this->exec($arguments); + + $keys = []; + if ($result) { + $key = null; // current key + $subKey = null; // current sub-key + + foreach (\explode(PHP_EOL, $result['output']) as $line) { + $tokens = \explode(':', $line); + + switch ($tokens[0]) + { + case 'tru': + break; + + case 'sec': + case 'pub': + // new primary key means last key should be added to the array + if ($key !== null) { + $keys[] = $key; + } + $key = [ + 'disabled' => false, + 'expired' => false, + 'revoked' => false, + 'is_secret' => 'ssb' === $tokens[0], + 'can_sign' => \str_contains($tokens[11], 's'), + 'can_encrypt' => \str_contains($tokens[11], 'e'), + 'uids' => [], + 'subkeys' => [] + ]; + // Fall through to add subkey + case 'ssb': // secure subkey + case 'sub': // public subkey + $key['subkeys'][] = [ + 'fingerprint' => '', // fpr:::::::::....: + 'keyid' => $tokens[4], + 'timestamp' => $tokens[5], + 'expires' => $tokens[6], + 'is_secret' => 'ssb' === $tokens[0], + 'invalid' => false, + // escaESCA + 'can_encrypt' => \str_contains($tokens[11], 'e'), + 'can_sign' => \str_contains($tokens[11], 's'), + 'can_certify' => \str_contains($tokens[11], 'c'), + 'can_authenticate' => \str_contains($tokens[11], 'a'), + 'disabled' => false, + 'expired' => false, + 'revoked' => 'r' === $tokens[1], + 'length' => $tokens[2], + 'algorithm' => $tokens[3], + ]; + break; + + case 'fpr': + $key['subkeys'][\array_key_last($key['subkeys'])]['fingerprint'] = $tokens[9]; + break; + + case 'grp': + $key['subkeys'][\array_key_last($key['subkeys'])]['keygrip'] = $tokens[9]; + break; + + case 'uid': + $string = \stripcslashes($tokens[9]); // as per documentation + $name = ''; + $email = ''; + $comment = ''; + $matches = []; + + // get email address from end of string if it exists + if (\preg_match('/^(.*?)<([^>]+)>$/', $string, $matches)) { + $string = \trim($matches[1]); + $email = $matches[2]; + } + + // get comment from end of string if it exists + $matches = []; + if (\preg_match('/^(.+?) \(([^\)]+)\)$/', $string, $matches)) { + $string = $matches[1]; + $comment = $matches[2]; + } + + // there can be an email without a name + if (!$email && \preg_match('/^[\S]+@[\S]+$/', $string, $matches)) { + $email = $string; + } else { + $name = $string; + } + + $key['uids'][] = [ + 'name' => $name, + 'comment' => $comment, + 'email' => $email, + 'uid' => $tokens[9], + 'revoked' => 'r' === $tokens[1], + 'invalid' => false, + ]; + break; + } + } + + // add last key + if ($key) { + $keys[] = $key; + } + } + + return $keys; + } + + /** + * Returns an array with information about all keys that matches the given pattern + */ + public function allKeysInfo(string $pattern) : array + { + $keys = [ + 'public' => [], + 'private' => [] + ]; + // Public + foreach (($this->keyinfo($pattern) ?: []) as $key) { + $key['can_verify'] = $key['can_sign']; + unset($key['can_sign']); + $keys['public'][] = $key; + } + // Private, read https://github.com/php-gnupg/php-gnupg/issues/5 + foreach (($this->keyinfo($pattern, 1) ?: []) as $key) { + $key['can_decrypt'] = $key['can_encrypt']; + unset($key['can_encrypt']); + $keys['private'][] = $key; + } + return $keys; + } + + /** + * Sets the mode for error_reporting + * GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT. + * By default GNUPG_ERROR_SILENT is used. + */ + public function setErrorMode(int $errormode) : void + { + } + + /** + * Sets the mode for signing + * GNUPG_SIG_MODE_NORMAL, GNUPG_SIG_MODE_DETACH, GNUPG_SIG_MODE_CLEAR + * By default GNUPG_SIG_MODE_CLEAR + */ + public function setSignMode(int $signmode) : bool + { + $this->signmode = $signmode; + return true; + } + + protected function _sign(/*string|resource*/ $input, /*string|resource*/ $output = null, bool $textmode = true) /*: string|false*/ + { + if (empty($this->pinentries)) { + throw new \Exception('No signing keys specified.'); + } + + $this->setInput($input); + + $arguments = []; + + switch ($this->signmode) + { + case 0: // GNUPG_SIG_MODE_NORMAL + $arguments[] = '--sign'; + break; + case 1: // GNUPG_SIG_MODE_DETACH + $arguments[] = '--detach-sign'; + break; + case 2: // GNUPG_SIG_MODE_CLEAR + default: + $arguments[] = '--clearsign'; + break; + } + + if ($this->armor) { + $arguments[] = '--armor'; + } + if ($textmode) { + $arguments[] = '--textmode'; + } + + if ($this->pinentries) { + foreach ($this->pinentries as $fingerprint => $pass) { + $arguments[] = '--local-user ' . \escapeshellarg($fingerprint); + } + $_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->pinentries)); + } + + return $this->execOutput($arguments, $output); + } + + /** + * Signs a given text + */ + public function sign(string $plaintext, /*string|resource*/ $output = null) /*: string|false*/ + { + return $this->_sign($plaintext, $output); + } + + /** + * Signs a given file + */ + public function signFile(string $filename, /*string|resource*/ $output = null) /*: string|false*/ + { + $fp = \fopen($filename, 'rb'); + try { + if (!$fp) { + throw new \Exception("Could not open file '{$filename}'"); + } + return $this->_sign($fp, $output); + } finally { + $fp && \fclose($fp); + } + } + + /** + * Signs a given file + */ + public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/ + { + if (!$fp || !\is_resource($fp)) { + throw new \Exception('Invalid stream resource'); + } + \rewind($fp); + return $this->_sign($fp, $output); + } + + protected function _verify($input, string $signature) /*: array|false*/ + { + $arguments = ['--verify']; + if ($signature) { + // detached signature + $this->setInput($signature); + $this->_message =& $input; + // Signed data goes in FD_MESSAGE, detached signature data goes in FD_INPUT. + $arguments[] = '--enable-special-filenames'; + $arguments[] = '- "-&' . self::FD_MESSAGE . '"'; + } else { + // signed or clearsigned data + $this->setInput($input); + } + + $result = $this->exec($arguments); + + $signatures = []; + if ($result) { + foreach ($result['status'] as $line) { + $tokens = \explode(' ', $line); + switch ($tokens[0]) + { + case 'VERIFICATION_COMPLIANCE_MODE': + case 'TRUST_FULLY': + break; + + case 'EXPSIG': + case 'EXPKEYSIG': + case 'REVKEYSIG': + case 'BADSIG': + case 'ERRSIG': + case 'GOODSIG': + $signatures[] = [ + 'fingerprint' => '', + 'validity' => 0, + 'timestamp' => 0, + 'status' => 'GOODSIG' === $tokens[0] ? 0 : 1, + 'summary' => 'GOODSIG' === $tokens[0] ? 0 : 4, + 'keyid' => $tokens[1], + 'uid' => \rawurldecode(\implode(' ', \array_splice($tokens, 2))), + 'valid' => false + ]; + break; + + case 'VALIDSIG': + $last = \array_key_last($signatures); + $signatures[$last]['fingerprint'] = $tokens[1]; + $signatures[$last]['timestamp'] = (int) $tokens[3]; + $signatures[$last]['expires'] = (int) $tokens[4]; + $signatures[$last]['version'] = (int) $tokens[5]; +// $signatures[$last]['reserved'] = (int) $tokens[6]; +// $signatures[$last]['pubkey-algo'] = (int) $tokens[7]; +// $signatures[$last]['hash-algo'] = (int) $tokens[8]; +// $signatures[$last]['sig-class'] = $tokens[9]; +// $signatures[$last]['primary-fingerprint'] = $tokens[10]; + $signatures[$last]['valid'] = 0; + } + } + } + + return $signatures; + } + + /** + * Verifies a signed text + */ + public function verify(string $signed_text, string $signature, ?string &$plaintext = null) /*: array|false*/ + { + return $this->_verify($signed_text, $signature); + } + + /** + * Verifies a signed file + */ + public function verifyFile(string $filename, string $signature, ?string &$plaintext = null) /*: array|false*/ + { + $fp = \fopen($filename, 'rb'); + try { + if (!$fp) { + throw new \Exception("Could not open file '{$filename}'"); + } + return $this->_verify($fp, $signature); + } finally { + $fp && \fclose($fp); + } + } + + /** + * Verifies a signed file + */ + public function verifyStream($fp, string $signature, ?string &$plaintext = null) /*: array|false*/ + { + if (!$fp || !\is_resource($fp)) { + throw new \Exception('Invalid stream resource'); + } +// \rewind($fp); + return $this->_verify($fp, $signature); + } + + public function getEncryptedMessageKeys(/*string|resource*/ $data) : array + { + $this->_debug('BEGIN DETECT MESSAGE KEY IDs'); + $this->setInput($data); +// $_ENV['PINENTRY_USER_DATA'] = null; + $result = $this->exec(['--decrypt','--skip-verify'], false); + $info = [ + 'ENC_TO' => [], +// 'KEY_CONSIDERED' => [], +// 'NO_SECKEY' => [], + ]; + if ($result) { + foreach ($result['status'] as $line) { + $tokens = \explode(' ', $line); + if (isset($info[$tokens[0]])) { + $info[$tokens[0]][] = $tokens[1]; + } + } + } + $this->_debug('END DETECT MESSAGE KEY IDs'); + return $info['ENC_TO']; + } + + protected function execOutput(array $arguments, /*string|resource*/ $output = null) + { + $fclose = $this->setOutput($output); + $result = $this->exec($arguments); + $fclose && \fclose($fclose); + return $output ? true : ($result ? $result['output'] : false); + } + + private function exec(array $arguments, bool $throw = true) /*: array|false*/ + { + if (\version_compare($this->version, '2.2.5', '<')) { + \SnappyMail\Log::error('GPG', "{$this->version} too old"); + return false; + } + + $defaultArguments = [ + '--status-fd ' . self::FD_STATUS, + '--command-fd ' . self::FD_COMMAND, +// '--no-greeting', + '--no-secmem-warning', + '--no-tty', + '--no-default-keyring', // ignored if keying files are not specified + '--no-options', // prevent creation of ~/.gnupg directory + '--no-permission-warning', // 1.0.7+ +// '--no-use-agent', // < 2.0.0 + '--exit-on-status-write-error', // 1.4.2+ + '--trust-model always', // 1.3.2+ else --always-trust + // If no passphrases are set, cancel them + '--pinentry-mode ' . (empty($_ENV['PINENTRY_USER_DATA']) ? 'cancel' : 'loopback') // 2.1.13+ + ]; + + if (!$this->strict) { + $defaultArguments[] = '--ignore-time-conflict'; + $defaultArguments[] = '--ignore-valid-from'; + } + + if ($this->options['digest-algo']) { + $this->options['s2k-digest-algo'] = $this->options['digest-algo']; + } + if ($this->options['cipher-algo']) { + $this->options['s2k-cipher-algo'] = $this->options['cipher-algo']; + } + + foreach ($this->options as $option => $value) { + if (\is_string($value)) { + if (\strlen($value)) { + $defaultArguments[] = "--{$option} " . \escapeshellarg($value); + } + } else if (true === $value) { + $defaultArguments[] = "--{$option}"; + } else if ('compress-algo' === $option && 2 !== $value) { + $defaultArguments[] = "--{$option} " . \intval($value); + } + } + + $commandLine = $this->binary . ' ' . \implode(' ', \array_merge($defaultArguments, $arguments)); + + $descriptorSpec = [ + self::FD_INPUT => array('pipe', 'rb'), // stdin + self::FD_OUTPUT => array('pipe', 'wb'), // stdout + self::FD_ERROR => array('pipe', 'wb'), // stderr + self::FD_STATUS => array('pipe', 'wb'), // status + self::FD_COMMAND => array('pipe', 'rb'), // command + self::FD_MESSAGE => array('pipe', 'rb') // message + ]; + + $this->_debug('OPENING SUBPROCESS WITH THE FOLLOWING COMMAND:'); + $this->_debug($commandLine); + + // Don't localize GnuPG results. + $env = $_ENV; + $env['LC_ALL'] = 'C'; + $env = \array_filter($env, fn($var) => \is_scalar($var)); + + $proc_pipes = []; + + $this->proc_resource = \proc_open( + $commandLine, + $descriptorSpec, + $proc_pipes, + null, + $env, + ['binary_pipes' => true] + ); + + if (!\is_resource($this->proc_resource)) { + throw new \Exception('Unable to open process.'); + } + + $this->_openPipes = new ProcPipes($proc_pipes); + + $this->_debug('BEGIN PROCESSING'); + + $commandBuffer = ''; // buffers input to GPG + $messageBuffer = ''; // buffers input to GPG + $inputBuffer = ''; // buffers input to GPG + $outputBuffer = ''; // buffers output from GPG + $inputComplete = false; // input stream is completely buffered + $messageComplete = false; // message stream is completely buffered + + if (\is_string($this->_input)) { + $inputBuffer = $this->_input; + $inputComplete = true; + } + + if (\is_string($this->_message)) { + $messageBuffer = $this->_message; + $messageComplete = true; + } + + $status = []; + $errors = []; + + // convenience variables + $fdInput = $proc_pipes[self::FD_INPUT]; + $fdOutput = $proc_pipes[self::FD_OUTPUT]; + $fdError = $proc_pipes[self::FD_ERROR]; + $fdStatus = $proc_pipes[self::FD_STATUS]; + $fdCommand = $proc_pipes[self::FD_COMMAND]; + $fdMessage = $proc_pipes[self::FD_MESSAGE]; + + // select loop delay in milliseconds + $delay = 0; + $inputPosition = 0; + + $start = \microtime(1); + + while (true) { + // Timeout after 5 seconds + if (5 < \microtime(1) - $start) { + $errors[] = 'timeout'; + throw new \RuntimeException(\implode("\n", $errors)); + } + + $inputStreams = []; + $outputStreams = []; + $exceptionStreams = []; + + // set up input streams + if (!$inputComplete && \is_resource($this->_input)) { + if (\feof($this->_input)) { + $inputComplete = true; + } else { + $inputStreams[] = $this->_input; + } + } + + // close GPG input pipe if there is no more data + if ('' == $inputBuffer && $inputComplete) { + $this->_debug('=> closing input pipe'); + $this->_openPipes->close(self::FD_INPUT); + } + + if (\is_resource($this->_message) && !$messageComplete) { + if (\feof($this->_message)) { + $messageComplete = true; + } else { + $inputStreams[] = $this->_message; + } + } + + if (!\feof($fdOutput)) { + $inputStreams[] = $fdOutput; + } + + if (!\feof($fdStatus)) { + $inputStreams[] = $fdStatus; + } + + if (!\feof($fdError)) { + $inputStreams[] = $fdError; + } + + // set up output streams + if ('' != $outputBuffer && $this->_output) { + $outputStreams[] = $this->_output; + } + + if ($commandBuffer != '' && \is_resource($fdCommand)) { + $outputStreams[] = $fdCommand; + } + + if ('' != $messageBuffer) { + if (\is_resource($fdMessage)) { + $outputStreams[] = $fdMessage; + } + } else if ($messageComplete) { + // close GPG message pipe if there is no more data + $this->_debug('=> closing message pipe'); + $this->_openPipes->close(self::FD_MESSAGE); + } + + if ($inputBuffer != '' && \is_resource($fdInput)) { + $outputStreams[] = $fdInput; + } + + // no streams left to read or write, we're all done + if (!\count($inputStreams) && !\count($outputStreams)) { + break; + } + + $this->_debug('selecting streams'); + + $ready = \stream_select( + $inputStreams, + $outputStreams, + $exceptionStreams, + 5 + ); + + $this->_debug('=> got ' . $ready); + + if ($ready === false) { + throw new \Exception( + 'Error selecting stream for communication with GPG ' . + 'subprocess. Please file a bug report at: ' . + 'http://pear.php.net/bugs/report.php?package=Crypt_GPG' + ); + } + + if ($ready === 0) { + throw new \Exception( + 'stream_select() returned 0. This can not happen! Please ' . + 'file a bug report at: ' . + 'http://pear.php.net/bugs/report.php?package=Crypt_GPG' + ); + } + + // write input (to GPG) + if (\in_array($fdInput, $outputStreams, true)) { + $this->_debug('ready for input'); + $chunk = \substr($inputBuffer, $inputPosition, self::CHUNK_SIZE); + $length = \strlen($chunk); + $this->_debug('=> about to write ' . $length . ' bytes to input'); + $length = $this->_openPipes->writePipe(self::FD_INPUT, $chunk, $length); + if ($length) { + $this->_debug('=> wrote ' . $length . ' bytes'); + // Move the position pointer, don't modify $inputBuffer (#21081) + if (\is_string($this->_input)) { + $inputPosition += $length; + } else { + $inputPosition = 0; + $inputBuffer = \substr($inputBuffer, $length); + } + } else { + $this->_debug('=> pipe broken and closed'); + } + } + + // read input (from PHP stream) + // If the buffer is too big wait until it's smaller, we don't want + // to use too much memory + if (\in_array($this->_input, $inputStreams, true) && \strlen($inputBuffer) < self::CHUNK_SIZE) { + $this->_debug('input stream is ready for reading'); + $chunk = \fread($this->_input, self::CHUNK_SIZE); + $length = \strlen($chunk); + $inputBuffer .= $chunk; + $this->_debug('=> read ' . $length . ' bytes'); + } + + // write message (to GPG) + if (\in_array($fdMessage, $outputStreams, true)) { + $this->_debug('ready for message data'); + $this->_debug('=> about to write ' . \min(self::CHUNK_SIZE, \strlen($messageBuffer)) . ' bytes to message'); + $length = $this->_openPipes->writePipe(self::FD_MESSAGE, $messageBuffer, self::CHUNK_SIZE); + if ($length) { + $this->_debug('=> wrote ' . $length . ' bytes'); + $messageBuffer = \substr($messageBuffer, $length); + } else { + $this->_debug('=> pipe broken and closed'); + } + } + + // read message (from PHP stream) + if (\in_array($this->_message, $inputStreams, true)) { + $this->_debug('message stream is ready for reading'); + $chunk = \fread($this->_message, self::CHUNK_SIZE); + $length = \strlen($chunk); + $messageBuffer .= $chunk; + $this->_debug('=> read ' . $length . ' bytes'); + } + + // read output (from GPG) + if (\in_array($fdOutput, $inputStreams, true)) { + $this->_debug('output stream ready for reading'); + $chunk = \fread($fdOutput, self::CHUNK_SIZE); + $length = \strlen($chunk); + $outputBuffer .= $chunk; + $this->_debug('=> read ' . $length . ' bytes'); + } + + // write output (to PHP stream) + if (\in_array($this->_output, $outputStreams, true)) { + $this->_debug('output stream is ready for data'); + $chunk = \substr($outputBuffer, 0, self::CHUNK_SIZE); + $length = \strlen($chunk); + $this->_debug('=> about to write ' . $length . ' bytes to output stream'); + $length = \fwrite($this->_output, $chunk, $length); + if (!$length) { + $this->_debug('=> broken pipe on output stream'); + $this->_debug('=> closing pipe output stream'); + $this->_openPipes->close(self::FD_OUTPUT); + } else { + $this->_debug('=> wrote ' . $length . ' bytes'); + $outputBuffer = \substr($outputBuffer, $length); + } + } + + // read error (from GPG) + if (\in_array($fdError, $inputStreams, true)) { + $this->_debug('error stream ready for reading'); + foreach ($this->_openPipes->readPipeLines(self::FD_ERROR) as $line) { + $this->_debug("\t{$line}"); + $errors[] = \preg_replace('/^gpg: /', '', $line); + if ($throw && (\str_contains($line, 'error') || \str_contains($line, 'failed'))) { + break 2; + } + } + } + + // read status (from GPG) + if (\in_array($fdStatus, $inputStreams, true)) { + $this->_debug('status stream ready for reading'); + // pass lines to status handlers + foreach ($this->_openPipes->readPipeLines(self::FD_STATUS) as $line) { + // only pass lines beginning with magic prefix + if ('[GNUPG:] ' == \substr($line, 0, 9)) { + $line = \substr($line, 9); + $status[] = $line; + $this->_debug("\t{$line}"); + + $tokens = \explode(' ', $line); + // NEED_PASSPHRASE 0123456789ABCDEF 0123456789ABCDEF 1 0 + if ('NEED_PASSPHRASE' === $tokens[0]) { + // key ?: subkey + $passphrase = $this->getPassphrase($tokens[1]) ?: $this->getPassphrase($tokens[2]); + $commandBuffer .= $passphrase . PHP_EOL; + } + } + } + } + + // write command (to GPG) + if (\in_array($fdCommand, $outputStreams, true)) { + $this->_debug('ready for command data'); + $chunk = \substr($commandBuffer, 0, self::CHUNK_SIZE); + $length = \strlen($chunk); + $this->_debug('=> about to write ' . $length . ' bytes to command'); + $length = $this->_openPipes->writePipe(self::FD_COMMAND, $chunk, $length); + if ($length) { + $this->_debug('=> wrote ' . $length); + $commandBuffer = \substr($commandBuffer, $length); + } else { + $this->_debug('=> pipe broken and closed'); + } + } + + if (\count($outputStreams) === 0 || \count($inputStreams) === 0) { + // we have an I/O imbalance, increase the select loop delay + // to smooth things out + $delay += 10; + } else { + // things are running smoothly, decrease the delay + $delay -= 8; + $delay = \max(0, $delay); + } + + if ($delay > 0) { + \usleep($delay); + } + + } // end loop while streams are open + + $this->_debug('END PROCESSING'); + + $exitCode = $this->proc_close(); + + $this->_message = null; + $this->_input = null; + $this->_output = null; + + if ($throw && $exitCode && $errors) { + throw new \RuntimeException(\implode(".\n", $errors), $exitCode); + } + + return [ + 'output' => $outputBuffer, + 'status' => $status, + 'errors' => $errors + ]; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/gpg/pgpkeysettings.php b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/pgpkeysettings.php new file mode 100644 index 0000000000..007a5548a2 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/pgpkeysettings.php @@ -0,0 +1,124 @@ + null, // One of pubkey_types + 'curve' => 'ed25519', // One of curves + 'length' => null, + 'usage' => 'sign' + ], + [ + 'type' => null, // One of pubkey_types + 'curve' => 'cv25519', // One of curves + 'length' => null, + 'usage' => 'encrypt' + ] + ], + + $name = '', + $email = '', + $comment = '', + $passphrase; + + public function algo() : string + { + if ($this->curve) { + return $this->curve; + } + return $this->type . ($this->length ?: ''); + } + + public function uid() : string + { + $result = ''; + if (\strlen($this->name)) { + $result .= $this->name; + } + if (\strlen($this->comment)) { + $result .= ' (' . $this->comment . ')'; + } + if (\strlen($this->email)) { + $result .= ' <' . $this->email. '>'; + } + return \trim($result); + } + + public function useDefault() : void + { + $this->type = 'default'; + $this->curve = null; + $this->length = null; + $this->usage = null; + $subkeys[0] = ['type' => 'default']; + } + + /** + * https://www.gnupg.org/documentation/manuals/gnupg/Unattended-GPG-key-generation.html + */ + public function asUnattendedData() : string + { + $keyParams = [ + "Key-Type: {$this->type}" + ]; + if ($this->curve) { + $keyParams[] = "Key-Curve: {$this->curve}"; + } + if ($this->length) { + $keyParams[] = "Key-Length: {$this->length}"; + } + if ($this->usage) { + $keyParams[] = "Key-Usage: {$this->usage}"; + } + + /** Somehow this is broken and not working in v2.3.4 + $subkey = $this->subkeys[0]; + if (!empty($subkey['type'])) { + $keyParams[] = "Subkey-Type: {$subkey['type']}"; + } + if (!empty($subkey['curve'])) { + $keyParams[] = "Subkey-Curve: {$subkey['curve']}"; + } + if (!empty($subkey['length'])) { + $keyParams[] = "Subkey-Length: {$subkey['length']}"; + } + if (!empty($subkey['usage'])) { + $keyParams[] = "Subkey-Usage: {$subkey['usage']}"; + } + */ + + if ($this->expires) { + $keyParams[] = "Expire-Date: " . \date('Y-m-d', $this->expires); + } + + if (\strlen($this->name)) { + $keyParams[] = "Name-Real: {$this->name}"; + } + if (\strlen($this->email)) { + $keyParams[] = "Name-Email: {$this->email}"; + } + if (\strlen($this->comment)) { + $keyParams[] = "Name-Comment: {$this->comment}"; + } + + if (\strlen($this->passphrase)) { + $keyParams[] = "Passphrase: {$this->passphrase}"; + } else { + $keyParams[] = '%no-protection'; + } + + $keyParams[] = '%commit'; + return \implode("\n", $keyParams) . "\n"; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/gpg/procpipes.php b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/procpipes.php new file mode 100644 index 0000000000..1d0c02d3d4 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/procpipes.php @@ -0,0 +1,89 @@ +pipes = $pipes; + } + + function __destruct() + { + $this->closeAll(); + } + + public function closeAll() : void + { + foreach (\array_keys($this->pipes) as $number) { + $this->close($number); + } + } + + public function get(int $number) + { + if (\array_key_exists($number, $this->pipes) && \is_resource($this->pipes[$number])) { + return $this->pipes[$number]; + } + } + + public function close(int $number) : void + { + if (\array_key_exists($number, $this->pipes)) { + \fflush($this->pipes[$number]); + \fclose($this->pipes[$number]); + unset($this->pipes[$number]); + } + } + + private $buffers = []; + public function readPipeLines(int $number) : iterable + { + $pipe = $this->get($number); + if ($pipe) { + $chunk = \fread($pipe, Base::CHUNK_SIZE); + $length = \strlen($chunk); + $eolLength = \strlen(\PHP_EOL); + if (!isset($this->buffers[$number])) { + $this->buffers[$number] = ''; + } + $this->buffers[$number] .= $chunk; + while (false !== ($pos = \strpos($this->buffers[$number], \PHP_EOL))) { + yield \substr($this->buffers[$number], 0, $pos); + $this->buffers[$number] = \substr($this->buffers[$number], $pos + $eolLength); + } + } + } + + public function writePipe(int $number, string $data, int $length = 0) : int + { + $pipe = $this->get($number); + if ($pipe) { + $chunk = \substr($data, 0, $length ?: \strlen($data)); + $length = \strlen($chunk); + $length = \fwrite($pipe, $chunk, $length); + if (!$length) { + // If we wrote 0 bytes it was either EAGAIN or EPIPE. Since + // the pipe was seleted for writing, we assume it was EPIPE. + // There's no way to get the actual error code in PHP. See + // PHP Bug #39598. https://bugs.php.net/bug.php?id=39598 + $this->close($number); + } + return $length ?: 0; + } + return 0; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/gpg/smime.php b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/smime.php new file mode 100644 index 0000000000..bc66c7291f --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/smime.php @@ -0,0 +1,1065 @@ + '', + 'keyring' => '', + 'digest-algo' => '', + 'cipher-algo' => '' + ]; + + function __construct(string $homedir) + { + parent::__construct($homedir); + + // the random seed file makes subsequent actions faster so only disable it if we have to. + if ($this->options['homedir'] && !\is_writable($this->options['homedir'])) { + $this->options['no-random-seed-file'] = true; + } + + // How to use gpgme-json ? + $this->binary = static::findBinary('gpgsm'); + + $info = \preg_replace('/\R +/', ' ', `$this->binary --version`); + if (\preg_match('/gpgsm.+([0-9]+\\.[0-9]+\\.[0-9]+)/', $info, $match)) { + $this->version = $match[1]; + } + if (\preg_match('/Cipher:(.+)/', $info, $match)) { + $this->ciphers = \array_map('trim', \explode(',', $match[1])); + } + if (\preg_match('/Pubkey:(.+)/', $info, $match)) { + $this->pubkey_types = \array_map('trim', \explode(',', $match[1])); + } + if (\preg_match('/Hash:(.+)/', $info, $match)) { + $this->hashes = \array_map('trim', \explode(',', $match[1])); + } + } + + public static function isSupported() : bool + { + return parent::isSupported() && static::findBinary('gpgsm'); + } + + /** + * TODO: parse result + * https://github.com/the-djmaze/snappymail/issues/89 + */ + protected function listDecryptKeys(/*string|resource*/ $input, /*string|resource*/ $output = null) + { + $this->setInput($input); + $fclose = $this->setOutput($output); + $_ENV['PINENTRY_USER_DATA'] = ''; + $result = $this->exec(['--list-secret-keys']); + $fclose && \fclose($fclose); + return $output ? true : ($result ? $result['output'] : false); + } + + protected function _decrypt(/*string|resource*/ $input, /*string|resource*/ $output = null) + { + $this->setInput($input); + + $fclose = $this->setOutput($output); + + if ($this->pinentries) { + $_ENV['PINENTRY_USER_DATA'] = \json_encode($this->pinentries); + } + + $result = $this->exec(['--decrypt','--skip-verify']); + + $fclose && \fclose($fclose); + + return $output ? true : ($result ? $result['output'] : false); + } + + /** + * Decrypts a given text + */ + public function decrypt(string $text) /*: string|false */ + { + return $this->_decrypt($text); + } + + /** + * Decrypts a given file + */ + public function decryptFile(string $filename) /*: string|false */ + { + $fp = \fopen($filename, 'rb'); + try { + if (!$fp) { + throw new \Exception("Could not open file '{$filename}'"); + } + return $this->_decrypt($fp, $output); + } finally { + $fp && \fclose($fp); + } + } + + /** + * Decrypts a given stream + */ + public function decryptStream($fp, /*string|resource*/ $output = null) /*: string|false*/ + { + if (!$fp || !\is_resource($fp)) { + throw new \Exception('Invalid stream resource'); + } + return $this->_decrypt($fp, $output); + } + + /** + * Decrypts and verifies a given text + */ + public function decryptVerify(string $text, string &$plaintext) /*: array|false*/ + { + // TODO: https://github.com/the-djmaze/snappymail/issues/89 + return false; + } + + /** + * Decrypts and verifies a given file + */ + public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/ + { + // TODO: https://github.com/the-djmaze/snappymail/issues/89 + return false; + } + + protected function _encrypt(/*string|resource*/ $input, /*string|resource*/ $output = null) + { + if (!$this->encryptKeys) { + throw new \Exception('No encryption keys specified.'); + } + + $this->setInput($input); + + $fclose = $this->setOutput($output); + + $arguments = [ + '--encrypt' + ]; + if ($this->armor) { + $arguments[] = '--armor'; + } + + foreach ($this->encryptKeys as $fingerprint => $dummy) { + $arguments[] = '--recipient ' . \escapeshellarg($fingerprint); + } + + $result = $this->exec($arguments); + + $fclose && \fclose($fclose); + + return $output ? true : $result['output']; + } + + /** + * Encrypts a given text + */ + public function encrypt(string $plaintext, /*string|resource*/ $output = null) /*: string|false*/ + { + return $this->_encrypt($plaintext, $output); + } + + /** + * Encrypts a given text + */ + public function encryptFile(string $filename, /*string|resource*/ $output = null) /*: string|false*/ + { + $fp = \fopen($filename, 'rb'); + try { + if (!$fp) { + throw new \Exception("Could not open file '{$filename}'"); + } + return $this->_encrypt($filename, $output); + } finally { + $fp && \fclose($fp); + } + } + + public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/ + { + if (!$fp || !\is_resource($fp)) { + throw new \Exception('Invalid stream resource'); + } + return $this->_encrypt($fp, $output); + } + + /** + * Encrypts and signs a given text + */ + public function encryptSign(string $plaintext) /*: string|false*/ + { + return false; + } + + /** + * Encrypts and signs a given text + */ + public function encryptSignFile(string $filename) /*: string|false*/ + { + return false; + } + + /** + * Exports a public or private key + */ + public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/ + { + $private = null !== $passphrase; + $keys = $this->keyInfo($fingerprint, $private); + if (!$keys) { + throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $fingerprint); + } + if ($private) { + $_ENV['PINENTRY_USER_DATA'] = \json_encode([$fingerprint => \strval($passphrase)]); + } + $result = $this->exec([ + $private ? '--export-secret-key-p12' : '--export', + '--armor', + \escapeshellarg($keys[0]['subkeys'][0]['fingerprint']), + ]); + return $result['output']; + } + + protected function _importKey($input) /*: array|false*/ + { + $arguments = ['--import']; + + if ($this->pinentries) { + $_ENV['PINENTRY_USER_DATA'] = \json_encode($this->pinentries); + } else { + $arguments[] = '--batch'; + } + + $this->setInput($input); + $result = $this->exec($arguments); + + foreach ($result['status'] as $line) { + if (false !== \strpos($line, 'IMPORT_RES')) { + $line = \explode(' ', \explode('IMPORT_RES ', $line)[1]); + return [ + 'imported' => (int) $line[2], + 'unchanged' => (int) $line[4], + 'newuserids' => (int) $line[5], + 'newsubkeys' => (int) $line[6], + 'secretimported' => (int) $line[10], + 'secretunchanged' => (int) $line[11], + 'newsignatures' => (int) $line[7], + 'skippedkeys' => (int) $line[12], + 'fingerprint' => '' + ]; + } + } + + if (!empty($result['errors'][0])) { + \SnappyMail\Log::warning('GPG', $result['errors'][0]); + } + + return false; + } + + /** + * Imports a key + */ + public function import(string $keydata) /*: array|false*/ + { + if (!$keydata) { + throw new \Exception('No valid input data found.'); + } + return $this->_importKey($keydata); + } + + /** + * Imports a key + */ + public function importFile(string $filename) /*: array|false*/ + { + $fp = \fopen($filename, 'rb'); + try { + if (!$fp) { + throw new \Exception("Could not open file '{$filename}'"); + } + return $this->_importKey($fp); + } finally { + $fp && \fclose($fp); + } + } + + public function deleteKey(string $keyId, bool $private) + { + $key = $this->keyInfo($keyId, $private); + if (!$key) { + throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId); + } + if ($private) { + throw new \Exception('Delete private key not possible: ' . $keyId); + } + + $result = $this->exec([ + '--batch', + '--yes', + '--delete-key', + \escapeshellarg($key[0]['subkeys'][0]['fingerprint']) + ]); + +// $result['status'][0] = '[GNUPG:] ERROR keylist.getkey 17' +// $result['errors'][0] = 'gpg: error reading key: No public key' + +// print_r($result); + return true; + } + + /** + * Returns an array with information about all keys that matches the given pattern + */ + public function keyInfo(string $pattern, bool $private = false) : array + { + // According to The file 'doc/DETAILS' in the GnuPG distribution, using + // double '--with-fingerprint' also prints the fingerprint for subkeys. + $arguments = [ + '--with-colons', + '--with-fingerprint', + '--with-fingerprint', + $private ? '--list-secret-keys' : '--list-keys' + ]; + if ($pattern) { + $arguments[] = \escapeshellarg($pattern); + } + + $result = $this->exec($arguments); + + $keys = []; + $key = null; // current key + $subKey = null; // current sub-key + + foreach (\explode(PHP_EOL, $result['output']) as $line) { + $tokens = \explode(':', $line); + + switch ($tokens[0]) + { + case 'tru': + break; + + case 'sec': + case 'pub': + // new primary key means last key should be added to the array + if ($key !== null) { + $keys[] = $key; + } + $key = [ + 'disabled' => false, + 'expired' => false, + 'revoked' => false, + 'is_secret' => 'ssb' === $tokens[0], + 'can_sign' => \str_contains($tokens[11], 's'), + 'can_encrypt' => \str_contains($tokens[11], 'e'), + 'uids' => [], + 'subkeys' => [] + ]; + // Fall through to add subkey + case 'ssb': // secure subkey + case 'sub': // public subkey + $key['subkeys'][] = [ + 'fingerprint' => '', // fpr:::::::::....: + 'keyid' => $tokens[4], + 'timestamp' => $tokens[5], + 'expires' => $tokens[6], + 'is_secret' => 'ssb' === $tokens[0], + 'invalid' => false, + // escaESCA + 'can_encrypt' => \str_contains($tokens[11], 'e'), + 'can_sign' => \str_contains($tokens[11], 's'), + 'can_certify' => \str_contains($tokens[11], 'c'), + 'can_authenticate' => \str_contains($tokens[11], 'a'), + 'disabled' => false, + 'expired' => false, + 'revoked' => 'r' === $tokens[1], + 'length' => $tokens[2], + 'algorithm' => $tokens[3], + ]; + break; + + case 'fpr': + $key['subkeys'][\array_key_last($key['subkeys'])]['fingerprint'] = $tokens[9]; + break; + + case 'grp': + $key['subkeys'][\array_key_last($key['subkeys'])]['keygrip'] = $tokens[9]; + break; + + case 'uid': + $string = \stripcslashes($tokens[9]); // as per documentation + $name = ''; + $email = ''; + $comment = ''; + $matches = []; + + // get email address from end of string if it exists + if (\preg_match('/^(.*?)<([^>]+)>$/', $string, $matches)) { + $string = \trim($matches[1]); + $email = $matches[2]; + } + + // get comment from end of string if it exists + $matches = []; + if (\preg_match('/^(.+?) \(([^\)]+)\)$/', $string, $matches)) { + $string = $matches[1]; + $comment = $matches[2]; + } + + // there can be an email without a name + if (!$email && \preg_match('/^[\S]+@[\S]+$/', $string, $matches)) { + $email = $string; + } else { + $name = $string; + } + + $key['uids'][] = [ + 'name' => $name, + 'comment' => $comment, + 'email' => $email, + 'uid' => $tokens[9], + 'revoked' => 'r' === $tokens[1], + 'invalid' => false, + ]; + break; + } + } + + // add last key + if ($key) { + $keys[] = $key; + } + + return $keys; + } + + protected function _sign(/*string|resource*/ $input, /*string|resource*/ $output = null) /*: string|false*/ + { + if (empty($this->pinentries)) { + throw new \Exception('No signing keys specified.'); + } + + $this->setInput($input); + + $fclose = $this->setOutput($output); + + $arguments = [ + '--sign' + ]; + + if ($this->armor) { + $arguments[] = '--armor'; + } + + if ($this->pinentries) { + foreach ($this->pinentries as $fingerprint => $pass) { + $arguments[] = '--local-user ' . \escapeshellarg($fingerprint); + } + $_ENV['PINENTRY_USER_DATA'] = \json_encode($this->pinentries); + } + + $result = $this->exec($arguments); + + $fclose && \fclose($fclose); + + return $output ? true : $result['output']; + } + + /** + * Signs a given text + */ + public function sign(string $plaintext, /*string|resource*/ $output = null) /*: string|false*/ + { + return $this->_sign($plaintext, $output); + } + + /** + * Signs a given file + */ + public function signFile(string $filename, /*string|resource*/ $output = null) /*: string|false*/ + { + $fp = \fopen($filename, 'rb'); + try { + if (!$fp) { + throw new \Exception("Could not open file '{$filename}'"); + } + return $this->_sign($fp, $output); + } finally { + $fp && \fclose($fp); + } + } + + /** + * Signs a given file + */ + public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/ + { + if (!$fp || !\is_resource($fp)) { + throw new \Exception('Invalid stream resource'); + } + return $this->_sign($fp, $output); + } + + protected function _verify($input, string $signature) /*: array|false*/ + { + $arguments = ['--verify']; + if ($signature) { + // detached signature + $this->setInput($signature); + $this->_message =& $input; + // Signed data goes in FD_MESSAGE, detached signature data goes in FD_INPUT. + $arguments[] = '--enable-special-filenames'; + $arguments[] = '- "-&' . self::FD_MESSAGE . '"'; + } else { + // signed or clearsigned data + $this->setInput($input); + } + + $result = $this->exec($arguments); + + $signatures = []; + foreach ($result['status'] as $line) { + $tokens = \explode(' ', $line); + switch ($tokens[0]) + { + case 'VERIFICATION_COMPLIANCE_MODE': + case 'TRUST_FULLY': + break; + + case 'EXPSIG': + case 'EXPKEYSIG': + case 'REVKEYSIG': + case 'BADSIG': + case 'ERRSIG': + case 'GOODSIG': + $signatures[] = [ + 'fingerprint' => '', + 'validity' => 0, + 'timestamp' => 0, + 'status' => 'GOODSIG' === $tokens[0] ? 0 : 1, + 'summary' => 'GOODSIG' === $tokens[0] ? 0 : 4, + 'keyid' => $tokens[1], + 'uid' => \rawurldecode(\implode(' ', \array_splice($tokens, 2))), + 'valid' => false + ]; + break; + + case 'VALIDSIG': + $last = \array_key_last($signatures); + $signatures[$last]['fingerprint'] = $tokens[1]; + $signatures[$last]['timestamp'] = (int) $tokens[3]; + $signatures[$last]['expires'] = (int) $tokens[4]; + $signatures[$last]['version'] = (int) $tokens[5]; +// $signatures[$last]['reserved'] = (int) $tokens[6]; +// $signatures[$last]['pubkey-algo'] = (int) $tokens[7]; +// $signatures[$last]['hash-algo'] = (int) $tokens[8]; +// $signatures[$last]['sig-class'] = $tokens[9]; +// $signatures[$last]['primary-fingerprint'] = $tokens[10]; + $signatures[$last]['valid'] = 0; + } + } + + return $signatures; + } + + /** + * Verifies a signed text + */ + public function verify(string $signed_text, string $signature, ?string &$plaintext = null) /*: array|false*/ + { + return $this->_verify($signed_text, $signature); + } + + /** + * Verifies a signed file + */ + public function verifyFile(string $filename, string $signature, ?string &$plaintext = null) /*: array|false*/ + { + $fp = \fopen($filename, 'rb'); + try { + if (!$fp) { + throw new \Exception("Could not open file '{$filename}'"); + } + return $this->_verify($fp, $signature); + } finally { + $fp && \fclose($fp); + } + } + + /** + * Verifies a signed file + */ + public function verifyStream($fp, string $signature, ?string &$plaintext = null) /*: array|false*/ + { + if (!$fp || !\is_resource($fp)) { + throw new \Exception('Invalid stream resource'); + } + return $this->_verify($fp, $signature); + } + + public function getEncryptedMessageKeys(/*string|resource*/ $data) : array + { + $this->_debug('BEGIN DETECT MESSAGE KEY IDs'); + $this->setInput($data); +// $_ENV['PINENTRY_USER_DATA'] = null; + $result = $this->exec(['--decrypt','--skip-verify']); + $info = [ + 'ENC_TO' => [], +// 'KEY_CONSIDERED' => [], +// 'NO_SECKEY' => [], +// 'errors' => $result['errors'] + ]; + foreach ($result['status'] as $line) { + $tokens = \explode(' ', $line); + if (isset($info[$tokens[0]])) { + $info[$tokens[0]][] = $tokens[1]; + } + } + $this->_debug('END DETECT MESSAGE KEY IDs'); + return $info['ENC_TO']; + } + + private function exec(array $arguments, bool $throw = true) /*: array|false*/ + { + if (\version_compare($this->version, '2.2.5', '<')) { + \SnappyMail\Log::error('GPG', "{$this->version} too old"); + return false; + } + + $defaultArguments = [ + '--status-fd ' . self::FD_STATUS, + '--passphrase-fd ' . self::FD_COMMAND, +// '--no-greeting', + '--no-secmem-warning', + '--no-tty', + '--no-default-keyring', // ignored if keying files are not specified + '--no-options', // prevent creation of ~/.gnupg directory + // If no passphrases are set, cancel them + '--pinentry-mode ' . (empty($_ENV['PINENTRY_USER_DATA']) ? 'cancel' : 'loopback') // 2.1.13+ + ]; + + if (!$this->strict) { + $defaultArguments[] = '--ignore-time-conflict'; + } + + foreach ($this->options as $option => $value) { + if (\is_string($value)) { + if (\strlen($value)) { + $defaultArguments[] = "--{$option} " . \escapeshellarg($value); + } + } else if (true === $value) { + $defaultArguments[] = "--{$option}"; + } + } + + $commandLine = $this->binary . ' ' . \implode(' ', \array_merge($defaultArguments, $arguments)); + + $descriptorSpec = [ + self::FD_INPUT => array('pipe', 'rb'), // stdin + self::FD_OUTPUT => array('pipe', 'wb'), // stdout + self::FD_ERROR => array('pipe', 'wb'), // stderr + self::FD_STATUS => array('pipe', 'wb'), // status + self::FD_COMMAND => array('pipe', 'rb'), // command + self::FD_MESSAGE => array('pipe', 'rb') // message + ]; + + $this->_debug('OPENING SUBPROCESS WITH THE FOLLOWING COMMAND:'); + $this->_debug($commandLine); + + // Don't localize GnuPG results. + $env = $_ENV; + $env['LC_ALL'] = 'C'; + + $proc_pipes = []; + + $this->proc_resource = \proc_open( + $commandLine, + $descriptorSpec, + $proc_pipes, + null, + $env, + ['binary_pipes' => true] + ); + + if (!\is_resource($this->proc_resource)) { + throw new \Exception('Unable to open process.'); + } + + $this->_openPipes = new ProcPipes($proc_pipes); + + $this->_debug('BEGIN PROCESSING'); + + $commandBuffer = ''; // buffers input to GPG + $messageBuffer = ''; // buffers input to GPG + $inputBuffer = ''; // buffers input to GPG + $outputBuffer = ''; // buffers output from GPG + $inputComplete = false; // input stream is completely buffered + $messageComplete = false; // message stream is completely buffered + + if (\is_string($this->_input)) { + $inputBuffer = $this->_input; + $inputComplete = true; + } + + if (\is_string($this->_message)) { + $messageBuffer = $this->_message; + $messageComplete = true; + } + + $status = []; + $errors = []; + + // convenience variables + $fdInput = $proc_pipes[self::FD_INPUT]; + $fdOutput = $proc_pipes[self::FD_OUTPUT]; + $fdError = $proc_pipes[self::FD_ERROR]; + $fdStatus = $proc_pipes[self::FD_STATUS]; + $fdCommand = $proc_pipes[self::FD_COMMAND]; + $fdMessage = $proc_pipes[self::FD_MESSAGE]; + + // select loop delay in milliseconds + $delay = 0; + $inputPosition = 0; + + $start = \microtime(1); + + while (true) { + // Timeout after 5 seconds + if (5 < \microtime(1) - $start) { + $errors[] = 'timeout'; + return [ + 'output' => '', + 'status' => $status, + 'errors' => $errors + ]; + exit; + } + + $inputStreams = []; + $outputStreams = []; + $exceptionStreams = []; + + // set up input streams + if (!$inputComplete && \is_resource($this->_input)) { + if (\feof($this->_input)) { + $inputComplete = true; + } else { + $inputStreams[] = $this->_input; + } + } + + // close GPG input pipe if there is no more data + if ('' == $inputBuffer && $inputComplete) { + $this->_debug('=> closing input pipe'); + $this->_openPipes->close(self::FD_INPUT); + } + + if (\is_resource($this->_message) && !$messageComplete) { + if (\feof($this->_message)) { + $messageComplete = true; + } else { + $inputStreams[] = $this->_message; + } + } + + if (!\feof($fdOutput)) { + $inputStreams[] = $fdOutput; + } + + if (!\feof($fdStatus)) { + $inputStreams[] = $fdStatus; + } + + if (!\feof($fdError)) { + $inputStreams[] = $fdError; + } + + // set up output streams + if ('' != $outputBuffer && $this->_output) { + $outputStreams[] = $this->_output; + } + + if ($commandBuffer != '' && \is_resource($fdCommand)) { + $outputStreams[] = $fdCommand; + } + + if ('' != $messageBuffer) { + if (\is_resource($fdMessage)) { + $outputStreams[] = $fdMessage; + } + } else if ($messageComplete) { + // close GPG message pipe if there is no more data + $this->_debug('=> closing message pipe'); + $this->_openPipes->close(self::FD_MESSAGE); + } + + if ($inputBuffer != '' && \is_resource($fdInput)) { + $outputStreams[] = $fdInput; + } + + // no streams left to read or write, we're all done + if (!\count($inputStreams) && !\count($outputStreams)) { + break; + } + + $this->_debug('selecting streams'); + + $ready = \stream_select( + $inputStreams, + $outputStreams, + $exceptionStreams, + 5 + ); + + $this->_debug('=> got ' . $ready); + + if ($ready === false) { + throw new \Exception( + 'Error selecting stream for communication with GPG ' . + 'subprocess. Please file a bug report at: ' . + 'http://pear.php.net/bugs/report.php?package=Crypt_GPG' + ); + } + + if ($ready === 0) { + throw new \Exception( + 'stream_select() returned 0. This can not happen! Please ' . + 'file a bug report at: ' . + 'http://pear.php.net/bugs/report.php?package=Crypt_GPG' + ); + } + + // write input (to GPG) + if (\in_array($fdInput, $outputStreams, true)) { + $this->_debug('ready for input'); + $chunk = \substr($inputBuffer, $inputPosition, self::CHUNK_SIZE); + $length = \strlen($chunk); + $this->_debug('=> about to write ' . $length . ' bytes to input'); + $length = $this->_openPipes->writePipe(self::FD_INPUT, $chunk, $length); + if ($length) { + $this->_debug('=> wrote ' . $length . ' bytes'); + // Move the position pointer, don't modify $inputBuffer (#21081) + if (\is_string($this->_input)) { + $inputPosition += $length; + } else { + $inputPosition = 0; + $inputBuffer = \substr($inputBuffer, $length); + } + } else { + $this->_debug('=> pipe broken and closed'); + } + } + + // read input (from PHP stream) + // If the buffer is too big wait until it's smaller, we don't want + // to use too much memory + if (\in_array($this->_input, $inputStreams, true) && \strlen($inputBuffer) < self::CHUNK_SIZE) { + $this->_debug('input stream is ready for reading'); + $this->_debug('=> about to read ' . self::CHUNK_SIZE . ' bytes from input stream'); + $chunk = \fread($this->_input, self::CHUNK_SIZE); + $length = \strlen($chunk); + $inputBuffer .= $chunk; + $this->_debug('=> read ' . $length . ' bytes'); + } + + // write message (to GPG) + if (\in_array($fdMessage, $outputStreams, true)) { + $this->_debug('ready for message data'); + $this->_debug('=> about to write ' . \min(self::CHUNK_SIZE, \strlen($messageBuffer)) . ' bytes to message'); + $length = $this->_openPipes->writePipe(self::FD_MESSAGE, $messageBuffer, self::CHUNK_SIZE); + if ($length) { + $this->_debug('=> wrote ' . $length . ' bytes'); + $messageBuffer = \substr($messageBuffer, $length); + } else { + $this->_debug('=> pipe broken and closed'); + } + } + + // read message (from PHP stream) + if (\in_array($this->_message, $inputStreams, true)) { + $this->_debug('message stream is ready for reading'); + $this->_debug('=> about to read ' . self::CHUNK_SIZE . ' bytes from message stream'); + $chunk = \fread($this->_message, self::CHUNK_SIZE); + $length = \strlen($chunk); + $messageBuffer .= $chunk; + $this->_debug('=> read ' . $length . ' bytes'); + } + + // read output (from GPG) + if (\in_array($fdOutput, $inputStreams, true)) { + $this->_debug('output stream ready for reading'); + $this->_debug('=> about to read ' . self::CHUNK_SIZE . ' bytes from output'); + $chunk = \fread($fdOutput, self::CHUNK_SIZE); + $length = \strlen($chunk); + $outputBuffer .= $chunk; + $this->_debug('=> read ' . $length . ' bytes'); + } + + // write output (to PHP stream) + if (\in_array($this->_output, $outputStreams, true)) { + $this->_debug('output stream is ready for data'); + $chunk = \substr($outputBuffer, 0, self::CHUNK_SIZE); + $length = \strlen($chunk); + $this->_debug('=> about to write ' . $length . ' bytes to output stream'); + $length = \fwrite($this->_output, $chunk, $length); + if (!$length) { + $this->_debug('=> broken pipe on output stream'); + $this->_debug('=> closing pipe output stream'); + $this->_openPipes->close(self::FD_OUTPUT); + } else { + $this->_debug('=> wrote ' . $length . ' bytes'); + $outputBuffer = \substr($outputBuffer, $length); + } + } + + // read error (from GPG) + if (\in_array($fdError, $inputStreams, true)) { + $this->_debug('error stream ready for reading'); + $this->_debug('=> about to read ' . self::CHUNK_SIZE . ' bytes from error'); + foreach ($this->_openPipes->readPipeLines(self::FD_ERROR) as $line) { + $errors[] = $line; + $this->_debug("\t{$line}"); + } + } + + // read status (from GPG) + if (\in_array($fdStatus, $inputStreams, true)) { + $this->_debug('status stream ready for reading'); + $this->_debug('=> about to read ' . self::CHUNK_SIZE . ' bytes from status'); + // pass lines to status handlers + foreach ($this->_openPipes->readPipeLines(self::FD_STATUS) as $line) { + // only pass lines beginning with magic prefix + if ('[GNUPG:] ' == \substr($line, 0, 9)) { + $line = \substr($line, 9); + $status[] = $line; + $this->_debug("\t{$line}"); + + $tokens = \explode(' ', $line); + // NEED_PASSPHRASE 0123456789ABCDEF 0123456789ABCDEF 1 0 + if ('NEED_PASSPHRASE' === $tokens[0]) { + // key ?: subkey + $passphrase = $this->getPassphrase($tokens[1]) ?: $this->getPassphrase($tokens[2]); + $commandBuffer .= $passphrase . PHP_EOL; + } + } + } + } + + // write command (to GPG) + if (\in_array($fdCommand, $outputStreams, true)) { + $this->_debug('ready for command data'); + $chunk = \substr($commandBuffer, 0, self::CHUNK_SIZE); + $length = \strlen($chunk); + $this->_debug('=> about to write ' . $length . ' bytes to command'); + $length = $this->_openPipes->writePipe(self::FD_COMMAND, $chunk, $length); + if ($length) { + $this->_debug('=> wrote ' . $length); + $commandBuffer = \substr($commandBuffer, $length); + } else { + $this->_debug('=> pipe broken and closed'); + } + } + + if (\count($outputStreams) === 0 || \count($inputStreams) === 0) { + // we have an I/O imbalance, increase the select loop delay + // to smooth things out + $delay += 10; + } else { + // things are running smoothly, decrease the delay + $delay -= 8; + $delay = \max(0, $delay); + } + + if ($delay > 0) { + \usleep($delay); + } + + } // end loop while streams are open + + $this->_debug('END PROCESSING'); + + $this->proc_close(); + + $this->_message = null; + $this->_input = null; + $this->_output = null; + + return [ + 'output' => $outputBuffer, + 'status' => $status, + 'errors' => $errors + ]; + } + + protected function getPassphrase($key) + { + $passphrase = ''; + $keyIdLength = \strlen($key); + if ($keyIdLength && !empty($_ENV['PINENTRY_USER_DATA'])) { + $passphrases = \json_decode($_ENV['PINENTRY_USER_DATA'], true); + foreach ($passphrases as $keyId => $pass) { + $length = \min($keyIdLength, \strlen($keyId)); + if (\substr($keyId, -$length) === \substr($key, -$length)) { + return $pass; + } + } + } +// throw new \Exception("Passphrase not found for {$key}"); + return ''; + } + + protected function proc_close() : int + { + $exitCode = 0; + + // clear PINs from environment if they were set + $_ENV['PINENTRY_USER_DATA'] = null; + + if (\is_resource($this->proc_resource)) { + $this->_debug('CLOSING SUBPROCESS'); + + // close remaining open pipes + $this->_openPipes->closeAll(); + + $status = \proc_get_status($this->proc_resource); + $exitCode = \proc_close($this->proc_resource); + $this->proc_resource = null; + + // proc_close() can return -1 in some cases, + // get the real exit code from the process status + if ($exitCode < 0 && $status && !$status['running']) { + $exitCode = $status['exitcode']; + } + + if ($exitCode > 0) { + $this->_debug('=> subprocess returned an unexpected exit code: ' . $exitCode); + } + } + + return $exitCode; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/hibp.php b/snappymail/v/0.0.0/app/libraries/snappymail/hibp.php new file mode 100644 index 0000000000..106968502d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/hibp.php @@ -0,0 +1,45 @@ +doRequest('GET', "https://api.pwnedpasswords.com/range/{$prefix}"); + if (200 !== $response->status) { + throw new HTTP\Exception('Hibp', $response->status); + } + foreach (\preg_split('/\\R/', $response->body) as $entry) { + if ($entry) { + $entry = \explode(':', $entry); + if ($entry[0] === $suffix) { + return (int) $entry[1]; + } + } + } + return 0; + } + + public static function account(string $api_key, string $email): ?array + { + if ($api_key) { + $email = \rawurlencode(IDN::emailToAscii($email)); + $response = HTTP\Request::factory()->doRequest('GET', "https://haveibeenpwned.com/api/v3/breachedaccount/{$email}", null, [ + 'hibp-api-key' => $api_key + ]); + if (200 !== $response->status) { + throw new HTTP\Exception('Hibp', $response->status); + } + return \json_decode($response->body, true); + } + return null; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php new file mode 100755 index 0000000000..79c7a0ff51 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php @@ -0,0 +1,104 @@ + ["'self'"], + 'default-src' => ["'self'", 'data:'], + // Knockout.js requires eval() for observable binding purposes + // Safari < 15.4 does not support strict-dynamic +// 'script-src' => ["'strict-dynamic'", "'unsafe-eval'"], + 'script-src' => ["'self'", "'unsafe-eval'"], + // Knockout.js requires unsafe-inline? +// 'script-src' => ["'self'", "'unsafe-inline'", "'unsafe-eval'"], + 'img-src' => ["'self'", 'data:'], + 'media-src' => ["'self'", 'data:'], + 'style-src' => ["'self'", "'unsafe-inline'"], + 'connect-src' => ["'self'", 'data:', "keys.openpgp.org"] + ]; + + function __construct(string $default = '') + { + if ($default) { + foreach (\explode(';', $default) as $directive) { + $sources = \preg_split('/\\s+/', \trim($directive)); + $directive = \array_shift($sources); + if (!isset($this->directives[$directive])) { + $this->directives[$directive] = []; + } + $this->directives[$directive] = \array_merge($this->directives[$directive], $sources); + } + } + } + + function __toString() : string + { + // report-uri deprecated + unset($this->directives['report-uri']); + if ($this->report || $this->report_only) { + $this->directives['report-uri'] = [\RainLoop\Utils::WebPath() . '?/CspReport']; + } + $params = []; + foreach ($this->directives as $directive => $sources) { + $params[] = $directive . ' ' . \implode(' ', \array_unique($sources)); + } +// if (empty($this->directives['frame-ancestors'])) { +// $params[] = "frame-ancestors 'none';"; +// } + return \implode('; ', $params); + } + + public function add(string $directive, string $source) : void + { + if (!isset($this->directives[$directive])) { + $this->directives[$directive] = []; + } + $this->directives[$directive][] = $source; + } + + public function get(string $directive) : array + { + return isset($this->directives[$directive]) + ? $this->directives[$directive] + : []; + } + + public function setHeaders() : void + { + if ($this->report_only) { + \header('Content-Security-Policy-Report-Only: ' . $this); + } else { + \header('Content-Security-Policy: ' . $this); + } + if (empty($this->directives['frame-ancestors']) || \in_array('none', $this->directives['frame-ancestors'])) { + \header('X-Frame-Options: DENY'); + } else { +// \header('X-Frame-Options: SAMEORIGIN'); + } + } + + public static function logReport() : void + { + \http_response_code(204); + $json = \file_get_contents('php://input'); + $report = \json_decode($json, true); + // Useless to log 'moz-extension' as there's no clue which extension violates + if ($json && $report && 'moz-extension' !== $report['csp-report']['source-file']) { + \SnappyMail\Log::error('CSP', $json); + } + exit; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/exception.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/exception.php new file mode 100644 index 0000000000..ceed8b7728 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/exception.php @@ -0,0 +1,85 @@ + 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', +// 306 => 'Switch Proxy', # obsolete + 307 => 'Temporary Redirect', + + // Client Error 4xx + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', # reserved for future use + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + // https://tools.ietf.org/html/rfc7540#section-9.1.2 + 421 => 'Misdirected Request', + // https://tools.ietf.org/html/rfc4918 + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + // http://tools.ietf.org/html/rfc2817 + 426 => 'Upgrade Required', + // http://tools.ietf.org/html/rfc6585 + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + + // Server Error 5xx + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', # may have Retry-After header + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', +// 506 => 'Variant Also Negotiates', +// 507 => 'Insufficient Storage', +// 508 => 'Loop Detected', +// 509 => 'Bandwidth Limit Exceeded', +// 510 => 'Not Extended', + 511 => 'Network Authentication Required', + ); + + function __construct(string $message = "", int $code = 0, ?Response $response = null) + { + if ($response) { + if (\in_array($code, array(301, 302, 303, 307))) { + $message = \trim("{$response->getRedirectLocation()}\n{$message}"); + } else if (405 === $code && ($allow = $response->getHeader('allow'))) { + $message = \trim((\is_array($allow) ? $allow[0] : $allow) . "\n{$message}"); + } + $message = \trim("{$message}\n{$response->body}"); + } + if (isset(static::CODES[$code])) { + $message = "{$code} " . static::CODES[$code] . ($message ? ": {$message}" : ''); + } + parent::__construct($message, $code); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/request.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/request.php new file mode 100644 index 0000000000..8c1c3604a8 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/request.php @@ -0,0 +1,169 @@ + 0, + 'user' => '', + 'pass' => '' + ], + $stream = null, + $headers = array(), + $ca_bundle = null; + + protected static $scheme_ports = array( + 'http' => 80, + 'https' => 443 + ); + + public static function factory(string $type = 'curl') + { + if ('curl' === $type && \function_exists('curl_init')) { + return new Request\CURL(); + } + return new Request\Socket(); + } + + function __construct() + { + $this->user_agent = 'SnappyMail/' . APP_VERSION; + } + + public function setAuth(int $type, string $user, + #[\SensitiveParameter] + string $pass + ) : void + { + $this->auth = [ + 'type' => $type, + 'user' => $user, + 'pass' => $pass + ]; + } + + public function addHeader($header) + { + $this->headers[] = $header; + return $this; + } + + public function streamBodyTo($stream) + { + if (!\is_resource($stream)) { + throw new \Exception('Invalid body target'); + } + $this->stream = $stream; + } + + public function setCABundleFile($file) + { + $this->ca_bundle = $file; + } + + /** + * Return whether a URI can be fetched. Returns false if the URI scheme is not allowed + * or is not supported by this fetcher implementation; returns true otherwise. + * + * @return bool + */ + public function canFetchURI($uri) + { + if ('https:' === \substr($uri, 0, 6) && !$this->supportsSSL()) { + \trigger_error('HTTPS URI unsupported fetching '.$uri, E_USER_WARNING); + return false; + } + if (!self::URIHasAllowedScheme($uri)) { + \trigger_error('URI fetching not allowed for '.$uri, E_USER_WARNING); + return false; + } + return true; + } + + /** + * Does this fetcher implementation (and runtime) support fetching HTTPS URIs? + * May inspect the runtime environment. + * + * @return bool $support True if this fetcher supports HTTPS + * fetching; false if not. + */ + abstract public function supportsSSL() : bool; + + abstract protected function __doRequest(string &$method, string &$request_url, &$body, array $extra_headers) : Response; + + public function doRequest($method, $request_url, /*string|array*/$body = null, array $extra_headers = array()) : ?Response + { + $method = \strtoupper($method); + $url = $request_url; + $etime = \time() + $this->timeout; + $redirects = \max(0, $this->max_redirects); + if (\is_array($body)) { + $body = \http_build_query($body, '', '&'); + } + if ($body && 'GET' === $method) { + $url .= (\strpos($url, '?') ? '&' : '?') . $body; + $body = null; + } + do + { + if (!$this->canFetchURI($url)) { + throw new \RuntimeException("Can't fetch URL: {$url}"); + } + + if (!self::URIHasAllowedScheme($url)) { + throw new \RuntimeException("Fetching URL not allowed: {$url}"); + } + + $this->stream && \rewind($this->stream); + $result = $this->__doRequest($method, $url, $body, \array_merge($this->headers, $extra_headers)); + + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3 + // In response to a request other than GET or HEAD, the user agent MUST NOT + // automatically redirect the request unless it can be confirmed by the user + if ($redirects-- && \in_array($result->status, array(301, 302, 303, 307)) && \in_array($method, ['GET','HEAD'])) { + $url = $result->getRedirectLocation(); + } else { + $result->final_uri = $url; + $result->request_uri = $request_url; + return $result; + } + + } while ($etime-time() > 0); + + return null; + } + + /** + * Return whether a URI should be allowed. Override this method to conform to your local policy. + * By default, will attempt to fetch any http or https URI. + */ + public static function URIHasAllowedScheme($uri) : bool + { + return (bool) \preg_match('#^https?://#i', $uri); + } + + public static function getSchemePort($scheme) : int + { + return self::$scheme_ports[$scheme] ?? 0; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/request/curl.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/request/curl.php new file mode 100644 index 0000000000..f25fef1c01 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/request/curl.php @@ -0,0 +1,128 @@ + $this->user_agent, + CURLOPT_CONNECTTIMEOUT => $this->timeout, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_URL => $request_url, + CURLOPT_HEADERFUNCTION => array($this, 'fetchHeader'), + CURLOPT_WRITEFUNCTION => array($this, \is_resource($this->stream) ? 'streamData' : 'fetchData'), + CURLOPT_SSL_VERIFYPEER => ($this->verify_peer || $this->ca_bundle), +// CURLOPT_SSL_VERIFYHOST => $this->verify_peer ? 2 : 0, +// CURLOPT_FOLLOWLOCATION => false, // follow redirects +// CURLOPT_MAXREDIRS => 0, // stop after 0 redirects + )); +// \curl_setopt($c, CURLOPT_ENCODING , 'gzip'); + if (\defined('CURLOPT_NOSIGNAL')) { + \curl_setopt($c, CURLOPT_NOSIGNAL, true); + } + if ($this->ca_bundle) { + \curl_setopt($c, CURLOPT_CAINFO, $this->ca_bundle); + } + if ($extra_headers) { + \curl_setopt($c, CURLOPT_HTTPHEADER, $extra_headers); + } + if ($this->auth['user'] && $this->auth['type']) { + if ($this->auth['type'] & self::AUTH_BEARER ) { + \curl_setopt($c, CURLOPT_HTTPAUTH, CURLAUTH_BEARER); + \curl_setopt($c, CURLOPT_XOAUTH2_BEARER, $this->auth['pass']); + } else { + $auth = 0; + if ($this->auth['type'] & self::AUTH_BASIC) { + $auth |= CURLAUTH_BASIC; + } + if ($this->auth['type'] & self::AUTH_DIGEST) { + $auth |= CURLAUTH_DIGEST; + } + \curl_setopt($c, CURLOPT_HTTPAUTH, $auth); + \curl_setopt($c, CURLOPT_USERPWD, $this->auth['user'] . ':' . $this->auth['pass']); + } + } + if ($this->proxy) { + \curl_setopt($c, CURLOPT_PROXY, $this->proxy); + if ($this->proxy_auth) { + \curl_setopt($c, CURLOPT_PROXYUSERPWD, $this->proxy_auth); + } + } + if ('HEAD' === $method) { + \curl_setopt($c, CURLOPT_NOBODY, true); + } else if ('GET' !== $method) { + if ('POST' === $method) { + \curl_setopt($c, CURLOPT_POST, true); + } else { + \curl_setopt($c, CURLOPT_CUSTOMREQUEST, $method); + } + if (!\is_null($body)) { + \curl_setopt($c, CURLOPT_POSTFIELDS, $body); + } + } + + \curl_exec($c); + + try { + $code = \curl_getinfo($c, CURLINFO_RESPONSE_CODE); + if (!$code) { + throw new \RuntimeException("Error " . \curl_errno($c) . ": " . \curl_error($c) . " for {$request_url}"); + } + return new Response($request_url, $code, $this->response_headers, $this->response_body); + } finally { + \curl_close($c); + $this->response_headers = array(); + $this->response_body = ''; + } + } + + protected function fetchHeader($ch, $header) + { + static $headers = []; + if (!\strlen(\rtrim($header))) { + $this->response_headers = $headers; + $headers = []; + } else { + $headers[] = \rtrim($header); + } + return \strlen($header); + } + + protected function fetchData($ch, $data) + { + if ($this->max_response_kb) { + $data = \substr($data, 0, \min(\strlen($data), ($this->max_response_kb*1024) - \strlen($this->response_body))); + } + $this->response_body .= $data; + return \strlen($data); + } + + protected function streamData($ch, $data) + { + return \fwrite($this->stream, $data); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/request/socket.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/request/socket.php new file mode 100644 index 0000000000..8b6ffc9755 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/request/socket.php @@ -0,0 +1,191 @@ +user_agent}", + 'Connection: Close', + ); + if (isset($extra_headers['Authorization'])) { + static::$Authorization[$host] = $extra_headers['Authorization']; + } else if (isset(static::$Authorization[$host])) { + $extra_headers['Authorization'] = static::$Authorization[$host]; + } + if ($extra_headers) { + $headers = \array_merge($headers, $extra_headers); + } + $headers = \implode("\r\n", $headers); + if (!\is_null($body)) { + if (!\stripos($headers,'Content-Type')) { + $headers .= "\r\nContent-Type: application/x-www-form-urlencoded"; + } + $headers .= "\r\nContent-Length: ".\strlen($body); + } + + $context = \stream_context_create(); + if ('https' === $parts['scheme']) { + $parts['host'] = 'ssl://'.$parts['host']; + \stream_context_set_option($context, 'ssl', 'verify_peer_name', true); + if ($this->verify_peer || $this->ca_bundle) { + \stream_context_set_option($context, 'ssl', 'verify_peer', true); + if ($this->ca_bundle) { + if (\is_dir($this->ca_bundle) || (\is_link($this->ca_bundle) && \is_dir(\readlink($this->ca_bundle)))) { + \stream_context_set_option($context, 'ssl', 'capath', $this->ca_bundle); + } else { + \stream_context_set_option($context, 'ssl', 'cafile', $this->ca_bundle); + } + } + } else { + \stream_context_set_option($context, 'ssl', 'allow_self_signed', true); + } + } else { + $parts['host'] = 'tcp://'.$parts['host']; + } + + $errno = 0; + $errstr = ''; + + $sock = \stream_socket_client("{$parts['host']}:{$parts['port']}", $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT, $context); + if (false === $sock) { + throw new \RuntimeException($errstr); + } + + \stream_set_timeout($sock, $this->timeout); + + \fwrite($sock, $headers . "\r\n\r\n"); + if (!\is_null($body)) { + \fwrite($sock, $body); + } + + # Read all headers + $chunked = false; + $response_headers = array(); + $data = \rtrim(\fgets($sock, 1024)); # read line + $code = \intval(\explode(' ', $data)[1]??0); + while (\strlen($data)) { + $response_headers[] = $data; + $chunked |= \preg_match('#Transfer-Encoding:.*chunked#i', $data); + + if (401 === $code && $this->auth['user'] && !isset($extra_headers['Authorization'])) { + // Basic authentication + if ($this->auth['type'] & self::AUTH_BASIC && \preg_match("/WWW-Authenticate:\\s+Basic\\s+realm=([^\\r\\n]*)/i", $data, $match)) { + $extra_headers['Authorization'] = "Authorization: Basic " . \base64_encode($this->auth['user'] . ':' . $this->auth['pass']); + \fclose($sock); + return $this->__doRequest($method, $request_url, $body, $extra_headers); + } + // Digest authentication + if ($this->auth['type'] & self::AUTH_DIGEST && \preg_match("/WWW-Authenticate:\\s+Digest\\s+([^\\r\\n]*)/i", $data, $match)) { + $challenge = []; + foreach (\split(',', $match[1]) as $i) { + $ii = \split('=', \trim($i), 2); + if (!empty($ii[1]) && !empty($ii[0])) { + $challenge[$ii[0]] = \preg_replace('/^"/','', \preg_replace('/"$/','', $ii[1])); + } + } + $a1 = \md5($this->auth['user'] . ':' . $challenge['realm'] . ':' . $this->auth['pass']); + $a2 = \md5($method . ':' . $request_url); + if (empty($challenge['qop'])) { + $digest = \md5($a1 . ':' . $challenge['nonce'] . ':' . $a2); + } else { + $challenge['cnonce'] = 'Req2.' . \random_int(); + if (empty($challenge['nc'])) { + $challenge['nc'] = 1; + } + $nc = \sprintf('%08x', $challenge['nc']++); + $digest = \md5($a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' . $challenge['cnonce'] . ':auth:' . $a2); + } + $extra_headers['Authorization'] = "Authorization: Digest " + . ' username="' . \addcslashes($this->auth['user'], '\\"') . '",' + . ' realm="' . $challenge['realm'] . '",' + . ' nonce="' . $challenge['nonce'] . '",' + . ' uri="' . $request_url . '",' + . ' response="' . $digest . '"' + . (empty($challenge['opaque']) ? '' : ', opaque="' . $challenge['opaque'] . '"') + . (empty($challenge['qop']) ? '' : ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"'); + + \fclose($sock); + return $this->__doRequest($method, $request_url, $body, $extra_headers); + } + if ($this->auth['type'] & self::AUTH_BEARER) { + $extra_headers['Authorization'] = "Authorization: Bearer {$this->auth['pass']}"; + \fclose($sock); + return $this->__doRequest($method, $request_url, $body, $extra_headers); + } + } + + $data = \rtrim(\fgets($sock, 1024)); # read next line + } + + # Read body + $body = ''; + if (\is_resource($this->stream)) { + while (!\feof($sock)) { + if ($chunked) { + $chunk = \hexdec(\trim(\fgets($sock, 8))); + if (!$chunk) { break; } + while ($chunk > 0) { + $tmp = \fread($sock, $chunk); + \fwrite($this->stream, $tmp); + $chunk -= \strlen($tmp); + } + "\r\n" === \fread($sock, 2); + } else { + \fwrite($this->stream, \fread($sock, 1024)); +// \stream_copy_to_stream($sock, $this->stream); + } + } + } else { + $max_bytes = $this->max_response_kb * 1024; + while (!\feof($sock) && (!$max_bytes || \strlen($body) < $max_bytes)) { + if ($chunked) { + $chunk = \hexdec(\trim(\fgets($sock, 8))); + if (!$chunk) { break; } + while ($chunk > 0) { + $tmp = \fread($sock, $chunk); + $body .= $tmp; + $chunk -= \strlen($tmp); + } + "\r\n" === \fread($sock, 2); + } else { + $body .= \fread($sock, 1024); + } + } + } + + \fclose($sock); + + return new Response($request_url, $code, $response_headers, $body); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/response.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/response.php new file mode 100644 index 0000000000..18411512fa --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/response.php @@ -0,0 +1,103 @@ +headers = array(); + foreach ($headers as $header) { + if (\strpos($header, ':')) { + list($name, $value) = \explode(':', $header, 2); + $name = \strtolower(\trim($name)); + $value = \trim($value); + if (isset($this->headers[$name])) { + if (\is_array($this->headers[$name])) { + $this->headers[$name][] = $value; + } else { + $this->headers[$name] = array($this->headers[$name], $value); + } + } else { + $this->headers[$name] = $value; + } + } else if ($name) { +// $this->headers[$name] .= \trim($header); + } + } + } + $this->request_uri = $request_uri; + $this->final_uri = $request_uri; + $this->status = (int) $status; + if (\function_exists('gzinflate') && isset($this->headers['content-encoding']) + && (false !== \stripos($this->headers['content-encoding'], 'gzip'))) { + $this->body = \gzinflate(\substr($body, 10, -4)); + if (false === $this->body) { + $err = \error_get_last() ?: ['message' => 'gzinflate failed']; + throw new \RuntimeException("{$err['message']} for {$request_uri}"); + } + } else { + $this->body = $body; + } + } + + function __get($k) + { + return \property_exists($this, $k) ? $this->$k : null; + } + + /** + * returns string, array or null + */ + public function getHeader($names) + { + $names = \is_array($names) ? $names : array($names); + foreach ($names as $n) { + $n = \strtolower($n); + if (isset($this->headers[$n])) { + return $this->headers[$n]; + } + } + return null; + } + + public function getHeaders() : array + { + return $this->headers; + } + + public function getRedirectLocation() : ?string + { + if ($location = $this->getHeader('location')) { + $uri = \is_array($location) ? $location[0] : $location; + if (!\preg_match('#^([a-z][a-z0-9\\+\\.\\-]+:)?//[^/]+#i', $uri)) { + // no host + \preg_match('#^((?:[a-z][a-z0-9\\+\\.\\-]+:)?//[^/]+)(/[^\\?\\#]*)#i', $this->final_uri, $match); + if ('/' === $uri[0]) { + // absolute path + $uri = $match[1] . $uri; + } else { + // relative path + $rpos = \strrpos($match[2], '/'); + $uri = $match[1] . \substr($match[2], 0, $rpos+1) . $uri; + } + } + if ('//' === \substr($uri, 0, 2)) { + $uri = \explode(':', $this->request_uri)[0] . ':' . $uri; + } + return $uri; + } + return null; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/secfetch.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/secfetch.php new file mode 100644 index 0000000000..536317b99b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/secfetch.php @@ -0,0 +1,149 @@ + tag. + * audioworklet + * The destination is data being fetched for use by an audio worklet. This might originate from a call to audioWorklet.addModule(). + * document + * The destination is a document (HTML or XML), and the request is the result of a user-initiated top-level navigation (e.g. resulting from a user clicking a link). + * embed + * The destination is embedded content. This might originate from an HTML tag. + * empty + * The destination is the empty string. This is used for destinations that do not have their own value. For exmaple fetch(), navigator.sendBeacon(), EventSource, XMLHttpRequest, WebSocket, etc. + * font + * The destination is a font. This might originate from CSS @font-face. + * frame + * The destination is a frame. This might originate from an HTML tag. + * iframe + * The destination is an iframe. This might originate from an HTML '; - - } else if (isVideo.vimeo) { - - a = '?autoplay=' + autoplay + '&api=1'; - if (this.core.s.vimeoPlayerParams) { - a = a + '&' + $.param(this.core.s.vimeoPlayerParams); - } - - video = ''; - - } else if (isVideo.dailymotion) { - - a = '?wmode=opaque&autoplay=' + autoplay + '&api=postMessage'; - if (this.core.s.dailymotionPlayerParams) { - a = a + '&' + $.param(this.core.s.dailymotionPlayerParams); - } - - video = ''; - - } else if (isVideo.html5) { - var fL = html.substring(0, 1); - if (fL === '.' || fL === '#') { - html = $(html).html(); - } - - video = html; - - } else if (isVideo.vk) { - - a = '&autoplay=' + autoplay; - if (this.core.s.vkPlayerParams) { - a = a + '&' + $.param(this.core.s.vkPlayerParams); - } - - video = ''; - - } - - return video; - }; - - Video.prototype.destroy = function() { - this.videoLoaded = false; - }; - - $.fn.lightGallery.modules.video = Video; - -})(jQuery, window, document); diff --git a/vendors/lightgallery/dist/js/lg-video.min.js b/vendors/lightgallery/dist/js/lg-video.min.js deleted file mode 100644 index ef4c2e8919..0000000000 --- a/vendors/lightgallery/dist/js/lg-video.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! lightgallery - v1.2.21 - 2016-06-28 -* http://sachinchoolur.github.io/lightGallery/ -* Copyright (c) 2016 Sachin N; Licensed Apache 2.0 */ -!function(a,b,c,d){"use strict";var e={videoMaxWidth:"855px",youtubePlayerParams:!1,vimeoPlayerParams:!1,dailymotionPlayerParams:!1,vkPlayerParams:!1,videojs:!1,videojsOptions:{}},f=function(b){return this.core=a(b).data("lightGallery"),this.$el=a(b),this.core.s=a.extend({},e,this.core.s),this.videoLoaded=!1,this.init(),this};f.prototype.init=function(){var b=this;b.core.$el.on("hasVideo.lg.tm",function(a,c,d,e){if(b.core.$slide.eq(c).find(".lg-video").append(b.loadVideo(d,"lg-object",!0,c,e)),e)if(b.core.s.videojs)try{videojs(b.core.$slide.eq(c).find(".lg-html5").get(0),b.core.s.videojsOptions,function(){b.videoLoaded||this.play()})}catch(f){console.error("Make sure you have included videojs")}else b.core.$slide.eq(c).find(".lg-html5").get(0).play()}),b.core.$el.on("onAferAppendSlide.lg.tm",function(a,c){b.core.$slide.eq(c).find(".lg-video-cont").css("max-width",b.core.s.videoMaxWidth),b.videoLoaded=!0});var c=function(a){if(a.find(".lg-object").hasClass("lg-has-poster")&&a.find(".lg-object").is(":visible"))if(a.hasClass("lg-has-video")){var c=a.find(".lg-youtube").get(0),d=a.find(".lg-vimeo").get(0),e=a.find(".lg-dailymotion").get(0),f=a.find(".lg-html5").get(0);if(c)c.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}',"*");else if(d)try{$f(d).api("play")}catch(g){console.error("Make sure you have included froogaloop2 js")}else if(e)e.contentWindow.postMessage("play","*");else if(f)if(b.core.s.videojs)try{videojs(f).play()}catch(g){console.error("Make sure you have included videojs")}else f.play();a.addClass("lg-video-playing")}else{a.addClass("lg-video-playing lg-has-video");var h,i,j=function(c,d){if(a.find(".lg-video").append(b.loadVideo(c,"",!1,b.core.index,d)),d)if(b.core.s.videojs)try{videojs(b.core.$slide.eq(b.core.index).find(".lg-html5").get(0),b.core.s.videojsOptions,function(){this.play()})}catch(e){console.error("Make sure you have included videojs")}else b.core.$slide.eq(b.core.index).find(".lg-html5").get(0).play()};b.core.s.dynamic?(h=b.core.s.dynamicEl[b.core.index].src,i=b.core.s.dynamicEl[b.core.index].html,j(h,i)):(h=b.core.$items.eq(b.core.index).attr("href")||b.core.$items.eq(b.core.index).attr("data-src"),i=b.core.$items.eq(b.core.index).attr("data-html"),j(h,i));var k=a.find(".lg-object");a.find(".lg-video").append(k),a.find(".lg-video-object").hasClass("lg-html5")||(a.removeClass("lg-complete"),a.find(".lg-video-object").on("load.lg error.lg",function(){a.addClass("lg-complete")}))}};b.core.doCss()&&b.core.$items.length>1&&(b.core.s.enableSwipe&&b.core.isTouch||b.core.s.enableDrag&&!b.core.isTouch)?b.core.$el.on("onSlideClick.lg.tm",function(){var a=b.core.$slide.eq(b.core.index);c(a)}):b.core.$slide.on("click.lg",function(){c(a(this))}),b.core.$el.on("onBeforeSlide.lg.tm",function(c,d,e){var f=b.core.$slide.eq(d),g=f.find(".lg-youtube").get(0),h=f.find(".lg-vimeo").get(0),i=f.find(".lg-dailymotion").get(0),j=f.find(".lg-vk").get(0),k=f.find(".lg-html5").get(0);if(g)g.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}',"*");else if(h)try{$f(h).api("pause")}catch(l){console.error("Make sure you have included froogaloop2 js")}else if(i)i.contentWindow.postMessage("pause","*");else if(k)if(b.core.s.videojs)try{videojs(k).pause()}catch(l){console.error("Make sure you have included videojs")}else k.pause();j&&a(j).attr("src",a(j).attr("src").replace("&autoplay","&noplay"));var m;m=b.core.s.dynamic?b.core.s.dynamicEl[e].src:b.core.$items.eq(e).attr("href")||b.core.$items.eq(e).attr("data-src");var n=b.core.isVideo(m,e)||{};(n.youtube||n.vimeo||n.dailymotion||n.vk)&&b.core.$outer.addClass("lg-hide-download")}),b.core.$el.on("onAfterSlide.lg.tm",function(a,c){b.core.$slide.eq(c).removeClass("lg-video-playing")})},f.prototype.loadVideo=function(b,c,d,e,f){var g="",h=1,i="",j=this.core.isVideo(b,e)||{};if(d&&(h=this.videoLoaded?0:1),j.youtube)i="?wmode=opaque&autoplay="+h+"&enablejsapi=1",this.core.s.youtubePlayerParams&&(i=i+"&"+a.param(this.core.s.youtubePlayerParams)),g='';else if(j.vimeo)i="?autoplay="+h+"&api=1",this.core.s.vimeoPlayerParams&&(i=i+"&"+a.param(this.core.s.vimeoPlayerParams)),g='';else if(j.dailymotion)i="?wmode=opaque&autoplay="+h+"&api=postMessage",this.core.s.dailymotionPlayerParams&&(i=i+"&"+a.param(this.core.s.dailymotionPlayerParams)),g='';else if(j.html5){var k=f.substring(0,1);"."!==k&&"#"!==k||(f=a(f).html()),g=f}else j.vk&&(i="&autoplay="+h,this.core.s.vkPlayerParams&&(i=i+"&"+a.param(this.core.s.vkPlayerParams)),g='');return g},f.prototype.destroy=function(){this.videoLoaded=!1},a.fn.lightGallery.modules.video=f}(jQuery,window,document); \ No newline at end of file diff --git a/vendors/lightgallery/dist/js/lg-zoom.js b/vendors/lightgallery/dist/js/lg-zoom.js deleted file mode 100644 index 701b12f9d2..0000000000 --- a/vendors/lightgallery/dist/js/lg-zoom.js +++ /dev/null @@ -1,477 +0,0 @@ -/*! lightgallery - v1.2.21 - 2016-06-28 -* http://sachinchoolur.github.io/lightGallery/ -* Copyright (c) 2016 Sachin N; Licensed Apache 2.0 */ -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - scale: 1, - zoom: true, - actualSize: true, - enableZoomAfter: 300 - }; - - var Zoom = function(element) { - - this.core = $(element).data('lightGallery'); - - this.core.s = $.extend({}, defaults, this.core.s); - - if (this.core.s.zoom && this.core.doCss()) { - this.init(); - - // Store the zoomable timeout value just to clear it while closing - this.zoomabletimeout = false; - - // Set the initial value center - this.pageX = $(window).width() / 2; - this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - } - - return this; - }; - - Zoom.prototype.init = function() { - - var _this = this; - var zoomIcons = ''; - - if (_this.core.s.actualSize) { - zoomIcons += ''; - } - - this.core.$outer.find('.lg-toolbar').append(zoomIcons); - - // Add zoomable class - _this.core.$el.on('onSlideItemLoad.lg.tm.zoom', function(event, index, delay) { - - // delay will be 0 except first time - var _speed = _this.core.s.enableZoomAfter + delay; - - // set _speed value 0 if gallery opened from direct url and if it is first slide - if ($('body').hasClass('lg-from-hash') && delay) { - - // will execute only once - _speed = 0; - } else { - - // Remove lg-from-hash to enable starting animation. - $('body').removeClass('lg-from-hash'); - } - - _this.zoomabletimeout = setTimeout(function() { - _this.core.$slide.eq(index).addClass('lg-zoomable'); - }, _speed + 30); - }); - - var scale = 1; - /** - * @desc Image zoom - * Translate the wrap and scale the image to get better user experience - * - * @param {String} scaleVal - Zoom decrement/increment value - */ - var zoom = function(scaleVal) { - - var $image = _this.core.$outer.find('.lg-current .lg-image'); - var _x; - var _y; - - // Find offset manually to avoid issue after zoom - var offsetX = ($(window).width() - $image.width()) / 2; - var offsetY = (($(window).height() - $image.height()) / 2) + $(window).scrollTop(); - - _x = _this.pageX - offsetX; - _y = _this.pageY - offsetY; - - var x = (scaleVal - 1) * (_x); - var y = (scaleVal - 1) * (_y); - - $image.css('transform', 'scale3d(' + scaleVal + ', ' + scaleVal + ', 1)').attr('data-scale', scaleVal); - - $image.parent().css({ - left: -x + 'px', - top: -y + 'px' - }).attr('data-x', x).attr('data-y', y); - }; - - var callScale = function() { - if (scale > 1) { - _this.core.$outer.addClass('lg-zoomed'); - } else { - _this.resetZoom(); - } - - if (scale < 1) { - scale = 1; - } - - zoom(scale); - }; - - var actualSize = function(event, $image, index, fromIcon) { - var w = $image.width(); - var nw; - if (_this.core.s.dynamic) { - nw = _this.core.s.dynamicEl[index].width || $image[0].naturalWidth || w; - } else { - nw = _this.core.$items.eq(index).attr('data-width') || $image[0].naturalWidth || w; - } - - var _scale; - - if (_this.core.$outer.hasClass('lg-zoomed')) { - scale = 1; - } else { - if (nw > w) { - _scale = nw / w; - scale = _scale || 2; - } - } - - if (fromIcon) { - _this.pageX = $(window).width() / 2; - _this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - } else { - _this.pageX = event.pageX || event.originalEvent.targetTouches[0].pageX; - _this.pageY = event.pageY || event.originalEvent.targetTouches[0].pageY; - } - - callScale(); - setTimeout(function() { - _this.core.$outer.removeClass('lg-grabbing').addClass('lg-grab'); - }, 10); - }; - - var tapped = false; - - // event triggered after appending slide content - _this.core.$el.on('onAferAppendSlide.lg.tm.zoom', function(event, index) { - - // Get the current element - var $image = _this.core.$slide.eq(index).find('.lg-image'); - - $image.on('dblclick', function(event) { - actualSize(event, $image, index); - }); - - $image.on('touchstart', function(event) { - if (!tapped) { - tapped = setTimeout(function() { - tapped = null; - }, 300); - } else { - clearTimeout(tapped); - tapped = null; - actualSize(event, $image, index); - } - - event.preventDefault(); - }); - - }); - - // Update zoom on resize and orientationchange - $(window).on('resize.lg.zoom scroll.lg.zoom orientationchange.lg.zoom', function() { - _this.pageX = $(window).width() / 2; - _this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - zoom(scale); - }); - - $('#lg-zoom-out').on('click.lg', function() { - if (_this.core.$outer.find('.lg-current .lg-image').length) { - scale -= _this.core.s.scale; - callScale(); - } - }); - - $('#lg-zoom-in').on('click.lg', function() { - if (_this.core.$outer.find('.lg-current .lg-image').length) { - scale += _this.core.s.scale; - callScale(); - } - }); - - $('#lg-actual-size').on('click.lg', function(event) { - actualSize(event, _this.core.$slide.eq(_this.core.index).find('.lg-image'), _this.core.index, true); - }); - - // Reset zoom on slide change - _this.core.$el.on('onBeforeSlide.lg.tm', function() { - scale = 1; - _this.resetZoom(); - }); - - // Drag option after zoom - if (!_this.core.isTouch) { - _this.zoomDrag(); - } - - if (_this.core.isTouch) { - _this.zoomSwipe(); - } - - }; - - // Reset zoom effect - Zoom.prototype.resetZoom = function() { - this.core.$outer.removeClass('lg-zoomed'); - this.core.$slide.find('.lg-img-wrap').removeAttr('style data-x data-y'); - this.core.$slide.find('.lg-image').removeAttr('style data-scale'); - - // Reset pagx pagy values to center - this.pageX = $(window).width() / 2; - this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - }; - - Zoom.prototype.zoomSwipe = function() { - var _this = this; - var startCoords = {}; - var endCoords = {}; - var isMoved = false; - - // Allow x direction drag - var allowX = false; - - // Allow Y direction drag - var allowY = false; - - _this.core.$slide.on('touchstart.lg', function(e) { - - if (_this.core.$outer.hasClass('lg-zoomed')) { - var $image = _this.core.$slide.eq(_this.core.index).find('.lg-object'); - - allowY = $image.outerHeight() * $image.attr('data-scale') > _this.core.$outer.find('.lg').height(); - allowX = $image.outerWidth() * $image.attr('data-scale') > _this.core.$outer.find('.lg').width(); - if ((allowX || allowY)) { - e.preventDefault(); - startCoords = { - x: e.originalEvent.targetTouches[0].pageX, - y: e.originalEvent.targetTouches[0].pageY - }; - } - } - - }); - - _this.core.$slide.on('touchmove.lg', function(e) { - - if (_this.core.$outer.hasClass('lg-zoomed')) { - - var _$el = _this.core.$slide.eq(_this.core.index).find('.lg-img-wrap'); - var distanceX; - var distanceY; - - e.preventDefault(); - isMoved = true; - - endCoords = { - x: e.originalEvent.targetTouches[0].pageX, - y: e.originalEvent.targetTouches[0].pageY - }; - - // reset opacity and transition duration - _this.core.$outer.addClass('lg-zoom-dragging'); - - if (allowY) { - distanceY = (-Math.abs(_$el.attr('data-y'))) + (endCoords.y - startCoords.y); - } else { - distanceY = -Math.abs(_$el.attr('data-y')); - } - - if (allowX) { - distanceX = (-Math.abs(_$el.attr('data-x'))) + (endCoords.x - startCoords.x); - } else { - distanceX = -Math.abs(_$el.attr('data-x')); - } - - if ((Math.abs(endCoords.x - startCoords.x) > 15) || (Math.abs(endCoords.y - startCoords.y) > 15)) { - _$el.css({ - left: distanceX + 'px', - top: distanceY + 'px' - }); - } - - } - - }); - - _this.core.$slide.on('touchend.lg', function() { - if (_this.core.$outer.hasClass('lg-zoomed')) { - if (isMoved) { - isMoved = false; - _this.core.$outer.removeClass('lg-zoom-dragging'); - _this.touchendZoom(startCoords, endCoords, allowX, allowY); - - } - } - }); - - }; - - Zoom.prototype.zoomDrag = function() { - - var _this = this; - var startCoords = {}; - var endCoords = {}; - var isDraging = false; - var isMoved = false; - - // Allow x direction drag - var allowX = false; - - // Allow Y direction drag - var allowY = false; - - _this.core.$slide.on('mousedown.lg.zoom', function(e) { - - // execute only on .lg-object - var $image = _this.core.$slide.eq(_this.core.index).find('.lg-object'); - - allowY = $image.outerHeight() * $image.attr('data-scale') > _this.core.$outer.find('.lg').height(); - allowX = $image.outerWidth() * $image.attr('data-scale') > _this.core.$outer.find('.lg').width(); - - if (_this.core.$outer.hasClass('lg-zoomed')) { - if ($(e.target).hasClass('lg-object') && (allowX || allowY)) { - e.preventDefault(); - startCoords = { - x: e.pageX, - y: e.pageY - }; - - isDraging = true; - - // ** Fix for webkit cursor issue https://code.google.com/p/chromium/issues/detail?id=26723 - _this.core.$outer.scrollLeft += 1; - _this.core.$outer.scrollLeft -= 1; - - _this.core.$outer.removeClass('lg-grab').addClass('lg-grabbing'); - } - } - }); - - $(window).on('mousemove.lg.zoom', function(e) { - if (isDraging) { - var _$el = _this.core.$slide.eq(_this.core.index).find('.lg-img-wrap'); - var distanceX; - var distanceY; - - isMoved = true; - endCoords = { - x: e.pageX, - y: e.pageY - }; - - // reset opacity and transition duration - _this.core.$outer.addClass('lg-zoom-dragging'); - - if (allowY) { - distanceY = (-Math.abs(_$el.attr('data-y'))) + (endCoords.y - startCoords.y); - } else { - distanceY = -Math.abs(_$el.attr('data-y')); - } - - if (allowX) { - distanceX = (-Math.abs(_$el.attr('data-x'))) + (endCoords.x - startCoords.x); - } else { - distanceX = -Math.abs(_$el.attr('data-x')); - } - - _$el.css({ - left: distanceX + 'px', - top: distanceY + 'px' - }); - } - }); - - $(window).on('mouseup.lg.zoom', function(e) { - - if (isDraging) { - isDraging = false; - _this.core.$outer.removeClass('lg-zoom-dragging'); - - // Fix for chrome mouse move on click - if (isMoved && ((startCoords.x !== endCoords.x) || (startCoords.y !== endCoords.y))) { - endCoords = { - x: e.pageX, - y: e.pageY - }; - _this.touchendZoom(startCoords, endCoords, allowX, allowY); - - } - - isMoved = false; - } - - _this.core.$outer.removeClass('lg-grabbing').addClass('lg-grab'); - - }); - }; - - Zoom.prototype.touchendZoom = function(startCoords, endCoords, allowX, allowY) { - - var _this = this; - var _$el = _this.core.$slide.eq(_this.core.index).find('.lg-img-wrap'); - var $image = _this.core.$slide.eq(_this.core.index).find('.lg-object'); - var distanceX = (-Math.abs(_$el.attr('data-x'))) + (endCoords.x - startCoords.x); - var distanceY = (-Math.abs(_$el.attr('data-y'))) + (endCoords.y - startCoords.y); - var minY = (_this.core.$outer.find('.lg').height() - $image.outerHeight()) / 2; - var maxY = Math.abs(($image.outerHeight() * Math.abs($image.attr('data-scale'))) - _this.core.$outer.find('.lg').height() + minY); - var minX = (_this.core.$outer.find('.lg').width() - $image.outerWidth()) / 2; - var maxX = Math.abs(($image.outerWidth() * Math.abs($image.attr('data-scale'))) - _this.core.$outer.find('.lg').width() + minX); - - if ((Math.abs(endCoords.x - startCoords.x) > 15) || (Math.abs(endCoords.y - startCoords.y) > 15)) { - if (allowY) { - if (distanceY <= -maxY) { - distanceY = -maxY; - } else if (distanceY >= -minY) { - distanceY = -minY; - } - } - - if (allowX) { - if (distanceX <= -maxX) { - distanceX = -maxX; - } else if (distanceX >= -minX) { - distanceX = -minX; - } - } - - if (allowY) { - _$el.attr('data-y', Math.abs(distanceY)); - } else { - distanceY = -Math.abs(_$el.attr('data-y')); - } - - if (allowX) { - _$el.attr('data-x', Math.abs(distanceX)); - } else { - distanceX = -Math.abs(_$el.attr('data-x')); - } - - _$el.css({ - left: distanceX + 'px', - top: distanceY + 'px' - }); - - } - }; - - Zoom.prototype.destroy = function() { - - var _this = this; - - // Unbind all events added by lightGallery zoom plugin - _this.core.$el.off('.lg.zoom'); - $(window).off('.lg.zoom'); - _this.core.$slide.off('.lg.zoom'); - _this.core.$el.off('.lg.tm.zoom'); - _this.resetZoom(); - clearTimeout(_this.zoomabletimeout); - _this.zoomabletimeout = false; - }; - - $.fn.lightGallery.modules.zoom = Zoom; - -})(jQuery, window, document); \ No newline at end of file diff --git a/vendors/lightgallery/dist/js/lg-zoom.min.js b/vendors/lightgallery/dist/js/lg-zoom.min.js deleted file mode 100644 index 721aeb8fe4..0000000000 --- a/vendors/lightgallery/dist/js/lg-zoom.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! lightgallery - v1.2.21 - 2016-06-28 -* http://sachinchoolur.github.io/lightGallery/ -* Copyright (c) 2016 Sachin N; Licensed Apache 2.0 */ -!function(a,b,c,d){"use strict";var e={scale:1,zoom:!0,actualSize:!0,enableZoomAfter:300},f=function(c){return this.core=a(c).data("lightGallery"),this.core.s=a.extend({},e,this.core.s),this.core.s.zoom&&this.core.doCss()&&(this.init(),this.zoomabletimeout=!1,this.pageX=a(b).width()/2,this.pageY=a(b).height()/2+a(b).scrollTop()),this};f.prototype.init=function(){var c=this,d='';c.core.s.actualSize&&(d+=''),this.core.$outer.find(".lg-toolbar").append(d),c.core.$el.on("onSlideItemLoad.lg.tm.zoom",function(b,d,e){var f=c.core.s.enableZoomAfter+e;a("body").hasClass("lg-from-hash")&&e?f=0:a("body").removeClass("lg-from-hash"),c.zoomabletimeout=setTimeout(function(){c.core.$slide.eq(d).addClass("lg-zoomable")},f+30)});var e=1,f=function(d){var e,f,g=c.core.$outer.find(".lg-current .lg-image"),h=(a(b).width()-g.width())/2,i=(a(b).height()-g.height())/2+a(b).scrollTop();e=c.pageX-h,f=c.pageY-i;var j=(d-1)*e,k=(d-1)*f;g.css("transform","scale3d("+d+", "+d+", 1)").attr("data-scale",d),g.parent().css({left:-j+"px",top:-k+"px"}).attr("data-x",j).attr("data-y",k)},g=function(){e>1?c.core.$outer.addClass("lg-zoomed"):c.resetZoom(),1>e&&(e=1),f(e)},h=function(d,f,h,i){var j,k=f.width();j=c.core.s.dynamic?c.core.s.dynamicEl[h].width||f[0].naturalWidth||k:c.core.$items.eq(h).attr("data-width")||f[0].naturalWidth||k;var l;c.core.$outer.hasClass("lg-zoomed")?e=1:j>k&&(l=j/k,e=l||2),i?(c.pageX=a(b).width()/2,c.pageY=a(b).height()/2+a(b).scrollTop()):(c.pageX=d.pageX||d.originalEvent.targetTouches[0].pageX,c.pageY=d.pageY||d.originalEvent.targetTouches[0].pageY),g(),setTimeout(function(){c.core.$outer.removeClass("lg-grabbing").addClass("lg-grab")},10)},i=!1;c.core.$el.on("onAferAppendSlide.lg.tm.zoom",function(a,b){var d=c.core.$slide.eq(b).find(".lg-image");d.on("dblclick",function(a){h(a,d,b)}),d.on("touchstart",function(a){i?(clearTimeout(i),i=null,h(a,d,b)):i=setTimeout(function(){i=null},300),a.preventDefault()})}),a(b).on("resize.lg.zoom scroll.lg.zoom orientationchange.lg.zoom",function(){c.pageX=a(b).width()/2,c.pageY=a(b).height()/2+a(b).scrollTop(),f(e)}),a("#lg-zoom-out").on("click.lg",function(){c.core.$outer.find(".lg-current .lg-image").length&&(e-=c.core.s.scale,g())}),a("#lg-zoom-in").on("click.lg",function(){c.core.$outer.find(".lg-current .lg-image").length&&(e+=c.core.s.scale,g())}),a("#lg-actual-size").on("click.lg",function(a){h(a,c.core.$slide.eq(c.core.index).find(".lg-image"),c.core.index,!0)}),c.core.$el.on("onBeforeSlide.lg.tm",function(){e=1,c.resetZoom()}),c.core.isTouch||c.zoomDrag(),c.core.isTouch&&c.zoomSwipe()},f.prototype.resetZoom=function(){this.core.$outer.removeClass("lg-zoomed"),this.core.$slide.find(".lg-img-wrap").removeAttr("style data-x data-y"),this.core.$slide.find(".lg-image").removeAttr("style data-scale"),this.pageX=a(b).width()/2,this.pageY=a(b).height()/2+a(b).scrollTop()},f.prototype.zoomSwipe=function(){var a=this,b={},c={},d=!1,e=!1,f=!1;a.core.$slide.on("touchstart.lg",function(c){if(a.core.$outer.hasClass("lg-zoomed")){var d=a.core.$slide.eq(a.core.index).find(".lg-object");f=d.outerHeight()*d.attr("data-scale")>a.core.$outer.find(".lg").height(),e=d.outerWidth()*d.attr("data-scale")>a.core.$outer.find(".lg").width(),(e||f)&&(c.preventDefault(),b={x:c.originalEvent.targetTouches[0].pageX,y:c.originalEvent.targetTouches[0].pageY})}}),a.core.$slide.on("touchmove.lg",function(g){if(a.core.$outer.hasClass("lg-zoomed")){var h,i,j=a.core.$slide.eq(a.core.index).find(".lg-img-wrap");g.preventDefault(),d=!0,c={x:g.originalEvent.targetTouches[0].pageX,y:g.originalEvent.targetTouches[0].pageY},a.core.$outer.addClass("lg-zoom-dragging"),i=f?-Math.abs(j.attr("data-y"))+(c.y-b.y):-Math.abs(j.attr("data-y")),h=e?-Math.abs(j.attr("data-x"))+(c.x-b.x):-Math.abs(j.attr("data-x")),(Math.abs(c.x-b.x)>15||Math.abs(c.y-b.y)>15)&&j.css({left:h+"px",top:i+"px"})}}),a.core.$slide.on("touchend.lg",function(){a.core.$outer.hasClass("lg-zoomed")&&d&&(d=!1,a.core.$outer.removeClass("lg-zoom-dragging"),a.touchendZoom(b,c,e,f))})},f.prototype.zoomDrag=function(){var c=this,d={},e={},f=!1,g=!1,h=!1,i=!1;c.core.$slide.on("mousedown.lg.zoom",function(b){var e=c.core.$slide.eq(c.core.index).find(".lg-object");i=e.outerHeight()*e.attr("data-scale")>c.core.$outer.find(".lg").height(),h=e.outerWidth()*e.attr("data-scale")>c.core.$outer.find(".lg").width(),c.core.$outer.hasClass("lg-zoomed")&&a(b.target).hasClass("lg-object")&&(h||i)&&(b.preventDefault(),d={x:b.pageX,y:b.pageY},f=!0,c.core.$outer.scrollLeft+=1,c.core.$outer.scrollLeft-=1,c.core.$outer.removeClass("lg-grab").addClass("lg-grabbing"))}),a(b).on("mousemove.lg.zoom",function(a){if(f){var b,j,k=c.core.$slide.eq(c.core.index).find(".lg-img-wrap");g=!0,e={x:a.pageX,y:a.pageY},c.core.$outer.addClass("lg-zoom-dragging"),j=i?-Math.abs(k.attr("data-y"))+(e.y-d.y):-Math.abs(k.attr("data-y")),b=h?-Math.abs(k.attr("data-x"))+(e.x-d.x):-Math.abs(k.attr("data-x")),k.css({left:b+"px",top:j+"px"})}}),a(b).on("mouseup.lg.zoom",function(a){f&&(f=!1,c.core.$outer.removeClass("lg-zoom-dragging"),!g||d.x===e.x&&d.y===e.y||(e={x:a.pageX,y:a.pageY},c.touchendZoom(d,e,h,i)),g=!1),c.core.$outer.removeClass("lg-grabbing").addClass("lg-grab")})},f.prototype.touchendZoom=function(a,b,c,d){var e=this,f=e.core.$slide.eq(e.core.index).find(".lg-img-wrap"),g=e.core.$slide.eq(e.core.index).find(".lg-object"),h=-Math.abs(f.attr("data-x"))+(b.x-a.x),i=-Math.abs(f.attr("data-y"))+(b.y-a.y),j=(e.core.$outer.find(".lg").height()-g.outerHeight())/2,k=Math.abs(g.outerHeight()*Math.abs(g.attr("data-scale"))-e.core.$outer.find(".lg").height()+j),l=(e.core.$outer.find(".lg").width()-g.outerWidth())/2,m=Math.abs(g.outerWidth()*Math.abs(g.attr("data-scale"))-e.core.$outer.find(".lg").width()+l);(Math.abs(b.x-a.x)>15||Math.abs(b.y-a.y)>15)&&(d&&(-k>=i?i=-k:i>=-j&&(i=-j)),c&&(-m>=h?h=-m:h>=-l&&(h=-l)),d?f.attr("data-y",Math.abs(i)):i=-Math.abs(f.attr("data-y")),c?f.attr("data-x",Math.abs(h)):h=-Math.abs(f.attr("data-x")),f.css({left:h+"px",top:i+"px"}))},f.prototype.destroy=function(){var c=this;c.core.$el.off(".lg.zoom"),a(b).off(".lg.zoom"),c.core.$slide.off(".lg.zoom"),c.core.$el.off(".lg.tm.zoom"),c.resetZoom(),clearTimeout(c.zoomabletimeout),c.zoomabletimeout=!1},a.fn.lightGallery.modules.zoom=f}(jQuery,window,document); \ No newline at end of file diff --git a/vendors/lightgallery/dist/js/lightgallery-all.js b/vendors/lightgallery/dist/js/lightgallery-all.js deleted file mode 100644 index 3890574c05..0000000000 --- a/vendors/lightgallery/dist/js/lightgallery-all.js +++ /dev/null @@ -1,2970 +0,0 @@ -/*! lightgallery - v1.2.21 - 2016-06-28 -* http://sachinchoolur.github.io/lightGallery/ -* Copyright (c) 2016 Sachin N; Licensed Apache 2.0 */ -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - - mode: 'lg-slide', - - // Ex : 'ease' - cssEasing: 'ease', - - //'for jquery animation' - easing: 'linear', - speed: 600, - height: '100%', - width: '100%', - addClass: '', - startClass: 'lg-start-zoom', - backdropDuration: 150, - hideBarsDelay: 6000, - - useLeft: false, - - closable: true, - loop: true, - escKey: true, - keyPress: true, - controls: true, - slideEndAnimatoin: true, - hideControlOnEnd: false, - mousewheel: true, - - getCaptionFromTitleOrAlt: true, - - // .lg-item || '.lg-sub-html' - appendSubHtmlTo: '.lg-sub-html', - - subHtmlSelectorRelative: false, - - /** - * @desc number of preload slides - * will exicute only after the current slide is fully loaded. - * - * @ex you clicked on 4th image and if preload = 1 then 3rd slide and 5th - * slide will be loaded in the background after the 4th slide is fully loaded.. - * if preload is 2 then 2nd 3rd 5th 6th slides will be preloaded.. ... ... - * - */ - preload: 1, - showAfterLoad: true, - selector: '', - selectWithin: '', - nextHtml: '', - prevHtml: '', - - // 0, 1 - index: false, - - iframeMaxWidth: '100%', - - download: true, - counter: true, - appendCounterTo: '.lg-toolbar', - - swipeThreshold: 50, - enableSwipe: true, - enableDrag: true, - - dynamic: false, - dynamicEl: [], - galleryId: 1 - }; - - function Plugin(element, options) { - - // Current lightGallery element - this.el = element; - - // Current jquery element - this.$el = $(element); - - // lightGallery settings - this.s = $.extend({}, defaults, options); - - // When using dynamic mode, ensure dynamicEl is an array - if (this.s.dynamic && this.s.dynamicEl !== 'undefined' && this.s.dynamicEl.constructor === Array && !this.s.dynamicEl.length) { - throw ('When using dynamic mode, you must also define dynamicEl as an Array.'); - } - - // lightGallery modules - this.modules = {}; - - // false when lightgallery complete first slide; - this.lGalleryOn = false; - - this.lgBusy = false; - - // Timeout function for hiding controls; - this.hideBartimeout = false; - - // To determine browser supports for touch events; - this.isTouch = ('ontouchstart' in document.documentElement); - - // Disable hideControlOnEnd if sildeEndAnimation is true - if (this.s.slideEndAnimatoin) { - this.s.hideControlOnEnd = false; - } - - // Gallery items - if (this.s.dynamic) { - this.$items = this.s.dynamicEl; - } else { - if (this.s.selector === 'this') { - this.$items = this.$el; - } else if (this.s.selector !== '') { - if (this.s.selectWithin) { - this.$items = $(this.s.selectWithin).find(this.s.selector); - } else { - this.$items = this.$el.find($(this.s.selector)); - } - } else { - this.$items = this.$el.children(); - } - } - - // .lg-item - this.$slide = ''; - - // .lg-outer - this.$outer = ''; - - this.init(); - - return this; - } - - Plugin.prototype.init = function() { - - var _this = this; - - // s.preload should not be more than $item.length - if (_this.s.preload > _this.$items.length) { - _this.s.preload = _this.$items.length; - } - - // if dynamic option is enabled execute immediately - var _hash = window.location.hash; - if (_hash.indexOf('lg=' + this.s.galleryId) > 0) { - - _this.index = parseInt(_hash.split('&slide=')[1], 10); - - $('body').addClass('lg-from-hash'); - if (!$('body').hasClass('lg-on')) { - setTimeout(function() { - _this.build(_this.index); - $('body').addClass('lg-on'); - }); - } - } - - if (_this.s.dynamic) { - - _this.$el.trigger('onBeforeOpen.lg'); - - _this.index = _this.s.index || 0; - - // prevent accidental double execution - if (!$('body').hasClass('lg-on')) { - setTimeout(function() { - _this.build(_this.index); - $('body').addClass('lg-on'); - }); - } - } else { - - // Using different namespace for click because click event should not unbind if selector is same object('this') - _this.$items.on('click.lgcustom', function(event) { - - // For IE8 - try { - event.preventDefault(); - event.preventDefault(); - } catch (er) { - event.returnValue = false; - } - - _this.$el.trigger('onBeforeOpen.lg'); - - _this.index = _this.s.index || _this.$items.index(this); - - // prevent accidental double execution - if (!$('body').hasClass('lg-on')) { - _this.build(_this.index); - $('body').addClass('lg-on'); - } - }); - } - - }; - - Plugin.prototype.build = function(index) { - - var _this = this; - - _this.structure(); - - // module constructor - $.each($.fn.lightGallery.modules, function(key) { - _this.modules[key] = new $.fn.lightGallery.modules[key](_this.el); - }); - - // initiate slide function - _this.slide(index, false, false); - - if (_this.s.keyPress) { - _this.keyPress(); - } - - if (_this.$items.length > 1) { - - _this.arrow(); - - setTimeout(function() { - _this.enableDrag(); - _this.enableSwipe(); - }, 50); - - if (_this.s.mousewheel) { - _this.mousewheel(); - } - } - - _this.counter(); - - _this.closeGallery(); - - _this.$el.trigger('onAfterOpen.lg'); - - // Hide controllers if mouse doesn't move for some period - _this.$outer.on('mousemove.lg click.lg touchstart.lg', function() { - - _this.$outer.removeClass('lg-hide-items'); - - clearTimeout(_this.hideBartimeout); - - // Timeout will be cleared on each slide movement also - _this.hideBartimeout = setTimeout(function() { - _this.$outer.addClass('lg-hide-items'); - }, _this.s.hideBarsDelay); - - }); - - }; - - Plugin.prototype.structure = function() { - var list = ''; - var controls = ''; - var i = 0; - var subHtmlCont = ''; - var template; - var _this = this; - - $('body').append('
    '); - $('.lg-backdrop').css('transition-duration', this.s.backdropDuration + 'ms'); - - // Create gallery items - for (i = 0; i < this.$items.length; i++) { - list += '
    '; - } - - // Create controlls - if (this.s.controls && this.$items.length > 1) { - controls = '
    ' + - '
    ' + this.s.prevHtml + '
    ' + - '
    ' + this.s.nextHtml + '
    ' + - '
    '; - } - - if (this.s.appendSubHtmlTo === '.lg-sub-html') { - subHtmlCont = '
    '; - } - - template = '
    ' + - '
    ' + - '
    ' + list + '
    ' + - '
    ' + - '' + - '
    ' + - controls + - subHtmlCont + - '
    ' + - '
    '; - - $('body').append(template); - this.$outer = $('.lg-outer'); - this.$slide = this.$outer.find('.lg-item'); - - if (this.s.useLeft) { - this.$outer.addClass('lg-use-left'); - - // Set mode lg-slide if use left is true; - this.s.mode = 'lg-slide'; - } else { - this.$outer.addClass('lg-use-css3'); - } - - // For fixed height gallery - _this.setTop(); - $(window).on('resize.lg orientationchange.lg', function() { - setTimeout(function() { - _this.setTop(); - }, 100); - }); - - // add class lg-current to remove initial transition - this.$slide.eq(this.index).addClass('lg-current'); - - // add Class for css support and transition mode - if (this.doCss()) { - this.$outer.addClass('lg-css3'); - } else { - this.$outer.addClass('lg-css'); - - // Set speed 0 because no animation will happen if browser doesn't support css3 - this.s.speed = 0; - } - - this.$outer.addClass(this.s.mode); - - if (this.s.enableDrag && this.$items.length > 1) { - this.$outer.addClass('lg-grab'); - } - - if (this.s.showAfterLoad) { - this.$outer.addClass('lg-show-after-load'); - } - - if (this.doCss()) { - var $inner = this.$outer.find('.lg-inner'); - $inner.css('transition-timing-function', this.s.cssEasing); - $inner.css('transition-duration', this.s.speed + 'ms'); - } - - $('.lg-backdrop').addClass('in'); - - setTimeout(function() { - _this.$outer.addClass('lg-visible'); - }, this.s.backdropDuration); - - if (this.s.download) { - this.$outer.find('.lg-toolbar').append(''); - } - - // Store the current scroll top value to scroll back after closing the gallery.. - this.prevScrollTop = $(window).scrollTop(); - - }; - - // For fixed height gallery - Plugin.prototype.setTop = function() { - if (this.s.height !== '100%') { - var wH = $(window).height(); - var top = (wH - parseInt(this.s.height, 10)) / 2; - var $lGallery = this.$outer.find('.lg'); - if (wH >= parseInt(this.s.height, 10)) { - $lGallery.css('top', top + 'px'); - } else { - $lGallery.css('top', '0px'); - } - } - }; - - // Find css3 support - Plugin.prototype.doCss = function() { - // check for css animation support - var support = function() { - var transition = ['transition', 'MozTransition', 'WebkitTransition', 'OTransition', 'msTransition', 'KhtmlTransition']; - var root = document.documentElement; - var i = 0; - for (i = 0; i < transition.length; i++) { - if (transition[i] in root.style) { - return true; - } - } - }; - - if (support()) { - return true; - } - - return false; - }; - - /** - * @desc Check the given src is video - * @param {String} src - * @return {Object} video type - * Ex:{ youtube : ["//www.youtube.com/watch?v=c0asJgSyxcY", "c0asJgSyxcY"] } - */ - Plugin.prototype.isVideo = function(src, index) { - - var html; - if (this.s.dynamic) { - html = this.s.dynamicEl[index].html; - } else { - html = this.$items.eq(index).attr('data-html'); - } - - if (!src && html) { - return { - html5: true - }; - } - - var youtube = src.match(/\/\/(?:www\.)?youtu(?:\.be|be\.com)\/(?:watch\?v=|embed\/)?([a-z0-9\-\_\%]+)/i); - var vimeo = src.match(/\/\/(?:www\.)?vimeo.com\/([0-9a-z\-_]+)/i); - var dailymotion = src.match(/\/\/(?:www\.)?dai.ly\/([0-9a-z\-_]+)/i); - var vk = src.match(/\/\/(?:www\.)?(?:vk\.com|vkontakte\.ru)\/(?:video_ext\.php\?)(.*)/i); - - if (youtube) { - return { - youtube: youtube - }; - } else if (vimeo) { - return { - vimeo: vimeo - }; - } else if (dailymotion) { - return { - dailymotion: dailymotion - }; - } else if (vk) { - return { - vk: vk - }; - } - }; - - /** - * @desc Create image counter - * Ex: 1/10 - */ - Plugin.prototype.counter = function() { - if (this.s.counter) { - $(this.s.appendCounterTo).append('
    ' + (parseInt(this.index, 10) + 1) + ' / ' + this.$items.length + '
    '); - } - }; - - /** - * @desc add sub-html into the slide - * @param {Number} index - index of the slide - */ - Plugin.prototype.addHtml = function(index) { - var subHtml = null; - var subHtmlUrl; - var $currentEle; - if (this.s.dynamic) { - if (this.s.dynamicEl[index].subHtmlUrl) { - subHtmlUrl = this.s.dynamicEl[index].subHtmlUrl; - } else { - subHtml = this.s.dynamicEl[index].subHtml; - } - } else { - $currentEle = this.$items.eq(index); - if ($currentEle.attr('data-sub-html-url')) { - subHtmlUrl = $currentEle.attr('data-sub-html-url'); - } else { - subHtml = $currentEle.attr('data-sub-html'); - if (this.s.getCaptionFromTitleOrAlt && !subHtml) { - subHtml = $currentEle.attr('title') || $currentEle.find('img').first().attr('alt'); - } - } - } - - if (!subHtmlUrl) { - if (typeof subHtml !== 'undefined' && subHtml !== null) { - - // get first letter of subhtml - // if first letter starts with . or # get the html form the jQuery object - var fL = subHtml.substring(0, 1); - if (fL === '.' || fL === '#') { - if (this.s.subHtmlSelectorRelative && !this.s.dynamic) { - subHtml = $currentEle.find(subHtml).html(); - } else { - subHtml = $(subHtml).html(); - } - } - } else { - subHtml = ''; - } - } - - if (this.s.appendSubHtmlTo === '.lg-sub-html') { - - if (subHtmlUrl) { - this.$outer.find(this.s.appendSubHtmlTo).load(subHtmlUrl); - } else { - this.$outer.find(this.s.appendSubHtmlTo).html(subHtml); - } - - } else { - - if (subHtmlUrl) { - this.$slide.eq(index).load(subHtmlUrl); - } else { - this.$slide.eq(index).append(subHtml); - } - } - - // Add lg-empty-html class if title doesn't exist - if (typeof subHtml !== 'undefined' && subHtml !== null) { - if (subHtml === '') { - this.$outer.find(this.s.appendSubHtmlTo).addClass('lg-empty-html'); - } else { - this.$outer.find(this.s.appendSubHtmlTo).removeClass('lg-empty-html'); - } - } - - this.$el.trigger('onAfterAppendSubHtml.lg', [index]); - }; - - /** - * @desc Preload slides - * @param {Number} index - index of the slide - */ - Plugin.prototype.preload = function(index) { - var i = 1; - var j = 1; - for (i = 1; i <= this.s.preload; i++) { - if (i >= this.$items.length - index) { - break; - } - - this.loadContent(index + i, false, 0); - } - - for (j = 1; j <= this.s.preload; j++) { - if (index - j < 0) { - break; - } - - this.loadContent(index - j, false, 0); - } - }; - - /** - * @desc Load slide content into slide. - * @param {Number} index - index of the slide. - * @param {Boolean} rec - if true call loadcontent() function again. - * @param {Boolean} delay - delay for adding complete class. it is 0 except first time. - */ - Plugin.prototype.loadContent = function(index, rec, delay) { - - var _this = this; - var _hasPoster = false; - var _$img; - var _src; - var _poster; - var _srcset; - var _sizes; - var _html; - var getResponsiveSrc = function(srcItms) { - var rsWidth = []; - var rsSrc = []; - for (var i = 0; i < srcItms.length; i++) { - var __src = srcItms[i].split(' '); - - // Manage empty space - if (__src[0] === '') { - __src.splice(0, 1); - } - - rsSrc.push(__src[0]); - rsWidth.push(__src[1]); - } - - var wWidth = $(window).width(); - for (var j = 0; j < rsWidth.length; j++) { - if (parseInt(rsWidth[j], 10) > wWidth) { - _src = rsSrc[j]; - break; - } - } - }; - - if (_this.s.dynamic) { - - if (_this.s.dynamicEl[index].poster) { - _hasPoster = true; - _poster = _this.s.dynamicEl[index].poster; - } - - _html = _this.s.dynamicEl[index].html; - _src = _this.s.dynamicEl[index].src; - - if (_this.s.dynamicEl[index].responsive) { - var srcDyItms = _this.s.dynamicEl[index].responsive.split(','); - getResponsiveSrc(srcDyItms); - } - - _srcset = _this.s.dynamicEl[index].srcset; - _sizes = _this.s.dynamicEl[index].sizes; - - } else { - - if (_this.$items.eq(index).attr('data-poster')) { - _hasPoster = true; - _poster = _this.$items.eq(index).attr('data-poster'); - } - - _html = _this.$items.eq(index).attr('data-html'); - _src = _this.$items.eq(index).attr('href') || _this.$items.eq(index).attr('data-src'); - - if (_this.$items.eq(index).attr('data-responsive')) { - var srcItms = _this.$items.eq(index).attr('data-responsive').split(','); - getResponsiveSrc(srcItms); - } - - _srcset = _this.$items.eq(index).attr('data-srcset'); - _sizes = _this.$items.eq(index).attr('data-sizes'); - - } - - //if (_src || _srcset || _sizes || _poster) { - - var iframe = false; - if (_this.s.dynamic) { - if (_this.s.dynamicEl[index].iframe) { - iframe = true; - } - } else { - if (_this.$items.eq(index).attr('data-iframe') === 'true') { - iframe = true; - } - } - - var _isVideo = _this.isVideo(_src, index); - if (!_this.$slide.eq(index).hasClass('lg-loaded')) { - if (iframe) { - _this.$slide.eq(index).prepend('
    '); - } else if (_hasPoster) { - var videoClass = ''; - if (_isVideo && _isVideo.youtube) { - videoClass = 'lg-has-youtube'; - } else if (_isVideo && _isVideo.vimeo) { - videoClass = 'lg-has-vimeo'; - } else { - videoClass = 'lg-has-html5'; - } - - _this.$slide.eq(index).prepend('
    '); - - } else if (_isVideo) { - _this.$slide.eq(index).prepend('
    '); - _this.$el.trigger('hasVideo.lg', [index, _src, _html]); - } else { - _this.$slide.eq(index).prepend('
    '); - } - - _this.$el.trigger('onAferAppendSlide.lg', [index]); - - _$img = _this.$slide.eq(index).find('.lg-object'); - if (_sizes) { - _$img.attr('sizes', _sizes); - } - - if (_srcset) { - _$img.attr('srcset', _srcset); - try { - picturefill({ - elements: [_$img[0]] - }); - } catch (e) { - console.error('Make sure you have included Picturefill version 2'); - } - } - - if (this.s.appendSubHtmlTo !== '.lg-sub-html') { - _this.addHtml(index); - } - - _this.$slide.eq(index).addClass('lg-loaded'); - } - - _this.$slide.eq(index).find('.lg-object').on('load.lg error.lg', function() { - - // For first time add some delay for displaying the start animation. - var _speed = 0; - - // Do not change the delay value because it is required for zoom plugin. - // If gallery opened from direct url (hash) speed value should be 0 - if (delay && !$('body').hasClass('lg-from-hash')) { - _speed = delay; - } - - setTimeout(function() { - _this.$slide.eq(index).addClass('lg-complete'); - _this.$el.trigger('onSlideItemLoad.lg', [index, delay || 0]); - }, _speed); - - }); - - // @todo check load state for html5 videos - if (_isVideo && _isVideo.html5 && !_hasPoster) { - _this.$slide.eq(index).addClass('lg-complete'); - } - - if (rec === true) { - if (!_this.$slide.eq(index).hasClass('lg-complete')) { - _this.$slide.eq(index).find('.lg-object').on('load.lg error.lg', function() { - _this.preload(index); - }); - } else { - _this.preload(index); - } - } - - //} - }; - - /** - * @desc slide function for lightgallery - ** Slide() gets call on start - ** ** Set lg.on true once slide() function gets called. - ** Call loadContent() on slide() function inside setTimeout - ** ** On first slide we do not want any animation like slide of fade - ** ** So on first slide( if lg.on if false that is first slide) loadContent() should start loading immediately - ** ** Else loadContent() should wait for the transition to complete. - ** ** So set timeout s.speed + 50 - <=> ** loadContent() will load slide content in to the particular slide - ** ** It has recursion (rec) parameter. if rec === true loadContent() will call preload() function. - ** ** preload will execute only when the previous slide is fully loaded (images iframe) - ** ** avoid simultaneous image load - <=> ** Preload() will check for s.preload value and call loadContent() again accoring to preload value - ** loadContent() <====> Preload(); - - * @param {Number} index - index of the slide - * @param {Boolean} fromTouch - true if slide function called via touch event or mouse drag - * @param {Boolean} fromThumb - true if slide function called via thumbnail click - */ - Plugin.prototype.slide = function(index, fromTouch, fromThumb) { - - var _prevIndex = this.$outer.find('.lg-current').index(); - var _this = this; - - // Prevent if multiple call - // Required for hsh plugin - if (_this.lGalleryOn && (_prevIndex === index)) { - return; - } - - var _length = this.$slide.length; - var _time = _this.lGalleryOn ? this.s.speed : 0; - var _next = false; - var _prev = false; - - if (!_this.lgBusy) { - - if (this.s.download) { - var _src; - if (_this.s.dynamic) { - _src = _this.s.dynamicEl[index].downloadUrl !== false && (_this.s.dynamicEl[index].downloadUrl || _this.s.dynamicEl[index].src); - } else { - _src = _this.$items.eq(index).attr('data-download-url') !== 'false' && (_this.$items.eq(index).attr('data-download-url') || _this.$items.eq(index).attr('href') || _this.$items.eq(index).attr('data-src')); - - } - - if (_src) { - $('#lg-download').attr('href', _src); - _this.$outer.removeClass('lg-hide-download'); - } else { - _this.$outer.addClass('lg-hide-download'); - } - } - - this.$el.trigger('onBeforeSlide.lg', [_prevIndex, index, fromTouch, fromThumb]); - - _this.lgBusy = true; - - clearTimeout(_this.hideBartimeout); - - // Add title if this.s.appendSubHtmlTo === lg-sub-html - if (this.s.appendSubHtmlTo === '.lg-sub-html') { - - // wait for slide animation to complete - setTimeout(function() { - _this.addHtml(index); - }, _time); - } - - this.arrowDisable(index); - - if (!fromTouch) { - - // remove all transitions - _this.$outer.addClass('lg-no-trans'); - - this.$slide.removeClass('lg-prev-slide lg-next-slide'); - - if (index < _prevIndex) { - _prev = true; - if ((index === 0) && (_prevIndex === _length - 1) && !fromThumb) { - _prev = false; - _next = true; - } - } else if (index > _prevIndex) { - _next = true; - if ((index === _length - 1) && (_prevIndex === 0) && !fromThumb) { - _prev = true; - _next = false; - } - } - - if (_prev) { - - //prevslide - this.$slide.eq(index).addClass('lg-prev-slide'); - this.$slide.eq(_prevIndex).addClass('lg-next-slide'); - } else if (_next) { - - // next slide - this.$slide.eq(index).addClass('lg-next-slide'); - this.$slide.eq(_prevIndex).addClass('lg-prev-slide'); - } - - // give 50 ms for browser to add/remove class - setTimeout(function() { - _this.$slide.removeClass('lg-current'); - - //_this.$slide.eq(_prevIndex).removeClass('lg-current'); - _this.$slide.eq(index).addClass('lg-current'); - - // reset all transitions - _this.$outer.removeClass('lg-no-trans'); - }, 50); - } else { - - var touchPrev = index - 1; - var touchNext = index + 1; - - if ((index === 0) && (_prevIndex === _length - 1)) { - - // next slide - touchNext = 0; - touchPrev = _length - 1; - } else if ((index === _length - 1) && (_prevIndex === 0)) { - - // prev slide - touchNext = 0; - touchPrev = _length - 1; - } - - this.$slide.removeClass('lg-prev-slide lg-current lg-next-slide'); - _this.$slide.eq(touchPrev).addClass('lg-prev-slide'); - _this.$slide.eq(touchNext).addClass('lg-next-slide'); - _this.$slide.eq(index).addClass('lg-current'); - } - - if (_this.lGalleryOn) { - setTimeout(function() { - _this.loadContent(index, true, 0); - }, this.s.speed + 50); - - setTimeout(function() { - _this.lgBusy = false; - _this.$el.trigger('onAfterSlide.lg', [_prevIndex, index, fromTouch, fromThumb]); - }, this.s.speed); - - } else { - _this.loadContent(index, true, _this.s.backdropDuration); - - _this.lgBusy = false; - _this.$el.trigger('onAfterSlide.lg', [_prevIndex, index, fromTouch, fromThumb]); - } - - _this.lGalleryOn = true; - - if (this.s.counter) { - $('#lg-counter-current').text(index + 1); - } - - } - - }; - - /** - * @desc Go to next slide - * @param {Boolean} fromTouch - true if slide function called via touch event - */ - Plugin.prototype.goToNextSlide = function(fromTouch) { - var _this = this; - if (!_this.lgBusy) { - if ((_this.index + 1) < _this.$slide.length) { - _this.index++; - _this.$el.trigger('onBeforeNextSlide.lg', [_this.index]); - _this.slide(_this.index, fromTouch, false); - } else { - if (_this.s.loop) { - _this.index = 0; - _this.$el.trigger('onBeforeNextSlide.lg', [_this.index]); - _this.slide(_this.index, fromTouch, false); - } else if (_this.s.slideEndAnimatoin) { - _this.$outer.addClass('lg-right-end'); - setTimeout(function() { - _this.$outer.removeClass('lg-right-end'); - }, 400); - } - } - } - }; - - /** - * @desc Go to previous slide - * @param {Boolean} fromTouch - true if slide function called via touch event - */ - Plugin.prototype.goToPrevSlide = function(fromTouch) { - var _this = this; - if (!_this.lgBusy) { - if (_this.index > 0) { - _this.index--; - _this.$el.trigger('onBeforePrevSlide.lg', [_this.index, fromTouch]); - _this.slide(_this.index, fromTouch, false); - } else { - if (_this.s.loop) { - _this.index = _this.$items.length - 1; - _this.$el.trigger('onBeforePrevSlide.lg', [_this.index, fromTouch]); - _this.slide(_this.index, fromTouch, false); - } else if (_this.s.slideEndAnimatoin) { - _this.$outer.addClass('lg-left-end'); - setTimeout(function() { - _this.$outer.removeClass('lg-left-end'); - }, 400); - } - } - } - }; - - Plugin.prototype.keyPress = function() { - var _this = this; - if (this.$items.length > 1) { - $(window).on('keyup.lg', function(e) { - if (_this.$items.length > 1) { - if (e.keyCode === 37) { - e.preventDefault(); - _this.goToPrevSlide(); - } - - if (e.keyCode === 39) { - e.preventDefault(); - _this.goToNextSlide(); - } - } - }); - } - - $(window).on('keydown.lg', function(e) { - if (_this.s.escKey === true && e.keyCode === 27) { - e.preventDefault(); - if (!_this.$outer.hasClass('lg-thumb-open')) { - _this.destroy(); - } else { - _this.$outer.removeClass('lg-thumb-open'); - } - } - }); - }; - - Plugin.prototype.arrow = function() { - var _this = this; - this.$outer.find('.lg-prev').on('click.lg', function() { - _this.goToPrevSlide(); - }); - - this.$outer.find('.lg-next').on('click.lg', function() { - _this.goToNextSlide(); - }); - }; - - Plugin.prototype.arrowDisable = function(index) { - - // Disable arrows if s.hideControlOnEnd is true - if (!this.s.loop && this.s.hideControlOnEnd) { - if ((index + 1) < this.$slide.length) { - this.$outer.find('.lg-next').removeAttr('disabled').removeClass('disabled'); - } else { - this.$outer.find('.lg-next').attr('disabled', 'disabled').addClass('disabled'); - } - - if (index > 0) { - this.$outer.find('.lg-prev').removeAttr('disabled').removeClass('disabled'); - } else { - this.$outer.find('.lg-prev').attr('disabled', 'disabled').addClass('disabled'); - } - } - }; - - Plugin.prototype.setTranslate = function($el, xValue, yValue) { - // jQuery supports Automatic CSS prefixing since jQuery 1.8.0 - if (this.s.useLeft) { - $el.css('left', xValue); - } else { - $el.css({ - transform: 'translate3d(' + (xValue) + 'px, ' + yValue + 'px, 0px)' - }); - } - }; - - Plugin.prototype.touchMove = function(startCoords, endCoords) { - - var distance = endCoords - startCoords; - - if (Math.abs(distance) > 15) { - // reset opacity and transition duration - this.$outer.addClass('lg-dragging'); - - // move current slide - this.setTranslate(this.$slide.eq(this.index), distance, 0); - - // move next and prev slide with current slide - this.setTranslate($('.lg-prev-slide'), -this.$slide.eq(this.index).width() + distance, 0); - this.setTranslate($('.lg-next-slide'), this.$slide.eq(this.index).width() + distance, 0); - } - }; - - Plugin.prototype.touchEnd = function(distance) { - var _this = this; - - // keep slide animation for any mode while dragg/swipe - if (_this.s.mode !== 'lg-slide') { - _this.$outer.addClass('lg-slide'); - } - - this.$slide.not('.lg-current, .lg-prev-slide, .lg-next-slide').css('opacity', '0'); - - // set transition duration - setTimeout(function() { - _this.$outer.removeClass('lg-dragging'); - if ((distance < 0) && (Math.abs(distance) > _this.s.swipeThreshold)) { - _this.goToNextSlide(true); - } else if ((distance > 0) && (Math.abs(distance) > _this.s.swipeThreshold)) { - _this.goToPrevSlide(true); - } else if (Math.abs(distance) < 5) { - - // Trigger click if distance is less than 5 pix - _this.$el.trigger('onSlideClick.lg'); - } - - _this.$slide.removeAttr('style'); - }); - - // remove slide class once drag/swipe is completed if mode is not slide - setTimeout(function() { - if (!_this.$outer.hasClass('lg-dragging') && _this.s.mode !== 'lg-slide') { - _this.$outer.removeClass('lg-slide'); - } - }, _this.s.speed + 100); - - }; - - Plugin.prototype.enableSwipe = function() { - var _this = this; - var startCoords = 0; - var endCoords = 0; - var isMoved = false; - - if (_this.s.enableSwipe && _this.isTouch && _this.doCss()) { - - _this.$slide.on('touchstart.lg', function(e) { - if (!_this.$outer.hasClass('lg-zoomed') && !_this.lgBusy) { - e.preventDefault(); - _this.manageSwipeClass(); - startCoords = e.originalEvent.targetTouches[0].pageX; - } - }); - - _this.$slide.on('touchmove.lg', function(e) { - if (!_this.$outer.hasClass('lg-zoomed')) { - e.preventDefault(); - endCoords = e.originalEvent.targetTouches[0].pageX; - _this.touchMove(startCoords, endCoords); - isMoved = true; - } - }); - - _this.$slide.on('touchend.lg', function() { - if (!_this.$outer.hasClass('lg-zoomed')) { - if (isMoved) { - isMoved = false; - _this.touchEnd(endCoords - startCoords); - } else { - _this.$el.trigger('onSlideClick.lg'); - } - } - }); - } - - }; - - Plugin.prototype.enableDrag = function() { - var _this = this; - var startCoords = 0; - var endCoords = 0; - var isDraging = false; - var isMoved = false; - if (_this.s.enableDrag && !_this.isTouch && _this.doCss()) { - _this.$slide.on('mousedown.lg', function(e) { - // execute only on .lg-object - if (!_this.$outer.hasClass('lg-zoomed')) { - if ($(e.target).hasClass('lg-object') || $(e.target).hasClass('lg-video-play')) { - e.preventDefault(); - - if (!_this.lgBusy) { - _this.manageSwipeClass(); - startCoords = e.pageX; - isDraging = true; - - // ** Fix for webkit cursor issue https://code.google.com/p/chromium/issues/detail?id=26723 - _this.$outer.scrollLeft += 1; - _this.$outer.scrollLeft -= 1; - - // * - - _this.$outer.removeClass('lg-grab').addClass('lg-grabbing'); - - _this.$el.trigger('onDragstart.lg'); - } - - } - } - }); - - $(window).on('mousemove.lg', function(e) { - if (isDraging) { - isMoved = true; - endCoords = e.pageX; - _this.touchMove(startCoords, endCoords); - _this.$el.trigger('onDragmove.lg'); - } - }); - - $(window).on('mouseup.lg', function(e) { - if (isMoved) { - isMoved = false; - _this.touchEnd(endCoords - startCoords); - _this.$el.trigger('onDragend.lg'); - } else if ($(e.target).hasClass('lg-object') || $(e.target).hasClass('lg-video-play')) { - _this.$el.trigger('onSlideClick.lg'); - } - - // Prevent execution on click - if (isDraging) { - isDraging = false; - _this.$outer.removeClass('lg-grabbing').addClass('lg-grab'); - } - }); - - } - }; - - Plugin.prototype.manageSwipeClass = function() { - var touchNext = this.index + 1; - var touchPrev = this.index - 1; - var length = this.$slide.length; - if (this.s.loop) { - if (this.index === 0) { - touchPrev = length - 1; - } else if (this.index === length - 1) { - touchNext = 0; - } - } - - this.$slide.removeClass('lg-next-slide lg-prev-slide'); - if (touchPrev > -1) { - this.$slide.eq(touchPrev).addClass('lg-prev-slide'); - } - - this.$slide.eq(touchNext).addClass('lg-next-slide'); - }; - - Plugin.prototype.mousewheel = function() { - var _this = this; - _this.$outer.on('mousewheel.lg', function(e) { - - if (!e.deltaY) { - return; - } - - if (e.deltaY > 0) { - _this.goToPrevSlide(); - } else { - _this.goToNextSlide(); - } - - e.preventDefault(); - }); - - }; - - Plugin.prototype.closeGallery = function() { - - var _this = this; - var mousedown = false; - this.$outer.find('.lg-close').on('click.lg', function() { - _this.destroy(); - }); - - if (_this.s.closable) { - - // If you drag the slide and release outside gallery gets close on chrome - // for preventing this check mousedown and mouseup happened on .lg-item or lg-outer - _this.$outer.on('mousedown.lg', function(e) { - - if ($(e.target).is('.lg-outer') || $(e.target).is('.lg-item ') || $(e.target).is('.lg-img-wrap')) { - mousedown = true; - } else { - mousedown = false; - } - - }); - - _this.$outer.on('mouseup.lg', function(e) { - - if ($(e.target).is('.lg-outer') || $(e.target).is('.lg-item ') || $(e.target).is('.lg-img-wrap') && mousedown) { - if (!_this.$outer.hasClass('lg-dragging')) { - _this.destroy(); - } - } - - }); - - } - - }; - - Plugin.prototype.destroy = function(d) { - - var _this = this; - - if (!d) { - _this.$el.trigger('onBeforeClose.lg'); - } - - $(window).scrollTop(_this.prevScrollTop); - - /** - * if d is false or undefined destroy will only close the gallery - * plugins instance remains with the element - * - * if d is true destroy will completely remove the plugin - */ - - if (d) { - if (!_this.s.dynamic) { - // only when not using dynamic mode is $items a jquery collection - this.$items.off('click.lg click.lgcustom'); - } - - $.removeData(_this.el, 'lightGallery'); - } - - // Unbind all events added by lightGallery - this.$el.off('.lg.tm'); - - // Distroy all lightGallery modules - $.each($.fn.lightGallery.modules, function(key) { - if (_this.modules[key]) { - _this.modules[key].destroy(); - } - }); - - this.lGalleryOn = false; - - clearTimeout(_this.hideBartimeout); - this.hideBartimeout = false; - $(window).off('.lg'); - $('body').removeClass('lg-on lg-from-hash'); - - if (_this.$outer) { - _this.$outer.removeClass('lg-visible'); - } - - $('.lg-backdrop').removeClass('in'); - - setTimeout(function() { - if (_this.$outer) { - _this.$outer.remove(); - } - - $('.lg-backdrop').remove(); - - if (!d) { - _this.$el.trigger('onCloseAfter.lg'); - } - - }, _this.s.backdropDuration + 50); - }; - - $.fn.lightGallery = function(options) { - return this.each(function() { - if (!$.data(this, 'lightGallery')) { - $.data(this, 'lightGallery', new Plugin(this, options)); - } else { - try { - $(this).data('lightGallery').init(); - } catch (err) { - console.error('lightGallery has not initiated properly'); - } - } - }); - }; - - $.fn.lightGallery.modules = {}; - -})(jQuery, window, document); - -/** - * Autoplay Plugin - * @version 1.2.0 - * @author Sachin N - @sachinchoolur - * @license MIT License (MIT) - */ - -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - autoplay: false, - pause: 5000, - progressBar: true, - fourceAutoplay: false, - autoplayControls: true, - appendAutoplayControlsTo: '.lg-toolbar' - }; - - /** - * Creates the autoplay plugin. - * @param {object} element - lightGallery element - */ - var Autoplay = function(element) { - - this.core = $(element).data('lightGallery'); - - this.$el = $(element); - - // Execute only if items are above 1 - if (this.core.$items.length < 2) { - return false; - } - - this.core.s = $.extend({}, defaults, this.core.s); - this.interval = false; - - // Identify if slide happened from autoplay - this.fromAuto = true; - - // Identify if autoplay canceled from touch/drag - this.canceledOnTouch = false; - - // save fourceautoplay value - this.fourceAutoplayTemp = this.core.s.fourceAutoplay; - - // do not allow progress bar if browser does not support css3 transitions - if (!this.core.doCss()) { - this.core.s.progressBar = false; - } - - this.init(); - - return this; - }; - - Autoplay.prototype.init = function() { - var _this = this; - - // append autoplay controls - if (_this.core.s.autoplayControls) { - _this.controls(); - } - - // Create progress bar - if (_this.core.s.progressBar) { - _this.core.$outer.find('.lg').append('
    '); - } - - // set progress - _this.progress(); - - // Start autoplay - if (_this.core.s.autoplay) { - _this.startlAuto(); - } - - // cancel interval on touchstart and dragstart - _this.$el.on('onDragstart.lg.tm touchstart.lg.tm', function() { - if (_this.interval) { - _this.cancelAuto(); - _this.canceledOnTouch = true; - } - }); - - // restore autoplay if autoplay canceled from touchstart / dragstart - _this.$el.on('onDragend.lg.tm touchend.lg.tm onSlideClick.lg.tm', function() { - if (!_this.interval && _this.canceledOnTouch) { - _this.startlAuto(); - _this.canceledOnTouch = false; - } - }); - - }; - - Autoplay.prototype.progress = function() { - - var _this = this; - var _$progressBar; - var _$progress; - - _this.$el.on('onBeforeSlide.lg.tm', function() { - - // start progress bar animation - if (_this.core.s.progressBar && _this.fromAuto) { - _$progressBar = _this.core.$outer.find('.lg-progress-bar'); - _$progress = _this.core.$outer.find('.lg-progress'); - if (_this.interval) { - _$progress.removeAttr('style'); - _$progressBar.removeClass('lg-start'); - setTimeout(function() { - _$progress.css('transition', 'width ' + (_this.core.s.speed + _this.core.s.pause) + 'ms ease 0s'); - _$progressBar.addClass('lg-start'); - }, 20); - } - } - - // Remove setinterval if slide is triggered manually and fourceautoplay is false - if (!_this.fromAuto && !_this.core.s.fourceAutoplay) { - _this.cancelAuto(); - } - - _this.fromAuto = false; - - }); - }; - - // Manage autoplay via play/stop buttons - Autoplay.prototype.controls = function() { - var _this = this; - var _html = ''; - - // Append autoplay controls - $(this.core.s.appendAutoplayControlsTo).append(_html); - - _this.core.$outer.find('.lg-autoplay-button').on('click.lg', function() { - if ($(_this.core.$outer).hasClass('lg-show-autoplay')) { - _this.cancelAuto(); - _this.core.s.fourceAutoplay = false; - } else { - if (!_this.interval) { - _this.startlAuto(); - _this.core.s.fourceAutoplay = _this.fourceAutoplayTemp; - } - } - }); - }; - - // Autostart gallery - Autoplay.prototype.startlAuto = function() { - var _this = this; - - _this.core.$outer.find('.lg-progress').css('transition', 'width ' + (_this.core.s.speed + _this.core.s.pause) + 'ms ease 0s'); - _this.core.$outer.addClass('lg-show-autoplay'); - _this.core.$outer.find('.lg-progress-bar').addClass('lg-start'); - - _this.interval = setInterval(function() { - if (_this.core.index + 1 < _this.core.$items.length) { - _this.core.index++; - } else { - _this.core.index = 0; - } - - _this.fromAuto = true; - _this.core.slide(_this.core.index, false, false); - }, _this.core.s.speed + _this.core.s.pause); - }; - - // cancel Autostart - Autoplay.prototype.cancelAuto = function() { - clearInterval(this.interval); - this.interval = false; - this.core.$outer.find('.lg-progress').removeAttr('style'); - this.core.$outer.removeClass('lg-show-autoplay'); - this.core.$outer.find('.lg-progress-bar').removeClass('lg-start'); - }; - - Autoplay.prototype.destroy = function() { - - this.cancelAuto(); - this.core.$outer.find('.lg-progress-bar').remove(); - }; - - $.fn.lightGallery.modules.autoplay = Autoplay; - -})(jQuery, window, document); - -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - fullScreen: true - }; - - var Fullscreen = function(element) { - - // get lightGallery core plugin data - this.core = $(element).data('lightGallery'); - - this.$el = $(element); - - // extend module defalut settings with lightGallery core settings - this.core.s = $.extend({}, defaults, this.core.s); - - this.init(); - - return this; - }; - - Fullscreen.prototype.init = function() { - var fullScreen = ''; - if (this.core.s.fullScreen) { - - // check for fullscreen browser support - if (!document.fullscreenEnabled && !document.webkitFullscreenEnabled && - !document.mozFullScreenEnabled && !document.msFullscreenEnabled) { - return; - } else { - fullScreen = ''; - this.core.$outer.find('.lg-toolbar').append(fullScreen); - this.fullScreen(); - } - } - }; - - Fullscreen.prototype.requestFullscreen = function() { - var el = document.documentElement; - if (el.requestFullscreen) { - el.requestFullscreen(); - } else if (el.msRequestFullscreen) { - el.msRequestFullscreen(); - } else if (el.mozRequestFullScreen) { - el.mozRequestFullScreen(); - } else if (el.webkitRequestFullscreen) { - el.webkitRequestFullscreen(); - } - }; - - Fullscreen.prototype.exitFullscreen = function() { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } - }; - - // https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode - Fullscreen.prototype.fullScreen = function() { - var _this = this; - - $(document).on('fullscreenchange.lg webkitfullscreenchange.lg mozfullscreenchange.lg MSFullscreenChange.lg', function() { - _this.core.$outer.toggleClass('lg-fullscreen-on'); - }); - - this.core.$outer.find('.lg-fullscreen').on('click.lg', function() { - if (!document.fullscreenElement && - !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { - _this.requestFullscreen(); - } else { - _this.exitFullscreen(); - } - }); - - }; - - Fullscreen.prototype.destroy = function() { - - // exit from fullscreen if activated - this.exitFullscreen(); - - $(document).off('fullscreenchange.lg webkitfullscreenchange.lg mozfullscreenchange.lg MSFullscreenChange.lg'); - }; - - $.fn.lightGallery.modules.fullscreen = Fullscreen; - -})(jQuery, window, document); - -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - pager: false - }; - - var Pager = function(element) { - - this.core = $(element).data('lightGallery'); - - this.$el = $(element); - this.core.s = $.extend({}, defaults, this.core.s); - if (this.core.s.pager && this.core.$items.length > 1) { - this.init(); - } - - return this; - }; - - Pager.prototype.init = function() { - var _this = this; - var pagerList = ''; - var $pagerCont; - var $pagerOuter; - var timeout; - - _this.core.$outer.find('.lg').append('
    '); - - if (_this.core.s.dynamic) { - for (var i = 0; i < _this.core.s.dynamicEl.length; i++) { - pagerList += '
    '; - } - } else { - _this.core.$items.each(function() { - - if (!_this.core.s.exThumbImage) { - pagerList += '
    '; - } else { - pagerList += '
    '; - } - - }); - } - - $pagerOuter = _this.core.$outer.find('.lg-pager-outer'); - - $pagerOuter.html(pagerList); - - $pagerCont = _this.core.$outer.find('.lg-pager-cont'); - $pagerCont.on('click.lg touchend.lg', function() { - var _$this = $(this); - _this.core.index = _$this.index(); - _this.core.slide(_this.core.index, false, false); - }); - - $pagerOuter.on('mouseover.lg', function() { - clearTimeout(timeout); - $pagerOuter.addClass('lg-pager-hover'); - }); - - $pagerOuter.on('mouseout.lg', function() { - timeout = setTimeout(function() { - $pagerOuter.removeClass('lg-pager-hover'); - }); - }); - - _this.core.$el.on('onBeforeSlide.lg.tm', function(e, prevIndex, index) { - $pagerCont.removeClass('lg-pager-active'); - $pagerCont.eq(index).addClass('lg-pager-active'); - }); - - }; - - Pager.prototype.destroy = function() { - - }; - - $.fn.lightGallery.modules.pager = Pager; - -})(jQuery, window, document); - -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - thumbnail: true, - - animateThumb: true, - currentPagerPosition: 'middle', - - thumbWidth: 100, - thumbContHeight: 100, - thumbMargin: 5, - - exThumbImage: false, - showThumbByDefault: true, - toogleThumb: true, - pullCaptionUp: true, - - enableThumbDrag: true, - enableThumbSwipe: true, - swipeThreshold: 50, - - loadYoutubeThumbnail: true, - youtubeThumbSize: 1, - - loadVimeoThumbnail: true, - vimeoThumbSize: 'thumbnail_small', - - loadDailymotionThumbnail: true - }; - - var Thumbnail = function(element) { - - // get lightGallery core plugin data - this.core = $(element).data('lightGallery'); - - // extend module default settings with lightGallery core settings - this.core.s = $.extend({}, defaults, this.core.s); - - this.$el = $(element); - this.$thumbOuter = null; - this.thumbOuterWidth = 0; - this.thumbTotalWidth = (this.core.$items.length * (this.core.s.thumbWidth + this.core.s.thumbMargin)); - this.thumbIndex = this.core.index; - - // Thumbnail animation value - this.left = 0; - - this.init(); - - return this; - }; - - Thumbnail.prototype.init = function() { - var _this = this; - if (this.core.s.thumbnail && this.core.$items.length > 1) { - if (this.core.s.showThumbByDefault) { - setTimeout(function(){ - _this.core.$outer.addClass('lg-thumb-open'); - }, 700); - } - - if (this.core.s.pullCaptionUp) { - this.core.$outer.addClass('lg-pull-caption-up'); - } - - this.build(); - if (this.core.s.animateThumb) { - if (this.core.s.enableThumbDrag && !this.core.isTouch && this.core.doCss()) { - this.enableThumbDrag(); - } - - if (this.core.s.enableThumbSwipe && this.core.isTouch && this.core.doCss()) { - this.enableThumbSwipe(); - } - - this.thumbClickable = false; - } else { - this.thumbClickable = true; - } - - this.toogle(); - this.thumbkeyPress(); - } - }; - - Thumbnail.prototype.build = function() { - var _this = this; - var thumbList = ''; - var vimeoErrorThumbSize = ''; - var $thumb; - var html = '
    ' + - '
    ' + - '
    ' + - '
    '; - - switch (this.core.s.vimeoThumbSize) { - case 'thumbnail_large': - vimeoErrorThumbSize = '640'; - break; - case 'thumbnail_medium': - vimeoErrorThumbSize = '200x150'; - break; - case 'thumbnail_small': - vimeoErrorThumbSize = '100x75'; - } - - _this.core.$outer.addClass('lg-has-thumb'); - - _this.core.$outer.find('.lg').append(html); - - _this.$thumbOuter = _this.core.$outer.find('.lg-thumb-outer'); - _this.thumbOuterWidth = _this.$thumbOuter.width(); - - if (_this.core.s.animateThumb) { - _this.core.$outer.find('.lg-thumb').css({ - width: _this.thumbTotalWidth + 'px', - position: 'relative' - }); - } - - if (this.core.s.animateThumb) { - _this.$thumbOuter.css('height', _this.core.s.thumbContHeight + 'px'); - } - - function getThumb(src, thumb, index) { - var isVideo = _this.core.isVideo(src, index) || {}; - var thumbImg; - var vimeoId = ''; - - if (isVideo.youtube || isVideo.vimeo || isVideo.dailymotion) { - if (isVideo.youtube) { - if (_this.core.s.loadYoutubeThumbnail) { - thumbImg = '//img.youtube.com/vi/' + isVideo.youtube[1] + '/' + _this.core.s.youtubeThumbSize + '.jpg'; - } else { - thumbImg = thumb; - } - } else if (isVideo.vimeo) { - if (_this.core.s.loadVimeoThumbnail) { - thumbImg = '//i.vimeocdn.com/video/error_' + vimeoErrorThumbSize + '.jpg'; - vimeoId = isVideo.vimeo[1]; - } else { - thumbImg = thumb; - } - } else if (isVideo.dailymotion) { - if (_this.core.s.loadDailymotionThumbnail) { - thumbImg = '//www.dailymotion.com/thumbnail/video/' + isVideo.dailymotion[1]; - } else { - thumbImg = thumb; - } - } - } else { - thumbImg = thumb; - } - - thumbList += '
    '; - vimeoId = ''; - } - - if (_this.core.s.dynamic) { - for (var i = 0; i < _this.core.s.dynamicEl.length; i++) { - getThumb(_this.core.s.dynamicEl[i].src, _this.core.s.dynamicEl[i].thumb, i); - } - } else { - _this.core.$items.each(function(i) { - - if (!_this.core.s.exThumbImage) { - getThumb($(this).attr('href') || $(this).attr('data-src'), $(this).find('img').attr('src'), i); - } else { - getThumb($(this).attr('href') || $(this).attr('data-src'), $(this).attr(_this.core.s.exThumbImage), i); - } - - }); - } - - _this.core.$outer.find('.lg-thumb').html(thumbList); - - $thumb = _this.core.$outer.find('.lg-thumb-item'); - - // Load vimeo thumbnails - $thumb.each(function() { - var $this = $(this); - var vimeoVideoId = $this.attr('data-vimeo-id'); - - if (vimeoVideoId) { - $.getJSON('//www.vimeo.com/api/v2/video/' + vimeoVideoId + '.json?callback=?', { - format: 'json' - }, function(data) { - $this.find('img').attr('src', data[0][_this.core.s.vimeoThumbSize]); - }); - } - }); - - // manage active class for thumbnail - $thumb.eq(_this.core.index).addClass('active'); - _this.core.$el.on('onBeforeSlide.lg.tm', function() { - $thumb.removeClass('active'); - $thumb.eq(_this.core.index).addClass('active'); - }); - - $thumb.on('click.lg touchend.lg', function() { - var _$this = $(this); - setTimeout(function() { - - // In IE9 and bellow touch does not support - // Go to slide if browser does not support css transitions - if ((_this.thumbClickable && !_this.core.lgBusy) || !_this.core.doCss()) { - _this.core.index = _$this.index(); - _this.core.slide(_this.core.index, false, true); - } - }, 50); - }); - - _this.core.$el.on('onBeforeSlide.lg.tm', function() { - _this.animateThumb(_this.core.index); - }); - - $(window).on('resize.lg.thumb orientationchange.lg.thumb', function() { - setTimeout(function() { - _this.animateThumb(_this.core.index); - _this.thumbOuterWidth = _this.$thumbOuter.width(); - }, 200); - }); - - }; - - Thumbnail.prototype.setTranslate = function(value) { - // jQuery supports Automatic CSS prefixing since jQuery 1.8.0 - this.core.$outer.find('.lg-thumb').css({ - transform: 'translate3d(-' + (value) + 'px, 0px, 0px)' - }); - }; - - Thumbnail.prototype.animateThumb = function(index) { - var $thumb = this.core.$outer.find('.lg-thumb'); - if (this.core.s.animateThumb) { - var position; - switch (this.core.s.currentPagerPosition) { - case 'left': - position = 0; - break; - case 'middle': - position = (this.thumbOuterWidth / 2) - (this.core.s.thumbWidth / 2); - break; - case 'right': - position = this.thumbOuterWidth - this.core.s.thumbWidth; - } - this.left = ((this.core.s.thumbWidth + this.core.s.thumbMargin) * index - 1) - position; - if (this.left > (this.thumbTotalWidth - this.thumbOuterWidth)) { - this.left = this.thumbTotalWidth - this.thumbOuterWidth; - } - - if (this.left < 0) { - this.left = 0; - } - - if (this.core.lGalleryOn) { - if (!$thumb.hasClass('on')) { - this.core.$outer.find('.lg-thumb').css('transition-duration', this.core.s.speed + 'ms'); - } - - if (!this.core.doCss()) { - $thumb.animate({ - left: -this.left + 'px' - }, this.core.s.speed); - } - } else { - if (!this.core.doCss()) { - $thumb.css('left', -this.left + 'px'); - } - } - - this.setTranslate(this.left); - - } - }; - - // Enable thumbnail dragging and swiping - Thumbnail.prototype.enableThumbDrag = function() { - - var _this = this; - var startCoords = 0; - var endCoords = 0; - var isDraging = false; - var isMoved = false; - var tempLeft = 0; - - _this.$thumbOuter.addClass('lg-grab'); - - _this.core.$outer.find('.lg-thumb').on('mousedown.lg.thumb', function(e) { - if (_this.thumbTotalWidth > _this.thumbOuterWidth) { - // execute only on .lg-object - e.preventDefault(); - startCoords = e.pageX; - isDraging = true; - - // ** Fix for webkit cursor issue https://code.google.com/p/chromium/issues/detail?id=26723 - _this.core.$outer.scrollLeft += 1; - _this.core.$outer.scrollLeft -= 1; - - // * - _this.thumbClickable = false; - _this.$thumbOuter.removeClass('lg-grab').addClass('lg-grabbing'); - } - }); - - $(window).on('mousemove.lg.thumb', function(e) { - if (isDraging) { - tempLeft = _this.left; - isMoved = true; - endCoords = e.pageX; - - _this.$thumbOuter.addClass('lg-dragging'); - - tempLeft = tempLeft - (endCoords - startCoords); - - if (tempLeft > (_this.thumbTotalWidth - _this.thumbOuterWidth)) { - tempLeft = _this.thumbTotalWidth - _this.thumbOuterWidth; - } - - if (tempLeft < 0) { - tempLeft = 0; - } - - // move current slide - _this.setTranslate(tempLeft); - - } - }); - - $(window).on('mouseup.lg.thumb', function() { - if (isMoved) { - isMoved = false; - _this.$thumbOuter.removeClass('lg-dragging'); - - _this.left = tempLeft; - - if (Math.abs(endCoords - startCoords) < _this.core.s.swipeThreshold) { - _this.thumbClickable = true; - } - - } else { - _this.thumbClickable = true; - } - - if (isDraging) { - isDraging = false; - _this.$thumbOuter.removeClass('lg-grabbing').addClass('lg-grab'); - } - }); - - }; - - Thumbnail.prototype.enableThumbSwipe = function() { - var _this = this; - var startCoords = 0; - var endCoords = 0; - var isMoved = false; - var tempLeft = 0; - - _this.core.$outer.find('.lg-thumb').on('touchstart.lg', function(e) { - if (_this.thumbTotalWidth > _this.thumbOuterWidth) { - e.preventDefault(); - startCoords = e.originalEvent.targetTouches[0].pageX; - _this.thumbClickable = false; - } - }); - - _this.core.$outer.find('.lg-thumb').on('touchmove.lg', function(e) { - if (_this.thumbTotalWidth > _this.thumbOuterWidth) { - e.preventDefault(); - endCoords = e.originalEvent.targetTouches[0].pageX; - isMoved = true; - - _this.$thumbOuter.addClass('lg-dragging'); - - tempLeft = _this.left; - - tempLeft = tempLeft - (endCoords - startCoords); - - if (tempLeft > (_this.thumbTotalWidth - _this.thumbOuterWidth)) { - tempLeft = _this.thumbTotalWidth - _this.thumbOuterWidth; - } - - if (tempLeft < 0) { - tempLeft = 0; - } - - // move current slide - _this.setTranslate(tempLeft); - - } - }); - - _this.core.$outer.find('.lg-thumb').on('touchend.lg', function() { - if (_this.thumbTotalWidth > _this.thumbOuterWidth) { - - if (isMoved) { - isMoved = false; - _this.$thumbOuter.removeClass('lg-dragging'); - if (Math.abs(endCoords - startCoords) < _this.core.s.swipeThreshold) { - _this.thumbClickable = true; - } - - _this.left = tempLeft; - } else { - _this.thumbClickable = true; - } - } else { - _this.thumbClickable = true; - } - }); - - }; - - Thumbnail.prototype.toogle = function() { - var _this = this; - if (_this.core.s.toogleThumb) { - _this.core.$outer.addClass('lg-can-toggle'); - _this.$thumbOuter.append(''); - _this.core.$outer.find('.lg-toogle-thumb').on('click.lg', function() { - _this.core.$outer.toggleClass('lg-thumb-open'); - }); - } - }; - - Thumbnail.prototype.thumbkeyPress = function() { - var _this = this; - $(window).on('keydown.lg.thumb', function(e) { - if (e.keyCode === 38) { - e.preventDefault(); - _this.core.$outer.addClass('lg-thumb-open'); - } else if (e.keyCode === 40) { - e.preventDefault(); - _this.core.$outer.removeClass('lg-thumb-open'); - } - }); - }; - - Thumbnail.prototype.destroy = function() { - if (this.core.s.thumbnail && this.core.$items.length > 1) { - $(window).off('resize.lg.thumb orientationchange.lg.thumb keydown.lg.thumb'); - this.$thumbOuter.remove(); - this.core.$outer.removeClass('lg-has-thumb'); - } - }; - - $.fn.lightGallery.modules.Thumbnail = Thumbnail; - -})(jQuery, window, document); - -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - videoMaxWidth: '855px', - youtubePlayerParams: false, - vimeoPlayerParams: false, - dailymotionPlayerParams: false, - vkPlayerParams: false, - videojs: false, - videojsOptions: {} - }; - - var Video = function(element) { - - this.core = $(element).data('lightGallery'); - - this.$el = $(element); - this.core.s = $.extend({}, defaults, this.core.s); - this.videoLoaded = false; - - this.init(); - - return this; - }; - - Video.prototype.init = function() { - var _this = this; - - // Event triggered when video url found without poster - _this.core.$el.on('hasVideo.lg.tm', function(event, index, src, html) { - _this.core.$slide.eq(index).find('.lg-video').append(_this.loadVideo(src, 'lg-object', true, index, html)); - if (html) { - if (_this.core.s.videojs) { - try { - videojs(_this.core.$slide.eq(index).find('.lg-html5').get(0), _this.core.s.videojsOptions, function() { - if (!_this.videoLoaded) { - this.play(); - } - }); - } catch (e) { - console.error('Make sure you have included videojs'); - } - } else { - _this.core.$slide.eq(index).find('.lg-html5').get(0).play(); - } - } - }); - - // Set max width for video - _this.core.$el.on('onAferAppendSlide.lg.tm', function(event, index) { - _this.core.$slide.eq(index).find('.lg-video-cont').css('max-width', _this.core.s.videoMaxWidth); - _this.videoLoaded = true; - }); - - var loadOnClick = function($el) { - // check slide has poster - if ($el.find('.lg-object').hasClass('lg-has-poster') && $el.find('.lg-object').is(':visible')) { - - // check already video element present - if (!$el.hasClass('lg-has-video')) { - - $el.addClass('lg-video-playing lg-has-video'); - - var _src; - var _html; - var _loadVideo = function(_src, _html) { - - $el.find('.lg-video').append(_this.loadVideo(_src, '', false, _this.core.index, _html)); - - if (_html) { - if (_this.core.s.videojs) { - try { - videojs(_this.core.$slide.eq(_this.core.index).find('.lg-html5').get(0), _this.core.s.videojsOptions, function() { - this.play(); - }); - } catch (e) { - console.error('Make sure you have included videojs'); - } - } else { - _this.core.$slide.eq(_this.core.index).find('.lg-html5').get(0).play(); - } - } - - }; - - if (_this.core.s.dynamic) { - - _src = _this.core.s.dynamicEl[_this.core.index].src; - _html = _this.core.s.dynamicEl[_this.core.index].html; - - _loadVideo(_src, _html); - - } else { - - _src = _this.core.$items.eq(_this.core.index).attr('href') || _this.core.$items.eq(_this.core.index).attr('data-src'); - _html = _this.core.$items.eq(_this.core.index).attr('data-html'); - - _loadVideo(_src, _html); - - } - - var $tempImg = $el.find('.lg-object'); - $el.find('.lg-video').append($tempImg); - - // @todo loading icon for html5 videos also - // for showing the loading indicator while loading video - if (!$el.find('.lg-video-object').hasClass('lg-html5')) { - $el.removeClass('lg-complete'); - $el.find('.lg-video-object').on('load.lg error.lg', function() { - $el.addClass('lg-complete'); - }); - } - - } else { - - var youtubePlayer = $el.find('.lg-youtube').get(0); - var vimeoPlayer = $el.find('.lg-vimeo').get(0); - var dailymotionPlayer = $el.find('.lg-dailymotion').get(0); - var html5Player = $el.find('.lg-html5').get(0); - if (youtubePlayer) { - youtubePlayer.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*'); - } else if (vimeoPlayer) { - try { - $f(vimeoPlayer).api('play'); - } catch (e) { - console.error('Make sure you have included froogaloop2 js'); - } - } else if (dailymotionPlayer) { - dailymotionPlayer.contentWindow.postMessage('play', '*'); - - } else if (html5Player) { - if (_this.core.s.videojs) { - try { - videojs(html5Player).play(); - } catch (e) { - console.error('Make sure you have included videojs'); - } - } else { - html5Player.play(); - } - } - - $el.addClass('lg-video-playing'); - - } - } - }; - - if (_this.core.doCss() && _this.core.$items.length > 1 && ((_this.core.s.enableSwipe && _this.core.isTouch) || (_this.core.s.enableDrag && !_this.core.isTouch))) { - _this.core.$el.on('onSlideClick.lg.tm', function() { - var $el = _this.core.$slide.eq(_this.core.index); - loadOnClick($el); - }); - } else { - - // For IE 9 and bellow - _this.core.$slide.on('click.lg', function() { - loadOnClick($(this)); - }); - } - - _this.core.$el.on('onBeforeSlide.lg.tm', function(event, prevIndex, index) { - - var $videoSlide = _this.core.$slide.eq(prevIndex); - var youtubePlayer = $videoSlide.find('.lg-youtube').get(0); - var vimeoPlayer = $videoSlide.find('.lg-vimeo').get(0); - var dailymotionPlayer = $videoSlide.find('.lg-dailymotion').get(0); - var vkPlayer = $videoSlide.find('.lg-vk').get(0); - var html5Player = $videoSlide.find('.lg-html5').get(0); - if (youtubePlayer) { - youtubePlayer.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*'); - } else if (vimeoPlayer) { - try { - $f(vimeoPlayer).api('pause'); - } catch (e) { - console.error('Make sure you have included froogaloop2 js'); - } - } else if (dailymotionPlayer) { - dailymotionPlayer.contentWindow.postMessage('pause', '*'); - - } else if (html5Player) { - if (_this.core.s.videojs) { - try { - videojs(html5Player).pause(); - } catch (e) { - console.error('Make sure you have included videojs'); - } - } else { - html5Player.pause(); - } - } if (vkPlayer) { - $(vkPlayer).attr('src', $(vkPlayer).attr('src').replace('&autoplay', '&noplay')); - } - - var _src; - if (_this.core.s.dynamic) { - _src = _this.core.s.dynamicEl[index].src; - } else { - _src = _this.core.$items.eq(index).attr('href') || _this.core.$items.eq(index).attr('data-src'); - - } - - var _isVideo = _this.core.isVideo(_src, index) || {}; - if (_isVideo.youtube || _isVideo.vimeo || _isVideo.dailymotion || _isVideo.vk) { - _this.core.$outer.addClass('lg-hide-download'); - } - - //$videoSlide.addClass('lg-complete'); - - }); - - _this.core.$el.on('onAfterSlide.lg.tm', function(event, prevIndex) { - _this.core.$slide.eq(prevIndex).removeClass('lg-video-playing'); - }); - }; - - Video.prototype.loadVideo = function(src, addClass, noposter, index, html) { - var video = ''; - var autoplay = 1; - var a = ''; - var isVideo = this.core.isVideo(src, index) || {}; - - // Enable autoplay for first video if poster doesn't exist - if (noposter) { - if (this.videoLoaded) { - autoplay = 0; - } else { - autoplay = 1; - } - } - - if (isVideo.youtube) { - - a = '?wmode=opaque&autoplay=' + autoplay + '&enablejsapi=1'; - if (this.core.s.youtubePlayerParams) { - a = a + '&' + $.param(this.core.s.youtubePlayerParams); - } - - video = ''; - - } else if (isVideo.vimeo) { - - a = '?autoplay=' + autoplay + '&api=1'; - if (this.core.s.vimeoPlayerParams) { - a = a + '&' + $.param(this.core.s.vimeoPlayerParams); - } - - video = ''; - - } else if (isVideo.dailymotion) { - - a = '?wmode=opaque&autoplay=' + autoplay + '&api=postMessage'; - if (this.core.s.dailymotionPlayerParams) { - a = a + '&' + $.param(this.core.s.dailymotionPlayerParams); - } - - video = ''; - - } else if (isVideo.html5) { - var fL = html.substring(0, 1); - if (fL === '.' || fL === '#') { - html = $(html).html(); - } - - video = html; - - } else if (isVideo.vk) { - - a = '&autoplay=' + autoplay; - if (this.core.s.vkPlayerParams) { - a = a + '&' + $.param(this.core.s.vkPlayerParams); - } - - video = ''; - - } - - return video; - }; - - Video.prototype.destroy = function() { - this.videoLoaded = false; - }; - - $.fn.lightGallery.modules.video = Video; - -})(jQuery, window, document); - -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - scale: 1, - zoom: true, - actualSize: true, - enableZoomAfter: 300 - }; - - var Zoom = function(element) { - - this.core = $(element).data('lightGallery'); - - this.core.s = $.extend({}, defaults, this.core.s); - - if (this.core.s.zoom && this.core.doCss()) { - this.init(); - - // Store the zoomable timeout value just to clear it while closing - this.zoomabletimeout = false; - - // Set the initial value center - this.pageX = $(window).width() / 2; - this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - } - - return this; - }; - - Zoom.prototype.init = function() { - - var _this = this; - var zoomIcons = ''; - - if (_this.core.s.actualSize) { - zoomIcons += ''; - } - - this.core.$outer.find('.lg-toolbar').append(zoomIcons); - - // Add zoomable class - _this.core.$el.on('onSlideItemLoad.lg.tm.zoom', function(event, index, delay) { - - // delay will be 0 except first time - var _speed = _this.core.s.enableZoomAfter + delay; - - // set _speed value 0 if gallery opened from direct url and if it is first slide - if ($('body').hasClass('lg-from-hash') && delay) { - - // will execute only once - _speed = 0; - } else { - - // Remove lg-from-hash to enable starting animation. - $('body').removeClass('lg-from-hash'); - } - - _this.zoomabletimeout = setTimeout(function() { - _this.core.$slide.eq(index).addClass('lg-zoomable'); - }, _speed + 30); - }); - - var scale = 1; - /** - * @desc Image zoom - * Translate the wrap and scale the image to get better user experience - * - * @param {String} scaleVal - Zoom decrement/increment value - */ - var zoom = function(scaleVal) { - - var $image = _this.core.$outer.find('.lg-current .lg-image'); - var _x; - var _y; - - // Find offset manually to avoid issue after zoom - var offsetX = ($(window).width() - $image.width()) / 2; - var offsetY = (($(window).height() - $image.height()) / 2) + $(window).scrollTop(); - - _x = _this.pageX - offsetX; - _y = _this.pageY - offsetY; - - var x = (scaleVal - 1) * (_x); - var y = (scaleVal - 1) * (_y); - - $image.css('transform', 'scale3d(' + scaleVal + ', ' + scaleVal + ', 1)').attr('data-scale', scaleVal); - - $image.parent().css({ - left: -x + 'px', - top: -y + 'px' - }).attr('data-x', x).attr('data-y', y); - }; - - var callScale = function() { - if (scale > 1) { - _this.core.$outer.addClass('lg-zoomed'); - } else { - _this.resetZoom(); - } - - if (scale < 1) { - scale = 1; - } - - zoom(scale); - }; - - var actualSize = function(event, $image, index, fromIcon) { - var w = $image.width(); - var nw; - if (_this.core.s.dynamic) { - nw = _this.core.s.dynamicEl[index].width || $image[0].naturalWidth || w; - } else { - nw = _this.core.$items.eq(index).attr('data-width') || $image[0].naturalWidth || w; - } - - var _scale; - - if (_this.core.$outer.hasClass('lg-zoomed')) { - scale = 1; - } else { - if (nw > w) { - _scale = nw / w; - scale = _scale || 2; - } - } - - if (fromIcon) { - _this.pageX = $(window).width() / 2; - _this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - } else { - _this.pageX = event.pageX || event.originalEvent.targetTouches[0].pageX; - _this.pageY = event.pageY || event.originalEvent.targetTouches[0].pageY; - } - - callScale(); - setTimeout(function() { - _this.core.$outer.removeClass('lg-grabbing').addClass('lg-grab'); - }, 10); - }; - - var tapped = false; - - // event triggered after appending slide content - _this.core.$el.on('onAferAppendSlide.lg.tm.zoom', function(event, index) { - - // Get the current element - var $image = _this.core.$slide.eq(index).find('.lg-image'); - - $image.on('dblclick', function(event) { - actualSize(event, $image, index); - }); - - $image.on('touchstart', function(event) { - if (!tapped) { - tapped = setTimeout(function() { - tapped = null; - }, 300); - } else { - clearTimeout(tapped); - tapped = null; - actualSize(event, $image, index); - } - - event.preventDefault(); - }); - - }); - - // Update zoom on resize and orientationchange - $(window).on('resize.lg.zoom scroll.lg.zoom orientationchange.lg.zoom', function() { - _this.pageX = $(window).width() / 2; - _this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - zoom(scale); - }); - - $('#lg-zoom-out').on('click.lg', function() { - if (_this.core.$outer.find('.lg-current .lg-image').length) { - scale -= _this.core.s.scale; - callScale(); - } - }); - - $('#lg-zoom-in').on('click.lg', function() { - if (_this.core.$outer.find('.lg-current .lg-image').length) { - scale += _this.core.s.scale; - callScale(); - } - }); - - $('#lg-actual-size').on('click.lg', function(event) { - actualSize(event, _this.core.$slide.eq(_this.core.index).find('.lg-image'), _this.core.index, true); - }); - - // Reset zoom on slide change - _this.core.$el.on('onBeforeSlide.lg.tm', function() { - scale = 1; - _this.resetZoom(); - }); - - // Drag option after zoom - if (!_this.core.isTouch) { - _this.zoomDrag(); - } - - if (_this.core.isTouch) { - _this.zoomSwipe(); - } - - }; - - // Reset zoom effect - Zoom.prototype.resetZoom = function() { - this.core.$outer.removeClass('lg-zoomed'); - this.core.$slide.find('.lg-img-wrap').removeAttr('style data-x data-y'); - this.core.$slide.find('.lg-image').removeAttr('style data-scale'); - - // Reset pagx pagy values to center - this.pageX = $(window).width() / 2; - this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - }; - - Zoom.prototype.zoomSwipe = function() { - var _this = this; - var startCoords = {}; - var endCoords = {}; - var isMoved = false; - - // Allow x direction drag - var allowX = false; - - // Allow Y direction drag - var allowY = false; - - _this.core.$slide.on('touchstart.lg', function(e) { - - if (_this.core.$outer.hasClass('lg-zoomed')) { - var $image = _this.core.$slide.eq(_this.core.index).find('.lg-object'); - - allowY = $image.outerHeight() * $image.attr('data-scale') > _this.core.$outer.find('.lg').height(); - allowX = $image.outerWidth() * $image.attr('data-scale') > _this.core.$outer.find('.lg').width(); - if ((allowX || allowY)) { - e.preventDefault(); - startCoords = { - x: e.originalEvent.targetTouches[0].pageX, - y: e.originalEvent.targetTouches[0].pageY - }; - } - } - - }); - - _this.core.$slide.on('touchmove.lg', function(e) { - - if (_this.core.$outer.hasClass('lg-zoomed')) { - - var _$el = _this.core.$slide.eq(_this.core.index).find('.lg-img-wrap'); - var distanceX; - var distanceY; - - e.preventDefault(); - isMoved = true; - - endCoords = { - x: e.originalEvent.targetTouches[0].pageX, - y: e.originalEvent.targetTouches[0].pageY - }; - - // reset opacity and transition duration - _this.core.$outer.addClass('lg-zoom-dragging'); - - if (allowY) { - distanceY = (-Math.abs(_$el.attr('data-y'))) + (endCoords.y - startCoords.y); - } else { - distanceY = -Math.abs(_$el.attr('data-y')); - } - - if (allowX) { - distanceX = (-Math.abs(_$el.attr('data-x'))) + (endCoords.x - startCoords.x); - } else { - distanceX = -Math.abs(_$el.attr('data-x')); - } - - if ((Math.abs(endCoords.x - startCoords.x) > 15) || (Math.abs(endCoords.y - startCoords.y) > 15)) { - _$el.css({ - left: distanceX + 'px', - top: distanceY + 'px' - }); - } - - } - - }); - - _this.core.$slide.on('touchend.lg', function() { - if (_this.core.$outer.hasClass('lg-zoomed')) { - if (isMoved) { - isMoved = false; - _this.core.$outer.removeClass('lg-zoom-dragging'); - _this.touchendZoom(startCoords, endCoords, allowX, allowY); - - } - } - }); - - }; - - Zoom.prototype.zoomDrag = function() { - - var _this = this; - var startCoords = {}; - var endCoords = {}; - var isDraging = false; - var isMoved = false; - - // Allow x direction drag - var allowX = false; - - // Allow Y direction drag - var allowY = false; - - _this.core.$slide.on('mousedown.lg.zoom', function(e) { - - // execute only on .lg-object - var $image = _this.core.$slide.eq(_this.core.index).find('.lg-object'); - - allowY = $image.outerHeight() * $image.attr('data-scale') > _this.core.$outer.find('.lg').height(); - allowX = $image.outerWidth() * $image.attr('data-scale') > _this.core.$outer.find('.lg').width(); - - if (_this.core.$outer.hasClass('lg-zoomed')) { - if ($(e.target).hasClass('lg-object') && (allowX || allowY)) { - e.preventDefault(); - startCoords = { - x: e.pageX, - y: e.pageY - }; - - isDraging = true; - - // ** Fix for webkit cursor issue https://code.google.com/p/chromium/issues/detail?id=26723 - _this.core.$outer.scrollLeft += 1; - _this.core.$outer.scrollLeft -= 1; - - _this.core.$outer.removeClass('lg-grab').addClass('lg-grabbing'); - } - } - }); - - $(window).on('mousemove.lg.zoom', function(e) { - if (isDraging) { - var _$el = _this.core.$slide.eq(_this.core.index).find('.lg-img-wrap'); - var distanceX; - var distanceY; - - isMoved = true; - endCoords = { - x: e.pageX, - y: e.pageY - }; - - // reset opacity and transition duration - _this.core.$outer.addClass('lg-zoom-dragging'); - - if (allowY) { - distanceY = (-Math.abs(_$el.attr('data-y'))) + (endCoords.y - startCoords.y); - } else { - distanceY = -Math.abs(_$el.attr('data-y')); - } - - if (allowX) { - distanceX = (-Math.abs(_$el.attr('data-x'))) + (endCoords.x - startCoords.x); - } else { - distanceX = -Math.abs(_$el.attr('data-x')); - } - - _$el.css({ - left: distanceX + 'px', - top: distanceY + 'px' - }); - } - }); - - $(window).on('mouseup.lg.zoom', function(e) { - - if (isDraging) { - isDraging = false; - _this.core.$outer.removeClass('lg-zoom-dragging'); - - // Fix for chrome mouse move on click - if (isMoved && ((startCoords.x !== endCoords.x) || (startCoords.y !== endCoords.y))) { - endCoords = { - x: e.pageX, - y: e.pageY - }; - _this.touchendZoom(startCoords, endCoords, allowX, allowY); - - } - - isMoved = false; - } - - _this.core.$outer.removeClass('lg-grabbing').addClass('lg-grab'); - - }); - }; - - Zoom.prototype.touchendZoom = function(startCoords, endCoords, allowX, allowY) { - - var _this = this; - var _$el = _this.core.$slide.eq(_this.core.index).find('.lg-img-wrap'); - var $image = _this.core.$slide.eq(_this.core.index).find('.lg-object'); - var distanceX = (-Math.abs(_$el.attr('data-x'))) + (endCoords.x - startCoords.x); - var distanceY = (-Math.abs(_$el.attr('data-y'))) + (endCoords.y - startCoords.y); - var minY = (_this.core.$outer.find('.lg').height() - $image.outerHeight()) / 2; - var maxY = Math.abs(($image.outerHeight() * Math.abs($image.attr('data-scale'))) - _this.core.$outer.find('.lg').height() + minY); - var minX = (_this.core.$outer.find('.lg').width() - $image.outerWidth()) / 2; - var maxX = Math.abs(($image.outerWidth() * Math.abs($image.attr('data-scale'))) - _this.core.$outer.find('.lg').width() + minX); - - if ((Math.abs(endCoords.x - startCoords.x) > 15) || (Math.abs(endCoords.y - startCoords.y) > 15)) { - if (allowY) { - if (distanceY <= -maxY) { - distanceY = -maxY; - } else if (distanceY >= -minY) { - distanceY = -minY; - } - } - - if (allowX) { - if (distanceX <= -maxX) { - distanceX = -maxX; - } else if (distanceX >= -minX) { - distanceX = -minX; - } - } - - if (allowY) { - _$el.attr('data-y', Math.abs(distanceY)); - } else { - distanceY = -Math.abs(_$el.attr('data-y')); - } - - if (allowX) { - _$el.attr('data-x', Math.abs(distanceX)); - } else { - distanceX = -Math.abs(_$el.attr('data-x')); - } - - _$el.css({ - left: distanceX + 'px', - top: distanceY + 'px' - }); - - } - }; - - Zoom.prototype.destroy = function() { - - var _this = this; - - // Unbind all events added by lightGallery zoom plugin - _this.core.$el.off('.lg.zoom'); - $(window).off('.lg.zoom'); - _this.core.$slide.off('.lg.zoom'); - _this.core.$el.off('.lg.tm.zoom'); - _this.resetZoom(); - clearTimeout(_this.zoomabletimeout); - _this.zoomabletimeout = false; - }; - - $.fn.lightGallery.modules.zoom = Zoom; - -})(jQuery, window, document); -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - hash: true - }; - - var Hash = function(element) { - - this.core = $(element).data('lightGallery'); - - this.core.s = $.extend({}, defaults, this.core.s); - - if (this.core.s.hash) { - this.oldHash = window.location.hash; - this.init(); - } - - return this; - }; - - Hash.prototype.init = function() { - var _this = this; - var _hash; - - // Change hash value on after each slide transition - _this.core.$el.on('onAfterSlide.lg.tm', function(event, prevIndex, index) { - window.location.hash = 'lg=' + _this.core.s.galleryId + '&slide=' + index; - }); - - // Listen hash change and change the slide according to slide value - $(window).on('hashchange.lg.hash', function() { - _hash = window.location.hash; - var _idx = parseInt(_hash.split('&slide=')[1], 10); - - // it galleryId doesn't exist in the url close the gallery - if ((_hash.indexOf('lg=' + _this.core.s.galleryId) > -1)) { - _this.core.slide(_idx, false, false); - } else if (_this.core.lGalleryOn) { - _this.core.destroy(); - } - - }); - }; - - Hash.prototype.destroy = function() { - - if (!this.core.s.hash) { - return; - } - - // Reset to old hash value - if (this.oldHash && this.oldHash.indexOf('lg=' + this.core.s.galleryId) < 0) { - window.location.hash = this.oldHash; - } else { - if (history.pushState) { - history.pushState('', document.title, window.location.pathname + window.location.search); - } else { - window.location.hash = ''; - } - } - - this.core.$el.off('.lg.hash'); - - }; - - $.fn.lightGallery.modules.hash = Hash; - -})(jQuery, window, document); \ No newline at end of file diff --git a/vendors/lightgallery/dist/js/lightgallery-all.min.js b/vendors/lightgallery/dist/js/lightgallery-all.min.js deleted file mode 100644 index 84b0beb81c..0000000000 --- a/vendors/lightgallery/dist/js/lightgallery-all.min.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! lightgallery - v1.2.21 - 2016-06-28 -* http://sachinchoolur.github.io/lightGallery/ -* Copyright (c) 2016 Sachin N; Licensed Apache 2.0 */ -!function(a,b,c,d){"use strict";function e(b,d){if(this.el=b,this.$el=a(b),this.s=a.extend({},f,d),this.s.dynamic&&"undefined"!==this.s.dynamicEl&&this.s.dynamicEl.constructor===Array&&!this.s.dynamicEl.length)throw"When using dynamic mode, you must also define dynamicEl as an Array.";return this.modules={},this.lGalleryOn=!1,this.lgBusy=!1,this.hideBartimeout=!1,this.isTouch="ontouchstart"in c.documentElement,this.s.slideEndAnimatoin&&(this.s.hideControlOnEnd=!1),this.s.dynamic?this.$items=this.s.dynamicEl:"this"===this.s.selector?this.$items=this.$el:""!==this.s.selector?this.s.selectWithin?this.$items=a(this.s.selectWithin).find(this.s.selector):this.$items=this.$el.find(a(this.s.selector)):this.$items=this.$el.children(),this.$slide="",this.$outer="",this.init(),this}var f={mode:"lg-slide",cssEasing:"ease",easing:"linear",speed:600,height:"100%",width:"100%",addClass:"",startClass:"lg-start-zoom",backdropDuration:150,hideBarsDelay:6e3,useLeft:!1,closable:!0,loop:!0,escKey:!0,keyPress:!0,controls:!0,slideEndAnimatoin:!0,hideControlOnEnd:!1,mousewheel:!0,getCaptionFromTitleOrAlt:!0,appendSubHtmlTo:".lg-sub-html",subHtmlSelectorRelative:!1,preload:1,showAfterLoad:!0,selector:"",selectWithin:"",nextHtml:"",prevHtml:"",index:!1,iframeMaxWidth:"100%",download:!0,counter:!0,appendCounterTo:".lg-toolbar",swipeThreshold:50,enableSwipe:!0,enableDrag:!0,dynamic:!1,dynamicEl:[],galleryId:1};e.prototype.init=function(){var c=this;c.s.preload>c.$items.length&&(c.s.preload=c.$items.length);var d=b.location.hash;d.indexOf("lg="+this.s.galleryId)>0&&(c.index=parseInt(d.split("&slide=")[1],10),a("body").addClass("lg-from-hash"),a("body").hasClass("lg-on")||setTimeout(function(){c.build(c.index),a("body").addClass("lg-on")})),c.s.dynamic?(c.$el.trigger("onBeforeOpen.lg"),c.index=c.s.index||0,a("body").hasClass("lg-on")||setTimeout(function(){c.build(c.index),a("body").addClass("lg-on")})):c.$items.on("click.lgcustom",function(b){try{b.preventDefault(),b.preventDefault()}catch(d){b.returnValue=!1}c.$el.trigger("onBeforeOpen.lg"),c.index=c.s.index||c.$items.index(this),a("body").hasClass("lg-on")||(c.build(c.index),a("body").addClass("lg-on"))})},e.prototype.build=function(b){var c=this;c.structure(),a.each(a.fn.lightGallery.modules,function(b){c.modules[b]=new a.fn.lightGallery.modules[b](c.el)}),c.slide(b,!1,!1),c.s.keyPress&&c.keyPress(),c.$items.length>1&&(c.arrow(),setTimeout(function(){c.enableDrag(),c.enableSwipe()},50),c.s.mousewheel&&c.mousewheel()),c.counter(),c.closeGallery(),c.$el.trigger("onAfterOpen.lg"),c.$outer.on("mousemove.lg click.lg touchstart.lg",function(){c.$outer.removeClass("lg-hide-items"),clearTimeout(c.hideBartimeout),c.hideBartimeout=setTimeout(function(){c.$outer.addClass("lg-hide-items")},c.s.hideBarsDelay)})},e.prototype.structure=function(){var c,d="",e="",f=0,g="",h=this;for(a("body").append('
    '),a(".lg-backdrop").css("transition-duration",this.s.backdropDuration+"ms"),f=0;f
    ';if(this.s.controls&&this.$items.length>1&&(e='
    '+this.s.prevHtml+'
    '+this.s.nextHtml+"
    "),".lg-sub-html"===this.s.appendSubHtmlTo&&(g='
    '),c='
    '+d+'
    '+e+g+"
    ",a("body").append(c),this.$outer=a(".lg-outer"),this.$slide=this.$outer.find(".lg-item"),this.s.useLeft?(this.$outer.addClass("lg-use-left"),this.s.mode="lg-slide"):this.$outer.addClass("lg-use-css3"),h.setTop(),a(b).on("resize.lg orientationchange.lg",function(){setTimeout(function(){h.setTop()},100)}),this.$slide.eq(this.index).addClass("lg-current"),this.doCss()?this.$outer.addClass("lg-css3"):(this.$outer.addClass("lg-css"),this.s.speed=0),this.$outer.addClass(this.s.mode),this.s.enableDrag&&this.$items.length>1&&this.$outer.addClass("lg-grab"),this.s.showAfterLoad&&this.$outer.addClass("lg-show-after-load"),this.doCss()){var i=this.$outer.find(".lg-inner");i.css("transition-timing-function",this.s.cssEasing),i.css("transition-duration",this.s.speed+"ms")}a(".lg-backdrop").addClass("in"),setTimeout(function(){h.$outer.addClass("lg-visible")},this.s.backdropDuration),this.s.download&&this.$outer.find(".lg-toolbar").append(''),this.prevScrollTop=a(b).scrollTop()},e.prototype.setTop=function(){if("100%"!==this.s.height){var c=a(b).height(),d=(c-parseInt(this.s.height,10))/2,e=this.$outer.find(".lg");c>=parseInt(this.s.height,10)?e.css("top",d+"px"):e.css("top","0px")}},e.prototype.doCss=function(){var a=function(){var a=["transition","MozTransition","WebkitTransition","OTransition","msTransition","KhtmlTransition"],b=c.documentElement,d=0;for(d=0;d'+(parseInt(this.index,10)+1)+' / '+this.$items.length+"
    ")},e.prototype.addHtml=function(b){var c,d,e=null;if(this.s.dynamic?this.s.dynamicEl[b].subHtmlUrl?c=this.s.dynamicEl[b].subHtmlUrl:e=this.s.dynamicEl[b].subHtml:(d=this.$items.eq(b),d.attr("data-sub-html-url")?c=d.attr("data-sub-html-url"):(e=d.attr("data-sub-html"),this.s.getCaptionFromTitleOrAlt&&!e&&(e=d.attr("title")||d.find("img").first().attr("alt")))),!c)if("undefined"!=typeof e&&null!==e){var f=e.substring(0,1);"."!==f&&"#"!==f||(e=this.s.subHtmlSelectorRelative&&!this.s.dynamic?d.find(e).html():a(e).html())}else e="";".lg-sub-html"===this.s.appendSubHtmlTo?c?this.$outer.find(this.s.appendSubHtmlTo).load(c):this.$outer.find(this.s.appendSubHtmlTo).html(e):c?this.$slide.eq(b).load(c):this.$slide.eq(b).append(e),"undefined"!=typeof e&&null!==e&&(""===e?this.$outer.find(this.s.appendSubHtmlTo).addClass("lg-empty-html"):this.$outer.find(this.s.appendSubHtmlTo).removeClass("lg-empty-html")),this.$el.trigger("onAfterAppendSubHtml.lg",[b])},e.prototype.preload=function(a){var b=1,c=1;for(b=1;b<=this.s.preload&&!(b>=this.$items.length-a);b++)this.loadContent(a+b,!1,0);for(c=1;c<=this.s.preload&&!(0>a-c);c++)this.loadContent(a-c,!1,0)},e.prototype.loadContent=function(c,d,e){var f,g,h,i,j,k,l=this,m=!1,n=function(c){for(var d=[],e=[],f=0;fi){g=e[j];break}};if(l.s.dynamic){if(l.s.dynamicEl[c].poster&&(m=!0,h=l.s.dynamicEl[c].poster),k=l.s.dynamicEl[c].html,g=l.s.dynamicEl[c].src,l.s.dynamicEl[c].responsive){var o=l.s.dynamicEl[c].responsive.split(",");n(o)}i=l.s.dynamicEl[c].srcset,j=l.s.dynamicEl[c].sizes}else{if(l.$items.eq(c).attr("data-poster")&&(m=!0,h=l.$items.eq(c).attr("data-poster")),k=l.$items.eq(c).attr("data-html"),g=l.$items.eq(c).attr("href")||l.$items.eq(c).attr("data-src"),l.$items.eq(c).attr("data-responsive")){var p=l.$items.eq(c).attr("data-responsive").split(",");n(p)}i=l.$items.eq(c).attr("data-srcset"),j=l.$items.eq(c).attr("data-sizes")}var q=!1;l.s.dynamic?l.s.dynamicEl[c].iframe&&(q=!0):"true"===l.$items.eq(c).attr("data-iframe")&&(q=!0);var r=l.isVideo(g,c);if(!l.$slide.eq(c).hasClass("lg-loaded")){if(q)l.$slide.eq(c).prepend('
    ');else if(m){var s="";s=r&&r.youtube?"lg-has-youtube":r&&r.vimeo?"lg-has-vimeo":"lg-has-html5",l.$slide.eq(c).prepend('
    ')}else r?(l.$slide.eq(c).prepend('
    '),l.$el.trigger("hasVideo.lg",[c,g,k])):l.$slide.eq(c).prepend('
    ');if(l.$el.trigger("onAferAppendSlide.lg",[c]),f=l.$slide.eq(c).find(".lg-object"),j&&f.attr("sizes",j),i){f.attr("srcset",i);try{picturefill({elements:[f[0]]})}catch(t){console.error("Make sure you have included Picturefill version 2")}}".lg-sub-html"!==this.s.appendSubHtmlTo&&l.addHtml(c),l.$slide.eq(c).addClass("lg-loaded")}l.$slide.eq(c).find(".lg-object").on("load.lg error.lg",function(){var b=0;e&&!a("body").hasClass("lg-from-hash")&&(b=e),setTimeout(function(){l.$slide.eq(c).addClass("lg-complete"),l.$el.trigger("onSlideItemLoad.lg",[c,e||0])},b)}),r&&r.html5&&!m&&l.$slide.eq(c).addClass("lg-complete"),d===!0&&(l.$slide.eq(c).hasClass("lg-complete")?l.preload(c):l.$slide.eq(c).find(".lg-object").on("load.lg error.lg",function(){l.preload(c)}))},e.prototype.slide=function(b,c,d){var e=this.$outer.find(".lg-current").index(),f=this;if(!f.lGalleryOn||e!==b){var g=this.$slide.length,h=f.lGalleryOn?this.s.speed:0,i=!1,j=!1;if(!f.lgBusy){if(this.s.download){var k;k=f.s.dynamic?f.s.dynamicEl[b].downloadUrl!==!1&&(f.s.dynamicEl[b].downloadUrl||f.s.dynamicEl[b].src):"false"!==f.$items.eq(b).attr("data-download-url")&&(f.$items.eq(b).attr("data-download-url")||f.$items.eq(b).attr("href")||f.$items.eq(b).attr("data-src")),k?(a("#lg-download").attr("href",k),f.$outer.removeClass("lg-hide-download")):f.$outer.addClass("lg-hide-download")}if(this.$el.trigger("onBeforeSlide.lg",[e,b,c,d]),f.lgBusy=!0,clearTimeout(f.hideBartimeout),".lg-sub-html"===this.s.appendSubHtmlTo&&setTimeout(function(){f.addHtml(b)},h),this.arrowDisable(b),c){var l=b-1,m=b+1;0===b&&e===g-1?(m=0,l=g-1):b===g-1&&0===e&&(m=0,l=g-1),this.$slide.removeClass("lg-prev-slide lg-current lg-next-slide"),f.$slide.eq(l).addClass("lg-prev-slide"),f.$slide.eq(m).addClass("lg-next-slide"),f.$slide.eq(b).addClass("lg-current")}else f.$outer.addClass("lg-no-trans"),this.$slide.removeClass("lg-prev-slide lg-next-slide"),e>b?(j=!0,0!==b||e!==g-1||d||(j=!1,i=!0)):b>e&&(i=!0,b!==g-1||0!==e||d||(j=!0,i=!1)),j?(this.$slide.eq(b).addClass("lg-prev-slide"),this.$slide.eq(e).addClass("lg-next-slide")):i&&(this.$slide.eq(b).addClass("lg-next-slide"),this.$slide.eq(e).addClass("lg-prev-slide")),setTimeout(function(){f.$slide.removeClass("lg-current"),f.$slide.eq(b).addClass("lg-current"),f.$outer.removeClass("lg-no-trans")},50);f.lGalleryOn?(setTimeout(function(){f.loadContent(b,!0,0)},this.s.speed+50),setTimeout(function(){f.lgBusy=!1,f.$el.trigger("onAfterSlide.lg",[e,b,c,d])},this.s.speed)):(f.loadContent(b,!0,f.s.backdropDuration),f.lgBusy=!1,f.$el.trigger("onAfterSlide.lg",[e,b,c,d])),f.lGalleryOn=!0,this.s.counter&&a("#lg-counter-current").text(b+1)}}},e.prototype.goToNextSlide=function(a){var b=this;b.lgBusy||(b.index+10?(b.index--,b.$el.trigger("onBeforePrevSlide.lg",[b.index,a]),b.slide(b.index,a,!1)):b.s.loop?(b.index=b.$items.length-1,b.$el.trigger("onBeforePrevSlide.lg",[b.index,a]),b.slide(b.index,a,!1)):b.s.slideEndAnimatoin&&(b.$outer.addClass("lg-left-end"),setTimeout(function(){b.$outer.removeClass("lg-left-end")},400)))},e.prototype.keyPress=function(){var c=this;this.$items.length>1&&a(b).on("keyup.lg",function(a){c.$items.length>1&&(37===a.keyCode&&(a.preventDefault(),c.goToPrevSlide()),39===a.keyCode&&(a.preventDefault(),c.goToNextSlide()))}),a(b).on("keydown.lg",function(a){c.s.escKey===!0&&27===a.keyCode&&(a.preventDefault(),c.$outer.hasClass("lg-thumb-open")?c.$outer.removeClass("lg-thumb-open"):c.destroy())})},e.prototype.arrow=function(){var a=this;this.$outer.find(".lg-prev").on("click.lg",function(){a.goToPrevSlide()}),this.$outer.find(".lg-next").on("click.lg",function(){a.goToNextSlide()})},e.prototype.arrowDisable=function(a){!this.s.loop&&this.s.hideControlOnEnd&&(a+10?this.$outer.find(".lg-prev").removeAttr("disabled").removeClass("disabled"):this.$outer.find(".lg-prev").attr("disabled","disabled").addClass("disabled"))},e.prototype.setTranslate=function(a,b,c){this.s.useLeft?a.css("left",b):a.css({transform:"translate3d("+b+"px, "+c+"px, 0px)"})},e.prototype.touchMove=function(b,c){var d=c-b;Math.abs(d)>15&&(this.$outer.addClass("lg-dragging"),this.setTranslate(this.$slide.eq(this.index),d,0),this.setTranslate(a(".lg-prev-slide"),-this.$slide.eq(this.index).width()+d,0),this.setTranslate(a(".lg-next-slide"),this.$slide.eq(this.index).width()+d,0))},e.prototype.touchEnd=function(a){var b=this;"lg-slide"!==b.s.mode&&b.$outer.addClass("lg-slide"),this.$slide.not(".lg-current, .lg-prev-slide, .lg-next-slide").css("opacity","0"),setTimeout(function(){b.$outer.removeClass("lg-dragging"),0>a&&Math.abs(a)>b.s.swipeThreshold?b.goToNextSlide(!0):a>0&&Math.abs(a)>b.s.swipeThreshold?b.goToPrevSlide(!0):Math.abs(a)<5&&b.$el.trigger("onSlideClick.lg"),b.$slide.removeAttr("style")}),setTimeout(function(){b.$outer.hasClass("lg-dragging")||"lg-slide"===b.s.mode||b.$outer.removeClass("lg-slide")},b.s.speed+100)},e.prototype.enableSwipe=function(){var a=this,b=0,c=0,d=!1;a.s.enableSwipe&&a.isTouch&&a.doCss()&&(a.$slide.on("touchstart.lg",function(c){a.$outer.hasClass("lg-zoomed")||a.lgBusy||(c.preventDefault(),a.manageSwipeClass(),b=c.originalEvent.targetTouches[0].pageX)}),a.$slide.on("touchmove.lg",function(e){a.$outer.hasClass("lg-zoomed")||(e.preventDefault(),c=e.originalEvent.targetTouches[0].pageX,a.touchMove(b,c),d=!0)}),a.$slide.on("touchend.lg",function(){a.$outer.hasClass("lg-zoomed")||(d?(d=!1,a.touchEnd(c-b)):a.$el.trigger("onSlideClick.lg"))}))},e.prototype.enableDrag=function(){var c=this,d=0,e=0,f=!1,g=!1;c.s.enableDrag&&!c.isTouch&&c.doCss()&&(c.$slide.on("mousedown.lg",function(b){c.$outer.hasClass("lg-zoomed")||(a(b.target).hasClass("lg-object")||a(b.target).hasClass("lg-video-play"))&&(b.preventDefault(),c.lgBusy||(c.manageSwipeClass(),d=b.pageX,f=!0,c.$outer.scrollLeft+=1,c.$outer.scrollLeft-=1,c.$outer.removeClass("lg-grab").addClass("lg-grabbing"),c.$el.trigger("onDragstart.lg")))}),a(b).on("mousemove.lg",function(a){f&&(g=!0,e=a.pageX,c.touchMove(d,e),c.$el.trigger("onDragmove.lg"))}),a(b).on("mouseup.lg",function(b){g?(g=!1,c.touchEnd(e-d),c.$el.trigger("onDragend.lg")):(a(b.target).hasClass("lg-object")||a(b.target).hasClass("lg-video-play"))&&c.$el.trigger("onSlideClick.lg"),f&&(f=!1,c.$outer.removeClass("lg-grabbing").addClass("lg-grab"))}))},e.prototype.manageSwipeClass=function(){var a=this.index+1,b=this.index-1,c=this.$slide.length;this.s.loop&&(0===this.index?b=c-1:this.index===c-1&&(a=0)),this.$slide.removeClass("lg-next-slide lg-prev-slide"),b>-1&&this.$slide.eq(b).addClass("lg-prev-slide"),this.$slide.eq(a).addClass("lg-next-slide")},e.prototype.mousewheel=function(){var a=this;a.$outer.on("mousewheel.lg",function(b){b.deltaY&&(b.deltaY>0?a.goToPrevSlide():a.goToNextSlide(),b.preventDefault())})},e.prototype.closeGallery=function(){var b=this,c=!1;this.$outer.find(".lg-close").on("click.lg",function(){b.destroy()}),b.s.closable&&(b.$outer.on("mousedown.lg",function(b){c=!!(a(b.target).is(".lg-outer")||a(b.target).is(".lg-item ")||a(b.target).is(".lg-img-wrap"))}),b.$outer.on("mouseup.lg",function(d){(a(d.target).is(".lg-outer")||a(d.target).is(".lg-item ")||a(d.target).is(".lg-img-wrap")&&c)&&(b.$outer.hasClass("lg-dragging")||b.destroy())}))},e.prototype.destroy=function(c){var d=this;c||d.$el.trigger("onBeforeClose.lg"),a(b).scrollTop(d.prevScrollTop),c&&(d.s.dynamic||this.$items.off("click.lg click.lgcustom"),a.removeData(d.el,"lightGallery")),this.$el.off(".lg.tm"),a.each(a.fn.lightGallery.modules,function(a){d.modules[a]&&d.modules[a].destroy()}),this.lGalleryOn=!1,clearTimeout(d.hideBartimeout),this.hideBartimeout=!1,a(b).off(".lg"),a("body").removeClass("lg-on lg-from-hash"),d.$outer&&d.$outer.removeClass("lg-visible"),a(".lg-backdrop").removeClass("in"),setTimeout(function(){d.$outer&&d.$outer.remove(),a(".lg-backdrop").remove(),c||d.$el.trigger("onCloseAfter.lg")},d.s.backdropDuration+50)},a.fn.lightGallery=function(b){return this.each(function(){if(a.data(this,"lightGallery"))try{a(this).data("lightGallery").init()}catch(c){console.error("lightGallery has not initiated properly")}else a.data(this,"lightGallery",new e(this,b))})},a.fn.lightGallery.modules={}}(jQuery,window,document),function(a,b,c,d){"use strict";var e={autoplay:!1,pause:5e3,progressBar:!0,fourceAutoplay:!1,autoplayControls:!0,appendAutoplayControlsTo:".lg-toolbar"},f=function(b){return this.core=a(b).data("lightGallery"),this.$el=a(b),this.core.$items.length<2?!1:(this.core.s=a.extend({},e,this.core.s),this.interval=!1,this.fromAuto=!0,this.canceledOnTouch=!1,this.fourceAutoplayTemp=this.core.s.fourceAutoplay,this.core.doCss()||(this.core.s.progressBar=!1),this.init(),this)};f.prototype.init=function(){var a=this;a.core.s.autoplayControls&&a.controls(),a.core.s.progressBar&&a.core.$outer.find(".lg").append('
    '),a.progress(),a.core.s.autoplay&&a.startlAuto(),a.$el.on("onDragstart.lg.tm touchstart.lg.tm",function(){a.interval&&(a.cancelAuto(),a.canceledOnTouch=!0)}),a.$el.on("onDragend.lg.tm touchend.lg.tm onSlideClick.lg.tm",function(){!a.interval&&a.canceledOnTouch&&(a.startlAuto(),a.canceledOnTouch=!1)})},f.prototype.progress=function(){var a,b,c=this;c.$el.on("onBeforeSlide.lg.tm",function(){c.core.s.progressBar&&c.fromAuto&&(a=c.core.$outer.find(".lg-progress-bar"),b=c.core.$outer.find(".lg-progress"),c.interval&&(b.removeAttr("style"),a.removeClass("lg-start"),setTimeout(function(){b.css("transition","width "+(c.core.s.speed+c.core.s.pause)+"ms ease 0s"),a.addClass("lg-start")},20))),c.fromAuto||c.core.s.fourceAutoplay||c.cancelAuto(),c.fromAuto=!1})},f.prototype.controls=function(){var b=this,c='';a(this.core.s.appendAutoplayControlsTo).append(c),b.core.$outer.find(".lg-autoplay-button").on("click.lg",function(){a(b.core.$outer).hasClass("lg-show-autoplay")?(b.cancelAuto(),b.core.s.fourceAutoplay=!1):b.interval||(b.startlAuto(),b.core.s.fourceAutoplay=b.fourceAutoplayTemp)})},f.prototype.startlAuto=function(){var a=this;a.core.$outer.find(".lg-progress").css("transition","width "+(a.core.s.speed+a.core.s.pause)+"ms ease 0s"),a.core.$outer.addClass("lg-show-autoplay"),a.core.$outer.find(".lg-progress-bar").addClass("lg-start"),a.interval=setInterval(function(){a.core.index+11&&this.init(),this};f.prototype.init=function(){var b,c,d,e=this,f="";if(e.core.$outer.find(".lg").append('
    '),e.core.s.dynamic)for(var g=0;g
    ';else e.core.$items.each(function(){f+=e.core.s.exThumbImage?'
    ':'
    '});c=e.core.$outer.find(".lg-pager-outer"),c.html(f),b=e.core.$outer.find(".lg-pager-cont"),b.on("click.lg touchend.lg",function(){var b=a(this);e.core.index=b.index(),e.core.slide(e.core.index,!1,!1)}),c.on("mouseover.lg",function(){clearTimeout(d),c.addClass("lg-pager-hover")}),c.on("mouseout.lg",function(){d=setTimeout(function(){c.removeClass("lg-pager-hover")})}),e.core.$el.on("onBeforeSlide.lg.tm",function(a,c,d){b.removeClass("lg-pager-active"),b.eq(d).addClass("lg-pager-active")})},f.prototype.destroy=function(){},a.fn.lightGallery.modules.pager=f}(jQuery,window,document),function(a,b,c,d){"use strict";var e={thumbnail:!0,animateThumb:!0,currentPagerPosition:"middle",thumbWidth:100,thumbContHeight:100,thumbMargin:5,exThumbImage:!1,showThumbByDefault:!0,toogleThumb:!0,pullCaptionUp:!0,enableThumbDrag:!0,enableThumbSwipe:!0,swipeThreshold:50,loadYoutubeThumbnail:!0,youtubeThumbSize:1,loadVimeoThumbnail:!0,vimeoThumbSize:"thumbnail_small",loadDailymotionThumbnail:!0},f=function(b){return this.core=a(b).data("lightGallery"),this.core.s=a.extend({},e,this.core.s),this.$el=a(b),this.$thumbOuter=null,this.thumbOuterWidth=0,this.thumbTotalWidth=this.core.$items.length*(this.core.s.thumbWidth+this.core.s.thumbMargin),this.thumbIndex=this.core.index,this.left=0,this.init(),this};f.prototype.init=function(){var a=this;this.core.s.thumbnail&&this.core.$items.length>1&&(this.core.s.showThumbByDefault&&setTimeout(function(){a.core.$outer.addClass("lg-thumb-open")},700),this.core.s.pullCaptionUp&&this.core.$outer.addClass("lg-pull-caption-up"),this.build(),this.core.s.animateThumb?(this.core.s.enableThumbDrag&&!this.core.isTouch&&this.core.doCss()&&this.enableThumbDrag(),this.core.s.enableThumbSwipe&&this.core.isTouch&&this.core.doCss()&&this.enableThumbSwipe(),this.thumbClickable=!1):this.thumbClickable=!0,this.toogle(),this.thumbkeyPress())},f.prototype.build=function(){function c(a,b,c){var d,h=e.core.isVideo(a,c)||{},i="";h.youtube||h.vimeo||h.dailymotion?h.youtube?d=e.core.s.loadYoutubeThumbnail?"//img.youtube.com/vi/"+h.youtube[1]+"/"+e.core.s.youtubeThumbSize+".jpg":b:h.vimeo?e.core.s.loadVimeoThumbnail?(d="//i.vimeocdn.com/video/error_"+g+".jpg",i=h.vimeo[1]):d=b:h.dailymotion&&(d=e.core.s.loadDailymotionThumbnail?"//www.dailymotion.com/thumbnail/video/"+h.dailymotion[1]:b):d=b,f+='
    ',i=""}var d,e=this,f="",g="",h='
    ';switch(this.core.s.vimeoThumbSize){case"thumbnail_large":g="640";break;case"thumbnail_medium":g="200x150";break;case"thumbnail_small":g="100x75"}if(e.core.$outer.addClass("lg-has-thumb"),e.core.$outer.find(".lg").append(h),e.$thumbOuter=e.core.$outer.find(".lg-thumb-outer"),e.thumbOuterWidth=e.$thumbOuter.width(),e.core.s.animateThumb&&e.core.$outer.find(".lg-thumb").css({width:e.thumbTotalWidth+"px",position:"relative"}),this.core.s.animateThumb&&e.$thumbOuter.css("height",e.core.s.thumbContHeight+"px"),e.core.s.dynamic)for(var i=0;ithis.thumbTotalWidth-this.thumbOuterWidth&&(this.left=this.thumbTotalWidth-this.thumbOuterWidth),this.left<0&&(this.left=0),this.core.lGalleryOn?(b.hasClass("on")||this.core.$outer.find(".lg-thumb").css("transition-duration",this.core.s.speed+"ms"),this.core.doCss()||b.animate({left:-this.left+"px"},this.core.s.speed)):this.core.doCss()||b.css("left",-this.left+"px"),this.setTranslate(this.left)}},f.prototype.enableThumbDrag=function(){var c=this,d=0,e=0,f=!1,g=!1,h=0;c.$thumbOuter.addClass("lg-grab"),c.core.$outer.find(".lg-thumb").on("mousedown.lg.thumb",function(a){c.thumbTotalWidth>c.thumbOuterWidth&&(a.preventDefault(),d=a.pageX,f=!0,c.core.$outer.scrollLeft+=1,c.core.$outer.scrollLeft-=1,c.thumbClickable=!1,c.$thumbOuter.removeClass("lg-grab").addClass("lg-grabbing"))}),a(b).on("mousemove.lg.thumb",function(a){f&&(h=c.left,g=!0,e=a.pageX,c.$thumbOuter.addClass("lg-dragging"),h-=e-d,h>c.thumbTotalWidth-c.thumbOuterWidth&&(h=c.thumbTotalWidth-c.thumbOuterWidth),0>h&&(h=0),c.setTranslate(h))}),a(b).on("mouseup.lg.thumb",function(){g?(g=!1,c.$thumbOuter.removeClass("lg-dragging"),c.left=h,Math.abs(e-d)a.thumbOuterWidth&&(c.preventDefault(),b=c.originalEvent.targetTouches[0].pageX,a.thumbClickable=!1)}),a.core.$outer.find(".lg-thumb").on("touchmove.lg",function(f){a.thumbTotalWidth>a.thumbOuterWidth&&(f.preventDefault(),c=f.originalEvent.targetTouches[0].pageX,d=!0,a.$thumbOuter.addClass("lg-dragging"),e=a.left,e-=c-b,e>a.thumbTotalWidth-a.thumbOuterWidth&&(e=a.thumbTotalWidth-a.thumbOuterWidth),0>e&&(e=0),a.setTranslate(e))}),a.core.$outer.find(".lg-thumb").on("touchend.lg",function(){a.thumbTotalWidth>a.thumbOuterWidth&&d?(d=!1,a.$thumbOuter.removeClass("lg-dragging"),Math.abs(c-b)'),a.core.$outer.find(".lg-toogle-thumb").on("click.lg",function(){a.core.$outer.toggleClass("lg-thumb-open")}))},f.prototype.thumbkeyPress=function(){var c=this;a(b).on("keydown.lg.thumb",function(a){38===a.keyCode?(a.preventDefault(),c.core.$outer.addClass("lg-thumb-open")):40===a.keyCode&&(a.preventDefault(),c.core.$outer.removeClass("lg-thumb-open"))})},f.prototype.destroy=function(){this.core.s.thumbnail&&this.core.$items.length>1&&(a(b).off("resize.lg.thumb orientationchange.lg.thumb keydown.lg.thumb"),this.$thumbOuter.remove(),this.core.$outer.removeClass("lg-has-thumb"))},a.fn.lightGallery.modules.Thumbnail=f}(jQuery,window,document),function(a,b,c,d){"use strict";var e={videoMaxWidth:"855px",youtubePlayerParams:!1,vimeoPlayerParams:!1,dailymotionPlayerParams:!1,vkPlayerParams:!1,videojs:!1,videojsOptions:{}},f=function(b){return this.core=a(b).data("lightGallery"),this.$el=a(b),this.core.s=a.extend({},e,this.core.s),this.videoLoaded=!1,this.init(),this};f.prototype.init=function(){var b=this;b.core.$el.on("hasVideo.lg.tm",function(a,c,d,e){if(b.core.$slide.eq(c).find(".lg-video").append(b.loadVideo(d,"lg-object",!0,c,e)),e)if(b.core.s.videojs)try{videojs(b.core.$slide.eq(c).find(".lg-html5").get(0),b.core.s.videojsOptions,function(){b.videoLoaded||this.play()})}catch(f){console.error("Make sure you have included videojs")}else b.core.$slide.eq(c).find(".lg-html5").get(0).play()}),b.core.$el.on("onAferAppendSlide.lg.tm",function(a,c){b.core.$slide.eq(c).find(".lg-video-cont").css("max-width",b.core.s.videoMaxWidth),b.videoLoaded=!0});var c=function(a){if(a.find(".lg-object").hasClass("lg-has-poster")&&a.find(".lg-object").is(":visible"))if(a.hasClass("lg-has-video")){var c=a.find(".lg-youtube").get(0),d=a.find(".lg-vimeo").get(0),e=a.find(".lg-dailymotion").get(0),f=a.find(".lg-html5").get(0);if(c)c.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}',"*");else if(d)try{$f(d).api("play")}catch(g){console.error("Make sure you have included froogaloop2 js")}else if(e)e.contentWindow.postMessage("play","*");else if(f)if(b.core.s.videojs)try{videojs(f).play()}catch(g){console.error("Make sure you have included videojs")}else f.play();a.addClass("lg-video-playing")}else{ -a.addClass("lg-video-playing lg-has-video");var h,i,j=function(c,d){if(a.find(".lg-video").append(b.loadVideo(c,"",!1,b.core.index,d)),d)if(b.core.s.videojs)try{videojs(b.core.$slide.eq(b.core.index).find(".lg-html5").get(0),b.core.s.videojsOptions,function(){this.play()})}catch(e){console.error("Make sure you have included videojs")}else b.core.$slide.eq(b.core.index).find(".lg-html5").get(0).play()};b.core.s.dynamic?(h=b.core.s.dynamicEl[b.core.index].src,i=b.core.s.dynamicEl[b.core.index].html,j(h,i)):(h=b.core.$items.eq(b.core.index).attr("href")||b.core.$items.eq(b.core.index).attr("data-src"),i=b.core.$items.eq(b.core.index).attr("data-html"),j(h,i));var k=a.find(".lg-object");a.find(".lg-video").append(k),a.find(".lg-video-object").hasClass("lg-html5")||(a.removeClass("lg-complete"),a.find(".lg-video-object").on("load.lg error.lg",function(){a.addClass("lg-complete")}))}};b.core.doCss()&&b.core.$items.length>1&&(b.core.s.enableSwipe&&b.core.isTouch||b.core.s.enableDrag&&!b.core.isTouch)?b.core.$el.on("onSlideClick.lg.tm",function(){var a=b.core.$slide.eq(b.core.index);c(a)}):b.core.$slide.on("click.lg",function(){c(a(this))}),b.core.$el.on("onBeforeSlide.lg.tm",function(c,d,e){var f=b.core.$slide.eq(d),g=f.find(".lg-youtube").get(0),h=f.find(".lg-vimeo").get(0),i=f.find(".lg-dailymotion").get(0),j=f.find(".lg-vk").get(0),k=f.find(".lg-html5").get(0);if(g)g.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}',"*");else if(h)try{$f(h).api("pause")}catch(l){console.error("Make sure you have included froogaloop2 js")}else if(i)i.contentWindow.postMessage("pause","*");else if(k)if(b.core.s.videojs)try{videojs(k).pause()}catch(l){console.error("Make sure you have included videojs")}else k.pause();j&&a(j).attr("src",a(j).attr("src").replace("&autoplay","&noplay"));var m;m=b.core.s.dynamic?b.core.s.dynamicEl[e].src:b.core.$items.eq(e).attr("href")||b.core.$items.eq(e).attr("data-src");var n=b.core.isVideo(m,e)||{};(n.youtube||n.vimeo||n.dailymotion||n.vk)&&b.core.$outer.addClass("lg-hide-download")}),b.core.$el.on("onAfterSlide.lg.tm",function(a,c){b.core.$slide.eq(c).removeClass("lg-video-playing")})},f.prototype.loadVideo=function(b,c,d,e,f){var g="",h=1,i="",j=this.core.isVideo(b,e)||{};if(d&&(h=this.videoLoaded?0:1),j.youtube)i="?wmode=opaque&autoplay="+h+"&enablejsapi=1",this.core.s.youtubePlayerParams&&(i=i+"&"+a.param(this.core.s.youtubePlayerParams)),g='';else if(j.vimeo)i="?autoplay="+h+"&api=1",this.core.s.vimeoPlayerParams&&(i=i+"&"+a.param(this.core.s.vimeoPlayerParams)),g='';else if(j.dailymotion)i="?wmode=opaque&autoplay="+h+"&api=postMessage",this.core.s.dailymotionPlayerParams&&(i=i+"&"+a.param(this.core.s.dailymotionPlayerParams)),g='';else if(j.html5){var k=f.substring(0,1);"."!==k&&"#"!==k||(f=a(f).html()),g=f}else j.vk&&(i="&autoplay="+h,this.core.s.vkPlayerParams&&(i=i+"&"+a.param(this.core.s.vkPlayerParams)),g='');return g},f.prototype.destroy=function(){this.videoLoaded=!1},a.fn.lightGallery.modules.video=f}(jQuery,window,document),function(a,b,c,d){"use strict";var e={scale:1,zoom:!0,actualSize:!0,enableZoomAfter:300},f=function(c){return this.core=a(c).data("lightGallery"),this.core.s=a.extend({},e,this.core.s),this.core.s.zoom&&this.core.doCss()&&(this.init(),this.zoomabletimeout=!1,this.pageX=a(b).width()/2,this.pageY=a(b).height()/2+a(b).scrollTop()),this};f.prototype.init=function(){var c=this,d='';c.core.s.actualSize&&(d+=''),this.core.$outer.find(".lg-toolbar").append(d),c.core.$el.on("onSlideItemLoad.lg.tm.zoom",function(b,d,e){var f=c.core.s.enableZoomAfter+e;a("body").hasClass("lg-from-hash")&&e?f=0:a("body").removeClass("lg-from-hash"),c.zoomabletimeout=setTimeout(function(){c.core.$slide.eq(d).addClass("lg-zoomable")},f+30)});var e=1,f=function(d){var e,f,g=c.core.$outer.find(".lg-current .lg-image"),h=(a(b).width()-g.width())/2,i=(a(b).height()-g.height())/2+a(b).scrollTop();e=c.pageX-h,f=c.pageY-i;var j=(d-1)*e,k=(d-1)*f;g.css("transform","scale3d("+d+", "+d+", 1)").attr("data-scale",d),g.parent().css({left:-j+"px",top:-k+"px"}).attr("data-x",j).attr("data-y",k)},g=function(){e>1?c.core.$outer.addClass("lg-zoomed"):c.resetZoom(),1>e&&(e=1),f(e)},h=function(d,f,h,i){var j,k=f.width();j=c.core.s.dynamic?c.core.s.dynamicEl[h].width||f[0].naturalWidth||k:c.core.$items.eq(h).attr("data-width")||f[0].naturalWidth||k;var l;c.core.$outer.hasClass("lg-zoomed")?e=1:j>k&&(l=j/k,e=l||2),i?(c.pageX=a(b).width()/2,c.pageY=a(b).height()/2+a(b).scrollTop()):(c.pageX=d.pageX||d.originalEvent.targetTouches[0].pageX,c.pageY=d.pageY||d.originalEvent.targetTouches[0].pageY),g(),setTimeout(function(){c.core.$outer.removeClass("lg-grabbing").addClass("lg-grab")},10)},i=!1;c.core.$el.on("onAferAppendSlide.lg.tm.zoom",function(a,b){var d=c.core.$slide.eq(b).find(".lg-image");d.on("dblclick",function(a){h(a,d,b)}),d.on("touchstart",function(a){i?(clearTimeout(i),i=null,h(a,d,b)):i=setTimeout(function(){i=null},300),a.preventDefault()})}),a(b).on("resize.lg.zoom scroll.lg.zoom orientationchange.lg.zoom",function(){c.pageX=a(b).width()/2,c.pageY=a(b).height()/2+a(b).scrollTop(),f(e)}),a("#lg-zoom-out").on("click.lg",function(){c.core.$outer.find(".lg-current .lg-image").length&&(e-=c.core.s.scale,g())}),a("#lg-zoom-in").on("click.lg",function(){c.core.$outer.find(".lg-current .lg-image").length&&(e+=c.core.s.scale,g())}),a("#lg-actual-size").on("click.lg",function(a){h(a,c.core.$slide.eq(c.core.index).find(".lg-image"),c.core.index,!0)}),c.core.$el.on("onBeforeSlide.lg.tm",function(){e=1,c.resetZoom()}),c.core.isTouch||c.zoomDrag(),c.core.isTouch&&c.zoomSwipe()},f.prototype.resetZoom=function(){this.core.$outer.removeClass("lg-zoomed"),this.core.$slide.find(".lg-img-wrap").removeAttr("style data-x data-y"),this.core.$slide.find(".lg-image").removeAttr("style data-scale"),this.pageX=a(b).width()/2,this.pageY=a(b).height()/2+a(b).scrollTop()},f.prototype.zoomSwipe=function(){var a=this,b={},c={},d=!1,e=!1,f=!1;a.core.$slide.on("touchstart.lg",function(c){if(a.core.$outer.hasClass("lg-zoomed")){var d=a.core.$slide.eq(a.core.index).find(".lg-object");f=d.outerHeight()*d.attr("data-scale")>a.core.$outer.find(".lg").height(),e=d.outerWidth()*d.attr("data-scale")>a.core.$outer.find(".lg").width(),(e||f)&&(c.preventDefault(),b={x:c.originalEvent.targetTouches[0].pageX,y:c.originalEvent.targetTouches[0].pageY})}}),a.core.$slide.on("touchmove.lg",function(g){if(a.core.$outer.hasClass("lg-zoomed")){var h,i,j=a.core.$slide.eq(a.core.index).find(".lg-img-wrap");g.preventDefault(),d=!0,c={x:g.originalEvent.targetTouches[0].pageX,y:g.originalEvent.targetTouches[0].pageY},a.core.$outer.addClass("lg-zoom-dragging"),i=f?-Math.abs(j.attr("data-y"))+(c.y-b.y):-Math.abs(j.attr("data-y")),h=e?-Math.abs(j.attr("data-x"))+(c.x-b.x):-Math.abs(j.attr("data-x")),(Math.abs(c.x-b.x)>15||Math.abs(c.y-b.y)>15)&&j.css({left:h+"px",top:i+"px"})}}),a.core.$slide.on("touchend.lg",function(){a.core.$outer.hasClass("lg-zoomed")&&d&&(d=!1,a.core.$outer.removeClass("lg-zoom-dragging"),a.touchendZoom(b,c,e,f))})},f.prototype.zoomDrag=function(){var c=this,d={},e={},f=!1,g=!1,h=!1,i=!1;c.core.$slide.on("mousedown.lg.zoom",function(b){var e=c.core.$slide.eq(c.core.index).find(".lg-object");i=e.outerHeight()*e.attr("data-scale")>c.core.$outer.find(".lg").height(),h=e.outerWidth()*e.attr("data-scale")>c.core.$outer.find(".lg").width(),c.core.$outer.hasClass("lg-zoomed")&&a(b.target).hasClass("lg-object")&&(h||i)&&(b.preventDefault(),d={x:b.pageX,y:b.pageY},f=!0,c.core.$outer.scrollLeft+=1,c.core.$outer.scrollLeft-=1,c.core.$outer.removeClass("lg-grab").addClass("lg-grabbing"))}),a(b).on("mousemove.lg.zoom",function(a){if(f){var b,j,k=c.core.$slide.eq(c.core.index).find(".lg-img-wrap");g=!0,e={x:a.pageX,y:a.pageY},c.core.$outer.addClass("lg-zoom-dragging"),j=i?-Math.abs(k.attr("data-y"))+(e.y-d.y):-Math.abs(k.attr("data-y")),b=h?-Math.abs(k.attr("data-x"))+(e.x-d.x):-Math.abs(k.attr("data-x")),k.css({left:b+"px",top:j+"px"})}}),a(b).on("mouseup.lg.zoom",function(a){f&&(f=!1,c.core.$outer.removeClass("lg-zoom-dragging"),!g||d.x===e.x&&d.y===e.y||(e={x:a.pageX,y:a.pageY},c.touchendZoom(d,e,h,i)),g=!1),c.core.$outer.removeClass("lg-grabbing").addClass("lg-grab")})},f.prototype.touchendZoom=function(a,b,c,d){var e=this,f=e.core.$slide.eq(e.core.index).find(".lg-img-wrap"),g=e.core.$slide.eq(e.core.index).find(".lg-object"),h=-Math.abs(f.attr("data-x"))+(b.x-a.x),i=-Math.abs(f.attr("data-y"))+(b.y-a.y),j=(e.core.$outer.find(".lg").height()-g.outerHeight())/2,k=Math.abs(g.outerHeight()*Math.abs(g.attr("data-scale"))-e.core.$outer.find(".lg").height()+j),l=(e.core.$outer.find(".lg").width()-g.outerWidth())/2,m=Math.abs(g.outerWidth()*Math.abs(g.attr("data-scale"))-e.core.$outer.find(".lg").width()+l);(Math.abs(b.x-a.x)>15||Math.abs(b.y-a.y)>15)&&(d&&(-k>=i?i=-k:i>=-j&&(i=-j)),c&&(-m>=h?h=-m:h>=-l&&(h=-l)),d?f.attr("data-y",Math.abs(i)):i=-Math.abs(f.attr("data-y")),c?f.attr("data-x",Math.abs(h)):h=-Math.abs(f.attr("data-x")),f.css({left:h+"px",top:i+"px"}))},f.prototype.destroy=function(){var c=this;c.core.$el.off(".lg.zoom"),a(b).off(".lg.zoom"),c.core.$slide.off(".lg.zoom"),c.core.$el.off(".lg.tm.zoom"),c.resetZoom(),clearTimeout(c.zoomabletimeout),c.zoomabletimeout=!1},a.fn.lightGallery.modules.zoom=f}(jQuery,window,document),function(a,b,c,d){"use strict";var e={hash:!0},f=function(c){return this.core=a(c).data("lightGallery"),this.core.s=a.extend({},e,this.core.s),this.core.s.hash&&(this.oldHash=b.location.hash,this.init()),this};f.prototype.init=function(){var c,d=this;d.core.$el.on("onAfterSlide.lg.tm",function(a,c,e){b.location.hash="lg="+d.core.s.galleryId+"&slide="+e}),a(b).on("hashchange.lg.hash",function(){c=b.location.hash;var a=parseInt(c.split("&slide=")[1],10);c.indexOf("lg="+d.core.s.galleryId)>-1?d.core.slide(a,!1,!1):d.core.lGalleryOn&&d.core.destroy()})},f.prototype.destroy=function(){this.core.s.hash&&(this.oldHash&&this.oldHash.indexOf("lg="+this.core.s.galleryId)<0?b.location.hash=this.oldHash:history.pushState?history.pushState("",c.title,b.location.pathname+b.location.search):b.location.hash="",this.core.$el.off(".lg.hash"))},a.fn.lightGallery.modules.hash=f}(jQuery,window,document); \ No newline at end of file diff --git a/vendors/lightgallery/dist/js/lightgallery.js b/vendors/lightgallery/dist/js/lightgallery.js deleted file mode 100644 index 3bf2700407..0000000000 --- a/vendors/lightgallery/dist/js/lightgallery.js +++ /dev/null @@ -1,1317 +0,0 @@ -/*! lightgallery - v1.2.21 - 2016-06-28 -* http://sachinchoolur.github.io/lightGallery/ -* Copyright (c) 2016 Sachin N; Licensed Apache 2.0 */ -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - - mode: 'lg-slide', - - // Ex : 'ease' - cssEasing: 'ease', - - //'for jquery animation' - easing: 'linear', - speed: 600, - height: '100%', - width: '100%', - addClass: '', - startClass: 'lg-start-zoom', - backdropDuration: 150, - hideBarsDelay: 6000, - - useLeft: false, - - closable: true, - loop: true, - escKey: true, - keyPress: true, - controls: true, - slideEndAnimatoin: true, - hideControlOnEnd: false, - mousewheel: true, - - getCaptionFromTitleOrAlt: true, - - // .lg-item || '.lg-sub-html' - appendSubHtmlTo: '.lg-sub-html', - - subHtmlSelectorRelative: false, - - /** - * @desc number of preload slides - * will exicute only after the current slide is fully loaded. - * - * @ex you clicked on 4th image and if preload = 1 then 3rd slide and 5th - * slide will be loaded in the background after the 4th slide is fully loaded.. - * if preload is 2 then 2nd 3rd 5th 6th slides will be preloaded.. ... ... - * - */ - preload: 1, - showAfterLoad: true, - selector: '', - selectWithin: '', - nextHtml: '', - prevHtml: '', - - // 0, 1 - index: false, - - iframeMaxWidth: '100%', - - download: true, - counter: true, - appendCounterTo: '.lg-toolbar', - - swipeThreshold: 50, - enableSwipe: true, - enableDrag: true, - - dynamic: false, - dynamicEl: [], - galleryId: 1 - }; - - function Plugin(element, options) { - - // Current lightGallery element - this.el = element; - - // Current jquery element - this.$el = $(element); - - // lightGallery settings - this.s = $.extend({}, defaults, options); - - // When using dynamic mode, ensure dynamicEl is an array - if (this.s.dynamic && this.s.dynamicEl !== 'undefined' && this.s.dynamicEl.constructor === Array && !this.s.dynamicEl.length) { - throw ('When using dynamic mode, you must also define dynamicEl as an Array.'); - } - - // lightGallery modules - this.modules = {}; - - // false when lightgallery complete first slide; - this.lGalleryOn = false; - - this.lgBusy = false; - - // Timeout function for hiding controls; - this.hideBartimeout = false; - - // To determine browser supports for touch events; - this.isTouch = ('ontouchstart' in document.documentElement); - - // Disable hideControlOnEnd if sildeEndAnimation is true - if (this.s.slideEndAnimatoin) { - this.s.hideControlOnEnd = false; - } - - // Gallery items - if (this.s.dynamic) { - this.$items = this.s.dynamicEl; - } else { - if (this.s.selector === 'this') { - this.$items = this.$el; - } else if (this.s.selector !== '') { - if (this.s.selectWithin) { - this.$items = $(this.s.selectWithin).find(this.s.selector); - } else { - this.$items = this.$el.find($(this.s.selector)); - } - } else { - this.$items = this.$el.children(); - } - } - - // .lg-item - this.$slide = ''; - - // .lg-outer - this.$outer = ''; - - this.init(); - - return this; - } - - Plugin.prototype.init = function() { - - var _this = this; - - // s.preload should not be more than $item.length - if (_this.s.preload > _this.$items.length) { - _this.s.preload = _this.$items.length; - } - - // if dynamic option is enabled execute immediately - var _hash = window.location.hash; - if (_hash.indexOf('lg=' + this.s.galleryId) > 0) { - - _this.index = parseInt(_hash.split('&slide=')[1], 10); - - $('body').addClass('lg-from-hash'); - if (!$('body').hasClass('lg-on')) { - setTimeout(function() { - _this.build(_this.index); - $('body').addClass('lg-on'); - }); - } - } - - if (_this.s.dynamic) { - - _this.$el.trigger('onBeforeOpen.lg'); - - _this.index = _this.s.index || 0; - - // prevent accidental double execution - if (!$('body').hasClass('lg-on')) { - setTimeout(function() { - _this.build(_this.index); - $('body').addClass('lg-on'); - }); - } - } else { - - // Using different namespace for click because click event should not unbind if selector is same object('this') - _this.$items.on('click.lgcustom', function(event) { - - // For IE8 - try { - event.preventDefault(); - event.preventDefault(); - } catch (er) { - event.returnValue = false; - } - - _this.$el.trigger('onBeforeOpen.lg'); - - _this.index = _this.s.index || _this.$items.index(this); - - // prevent accidental double execution - if (!$('body').hasClass('lg-on')) { - _this.build(_this.index); - $('body').addClass('lg-on'); - } - }); - } - - }; - - Plugin.prototype.build = function(index) { - - var _this = this; - - _this.structure(); - - // module constructor - $.each($.fn.lightGallery.modules, function(key) { - _this.modules[key] = new $.fn.lightGallery.modules[key](_this.el); - }); - - // initiate slide function - _this.slide(index, false, false); - - if (_this.s.keyPress) { - _this.keyPress(); - } - - if (_this.$items.length > 1) { - - _this.arrow(); - - setTimeout(function() { - _this.enableDrag(); - _this.enableSwipe(); - }, 50); - - if (_this.s.mousewheel) { - _this.mousewheel(); - } - } - - _this.counter(); - - _this.closeGallery(); - - _this.$el.trigger('onAfterOpen.lg'); - - // Hide controllers if mouse doesn't move for some period - _this.$outer.on('mousemove.lg click.lg touchstart.lg', function() { - - _this.$outer.removeClass('lg-hide-items'); - - clearTimeout(_this.hideBartimeout); - - // Timeout will be cleared on each slide movement also - _this.hideBartimeout = setTimeout(function() { - _this.$outer.addClass('lg-hide-items'); - }, _this.s.hideBarsDelay); - - }); - - }; - - Plugin.prototype.structure = function() { - var list = ''; - var controls = ''; - var i = 0; - var subHtmlCont = ''; - var template; - var _this = this; - - $('body').append('
    '); - $('.lg-backdrop').css('transition-duration', this.s.backdropDuration + 'ms'); - - // Create gallery items - for (i = 0; i < this.$items.length; i++) { - list += '
    '; - } - - // Create controlls - if (this.s.controls && this.$items.length > 1) { - controls = '
    ' + - '
    ' + this.s.prevHtml + '
    ' + - '
    ' + this.s.nextHtml + '
    ' + - '
    '; - } - - if (this.s.appendSubHtmlTo === '.lg-sub-html') { - subHtmlCont = '
    '; - } - - template = '
    ' + - '
    ' + - '
    ' + list + '
    ' + - '
    ' + - '' + - '
    ' + - controls + - subHtmlCont + - '
    ' + - '
    '; - - $('body').append(template); - this.$outer = $('.lg-outer'); - this.$slide = this.$outer.find('.lg-item'); - - if (this.s.useLeft) { - this.$outer.addClass('lg-use-left'); - - // Set mode lg-slide if use left is true; - this.s.mode = 'lg-slide'; - } else { - this.$outer.addClass('lg-use-css3'); - } - - // For fixed height gallery - _this.setTop(); - $(window).on('resize.lg orientationchange.lg', function() { - setTimeout(function() { - _this.setTop(); - }, 100); - }); - - // add class lg-current to remove initial transition - this.$slide.eq(this.index).addClass('lg-current'); - - // add Class for css support and transition mode - if (this.doCss()) { - this.$outer.addClass('lg-css3'); - } else { - this.$outer.addClass('lg-css'); - - // Set speed 0 because no animation will happen if browser doesn't support css3 - this.s.speed = 0; - } - - this.$outer.addClass(this.s.mode); - - if (this.s.enableDrag && this.$items.length > 1) { - this.$outer.addClass('lg-grab'); - } - - if (this.s.showAfterLoad) { - this.$outer.addClass('lg-show-after-load'); - } - - if (this.doCss()) { - var $inner = this.$outer.find('.lg-inner'); - $inner.css('transition-timing-function', this.s.cssEasing); - $inner.css('transition-duration', this.s.speed + 'ms'); - } - - $('.lg-backdrop').addClass('in'); - - setTimeout(function() { - _this.$outer.addClass('lg-visible'); - }, this.s.backdropDuration); - - if (this.s.download) { - this.$outer.find('.lg-toolbar').append(''); - } - - // Store the current scroll top value to scroll back after closing the gallery.. - this.prevScrollTop = $(window).scrollTop(); - - }; - - // For fixed height gallery - Plugin.prototype.setTop = function() { - if (this.s.height !== '100%') { - var wH = $(window).height(); - var top = (wH - parseInt(this.s.height, 10)) / 2; - var $lGallery = this.$outer.find('.lg'); - if (wH >= parseInt(this.s.height, 10)) { - $lGallery.css('top', top + 'px'); - } else { - $lGallery.css('top', '0px'); - } - } - }; - - // Find css3 support - Plugin.prototype.doCss = function() { - // check for css animation support - var support = function() { - var transition = ['transition', 'MozTransition', 'WebkitTransition', 'OTransition', 'msTransition', 'KhtmlTransition']; - var root = document.documentElement; - var i = 0; - for (i = 0; i < transition.length; i++) { - if (transition[i] in root.style) { - return true; - } - } - }; - - if (support()) { - return true; - } - - return false; - }; - - /** - * @desc Check the given src is video - * @param {String} src - * @return {Object} video type - * Ex:{ youtube : ["//www.youtube.com/watch?v=c0asJgSyxcY", "c0asJgSyxcY"] } - */ - Plugin.prototype.isVideo = function(src, index) { - - var html; - if (this.s.dynamic) { - html = this.s.dynamicEl[index].html; - } else { - html = this.$items.eq(index).attr('data-html'); - } - - if (!src && html) { - return { - html5: true - }; - } - - var youtube = src.match(/\/\/(?:www\.)?youtu(?:\.be|be\.com)\/(?:watch\?v=|embed\/)?([a-z0-9\-\_\%]+)/i); - var vimeo = src.match(/\/\/(?:www\.)?vimeo.com\/([0-9a-z\-_]+)/i); - var dailymotion = src.match(/\/\/(?:www\.)?dai.ly\/([0-9a-z\-_]+)/i); - var vk = src.match(/\/\/(?:www\.)?(?:vk\.com|vkontakte\.ru)\/(?:video_ext\.php\?)(.*)/i); - - if (youtube) { - return { - youtube: youtube - }; - } else if (vimeo) { - return { - vimeo: vimeo - }; - } else if (dailymotion) { - return { - dailymotion: dailymotion - }; - } else if (vk) { - return { - vk: vk - }; - } - }; - - /** - * @desc Create image counter - * Ex: 1/10 - */ - Plugin.prototype.counter = function() { - if (this.s.counter) { - $(this.s.appendCounterTo).append('
    ' + (parseInt(this.index, 10) + 1) + ' / ' + this.$items.length + '
    '); - } - }; - - /** - * @desc add sub-html into the slide - * @param {Number} index - index of the slide - */ - Plugin.prototype.addHtml = function(index) { - var subHtml = null; - var subHtmlUrl; - var $currentEle; - if (this.s.dynamic) { - if (this.s.dynamicEl[index].subHtmlUrl) { - subHtmlUrl = this.s.dynamicEl[index].subHtmlUrl; - } else { - subHtml = this.s.dynamicEl[index].subHtml; - } - } else { - $currentEle = this.$items.eq(index); - if ($currentEle.attr('data-sub-html-url')) { - subHtmlUrl = $currentEle.attr('data-sub-html-url'); - } else { - subHtml = $currentEle.attr('data-sub-html'); - if (this.s.getCaptionFromTitleOrAlt && !subHtml) { - subHtml = $currentEle.attr('title') || $currentEle.find('img').first().attr('alt'); - } - } - } - - if (!subHtmlUrl) { - if (typeof subHtml !== 'undefined' && subHtml !== null) { - - // get first letter of subhtml - // if first letter starts with . or # get the html form the jQuery object - var fL = subHtml.substring(0, 1); - if (fL === '.' || fL === '#') { - if (this.s.subHtmlSelectorRelative && !this.s.dynamic) { - subHtml = $currentEle.find(subHtml).html(); - } else { - subHtml = $(subHtml).html(); - } - } - } else { - subHtml = ''; - } - } - - if (this.s.appendSubHtmlTo === '.lg-sub-html') { - - if (subHtmlUrl) { - this.$outer.find(this.s.appendSubHtmlTo).load(subHtmlUrl); - } else { - this.$outer.find(this.s.appendSubHtmlTo).html(subHtml); - } - - } else { - - if (subHtmlUrl) { - this.$slide.eq(index).load(subHtmlUrl); - } else { - this.$slide.eq(index).append(subHtml); - } - } - - // Add lg-empty-html class if title doesn't exist - if (typeof subHtml !== 'undefined' && subHtml !== null) { - if (subHtml === '') { - this.$outer.find(this.s.appendSubHtmlTo).addClass('lg-empty-html'); - } else { - this.$outer.find(this.s.appendSubHtmlTo).removeClass('lg-empty-html'); - } - } - - this.$el.trigger('onAfterAppendSubHtml.lg', [index]); - }; - - /** - * @desc Preload slides - * @param {Number} index - index of the slide - */ - Plugin.prototype.preload = function(index) { - var i = 1; - var j = 1; - for (i = 1; i <= this.s.preload; i++) { - if (i >= this.$items.length - index) { - break; - } - - this.loadContent(index + i, false, 0); - } - - for (j = 1; j <= this.s.preload; j++) { - if (index - j < 0) { - break; - } - - this.loadContent(index - j, false, 0); - } - }; - - /** - * @desc Load slide content into slide. - * @param {Number} index - index of the slide. - * @param {Boolean} rec - if true call loadcontent() function again. - * @param {Boolean} delay - delay for adding complete class. it is 0 except first time. - */ - Plugin.prototype.loadContent = function(index, rec, delay) { - - var _this = this; - var _hasPoster = false; - var _$img; - var _src; - var _poster; - var _srcset; - var _sizes; - var _html; - var getResponsiveSrc = function(srcItms) { - var rsWidth = []; - var rsSrc = []; - for (var i = 0; i < srcItms.length; i++) { - var __src = srcItms[i].split(' '); - - // Manage empty space - if (__src[0] === '') { - __src.splice(0, 1); - } - - rsSrc.push(__src[0]); - rsWidth.push(__src[1]); - } - - var wWidth = $(window).width(); - for (var j = 0; j < rsWidth.length; j++) { - if (parseInt(rsWidth[j], 10) > wWidth) { - _src = rsSrc[j]; - break; - } - } - }; - - if (_this.s.dynamic) { - - if (_this.s.dynamicEl[index].poster) { - _hasPoster = true; - _poster = _this.s.dynamicEl[index].poster; - } - - _html = _this.s.dynamicEl[index].html; - _src = _this.s.dynamicEl[index].src; - - if (_this.s.dynamicEl[index].responsive) { - var srcDyItms = _this.s.dynamicEl[index].responsive.split(','); - getResponsiveSrc(srcDyItms); - } - - _srcset = _this.s.dynamicEl[index].srcset; - _sizes = _this.s.dynamicEl[index].sizes; - - } else { - - if (_this.$items.eq(index).attr('data-poster')) { - _hasPoster = true; - _poster = _this.$items.eq(index).attr('data-poster'); - } - - _html = _this.$items.eq(index).attr('data-html'); - _src = _this.$items.eq(index).attr('href') || _this.$items.eq(index).attr('data-src'); - - if (_this.$items.eq(index).attr('data-responsive')) { - var srcItms = _this.$items.eq(index).attr('data-responsive').split(','); - getResponsiveSrc(srcItms); - } - - _srcset = _this.$items.eq(index).attr('data-srcset'); - _sizes = _this.$items.eq(index).attr('data-sizes'); - - } - - //if (_src || _srcset || _sizes || _poster) { - - var iframe = false; - if (_this.s.dynamic) { - if (_this.s.dynamicEl[index].iframe) { - iframe = true; - } - } else { - if (_this.$items.eq(index).attr('data-iframe') === 'true') { - iframe = true; - } - } - - var _isVideo = _this.isVideo(_src, index); - if (!_this.$slide.eq(index).hasClass('lg-loaded')) { - if (iframe) { - _this.$slide.eq(index).prepend('
    '); - } else if (_hasPoster) { - var videoClass = ''; - if (_isVideo && _isVideo.youtube) { - videoClass = 'lg-has-youtube'; - } else if (_isVideo && _isVideo.vimeo) { - videoClass = 'lg-has-vimeo'; - } else { - videoClass = 'lg-has-html5'; - } - - _this.$slide.eq(index).prepend('
    '); - - } else if (_isVideo) { - _this.$slide.eq(index).prepend('
    '); - _this.$el.trigger('hasVideo.lg', [index, _src, _html]); - } else { - _this.$slide.eq(index).prepend('
    '); - } - - _this.$el.trigger('onAferAppendSlide.lg', [index]); - - _$img = _this.$slide.eq(index).find('.lg-object'); - if (_sizes) { - _$img.attr('sizes', _sizes); - } - - if (_srcset) { - _$img.attr('srcset', _srcset); - try { - picturefill({ - elements: [_$img[0]] - }); - } catch (e) { - console.error('Make sure you have included Picturefill version 2'); - } - } - - if (this.s.appendSubHtmlTo !== '.lg-sub-html') { - _this.addHtml(index); - } - - _this.$slide.eq(index).addClass('lg-loaded'); - } - - _this.$slide.eq(index).find('.lg-object').on('load.lg error.lg', function() { - - // For first time add some delay for displaying the start animation. - var _speed = 0; - - // Do not change the delay value because it is required for zoom plugin. - // If gallery opened from direct url (hash) speed value should be 0 - if (delay && !$('body').hasClass('lg-from-hash')) { - _speed = delay; - } - - setTimeout(function() { - _this.$slide.eq(index).addClass('lg-complete'); - _this.$el.trigger('onSlideItemLoad.lg', [index, delay || 0]); - }, _speed); - - }); - - // @todo check load state for html5 videos - if (_isVideo && _isVideo.html5 && !_hasPoster) { - _this.$slide.eq(index).addClass('lg-complete'); - } - - if (rec === true) { - if (!_this.$slide.eq(index).hasClass('lg-complete')) { - _this.$slide.eq(index).find('.lg-object').on('load.lg error.lg', function() { - _this.preload(index); - }); - } else { - _this.preload(index); - } - } - - //} - }; - - /** - * @desc slide function for lightgallery - ** Slide() gets call on start - ** ** Set lg.on true once slide() function gets called. - ** Call loadContent() on slide() function inside setTimeout - ** ** On first slide we do not want any animation like slide of fade - ** ** So on first slide( if lg.on if false that is first slide) loadContent() should start loading immediately - ** ** Else loadContent() should wait for the transition to complete. - ** ** So set timeout s.speed + 50 - <=> ** loadContent() will load slide content in to the particular slide - ** ** It has recursion (rec) parameter. if rec === true loadContent() will call preload() function. - ** ** preload will execute only when the previous slide is fully loaded (images iframe) - ** ** avoid simultaneous image load - <=> ** Preload() will check for s.preload value and call loadContent() again accoring to preload value - ** loadContent() <====> Preload(); - - * @param {Number} index - index of the slide - * @param {Boolean} fromTouch - true if slide function called via touch event or mouse drag - * @param {Boolean} fromThumb - true if slide function called via thumbnail click - */ - Plugin.prototype.slide = function(index, fromTouch, fromThumb) { - - var _prevIndex = this.$outer.find('.lg-current').index(); - var _this = this; - - // Prevent if multiple call - // Required for hsh plugin - if (_this.lGalleryOn && (_prevIndex === index)) { - return; - } - - var _length = this.$slide.length; - var _time = _this.lGalleryOn ? this.s.speed : 0; - var _next = false; - var _prev = false; - - if (!_this.lgBusy) { - - if (this.s.download) { - var _src; - if (_this.s.dynamic) { - _src = _this.s.dynamicEl[index].downloadUrl !== false && (_this.s.dynamicEl[index].downloadUrl || _this.s.dynamicEl[index].src); - } else { - _src = _this.$items.eq(index).attr('data-download-url') !== 'false' && (_this.$items.eq(index).attr('data-download-url') || _this.$items.eq(index).attr('href') || _this.$items.eq(index).attr('data-src')); - - } - - if (_src) { - $('#lg-download').attr('href', _src); - _this.$outer.removeClass('lg-hide-download'); - } else { - _this.$outer.addClass('lg-hide-download'); - } - } - - this.$el.trigger('onBeforeSlide.lg', [_prevIndex, index, fromTouch, fromThumb]); - - _this.lgBusy = true; - - clearTimeout(_this.hideBartimeout); - - // Add title if this.s.appendSubHtmlTo === lg-sub-html - if (this.s.appendSubHtmlTo === '.lg-sub-html') { - - // wait for slide animation to complete - setTimeout(function() { - _this.addHtml(index); - }, _time); - } - - this.arrowDisable(index); - - if (!fromTouch) { - - // remove all transitions - _this.$outer.addClass('lg-no-trans'); - - this.$slide.removeClass('lg-prev-slide lg-next-slide'); - - if (index < _prevIndex) { - _prev = true; - if ((index === 0) && (_prevIndex === _length - 1) && !fromThumb) { - _prev = false; - _next = true; - } - } else if (index > _prevIndex) { - _next = true; - if ((index === _length - 1) && (_prevIndex === 0) && !fromThumb) { - _prev = true; - _next = false; - } - } - - if (_prev) { - - //prevslide - this.$slide.eq(index).addClass('lg-prev-slide'); - this.$slide.eq(_prevIndex).addClass('lg-next-slide'); - } else if (_next) { - - // next slide - this.$slide.eq(index).addClass('lg-next-slide'); - this.$slide.eq(_prevIndex).addClass('lg-prev-slide'); - } - - // give 50 ms for browser to add/remove class - setTimeout(function() { - _this.$slide.removeClass('lg-current'); - - //_this.$slide.eq(_prevIndex).removeClass('lg-current'); - _this.$slide.eq(index).addClass('lg-current'); - - // reset all transitions - _this.$outer.removeClass('lg-no-trans'); - }, 50); - } else { - - var touchPrev = index - 1; - var touchNext = index + 1; - - if ((index === 0) && (_prevIndex === _length - 1)) { - - // next slide - touchNext = 0; - touchPrev = _length - 1; - } else if ((index === _length - 1) && (_prevIndex === 0)) { - - // prev slide - touchNext = 0; - touchPrev = _length - 1; - } - - this.$slide.removeClass('lg-prev-slide lg-current lg-next-slide'); - _this.$slide.eq(touchPrev).addClass('lg-prev-slide'); - _this.$slide.eq(touchNext).addClass('lg-next-slide'); - _this.$slide.eq(index).addClass('lg-current'); - } - - if (_this.lGalleryOn) { - setTimeout(function() { - _this.loadContent(index, true, 0); - }, this.s.speed + 50); - - setTimeout(function() { - _this.lgBusy = false; - _this.$el.trigger('onAfterSlide.lg', [_prevIndex, index, fromTouch, fromThumb]); - }, this.s.speed); - - } else { - _this.loadContent(index, true, _this.s.backdropDuration); - - _this.lgBusy = false; - _this.$el.trigger('onAfterSlide.lg', [_prevIndex, index, fromTouch, fromThumb]); - } - - _this.lGalleryOn = true; - - if (this.s.counter) { - $('#lg-counter-current').text(index + 1); - } - - } - - }; - - /** - * @desc Go to next slide - * @param {Boolean} fromTouch - true if slide function called via touch event - */ - Plugin.prototype.goToNextSlide = function(fromTouch) { - var _this = this; - if (!_this.lgBusy) { - if ((_this.index + 1) < _this.$slide.length) { - _this.index++; - _this.$el.trigger('onBeforeNextSlide.lg', [_this.index]); - _this.slide(_this.index, fromTouch, false); - } else { - if (_this.s.loop) { - _this.index = 0; - _this.$el.trigger('onBeforeNextSlide.lg', [_this.index]); - _this.slide(_this.index, fromTouch, false); - } else if (_this.s.slideEndAnimatoin) { - _this.$outer.addClass('lg-right-end'); - setTimeout(function() { - _this.$outer.removeClass('lg-right-end'); - }, 400); - } - } - } - }; - - /** - * @desc Go to previous slide - * @param {Boolean} fromTouch - true if slide function called via touch event - */ - Plugin.prototype.goToPrevSlide = function(fromTouch) { - var _this = this; - if (!_this.lgBusy) { - if (_this.index > 0) { - _this.index--; - _this.$el.trigger('onBeforePrevSlide.lg', [_this.index, fromTouch]); - _this.slide(_this.index, fromTouch, false); - } else { - if (_this.s.loop) { - _this.index = _this.$items.length - 1; - _this.$el.trigger('onBeforePrevSlide.lg', [_this.index, fromTouch]); - _this.slide(_this.index, fromTouch, false); - } else if (_this.s.slideEndAnimatoin) { - _this.$outer.addClass('lg-left-end'); - setTimeout(function() { - _this.$outer.removeClass('lg-left-end'); - }, 400); - } - } - } - }; - - Plugin.prototype.keyPress = function() { - var _this = this; - if (this.$items.length > 1) { - $(window).on('keyup.lg', function(e) { - if (_this.$items.length > 1) { - if (e.keyCode === 37) { - e.preventDefault(); - _this.goToPrevSlide(); - } - - if (e.keyCode === 39) { - e.preventDefault(); - _this.goToNextSlide(); - } - } - }); - } - - $(window).on('keydown.lg', function(e) { - if (_this.s.escKey === true && e.keyCode === 27) { - e.preventDefault(); - if (!_this.$outer.hasClass('lg-thumb-open')) { - _this.destroy(); - } else { - _this.$outer.removeClass('lg-thumb-open'); - } - } - }); - }; - - Plugin.prototype.arrow = function() { - var _this = this; - this.$outer.find('.lg-prev').on('click.lg', function() { - _this.goToPrevSlide(); - }); - - this.$outer.find('.lg-next').on('click.lg', function() { - _this.goToNextSlide(); - }); - }; - - Plugin.prototype.arrowDisable = function(index) { - - // Disable arrows if s.hideControlOnEnd is true - if (!this.s.loop && this.s.hideControlOnEnd) { - if ((index + 1) < this.$slide.length) { - this.$outer.find('.lg-next').removeAttr('disabled').removeClass('disabled'); - } else { - this.$outer.find('.lg-next').attr('disabled', 'disabled').addClass('disabled'); - } - - if (index > 0) { - this.$outer.find('.lg-prev').removeAttr('disabled').removeClass('disabled'); - } else { - this.$outer.find('.lg-prev').attr('disabled', 'disabled').addClass('disabled'); - } - } - }; - - Plugin.prototype.setTranslate = function($el, xValue, yValue) { - // jQuery supports Automatic CSS prefixing since jQuery 1.8.0 - if (this.s.useLeft) { - $el.css('left', xValue); - } else { - $el.css({ - transform: 'translate3d(' + (xValue) + 'px, ' + yValue + 'px, 0px)' - }); - } - }; - - Plugin.prototype.touchMove = function(startCoords, endCoords) { - - var distance = endCoords - startCoords; - - if (Math.abs(distance) > 15) { - // reset opacity and transition duration - this.$outer.addClass('lg-dragging'); - - // move current slide - this.setTranslate(this.$slide.eq(this.index), distance, 0); - - // move next and prev slide with current slide - this.setTranslate($('.lg-prev-slide'), -this.$slide.eq(this.index).width() + distance, 0); - this.setTranslate($('.lg-next-slide'), this.$slide.eq(this.index).width() + distance, 0); - } - }; - - Plugin.prototype.touchEnd = function(distance) { - var _this = this; - - // keep slide animation for any mode while dragg/swipe - if (_this.s.mode !== 'lg-slide') { - _this.$outer.addClass('lg-slide'); - } - - this.$slide.not('.lg-current, .lg-prev-slide, .lg-next-slide').css('opacity', '0'); - - // set transition duration - setTimeout(function() { - _this.$outer.removeClass('lg-dragging'); - if ((distance < 0) && (Math.abs(distance) > _this.s.swipeThreshold)) { - _this.goToNextSlide(true); - } else if ((distance > 0) && (Math.abs(distance) > _this.s.swipeThreshold)) { - _this.goToPrevSlide(true); - } else if (Math.abs(distance) < 5) { - - // Trigger click if distance is less than 5 pix - _this.$el.trigger('onSlideClick.lg'); - } - - _this.$slide.removeAttr('style'); - }); - - // remove slide class once drag/swipe is completed if mode is not slide - setTimeout(function() { - if (!_this.$outer.hasClass('lg-dragging') && _this.s.mode !== 'lg-slide') { - _this.$outer.removeClass('lg-slide'); - } - }, _this.s.speed + 100); - - }; - - Plugin.prototype.enableSwipe = function() { - var _this = this; - var startCoords = 0; - var endCoords = 0; - var isMoved = false; - - if (_this.s.enableSwipe && _this.isTouch && _this.doCss()) { - - _this.$slide.on('touchstart.lg', function(e) { - if (!_this.$outer.hasClass('lg-zoomed') && !_this.lgBusy) { - e.preventDefault(); - _this.manageSwipeClass(); - startCoords = e.originalEvent.targetTouches[0].pageX; - } - }); - - _this.$slide.on('touchmove.lg', function(e) { - if (!_this.$outer.hasClass('lg-zoomed')) { - e.preventDefault(); - endCoords = e.originalEvent.targetTouches[0].pageX; - _this.touchMove(startCoords, endCoords); - isMoved = true; - } - }); - - _this.$slide.on('touchend.lg', function() { - if (!_this.$outer.hasClass('lg-zoomed')) { - if (isMoved) { - isMoved = false; - _this.touchEnd(endCoords - startCoords); - } else { - _this.$el.trigger('onSlideClick.lg'); - } - } - }); - } - - }; - - Plugin.prototype.enableDrag = function() { - var _this = this; - var startCoords = 0; - var endCoords = 0; - var isDraging = false; - var isMoved = false; - if (_this.s.enableDrag && !_this.isTouch && _this.doCss()) { - _this.$slide.on('mousedown.lg', function(e) { - // execute only on .lg-object - if (!_this.$outer.hasClass('lg-zoomed')) { - if ($(e.target).hasClass('lg-object') || $(e.target).hasClass('lg-video-play')) { - e.preventDefault(); - - if (!_this.lgBusy) { - _this.manageSwipeClass(); - startCoords = e.pageX; - isDraging = true; - - // ** Fix for webkit cursor issue https://code.google.com/p/chromium/issues/detail?id=26723 - _this.$outer.scrollLeft += 1; - _this.$outer.scrollLeft -= 1; - - // * - - _this.$outer.removeClass('lg-grab').addClass('lg-grabbing'); - - _this.$el.trigger('onDragstart.lg'); - } - - } - } - }); - - $(window).on('mousemove.lg', function(e) { - if (isDraging) { - isMoved = true; - endCoords = e.pageX; - _this.touchMove(startCoords, endCoords); - _this.$el.trigger('onDragmove.lg'); - } - }); - - $(window).on('mouseup.lg', function(e) { - if (isMoved) { - isMoved = false; - _this.touchEnd(endCoords - startCoords); - _this.$el.trigger('onDragend.lg'); - } else if ($(e.target).hasClass('lg-object') || $(e.target).hasClass('lg-video-play')) { - _this.$el.trigger('onSlideClick.lg'); - } - - // Prevent execution on click - if (isDraging) { - isDraging = false; - _this.$outer.removeClass('lg-grabbing').addClass('lg-grab'); - } - }); - - } - }; - - Plugin.prototype.manageSwipeClass = function() { - var touchNext = this.index + 1; - var touchPrev = this.index - 1; - var length = this.$slide.length; - if (this.s.loop) { - if (this.index === 0) { - touchPrev = length - 1; - } else if (this.index === length - 1) { - touchNext = 0; - } - } - - this.$slide.removeClass('lg-next-slide lg-prev-slide'); - if (touchPrev > -1) { - this.$slide.eq(touchPrev).addClass('lg-prev-slide'); - } - - this.$slide.eq(touchNext).addClass('lg-next-slide'); - }; - - Plugin.prototype.mousewheel = function() { - var _this = this; - _this.$outer.on('mousewheel.lg', function(e) { - - if (!e.deltaY) { - return; - } - - if (e.deltaY > 0) { - _this.goToPrevSlide(); - } else { - _this.goToNextSlide(); - } - - e.preventDefault(); - }); - - }; - - Plugin.prototype.closeGallery = function() { - - var _this = this; - var mousedown = false; - this.$outer.find('.lg-close').on('click.lg', function() { - _this.destroy(); - }); - - if (_this.s.closable) { - - // If you drag the slide and release outside gallery gets close on chrome - // for preventing this check mousedown and mouseup happened on .lg-item or lg-outer - _this.$outer.on('mousedown.lg', function(e) { - - if ($(e.target).is('.lg-outer') || $(e.target).is('.lg-item ') || $(e.target).is('.lg-img-wrap')) { - mousedown = true; - } else { - mousedown = false; - } - - }); - - _this.$outer.on('mouseup.lg', function(e) { - - if ($(e.target).is('.lg-outer') || $(e.target).is('.lg-item ') || $(e.target).is('.lg-img-wrap') && mousedown) { - if (!_this.$outer.hasClass('lg-dragging')) { - _this.destroy(); - } - } - - }); - - } - - }; - - Plugin.prototype.destroy = function(d) { - - var _this = this; - - if (!d) { - _this.$el.trigger('onBeforeClose.lg'); - } - - $(window).scrollTop(_this.prevScrollTop); - - /** - * if d is false or undefined destroy will only close the gallery - * plugins instance remains with the element - * - * if d is true destroy will completely remove the plugin - */ - - if (d) { - if (!_this.s.dynamic) { - // only when not using dynamic mode is $items a jquery collection - this.$items.off('click.lg click.lgcustom'); - } - - $.removeData(_this.el, 'lightGallery'); - } - - // Unbind all events added by lightGallery - this.$el.off('.lg.tm'); - - // Distroy all lightGallery modules - $.each($.fn.lightGallery.modules, function(key) { - if (_this.modules[key]) { - _this.modules[key].destroy(); - } - }); - - this.lGalleryOn = false; - - clearTimeout(_this.hideBartimeout); - this.hideBartimeout = false; - $(window).off('.lg'); - $('body').removeClass('lg-on lg-from-hash'); - - if (_this.$outer) { - _this.$outer.removeClass('lg-visible'); - } - - $('.lg-backdrop').removeClass('in'); - - setTimeout(function() { - if (_this.$outer) { - _this.$outer.remove(); - } - - $('.lg-backdrop').remove(); - - if (!d) { - _this.$el.trigger('onCloseAfter.lg'); - } - - }, _this.s.backdropDuration + 50); - }; - - $.fn.lightGallery = function(options) { - return this.each(function() { - if (!$.data(this, 'lightGallery')) { - $.data(this, 'lightGallery', new Plugin(this, options)); - } else { - try { - $(this).data('lightGallery').init(); - } catch (err) { - console.error('lightGallery has not initiated properly'); - } - } - }); - }; - - $.fn.lightGallery.modules = {}; - -})(jQuery, window, document); diff --git a/vendors/lightgallery/dist/js/lightgallery.min.js b/vendors/lightgallery/dist/js/lightgallery.min.js deleted file mode 100644 index 159815dd46..0000000000 --- a/vendors/lightgallery/dist/js/lightgallery.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! lightgallery - v1.2.21 - 2016-06-28 -* http://sachinchoolur.github.io/lightGallery/ -* Copyright (c) 2016 Sachin N; Licensed Apache 2.0 */ -!function(a,b,c,d){"use strict";function e(b,d){if(this.el=b,this.$el=a(b),this.s=a.extend({},f,d),this.s.dynamic&&"undefined"!==this.s.dynamicEl&&this.s.dynamicEl.constructor===Array&&!this.s.dynamicEl.length)throw"When using dynamic mode, you must also define dynamicEl as an Array.";return this.modules={},this.lGalleryOn=!1,this.lgBusy=!1,this.hideBartimeout=!1,this.isTouch="ontouchstart"in c.documentElement,this.s.slideEndAnimatoin&&(this.s.hideControlOnEnd=!1),this.s.dynamic?this.$items=this.s.dynamicEl:"this"===this.s.selector?this.$items=this.$el:""!==this.s.selector?this.s.selectWithin?this.$items=a(this.s.selectWithin).find(this.s.selector):this.$items=this.$el.find(a(this.s.selector)):this.$items=this.$el.children(),this.$slide="",this.$outer="",this.init(),this}var f={mode:"lg-slide",cssEasing:"ease",easing:"linear",speed:600,height:"100%",width:"100%",addClass:"",startClass:"lg-start-zoom",backdropDuration:150,hideBarsDelay:6e3,useLeft:!1,closable:!0,loop:!0,escKey:!0,keyPress:!0,controls:!0,slideEndAnimatoin:!0,hideControlOnEnd:!1,mousewheel:!0,getCaptionFromTitleOrAlt:!0,appendSubHtmlTo:".lg-sub-html",subHtmlSelectorRelative:!1,preload:1,showAfterLoad:!0,selector:"",selectWithin:"",nextHtml:"",prevHtml:"",index:!1,iframeMaxWidth:"100%",download:!0,counter:!0,appendCounterTo:".lg-toolbar",swipeThreshold:50,enableSwipe:!0,enableDrag:!0,dynamic:!1,dynamicEl:[],galleryId:1};e.prototype.init=function(){var c=this;c.s.preload>c.$items.length&&(c.s.preload=c.$items.length);var d=b.location.hash;d.indexOf("lg="+this.s.galleryId)>0&&(c.index=parseInt(d.split("&slide=")[1],10),a("body").addClass("lg-from-hash"),a("body").hasClass("lg-on")||setTimeout(function(){c.build(c.index),a("body").addClass("lg-on")})),c.s.dynamic?(c.$el.trigger("onBeforeOpen.lg"),c.index=c.s.index||0,a("body").hasClass("lg-on")||setTimeout(function(){c.build(c.index),a("body").addClass("lg-on")})):c.$items.on("click.lgcustom",function(b){try{b.preventDefault(),b.preventDefault()}catch(d){b.returnValue=!1}c.$el.trigger("onBeforeOpen.lg"),c.index=c.s.index||c.$items.index(this),a("body").hasClass("lg-on")||(c.build(c.index),a("body").addClass("lg-on"))})},e.prototype.build=function(b){var c=this;c.structure(),a.each(a.fn.lightGallery.modules,function(b){c.modules[b]=new a.fn.lightGallery.modules[b](c.el)}),c.slide(b,!1,!1),c.s.keyPress&&c.keyPress(),c.$items.length>1&&(c.arrow(),setTimeout(function(){c.enableDrag(),c.enableSwipe()},50),c.s.mousewheel&&c.mousewheel()),c.counter(),c.closeGallery(),c.$el.trigger("onAfterOpen.lg"),c.$outer.on("mousemove.lg click.lg touchstart.lg",function(){c.$outer.removeClass("lg-hide-items"),clearTimeout(c.hideBartimeout),c.hideBartimeout=setTimeout(function(){c.$outer.addClass("lg-hide-items")},c.s.hideBarsDelay)})},e.prototype.structure=function(){var c,d="",e="",f=0,g="",h=this;for(a("body").append('
    '),a(".lg-backdrop").css("transition-duration",this.s.backdropDuration+"ms"),f=0;f';if(this.s.controls&&this.$items.length>1&&(e='
    '+this.s.prevHtml+'
    '+this.s.nextHtml+"
    "),".lg-sub-html"===this.s.appendSubHtmlTo&&(g='
    '),c='
    '+d+'
    '+e+g+"
    ",a("body").append(c),this.$outer=a(".lg-outer"),this.$slide=this.$outer.find(".lg-item"),this.s.useLeft?(this.$outer.addClass("lg-use-left"),this.s.mode="lg-slide"):this.$outer.addClass("lg-use-css3"),h.setTop(),a(b).on("resize.lg orientationchange.lg",function(){setTimeout(function(){h.setTop()},100)}),this.$slide.eq(this.index).addClass("lg-current"),this.doCss()?this.$outer.addClass("lg-css3"):(this.$outer.addClass("lg-css"),this.s.speed=0),this.$outer.addClass(this.s.mode),this.s.enableDrag&&this.$items.length>1&&this.$outer.addClass("lg-grab"),this.s.showAfterLoad&&this.$outer.addClass("lg-show-after-load"),this.doCss()){var i=this.$outer.find(".lg-inner");i.css("transition-timing-function",this.s.cssEasing),i.css("transition-duration",this.s.speed+"ms")}a(".lg-backdrop").addClass("in"),setTimeout(function(){h.$outer.addClass("lg-visible")},this.s.backdropDuration),this.s.download&&this.$outer.find(".lg-toolbar").append(''),this.prevScrollTop=a(b).scrollTop()},e.prototype.setTop=function(){if("100%"!==this.s.height){var c=a(b).height(),d=(c-parseInt(this.s.height,10))/2,e=this.$outer.find(".lg");c>=parseInt(this.s.height,10)?e.css("top",d+"px"):e.css("top","0px")}},e.prototype.doCss=function(){var a=function(){var a=["transition","MozTransition","WebkitTransition","OTransition","msTransition","KhtmlTransition"],b=c.documentElement,d=0;for(d=0;d'+(parseInt(this.index,10)+1)+' / '+this.$items.length+"")},e.prototype.addHtml=function(b){var c,d,e=null;if(this.s.dynamic?this.s.dynamicEl[b].subHtmlUrl?c=this.s.dynamicEl[b].subHtmlUrl:e=this.s.dynamicEl[b].subHtml:(d=this.$items.eq(b),d.attr("data-sub-html-url")?c=d.attr("data-sub-html-url"):(e=d.attr("data-sub-html"),this.s.getCaptionFromTitleOrAlt&&!e&&(e=d.attr("title")||d.find("img").first().attr("alt")))),!c)if("undefined"!=typeof e&&null!==e){var f=e.substring(0,1);"."!==f&&"#"!==f||(e=this.s.subHtmlSelectorRelative&&!this.s.dynamic?d.find(e).html():a(e).html())}else e="";".lg-sub-html"===this.s.appendSubHtmlTo?c?this.$outer.find(this.s.appendSubHtmlTo).load(c):this.$outer.find(this.s.appendSubHtmlTo).html(e):c?this.$slide.eq(b).load(c):this.$slide.eq(b).append(e),"undefined"!=typeof e&&null!==e&&(""===e?this.$outer.find(this.s.appendSubHtmlTo).addClass("lg-empty-html"):this.$outer.find(this.s.appendSubHtmlTo).removeClass("lg-empty-html")),this.$el.trigger("onAfterAppendSubHtml.lg",[b])},e.prototype.preload=function(a){var b=1,c=1;for(b=1;b<=this.s.preload&&!(b>=this.$items.length-a);b++)this.loadContent(a+b,!1,0);for(c=1;c<=this.s.preload&&!(0>a-c);c++)this.loadContent(a-c,!1,0)},e.prototype.loadContent=function(c,d,e){var f,g,h,i,j,k,l=this,m=!1,n=function(c){for(var d=[],e=[],f=0;fi){g=e[j];break}};if(l.s.dynamic){if(l.s.dynamicEl[c].poster&&(m=!0,h=l.s.dynamicEl[c].poster),k=l.s.dynamicEl[c].html,g=l.s.dynamicEl[c].src,l.s.dynamicEl[c].responsive){var o=l.s.dynamicEl[c].responsive.split(",");n(o)}i=l.s.dynamicEl[c].srcset,j=l.s.dynamicEl[c].sizes}else{if(l.$items.eq(c).attr("data-poster")&&(m=!0,h=l.$items.eq(c).attr("data-poster")),k=l.$items.eq(c).attr("data-html"),g=l.$items.eq(c).attr("href")||l.$items.eq(c).attr("data-src"),l.$items.eq(c).attr("data-responsive")){var p=l.$items.eq(c).attr("data-responsive").split(",");n(p)}i=l.$items.eq(c).attr("data-srcset"),j=l.$items.eq(c).attr("data-sizes")}var q=!1;l.s.dynamic?l.s.dynamicEl[c].iframe&&(q=!0):"true"===l.$items.eq(c).attr("data-iframe")&&(q=!0);var r=l.isVideo(g,c);if(!l.$slide.eq(c).hasClass("lg-loaded")){if(q)l.$slide.eq(c).prepend('
    ');else if(m){var s="";s=r&&r.youtube?"lg-has-youtube":r&&r.vimeo?"lg-has-vimeo":"lg-has-html5",l.$slide.eq(c).prepend('
    ')}else r?(l.$slide.eq(c).prepend('
    '),l.$el.trigger("hasVideo.lg",[c,g,k])):l.$slide.eq(c).prepend('
    ');if(l.$el.trigger("onAferAppendSlide.lg",[c]),f=l.$slide.eq(c).find(".lg-object"),j&&f.attr("sizes",j),i){f.attr("srcset",i);try{picturefill({elements:[f[0]]})}catch(t){console.error("Make sure you have included Picturefill version 2")}}".lg-sub-html"!==this.s.appendSubHtmlTo&&l.addHtml(c),l.$slide.eq(c).addClass("lg-loaded")}l.$slide.eq(c).find(".lg-object").on("load.lg error.lg",function(){var b=0;e&&!a("body").hasClass("lg-from-hash")&&(b=e),setTimeout(function(){l.$slide.eq(c).addClass("lg-complete"),l.$el.trigger("onSlideItemLoad.lg",[c,e||0])},b)}),r&&r.html5&&!m&&l.$slide.eq(c).addClass("lg-complete"),d===!0&&(l.$slide.eq(c).hasClass("lg-complete")?l.preload(c):l.$slide.eq(c).find(".lg-object").on("load.lg error.lg",function(){l.preload(c)}))},e.prototype.slide=function(b,c,d){var e=this.$outer.find(".lg-current").index(),f=this;if(!f.lGalleryOn||e!==b){var g=this.$slide.length,h=f.lGalleryOn?this.s.speed:0,i=!1,j=!1;if(!f.lgBusy){if(this.s.download){var k;k=f.s.dynamic?f.s.dynamicEl[b].downloadUrl!==!1&&(f.s.dynamicEl[b].downloadUrl||f.s.dynamicEl[b].src):"false"!==f.$items.eq(b).attr("data-download-url")&&(f.$items.eq(b).attr("data-download-url")||f.$items.eq(b).attr("href")||f.$items.eq(b).attr("data-src")),k?(a("#lg-download").attr("href",k),f.$outer.removeClass("lg-hide-download")):f.$outer.addClass("lg-hide-download")}if(this.$el.trigger("onBeforeSlide.lg",[e,b,c,d]),f.lgBusy=!0,clearTimeout(f.hideBartimeout),".lg-sub-html"===this.s.appendSubHtmlTo&&setTimeout(function(){f.addHtml(b)},h),this.arrowDisable(b),c){var l=b-1,m=b+1;0===b&&e===g-1?(m=0,l=g-1):b===g-1&&0===e&&(m=0,l=g-1),this.$slide.removeClass("lg-prev-slide lg-current lg-next-slide"),f.$slide.eq(l).addClass("lg-prev-slide"),f.$slide.eq(m).addClass("lg-next-slide"),f.$slide.eq(b).addClass("lg-current")}else f.$outer.addClass("lg-no-trans"),this.$slide.removeClass("lg-prev-slide lg-next-slide"),e>b?(j=!0,0!==b||e!==g-1||d||(j=!1,i=!0)):b>e&&(i=!0,b!==g-1||0!==e||d||(j=!0,i=!1)),j?(this.$slide.eq(b).addClass("lg-prev-slide"),this.$slide.eq(e).addClass("lg-next-slide")):i&&(this.$slide.eq(b).addClass("lg-next-slide"),this.$slide.eq(e).addClass("lg-prev-slide")),setTimeout(function(){f.$slide.removeClass("lg-current"),f.$slide.eq(b).addClass("lg-current"),f.$outer.removeClass("lg-no-trans")},50);f.lGalleryOn?(setTimeout(function(){f.loadContent(b,!0,0)},this.s.speed+50),setTimeout(function(){f.lgBusy=!1,f.$el.trigger("onAfterSlide.lg",[e,b,c,d])},this.s.speed)):(f.loadContent(b,!0,f.s.backdropDuration),f.lgBusy=!1,f.$el.trigger("onAfterSlide.lg",[e,b,c,d])),f.lGalleryOn=!0,this.s.counter&&a("#lg-counter-current").text(b+1)}}},e.prototype.goToNextSlide=function(a){var b=this;b.lgBusy||(b.index+10?(b.index--,b.$el.trigger("onBeforePrevSlide.lg",[b.index,a]),b.slide(b.index,a,!1)):b.s.loop?(b.index=b.$items.length-1,b.$el.trigger("onBeforePrevSlide.lg",[b.index,a]),b.slide(b.index,a,!1)):b.s.slideEndAnimatoin&&(b.$outer.addClass("lg-left-end"),setTimeout(function(){b.$outer.removeClass("lg-left-end")},400)))},e.prototype.keyPress=function(){var c=this;this.$items.length>1&&a(b).on("keyup.lg",function(a){c.$items.length>1&&(37===a.keyCode&&(a.preventDefault(),c.goToPrevSlide()),39===a.keyCode&&(a.preventDefault(),c.goToNextSlide()))}),a(b).on("keydown.lg",function(a){c.s.escKey===!0&&27===a.keyCode&&(a.preventDefault(),c.$outer.hasClass("lg-thumb-open")?c.$outer.removeClass("lg-thumb-open"):c.destroy())})},e.prototype.arrow=function(){var a=this;this.$outer.find(".lg-prev").on("click.lg",function(){a.goToPrevSlide()}),this.$outer.find(".lg-next").on("click.lg",function(){a.goToNextSlide()})},e.prototype.arrowDisable=function(a){!this.s.loop&&this.s.hideControlOnEnd&&(a+10?this.$outer.find(".lg-prev").removeAttr("disabled").removeClass("disabled"):this.$outer.find(".lg-prev").attr("disabled","disabled").addClass("disabled"))},e.prototype.setTranslate=function(a,b,c){this.s.useLeft?a.css("left",b):a.css({transform:"translate3d("+b+"px, "+c+"px, 0px)"})},e.prototype.touchMove=function(b,c){var d=c-b;Math.abs(d)>15&&(this.$outer.addClass("lg-dragging"),this.setTranslate(this.$slide.eq(this.index),d,0),this.setTranslate(a(".lg-prev-slide"),-this.$slide.eq(this.index).width()+d,0),this.setTranslate(a(".lg-next-slide"),this.$slide.eq(this.index).width()+d,0))},e.prototype.touchEnd=function(a){var b=this;"lg-slide"!==b.s.mode&&b.$outer.addClass("lg-slide"),this.$slide.not(".lg-current, .lg-prev-slide, .lg-next-slide").css("opacity","0"),setTimeout(function(){b.$outer.removeClass("lg-dragging"),0>a&&Math.abs(a)>b.s.swipeThreshold?b.goToNextSlide(!0):a>0&&Math.abs(a)>b.s.swipeThreshold?b.goToPrevSlide(!0):Math.abs(a)<5&&b.$el.trigger("onSlideClick.lg"),b.$slide.removeAttr("style")}),setTimeout(function(){b.$outer.hasClass("lg-dragging")||"lg-slide"===b.s.mode||b.$outer.removeClass("lg-slide")},b.s.speed+100)},e.prototype.enableSwipe=function(){var a=this,b=0,c=0,d=!1;a.s.enableSwipe&&a.isTouch&&a.doCss()&&(a.$slide.on("touchstart.lg",function(c){a.$outer.hasClass("lg-zoomed")||a.lgBusy||(c.preventDefault(),a.manageSwipeClass(),b=c.originalEvent.targetTouches[0].pageX)}),a.$slide.on("touchmove.lg",function(e){a.$outer.hasClass("lg-zoomed")||(e.preventDefault(),c=e.originalEvent.targetTouches[0].pageX,a.touchMove(b,c),d=!0)}),a.$slide.on("touchend.lg",function(){a.$outer.hasClass("lg-zoomed")||(d?(d=!1,a.touchEnd(c-b)):a.$el.trigger("onSlideClick.lg"))}))},e.prototype.enableDrag=function(){var c=this,d=0,e=0,f=!1,g=!1;c.s.enableDrag&&!c.isTouch&&c.doCss()&&(c.$slide.on("mousedown.lg",function(b){c.$outer.hasClass("lg-zoomed")||(a(b.target).hasClass("lg-object")||a(b.target).hasClass("lg-video-play"))&&(b.preventDefault(),c.lgBusy||(c.manageSwipeClass(),d=b.pageX,f=!0,c.$outer.scrollLeft+=1,c.$outer.scrollLeft-=1,c.$outer.removeClass("lg-grab").addClass("lg-grabbing"),c.$el.trigger("onDragstart.lg")))}),a(b).on("mousemove.lg",function(a){f&&(g=!0,e=a.pageX,c.touchMove(d,e),c.$el.trigger("onDragmove.lg"))}),a(b).on("mouseup.lg",function(b){g?(g=!1,c.touchEnd(e-d),c.$el.trigger("onDragend.lg")):(a(b.target).hasClass("lg-object")||a(b.target).hasClass("lg-video-play"))&&c.$el.trigger("onSlideClick.lg"),f&&(f=!1,c.$outer.removeClass("lg-grabbing").addClass("lg-grab"))}))},e.prototype.manageSwipeClass=function(){var a=this.index+1,b=this.index-1,c=this.$slide.length;this.s.loop&&(0===this.index?b=c-1:this.index===c-1&&(a=0)),this.$slide.removeClass("lg-next-slide lg-prev-slide"),b>-1&&this.$slide.eq(b).addClass("lg-prev-slide"),this.$slide.eq(a).addClass("lg-next-slide")},e.prototype.mousewheel=function(){var a=this;a.$outer.on("mousewheel.lg",function(b){b.deltaY&&(b.deltaY>0?a.goToPrevSlide():a.goToNextSlide(),b.preventDefault())})},e.prototype.closeGallery=function(){var b=this,c=!1;this.$outer.find(".lg-close").on("click.lg",function(){b.destroy()}),b.s.closable&&(b.$outer.on("mousedown.lg",function(b){c=!!(a(b.target).is(".lg-outer")||a(b.target).is(".lg-item ")||a(b.target).is(".lg-img-wrap"))}),b.$outer.on("mouseup.lg",function(d){(a(d.target).is(".lg-outer")||a(d.target).is(".lg-item ")||a(d.target).is(".lg-img-wrap")&&c)&&(b.$outer.hasClass("lg-dragging")||b.destroy())}))},e.prototype.destroy=function(c){var d=this;c||d.$el.trigger("onBeforeClose.lg"),a(b).scrollTop(d.prevScrollTop),c&&(d.s.dynamic||this.$items.off("click.lg click.lgcustom"),a.removeData(d.el,"lightGallery")),this.$el.off(".lg.tm"),a.each(a.fn.lightGallery.modules,function(a){d.modules[a]&&d.modules[a].destroy()}),this.lGalleryOn=!1,clearTimeout(d.hideBartimeout),this.hideBartimeout=!1,a(b).off(".lg"),a("body").removeClass("lg-on lg-from-hash"),d.$outer&&d.$outer.removeClass("lg-visible"),a(".lg-backdrop").removeClass("in"),setTimeout(function(){d.$outer&&d.$outer.remove(),a(".lg-backdrop").remove(),c||d.$el.trigger("onCloseAfter.lg")},d.s.backdropDuration+50)},a.fn.lightGallery=function(b){return this.each(function(){if(a.data(this,"lightGallery"))try{a(this).data("lightGallery").init()}catch(c){console.error("lightGallery has not initiated properly")}else a.data(this,"lightGallery",new e(this,b))})},a.fn.lightGallery.modules={}}(jQuery,window,document); \ No newline at end of file diff --git a/vendors/lightgallery/lib/jquery.mousewheel.min.js b/vendors/lightgallery/lib/jquery.mousewheel.min.js deleted file mode 100644 index 7c331e5e42..0000000000 --- a/vendors/lightgallery/lib/jquery.mousewheel.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh) - * Licensed under the MIT License (LICENSE.txt). - * - * Version: 3.1.12 - * - * Requires: jQuery 1.2.2+ - */ -!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a:a(jQuery)}(function(a){function b(b){var g=b||window.event,h=i.call(arguments,1),j=0,l=0,m=0,n=0,o=0,p=0;if(b=a.event.fix(g),b.type="mousewheel","detail"in g&&(m=-1*g.detail),"wheelDelta"in g&&(m=g.wheelDelta),"wheelDeltaY"in g&&(m=g.wheelDeltaY),"wheelDeltaX"in g&&(l=-1*g.wheelDeltaX),"axis"in g&&g.axis===g.HORIZONTAL_AXIS&&(l=-1*m,m=0),j=0===m?l:m,"deltaY"in g&&(m=-1*g.deltaY,j=m),"deltaX"in g&&(l=g.deltaX,0===m&&(j=-1*l)),0!==m||0!==l){if(1===g.deltaMode){var q=a.data(this,"mousewheel-line-height");j*=q,m*=q,l*=q}else if(2===g.deltaMode){var r=a.data(this,"mousewheel-page-height");j*=r,m*=r,l*=r}if(n=Math.max(Math.abs(m),Math.abs(l)),(!f||f>n)&&(f=n,d(g,n)&&(f/=40)),d(g,n)&&(j/=40,l/=40,m/=40),j=Math[j>=1?"floor":"ceil"](j/f),l=Math[l>=1?"floor":"ceil"](l/f),m=Math[m>=1?"floor":"ceil"](m/f),k.settings.normalizeOffset&&this.getBoundingClientRect){var s=this.getBoundingClientRect();o=b.clientX-s.left,p=b.clientY-s.top}return b.deltaX=l,b.deltaY=m,b.deltaFactor=f,b.offsetX=o,b.offsetY=p,b.deltaMode=0,h.unshift(b,j,l,m),e&&clearTimeout(e),e=setTimeout(c,200),(a.event.dispatch||a.event.handle).apply(this,h)}}function c(){f=null}function d(a,b){return k.settings.adjustOldDeltas&&"mousewheel"===a.type&&b%120===0}var e,f,g=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],h="onwheel"in document||document.documentMode>=9?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],i=Array.prototype.slice;if(a.event.fixHooks)for(var j=g.length;j;)a.event.fixHooks[g[--j]]=a.event.mouseHooks;var k=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var c=h.length;c;)this.addEventListener(h[--c],b,!1);else this.onmousewheel=b;a.data(this,"mousewheel-line-height",k.getLineHeight(this)),a.data(this,"mousewheel-page-height",k.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var c=h.length;c;)this.removeEventListener(h[--c],b,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(b){var c=a(b),d=c["offsetParent"in a.fn?"offsetParent":"parent"]();return d.length||(d=a("body")),parseInt(d.css("fontSize"),10)||parseInt(c.css("fontSize"),10)||16},getPageHeight:function(b){return a(b).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};a.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})}); \ No newline at end of file diff --git a/vendors/lightgallery/lib/lg.png b/vendors/lightgallery/lib/lg.png deleted file mode 100644 index 0b32daa333..0000000000 Binary files a/vendors/lightgallery/lib/lg.png and /dev/null differ diff --git a/vendors/lightgallery/lightGallery.jquery.json b/vendors/lightgallery/lightGallery.jquery.json deleted file mode 100644 index a5713d7f0d..0000000000 --- a/vendors/lightgallery/lightGallery.jquery.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "lightgallery", - "title": "jQuery lightgallery", - "description": "JQuery lightGallery is a lightweight jQuery lightbox gallery for displaying image and video gallery", - "version": "1.2.19", - "homepage": "https://github.com/sachinchoolur/lightGallery", - "author": { - "name": "Sachin N", - "url": "https://github.com/sachinchoolur" - }, - "repository": { - "type": "git", - "url": "https://github.com/sachinchoolur/lightGallery" - }, - "bugs": "", - "licenses": [ - { - "type": "MIT", - "url": "http://opensource.org/licenses/MIT" - } - ], - "dependencies": { - "jquery": ">=1.7.0" - }, - "keywords": [ - "jquery-plugin" - ] -} diff --git a/vendors/lightgallery/package.json b/vendors/lightgallery/package.json deleted file mode 100644 index 330fa8fea6..0000000000 --- a/vendors/lightgallery/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "lightgallery", - "version": "1.2.21", - "description": "A lightweight, customizable, modular, responsive, lightbox gallery plugin for jQuery.", - "keywords": [ - "jquery-plugin", - "gallery", - "lightbox", - "image", - "youtube", - "vimeo", - "dailymotion", - "html5 videos", - "thumbnails", - "zoom", - "fullscreen", - "responsive", - "touch", - "drag" - ], - "homepage": "http://sachinchoolur.github.io/lightGallery/", - "bugs": { - "url": "https://github.com/sachinchoolur/lightGallery/issues" - }, - "license": "Apache-2.0", - "author": { - "name": "Sachin N", - "email": "sachi77n@gmail.com", - "url": "https://github.com/sachinchoolur" - }, - "main": "dist/js/lightgallery.js", - "repository": { - "type": "git", - "url": "https://github.com/sachinchoolur/lightGallery.git" - }, - "dependencies": { - "jquery": ">=1.7.0" - }, - "scripts": { - "test": "grunt" - }, - "devDependencies": { - "grunt": "^0.4.5", - "grunt-contrib-clean": "^0.6.0", - "grunt-contrib-concat": "^0.5.0", - "grunt-contrib-connect": "^0.9.0", - "grunt-contrib-copy": "^0.8.0", - "grunt-contrib-cssmin": "^0.12.1", - "grunt-contrib-jshint": "^0.10.0", - "grunt-contrib-qunit": "^0.5.1", - "grunt-contrib-sass": "^0.9.2", - "grunt-contrib-uglify": "^0.7.0", - "grunt-contrib-watch": "^0.6.1", - "jshint-stylish": "^1.0.0", - "load-grunt-tasks": "^2.0.0", - "time-grunt": "^1.0.0" - } -} diff --git a/vendors/lightgallery/selection.json b/vendors/lightgallery/selection.json deleted file mode 100644 index e9c2e39b93..0000000000 --- a/vendors/lightgallery/selection.json +++ /dev/null @@ -1,407 +0,0 @@ -{ - "IcoMoonType": "selection", - "icons": [ - { - "icon": { - "paths": [ - "M554 682v-340h86v340h-86zM512 854q140 0 241-101t101-241-101-241-241-101-241 101-101 241 101 241 241 101zM512 86q176 0 301 125t125 301-125 301-301 125-301-125-125-301 125-301 301-125zM384 682v-340h86v340h-86z" - ], - "attrs": [ - {} - ], - "isMulticolor": false, - "tags": [ - "pause_circle_outline" - ], - "defaultCode": 57370, - "grid": 24 - }, - "attrs": [ - {} - ], - "properties": { - "order": 18, - "id": 0, - "prevSize": 24, - "code": 57370, - "name": "pause_circle_outline", - "ligatures": "pause_circle_outline" - }, - "setIdx": 1, - "setId": 4, - "iconIdx": 0 - }, - { - "icon": { - "paths": [ - "M512 854q140 0 241-101t101-241-101-241-241-101-241 101-101 241 101 241 241 101zM512 86q176 0 301 125t125 301-125 301-301 125-301-125-125-301 125-301 301-125zM426 704v-384l256 192z" - ], - "attrs": [ - {} - ], - "isMulticolor": false, - "tags": [ - "play_circle_outline" - ], - "defaultCode": 57373, - "grid": 24 - }, - "attrs": [ - {} - ], - "properties": { - "order": 20, - "id": 1, - "prevSize": 24, - "code": 57373, - "name": "play_circle_outline", - "ligatures": "play_circle_outline" - }, - "setIdx": 1, - "setId": 4, - "iconIdx": 1 - }, - { - "icon": { - "paths": [ - "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z" - ], - "attrs": [ - {} - ], - "isMulticolor": false, - "tags": [ - "clear" - ], - "defaultCode": 57456, - "grid": 24 - }, - "attrs": [ - {} - ], - "properties": { - "order": 14, - "id": 2, - "prevSize": 24, - "code": 57456, - "name": "clear", - "ligatures": "clear" - }, - "setIdx": 1, - "setId": 4, - "iconIdx": 2 - }, - { - "icon": { - "paths": [ - "M170 810h684v86h-684v-86zM682 554l-170 172-170-172h128v-426h84v426h128z" - ], - "attrs": [ - {} - ], - "isMulticolor": false, - "tags": [ - "vertical_align_bottom" - ], - "defaultCode": 57586, - "grid": 24 - }, - "attrs": [ - {} - ], - "properties": { - "order": 21, - "id": 3, - "prevSize": 24, - "code": 57586, - "name": "vertical_align_bottom", - "ligatures": "vertical_align_bottom" - }, - "setIdx": 1, - "setId": 4, - "iconIdx": 3 - }, - { - "icon": { - "paths": [ - "M682 854v-172h172v172h-172zM682 598v-172h172v172h-172zM426 342v-172h172v172h-172zM682 170h172v172h-172v-172zM426 598v-172h172v172h-172zM170 598v-172h172v172h-172zM170 854v-172h172v172h-172zM426 854v-172h172v172h-172zM170 342v-172h172v172h-172z" - ], - "attrs": [ - {} - ], - "isMulticolor": false, - "tags": [ - "apps" - ], - "defaultCode": 57855, - "grid": 24 - }, - "attrs": [ - {} - ], - "properties": { - "order": 28, - "id": 4, - "prevSize": 24, - "code": 57855, - "name": "apps", - "ligatures": "apps" - }, - "setIdx": 1, - "setId": 4, - "iconIdx": 4 - }, - { - "icon": { - "paths": [ - "M598 214h212v212h-84v-128h-128v-84zM726 726v-128h84v212h-212v-84h128zM214 426v-212h212v84h-128v128h-84zM298 598v128h128v84h-212v-212h84z" - ], - "attrs": [ - {} - ], - "isMulticolor": false, - "tags": [ - "fullscreen" - ], - "defaultCode": 57868, - "grid": 24 - }, - "attrs": [ - {} - ], - "properties": { - "order": 32, - "id": 5, - "prevSize": 24, - "code": 57868, - "name": "fullscreen", - "ligatures": "fullscreen" - }, - "setIdx": 1, - "setId": 4, - "iconIdx": 5 - }, - { - "icon": { - "paths": [ - "M682 342h128v84h-212v-212h84v128zM598 810v-212h212v84h-128v128h-84zM342 342v-128h84v212h-212v-84h128zM214 682v-84h212v212h-84v-128h-128z" - ], - "attrs": [ - {} - ], - "isMulticolor": false, - "tags": [ - "fullscreen_exit" - ], - "defaultCode": 57869, - "grid": 24 - }, - "attrs": [ - {} - ], - "properties": { - "order": 31, - "id": 6, - "prevSize": 24, - "code": 57869, - "name": "fullscreen_exit", - "ligatures": "fullscreen_exit" - }, - "setIdx": 1, - "setId": 4, - "iconIdx": 6 - }, - { - "icon": { - "paths": [ - "M512 426h-86v86h-42v-86h-86v-42h86v-86h42v86h86v42zM406 598q80 0 136-56t56-136-56-136-136-56-136 56-56 136 56 136 136 56zM662 598l212 212-64 64-212-212v-34l-12-12q-76 66-180 66-116 0-197-80t-81-196 81-197 197-81 196 81 80 197q0 104-66 180l12 12h34z" - ], - "attrs": [ - {} - ], - "isMulticolor": false, - "tags": [ - "zoom_in" - ], - "defaultCode": 58129, - "grid": 24 - }, - "attrs": [ - {} - ], - "properties": { - "order": 35, - "id": 7, - "prevSize": 24, - "code": 58129, - "name": "zoom_in", - "ligatures": "zoom_in" - }, - "setIdx": 1, - "setId": 4, - "iconIdx": 7 - }, - { - "icon": { - "paths": [ - "M298 384h214v42h-214v-42zM406 598q80 0 136-56t56-136-56-136-136-56-136 56-56 136 56 136 136 56zM662 598l212 212-64 64-212-212v-34l-12-12q-76 66-180 66-116 0-197-80t-81-196 81-197 197-81 196 81 80 197q0 104-66 180l12 12h34z" - ], - "attrs": [ - {} - ], - "isMulticolor": false, - "tags": [ - "zoom_out" - ], - "defaultCode": 58130, - "grid": 24 - }, - "attrs": [ - {} - ], - "properties": { - "order": 36, - "id": 8, - "prevSize": 24, - "code": 58130, - "name": "zoom_out", - "ligatures": "zoom_out" - }, - "setIdx": 1, - "setId": 4, - "iconIdx": 8 - }, - { - "icon": { - "paths": [ - "M426.667 170.667q17.667 0 30.167 12.5t12.5 30.167q0 18-12.667 30.333l-225.667 225.667h665q17.667 0 30.167 12.5t12.5 30.167-12.5 30.167-30.167 12.5h-665l225.667 225.667q12.667 12.333 12.667 30.333 0 17.667-12.5 30.167t-30.167 12.5q-18 0-30.333-12.333l-298.667-298.667q-12.333-13-12.333-30.333t12.333-30.333l298.667-298.667q12.667-12.333 30.333-12.333z" - ], - "attrs": [], - "isMulticolor": false, - "tags": [ - "arrow-left" - ], - "defaultCode": 57492, - "grid": 24 - }, - "attrs": [], - "properties": { - "id": 9, - "order": 6, - "prevSize": 24, - "code": 57492, - "name": "arrow-left" - }, - "setIdx": 1, - "setId": 4, - "iconIdx": 9 - }, - { - "icon": { - "paths": [ - "M597.333 170.667q18 0 30.333 12.333l298.667 298.667q12.333 12.333 12.333 30.333t-12.333 30.333l-298.667 298.667q-12.333 12.333-30.333 12.333-18.333 0-30.5-12.167t-12.167-30.5q0-18 12.333-30.333l226-225.667h-665q-17.667 0-30.167-12.5t-12.5-30.167 12.5-30.167 30.167-12.5h665l-226-225.667q-12.333-12.333-12.333-30.333 0-18.333 12.167-30.5t30.5-12.167z" - ], - "attrs": [], - "isMulticolor": false, - "tags": [ - "arrow-right" - ], - "defaultCode": 57493, - "grid": 24 - }, - "attrs": [], - "properties": { - "id": 10, - "order": 5, - "prevSize": 24, - "code": 57493, - "name": "arrow-right" - }, - "setIdx": 1, - "setId": 4, - "iconIdx": 10 - }, - { - "icon": { - "paths": [ - "M384 85.333h426.667q53 0 90.5 37.5t37.5 90.5v426.667q0 53-37.5 90.5t-90.5 37.5h-426.667q-53 0-90.5-37.5t-37.5-90.5v-426.667q0-53 37.5-90.5t90.5-37.5zM170.667 263.333v547.333q0 17.667 12.5 30.167t30.167 12.5h547.333q-13.333 37.667-46.333 61.5t-74.333 23.833h-426.667q-53 0-90.5-37.5t-37.5-90.5v-426.667q0-41.333 23.833-74.333t61.5-46.333zM810.667 170.667h-426.667q-17.667 0-30.167 12.5t-12.5 30.167v426.667q0 17.667 12.5 30.167t30.167 12.5h426.667q17.667 0 30.167-12.5t12.5-30.167v-426.667q0-17.667-12.5-30.167t-30.167-12.5z" - ], - "attrs": [], - "isMulticolor": false, - "tags": [ - "stack-2" - ], - "defaultCode": 57395, - "grid": 24 - }, - "attrs": [], - "properties": { - "id": 33, - "order": 18, - "prevSize": 24, - "code": 57395, - "name": "stack-2" - }, - "setIdx": 2, - "setId": 3, - "iconIdx": 33 - } - ], - "height": 1024, - "metadata": { - "name": "lg", - "url": "https://github.com/sachinchoolur/lightGallery", - "license": "MLT", - "licenseURL": "http://opensource.org/licenses/MIT" - }, - "preferences": { - "showGlyphs": true, - "showQuickUse": true, - "showQuickUse2": true, - "showSVGs": true, - "fontPref": { - "prefix": "lg-", - "metadata": { - "fontFamily": "lg", - "majorVersion": 1, - "minorVersion": 0, - "fontURL": "https://github.com/sachinchoolur/lightGallery", - "copyright": "sachin", - "license": "MLT", - "licenseURL": "http://opensource.org/licenses/MIT" - }, - "metrics": { - "emSize": 1024, - "baseline": 6.25, - "whitespace": 50 - }, - "showSelector": true, - "showMetrics": true, - "showMetadata": true, - "showVersion": true, - "embed": false, - "includeMetadata": true, - "ie7": false, - "resetPoint": 58880, - "selector": "class", - "classSelector": ".lg-icon", - "cssVars": false, - "autoHost": true - }, - "imagePref": { - "prefix": "icon-", - "png": true, - "useClassSelector": true, - "color": 4473924, - "bgColor": 16777215, - "classSelector": ".icon" - }, - "historySize": 100, - "showCodes": true, - "gridSize": 16, - "showLiga": false, - "quickUsageToken": { - "UntitledProject": "Y2Q2M2U4MWIyMWQ0NDQ4MDYyMzk4NTJmYzFlYmIyMjkjMSMxNDMyMDU3NDgwIyMj" - } - } -} \ No newline at end of file diff --git a/vendors/lightgallery/src/css/lg-fb-comment-box.css b/vendors/lightgallery/src/css/lg-fb-comment-box.css deleted file mode 100644 index 76d8613b92..0000000000 --- a/vendors/lightgallery/src/css/lg-fb-comment-box.css +++ /dev/null @@ -1,31 +0,0 @@ -.lg-outer.fb-comments .lg-img-wrap { - padding-right: 400px !important; } -.lg-outer.fb-comments .fb-comments { - height: 100%; - overflow-y: auto; - position: absolute; - right: 0; - top: 0; - width: 420px; - z-index: 99999; - background: #fff url("../img/loading.gif") no-repeat scroll center center; } - .lg-outer.fb-comments .fb-comments.fb_iframe_widget { - background-image: none; } - .lg-outer.fb-comments .fb-comments.fb_iframe_widget.fb_iframe_widget_loader { - background: #fff url("../img/loading.gif") no-repeat scroll center center; } -.lg-outer.fb-comments .lg-toolbar { - right: 420px; - width: auto; } -.lg-outer.fb-comments .lg-actions .lg-next { - right: 420px; } -.lg-outer.fb-comments .lg-item { - background-image: none; } - .lg-outer.fb-comments .lg-item.lg-complete .lg-img-wrap { - background-image: none; } -.lg-outer.fb-comments .lg-img-wrap { - background: url(../img/loading.gif) no-repeat scroll center center transparent; } -.lg-outer.fb-comments .lg-sub-html { - padding: 0; - position: static; } - -/*# sourceMappingURL=lg-fb-comment-box.css.map */ diff --git a/vendors/lightgallery/src/css/lg-fb-comment-box.css.map b/vendors/lightgallery/src/css/lg-fb-comment-box.css.map deleted file mode 100644 index 9c6fc14b33..0000000000 --- a/vendors/lightgallery/src/css/lg-fb-comment-box.css.map +++ /dev/null @@ -1,7 +0,0 @@ -{ -"version": 3, -"mappings": "AAGI,kCAAa;EACT,aAAa,EAAE,gBAAgB;AAEnC,kCAAa;EACT,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,IAAI;EAChB,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,CAAC;EACR,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,KAAK;EACZ,OAAO,EAAE,KAAK;EACd,UAAU,EAAE,6DAA6D;EACzE,mDAAmB;IACf,gBAAgB,EAAE,IAAI;IACtB,2EAAyB;MACrB,UAAU,EAAE,6DAA6D;AAIrF,iCAAY;EACR,KAAK,EAAE,KAAK;EACZ,KAAK,EAAE,IAAI;AAEf,0CAAqB;EACjB,KAAK,EAAE,KAAK;AAEhB,8BAAS;EACL,gBAAgB,EAAE,IAAI;EAElB,uDAAY;IACR,gBAAgB,EAAE,IAAI;AAIlC,kCAAa;EACT,UAAU,EAAE,kEAAkE;AAGlF,kCAAa;EACT,OAAO,EAAE,CAAC;EACV,QAAQ,EAAE,MAAM", -"sources": ["../sass/lg-fb-comment-box.scss"], -"names": [], -"file": "lg-fb-comment-box.css" -} diff --git a/vendors/lightgallery/src/css/lg-transitions.css b/vendors/lightgallery/src/css/lg-transitions.css deleted file mode 100644 index 3b94732911..0000000000 --- a/vendors/lightgallery/src/css/lg-transitions.css +++ /dev/null @@ -1,776 +0,0 @@ -.lg-css3.lg-zoom-in .lg-item { - opacity: 0; } - .lg-css3.lg-zoom-in .lg-item.lg-prev-slide { - -webkit-transform: scale3d(1.3, 1.3, 1.3); - transform: scale3d(1.3, 1.3, 1.3); } - .lg-css3.lg-zoom-in .lg-item.lg-next-slide { - -webkit-transform: scale3d(1.3, 1.3, 1.3); - transform: scale3d(1.3, 1.3, 1.3); } - .lg-css3.lg-zoom-in .lg-item.lg-current { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - opacity: 1; } - .lg-css3.lg-zoom-in .lg-item.lg-prev-slide, .lg-css3.lg-zoom-in .lg-item.lg-next-slide, .lg-css3.lg-zoom-in .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-zoom-in-big .lg-item { - opacity: 0; } - .lg-css3.lg-zoom-in-big .lg-item.lg-prev-slide { - -webkit-transform: scale3d(2, 2, 2); - transform: scale3d(2, 2, 2); } - .lg-css3.lg-zoom-in-big .lg-item.lg-next-slide { - -webkit-transform: scale3d(2, 2, 2); - transform: scale3d(2, 2, 2); } - .lg-css3.lg-zoom-in-big .lg-item.lg-current { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - opacity: 1; } - .lg-css3.lg-zoom-in-big .lg-item.lg-prev-slide, .lg-css3.lg-zoom-in-big .lg-item.lg-next-slide, .lg-css3.lg-zoom-in-big .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-zoom-out .lg-item { - opacity: 0; } - .lg-css3.lg-zoom-out .lg-item.lg-prev-slide { - -webkit-transform: scale3d(0.7, 0.7, 0.7); - transform: scale3d(0.7, 0.7, 0.7); } - .lg-css3.lg-zoom-out .lg-item.lg-next-slide { - -webkit-transform: scale3d(0.7, 0.7, 0.7); - transform: scale3d(0.7, 0.7, 0.7); } - .lg-css3.lg-zoom-out .lg-item.lg-current { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - opacity: 1; } - .lg-css3.lg-zoom-out .lg-item.lg-prev-slide, .lg-css3.lg-zoom-out .lg-item.lg-next-slide, .lg-css3.lg-zoom-out .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-zoom-out-big .lg-item { - opacity: 0; } - .lg-css3.lg-zoom-out-big .lg-item.lg-prev-slide { - -webkit-transform: scale3d(0, 0, 0); - transform: scale3d(0, 0, 0); } - .lg-css3.lg-zoom-out-big .lg-item.lg-next-slide { - -webkit-transform: scale3d(0, 0, 0); - transform: scale3d(0, 0, 0); } - .lg-css3.lg-zoom-out-big .lg-item.lg-current { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - opacity: 1; } - .lg-css3.lg-zoom-out-big .lg-item.lg-prev-slide, .lg-css3.lg-zoom-out-big .lg-item.lg-next-slide, .lg-css3.lg-zoom-out-big .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-zoom-out-in .lg-item { - opacity: 0; } - .lg-css3.lg-zoom-out-in .lg-item.lg-prev-slide { - -webkit-transform: scale3d(0, 0, 0); - transform: scale3d(0, 0, 0); } - .lg-css3.lg-zoom-out-in .lg-item.lg-next-slide { - -webkit-transform: scale3d(2, 2, 2); - transform: scale3d(2, 2, 2); } - .lg-css3.lg-zoom-out-in .lg-item.lg-current { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - opacity: 1; } - .lg-css3.lg-zoom-out-in .lg-item.lg-prev-slide, .lg-css3.lg-zoom-out-in .lg-item.lg-next-slide, .lg-css3.lg-zoom-out-in .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-zoom-in-out .lg-item { - opacity: 0; } - .lg-css3.lg-zoom-in-out .lg-item.lg-prev-slide { - -webkit-transform: scale3d(2, 2, 2); - transform: scale3d(2, 2, 2); } - .lg-css3.lg-zoom-in-out .lg-item.lg-next-slide { - -webkit-transform: scale3d(0, 0, 0); - transform: scale3d(0, 0, 0); } - .lg-css3.lg-zoom-in-out .lg-item.lg-current { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - opacity: 1; } - .lg-css3.lg-zoom-in-out .lg-item.lg-prev-slide, .lg-css3.lg-zoom-in-out .lg-item.lg-next-slide, .lg-css3.lg-zoom-in-out .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-soft-zoom .lg-item { - opacity: 0; } - .lg-css3.lg-soft-zoom .lg-item.lg-prev-slide { - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); } - .lg-css3.lg-soft-zoom .lg-item.lg-next-slide { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); } - .lg-css3.lg-soft-zoom .lg-item.lg-current { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - opacity: 1; } - .lg-css3.lg-soft-zoom .lg-item.lg-prev-slide, .lg-css3.lg-soft-zoom .lg-item.lg-next-slide, .lg-css3.lg-soft-zoom .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-scale-up .lg-item { - opacity: 0; } - .lg-css3.lg-scale-up .lg-item.lg-prev-slide { - -moz-transform: scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0); - -o-transform: scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0); - -ms-transform: scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0); - -webkit-transform: scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0); - transform: scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0); } - .lg-css3.lg-scale-up .lg-item.lg-next-slide { - -moz-transform: scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0); - -o-transform: scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0); - -ms-transform: scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0); - -webkit-transform: scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0); - transform: scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0); } - .lg-css3.lg-scale-up .lg-item.lg-current { - -moz-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -o-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -ms-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -webkit-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-scale-up .lg-item.lg-prev-slide, .lg-css3.lg-scale-up .lg-item.lg-next-slide, .lg-css3.lg-scale-up .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-slide-circular .lg-item { - opacity: 0; } - .lg-css3.lg-slide-circular .lg-item.lg-prev-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(-100%, 0, 0); - -o-transform: scale3d(0, 0, 0) translate3d(-100%, 0, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(-100%, 0, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(-100%, 0, 0); - transform: scale3d(0, 0, 0) translate3d(-100%, 0, 0); } - .lg-css3.lg-slide-circular .lg-item.lg-next-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(100%, 0, 0); - -o-transform: scale3d(0, 0, 0) translate3d(100%, 0, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(100%, 0, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(100%, 0, 0); - transform: scale3d(0, 0, 0) translate3d(100%, 0, 0); } - .lg-css3.lg-slide-circular .lg-item.lg-current { - -moz-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -o-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -ms-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -webkit-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-slide-circular .lg-item.lg-prev-slide, .lg-css3.lg-slide-circular .lg-item.lg-next-slide, .lg-css3.lg-slide-circular .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-slide-circular-up .lg-item { - opacity: 0; } - .lg-css3.lg-slide-circular-up .lg-item.lg-prev-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(-100%, -100%, 0); - -o-transform: scale3d(0, 0, 0) translate3d(-100%, -100%, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(-100%, -100%, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(-100%, -100%, 0); - transform: scale3d(0, 0, 0) translate3d(-100%, -100%, 0); } - .lg-css3.lg-slide-circular-up .lg-item.lg-next-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(100%, -100%, 0); - -o-transform: scale3d(0, 0, 0) translate3d(100%, -100%, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(100%, -100%, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(100%, -100%, 0); - transform: scale3d(0, 0, 0) translate3d(100%, -100%, 0); } - .lg-css3.lg-slide-circular-up .lg-item.lg-current { - -moz-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -o-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -ms-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -webkit-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-slide-circular-up .lg-item.lg-prev-slide, .lg-css3.lg-slide-circular-up .lg-item.lg-next-slide, .lg-css3.lg-slide-circular-up .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-slide-circular-down .lg-item { - opacity: 0; } - .lg-css3.lg-slide-circular-down .lg-item.lg-prev-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(-100%, 100%, 0); - -o-transform: scale3d(0, 0, 0) translate3d(-100%, 100%, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(-100%, 100%, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(-100%, 100%, 0); - transform: scale3d(0, 0, 0) translate3d(-100%, 100%, 0); } - .lg-css3.lg-slide-circular-down .lg-item.lg-next-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(100%, 100%, 0); - -o-transform: scale3d(0, 0, 0) translate3d(100%, 100%, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(100%, 100%, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(100%, 100%, 0); - transform: scale3d(0, 0, 0) translate3d(100%, 100%, 0); } - .lg-css3.lg-slide-circular-down .lg-item.lg-current { - -moz-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -o-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -ms-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -webkit-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-slide-circular-down .lg-item.lg-prev-slide, .lg-css3.lg-slide-circular-down .lg-item.lg-next-slide, .lg-css3.lg-slide-circular-down .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-slide-circular-vertical .lg-item { - opacity: 0; } - .lg-css3.lg-slide-circular-vertical .lg-item.lg-prev-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(0, -100%, 0); - -o-transform: scale3d(0, 0, 0) translate3d(0, -100%, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(0, -100%, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(0, -100%, 0); - transform: scale3d(0, 0, 0) translate3d(0, -100%, 0); } - .lg-css3.lg-slide-circular-vertical .lg-item.lg-next-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(0, 100%, 0); - -o-transform: scale3d(0, 0, 0) translate3d(0, 100%, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(0, 100%, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(0, 100%, 0); - transform: scale3d(0, 0, 0) translate3d(0, 100%, 0); } - .lg-css3.lg-slide-circular-vertical .lg-item.lg-current { - -moz-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -o-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -ms-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -webkit-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-slide-circular-vertical .lg-item.lg-prev-slide, .lg-css3.lg-slide-circular-vertical .lg-item.lg-next-slide, .lg-css3.lg-slide-circular-vertical .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-slide-circular-vertical-left .lg-item { - opacity: 0; } - .lg-css3.lg-slide-circular-vertical-left .lg-item.lg-prev-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(-100%, -100%, 0); - -o-transform: scale3d(0, 0, 0) translate3d(-100%, -100%, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(-100%, -100%, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(-100%, -100%, 0); - transform: scale3d(0, 0, 0) translate3d(-100%, -100%, 0); } - .lg-css3.lg-slide-circular-vertical-left .lg-item.lg-next-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(-100%, 100%, 0); - -o-transform: scale3d(0, 0, 0) translate3d(-100%, 100%, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(-100%, 100%, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(-100%, 100%, 0); - transform: scale3d(0, 0, 0) translate3d(-100%, 100%, 0); } - .lg-css3.lg-slide-circular-vertical-left .lg-item.lg-current { - -moz-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -o-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -ms-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -webkit-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-slide-circular-vertical-left .lg-item.lg-prev-slide, .lg-css3.lg-slide-circular-vertical-left .lg-item.lg-next-slide, .lg-css3.lg-slide-circular-vertical-left .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-slide-circular-vertical-down .lg-item { - opacity: 0; } - .lg-css3.lg-slide-circular-vertical-down .lg-item.lg-prev-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(100%, -100%, 0); - -o-transform: scale3d(0, 0, 0) translate3d(100%, -100%, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(100%, -100%, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(100%, -100%, 0); - transform: scale3d(0, 0, 0) translate3d(100%, -100%, 0); } - .lg-css3.lg-slide-circular-vertical-down .lg-item.lg-next-slide { - -moz-transform: scale3d(0, 0, 0) translate3d(100%, 100%, 0); - -o-transform: scale3d(0, 0, 0) translate3d(100%, 100%, 0); - -ms-transform: scale3d(0, 0, 0) translate3d(100%, 100%, 0); - -webkit-transform: scale3d(0, 0, 0) translate3d(100%, 100%, 0); - transform: scale3d(0, 0, 0) translate3d(100%, 100%, 0); } - .lg-css3.lg-slide-circular-vertical-down .lg-item.lg-current { - -moz-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -o-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -ms-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -webkit-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-slide-circular-vertical-down .lg-item.lg-prev-slide, .lg-css3.lg-slide-circular-vertical-down .lg-item.lg-next-slide, .lg-css3.lg-slide-circular-vertical-down .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s; } -.lg-css3.lg-slide-vertical .lg-item { - opacity: 0; } - .lg-css3.lg-slide-vertical .lg-item.lg-prev-slide { - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); } - .lg-css3.lg-slide-vertical .lg-item.lg-next-slide { - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); } - .lg-css3.lg-slide-vertical .lg-item.lg-current { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-slide-vertical .lg-item.lg-prev-slide, .lg-css3.lg-slide-vertical .lg-item.lg-next-slide, .lg-css3.lg-slide-vertical .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-vertical-growth .lg-item { - opacity: 0; } - .lg-css3.lg-slide-vertical-growth .lg-item.lg-prev-slide { - -moz-transform: scale3d(0.5, 0.5, 0.5) translate3d(0, -150%, 0); - -o-transform: scale3d(0.5, 0.5, 0.5) translate3d(0, -150%, 0); - -ms-transform: scale3d(0.5, 0.5, 0.5) translate3d(0, -150%, 0); - -webkit-transform: scale3d(0.5, 0.5, 0.5) translate3d(0, -150%, 0); - transform: scale3d(0.5, 0.5, 0.5) translate3d(0, -150%, 0); } - .lg-css3.lg-slide-vertical-growth .lg-item.lg-next-slide { - -moz-transform: scale3d(0.5, 0.5, 0.5) translate3d(0, 150%, 0); - -o-transform: scale3d(0.5, 0.5, 0.5) translate3d(0, 150%, 0); - -ms-transform: scale3d(0.5, 0.5, 0.5) translate3d(0, 150%, 0); - -webkit-transform: scale3d(0.5, 0.5, 0.5) translate3d(0, 150%, 0); - transform: scale3d(0.5, 0.5, 0.5) translate3d(0, 150%, 0); } - .lg-css3.lg-slide-vertical-growth .lg-item.lg-current { - -moz-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -o-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -ms-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -webkit-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-slide-vertical-growth .lg-item.lg-prev-slide, .lg-css3.lg-slide-vertical-growth .lg-item.lg-next-slide, .lg-css3.lg-slide-vertical-growth .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew-only .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew-only .lg-item.lg-prev-slide { - -moz-transform: skew(10deg, 0deg); - -o-transform: skew(10deg, 0deg); - -ms-transform: skew(10deg, 0deg); - -webkit-transform: skew(10deg, 0deg); - transform: skew(10deg, 0deg); } - .lg-css3.lg-slide-skew-only .lg-item.lg-next-slide { - -moz-transform: skew(10deg, 0deg); - -o-transform: skew(10deg, 0deg); - -ms-transform: skew(10deg, 0deg); - -webkit-transform: skew(10deg, 0deg); - transform: skew(10deg, 0deg); } - .lg-css3.lg-slide-skew-only .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg); - -o-transform: skew(0deg, 0deg); - -ms-transform: skew(0deg, 0deg); - -webkit-transform: skew(0deg, 0deg); - transform: skew(0deg, 0deg); - opacity: 1; } - .lg-css3.lg-slide-skew-only .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew-only .lg-item.lg-next-slide, .lg-css3.lg-slide-skew-only .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew-only-rev .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew-only-rev .lg-item.lg-prev-slide { - -moz-transform: skew(-10deg, 0deg); - -o-transform: skew(-10deg, 0deg); - -ms-transform: skew(-10deg, 0deg); - -webkit-transform: skew(-10deg, 0deg); - transform: skew(-10deg, 0deg); } - .lg-css3.lg-slide-skew-only-rev .lg-item.lg-next-slide { - -moz-transform: skew(-10deg, 0deg); - -o-transform: skew(-10deg, 0deg); - -ms-transform: skew(-10deg, 0deg); - -webkit-transform: skew(-10deg, 0deg); - transform: skew(-10deg, 0deg); } - .lg-css3.lg-slide-skew-only-rev .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg); - -o-transform: skew(0deg, 0deg); - -ms-transform: skew(0deg, 0deg); - -webkit-transform: skew(0deg, 0deg); - transform: skew(0deg, 0deg); - opacity: 1; } - .lg-css3.lg-slide-skew-only-rev .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew-only-rev .lg-item.lg-next-slide, .lg-css3.lg-slide-skew-only-rev .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew-only-y .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew-only-y .lg-item.lg-prev-slide { - -moz-transform: skew(0deg, 10deg); - -o-transform: skew(0deg, 10deg); - -ms-transform: skew(0deg, 10deg); - -webkit-transform: skew(0deg, 10deg); - transform: skew(0deg, 10deg); } - .lg-css3.lg-slide-skew-only-y .lg-item.lg-next-slide { - -moz-transform: skew(0deg, 10deg); - -o-transform: skew(0deg, 10deg); - -ms-transform: skew(0deg, 10deg); - -webkit-transform: skew(0deg, 10deg); - transform: skew(0deg, 10deg); } - .lg-css3.lg-slide-skew-only-y .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg); - -o-transform: skew(0deg, 0deg); - -ms-transform: skew(0deg, 0deg); - -webkit-transform: skew(0deg, 0deg); - transform: skew(0deg, 0deg); - opacity: 1; } - .lg-css3.lg-slide-skew-only-y .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew-only-y .lg-item.lg-next-slide, .lg-css3.lg-slide-skew-only-y .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew-only-y-rev .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew-only-y-rev .lg-item.lg-prev-slide { - -moz-transform: skew(0deg, -10deg); - -o-transform: skew(0deg, -10deg); - -ms-transform: skew(0deg, -10deg); - -webkit-transform: skew(0deg, -10deg); - transform: skew(0deg, -10deg); } - .lg-css3.lg-slide-skew-only-y-rev .lg-item.lg-next-slide { - -moz-transform: skew(0deg, -10deg); - -o-transform: skew(0deg, -10deg); - -ms-transform: skew(0deg, -10deg); - -webkit-transform: skew(0deg, -10deg); - transform: skew(0deg, -10deg); } - .lg-css3.lg-slide-skew-only-y-rev .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg); - -o-transform: skew(0deg, 0deg); - -ms-transform: skew(0deg, 0deg); - -webkit-transform: skew(0deg, 0deg); - transform: skew(0deg, 0deg); - opacity: 1; } - .lg-css3.lg-slide-skew-only-y-rev .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew-only-y-rev .lg-item.lg-next-slide, .lg-css3.lg-slide-skew-only-y-rev .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew .lg-item.lg-prev-slide { - -moz-transform: skew(20deg, 0deg) translate3d(-100%, 0%, 0px); - -o-transform: skew(20deg, 0deg) translate3d(-100%, 0%, 0px); - -ms-transform: skew(20deg, 0deg) translate3d(-100%, 0%, 0px); - -webkit-transform: skew(20deg, 0deg) translate3d(-100%, 0%, 0px); - transform: skew(20deg, 0deg) translate3d(-100%, 0%, 0px); } - .lg-css3.lg-slide-skew .lg-item.lg-next-slide { - -moz-transform: skew(20deg, 0deg) translate3d(100%, 0%, 0px); - -o-transform: skew(20deg, 0deg) translate3d(100%, 0%, 0px); - -ms-transform: skew(20deg, 0deg) translate3d(100%, 0%, 0px); - -webkit-transform: skew(20deg, 0deg) translate3d(100%, 0%, 0px); - transform: skew(20deg, 0deg) translate3d(100%, 0%, 0px); } - .lg-css3.lg-slide-skew .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -o-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -ms-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -webkit-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - opacity: 1; } - .lg-css3.lg-slide-skew .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew .lg-item.lg-next-slide, .lg-css3.lg-slide-skew .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew-rev .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew-rev .lg-item.lg-prev-slide { - -moz-transform: skew(-20deg, 0deg) translate3d(-100%, 0%, 0px); - -o-transform: skew(-20deg, 0deg) translate3d(-100%, 0%, 0px); - -ms-transform: skew(-20deg, 0deg) translate3d(-100%, 0%, 0px); - -webkit-transform: skew(-20deg, 0deg) translate3d(-100%, 0%, 0px); - transform: skew(-20deg, 0deg) translate3d(-100%, 0%, 0px); } - .lg-css3.lg-slide-skew-rev .lg-item.lg-next-slide { - -moz-transform: skew(-20deg, 0deg) translate3d(100%, 0%, 0px); - -o-transform: skew(-20deg, 0deg) translate3d(100%, 0%, 0px); - -ms-transform: skew(-20deg, 0deg) translate3d(100%, 0%, 0px); - -webkit-transform: skew(-20deg, 0deg) translate3d(100%, 0%, 0px); - transform: skew(-20deg, 0deg) translate3d(100%, 0%, 0px); } - .lg-css3.lg-slide-skew-rev .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -o-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -ms-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -webkit-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - opacity: 1; } - .lg-css3.lg-slide-skew-rev .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew-rev .lg-item.lg-next-slide, .lg-css3.lg-slide-skew-rev .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew-cross .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew-cross .lg-item.lg-prev-slide { - -moz-transform: skew(0deg, 60deg) translate3d(-100%, 0%, 0px); - -o-transform: skew(0deg, 60deg) translate3d(-100%, 0%, 0px); - -ms-transform: skew(0deg, 60deg) translate3d(-100%, 0%, 0px); - -webkit-transform: skew(0deg, 60deg) translate3d(-100%, 0%, 0px); - transform: skew(0deg, 60deg) translate3d(-100%, 0%, 0px); } - .lg-css3.lg-slide-skew-cross .lg-item.lg-next-slide { - -moz-transform: skew(0deg, 60deg) translate3d(100%, 0%, 0px); - -o-transform: skew(0deg, 60deg) translate3d(100%, 0%, 0px); - -ms-transform: skew(0deg, 60deg) translate3d(100%, 0%, 0px); - -webkit-transform: skew(0deg, 60deg) translate3d(100%, 0%, 0px); - transform: skew(0deg, 60deg) translate3d(100%, 0%, 0px); } - .lg-css3.lg-slide-skew-cross .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -o-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -ms-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -webkit-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - opacity: 1; } - .lg-css3.lg-slide-skew-cross .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew-cross .lg-item.lg-next-slide, .lg-css3.lg-slide-skew-cross .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew-cross-rev .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew-cross-rev .lg-item.lg-prev-slide { - -moz-transform: skew(0deg, -60deg) translate3d(-100%, 0%, 0px); - -o-transform: skew(0deg, -60deg) translate3d(-100%, 0%, 0px); - -ms-transform: skew(0deg, -60deg) translate3d(-100%, 0%, 0px); - -webkit-transform: skew(0deg, -60deg) translate3d(-100%, 0%, 0px); - transform: skew(0deg, -60deg) translate3d(-100%, 0%, 0px); } - .lg-css3.lg-slide-skew-cross-rev .lg-item.lg-next-slide { - -moz-transform: skew(0deg, -60deg) translate3d(100%, 0%, 0px); - -o-transform: skew(0deg, -60deg) translate3d(100%, 0%, 0px); - -ms-transform: skew(0deg, -60deg) translate3d(100%, 0%, 0px); - -webkit-transform: skew(0deg, -60deg) translate3d(100%, 0%, 0px); - transform: skew(0deg, -60deg) translate3d(100%, 0%, 0px); } - .lg-css3.lg-slide-skew-cross-rev .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -o-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -ms-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -webkit-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - opacity: 1; } - .lg-css3.lg-slide-skew-cross-rev .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew-cross-rev .lg-item.lg-next-slide, .lg-css3.lg-slide-skew-cross-rev .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew-ver .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew-ver .lg-item.lg-prev-slide { - -moz-transform: skew(60deg, 0deg) translate3d(0, -100%, 0px); - -o-transform: skew(60deg, 0deg) translate3d(0, -100%, 0px); - -ms-transform: skew(60deg, 0deg) translate3d(0, -100%, 0px); - -webkit-transform: skew(60deg, 0deg) translate3d(0, -100%, 0px); - transform: skew(60deg, 0deg) translate3d(0, -100%, 0px); } - .lg-css3.lg-slide-skew-ver .lg-item.lg-next-slide { - -moz-transform: skew(60deg, 0deg) translate3d(0, 100%, 0px); - -o-transform: skew(60deg, 0deg) translate3d(0, 100%, 0px); - -ms-transform: skew(60deg, 0deg) translate3d(0, 100%, 0px); - -webkit-transform: skew(60deg, 0deg) translate3d(0, 100%, 0px); - transform: skew(60deg, 0deg) translate3d(0, 100%, 0px); } - .lg-css3.lg-slide-skew-ver .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -o-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -ms-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -webkit-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - opacity: 1; } - .lg-css3.lg-slide-skew-ver .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew-ver .lg-item.lg-next-slide, .lg-css3.lg-slide-skew-ver .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew-ver-rev .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew-ver-rev .lg-item.lg-prev-slide { - -moz-transform: skew(-60deg, 0deg) translate3d(0, -100%, 0px); - -o-transform: skew(-60deg, 0deg) translate3d(0, -100%, 0px); - -ms-transform: skew(-60deg, 0deg) translate3d(0, -100%, 0px); - -webkit-transform: skew(-60deg, 0deg) translate3d(0, -100%, 0px); - transform: skew(-60deg, 0deg) translate3d(0, -100%, 0px); } - .lg-css3.lg-slide-skew-ver-rev .lg-item.lg-next-slide { - -moz-transform: skew(-60deg, 0deg) translate3d(0, 100%, 0px); - -o-transform: skew(-60deg, 0deg) translate3d(0, 100%, 0px); - -ms-transform: skew(-60deg, 0deg) translate3d(0, 100%, 0px); - -webkit-transform: skew(-60deg, 0deg) translate3d(0, 100%, 0px); - transform: skew(-60deg, 0deg) translate3d(0, 100%, 0px); } - .lg-css3.lg-slide-skew-ver-rev .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -o-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -ms-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -webkit-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - opacity: 1; } - .lg-css3.lg-slide-skew-ver-rev .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew-ver-rev .lg-item.lg-next-slide, .lg-css3.lg-slide-skew-ver-rev .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew-ver-cross .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew-ver-cross .lg-item.lg-prev-slide { - -moz-transform: skew(0deg, 20deg) translate3d(0, -100%, 0px); - -o-transform: skew(0deg, 20deg) translate3d(0, -100%, 0px); - -ms-transform: skew(0deg, 20deg) translate3d(0, -100%, 0px); - -webkit-transform: skew(0deg, 20deg) translate3d(0, -100%, 0px); - transform: skew(0deg, 20deg) translate3d(0, -100%, 0px); } - .lg-css3.lg-slide-skew-ver-cross .lg-item.lg-next-slide { - -moz-transform: skew(0deg, 20deg) translate3d(0, 100%, 0px); - -o-transform: skew(0deg, 20deg) translate3d(0, 100%, 0px); - -ms-transform: skew(0deg, 20deg) translate3d(0, 100%, 0px); - -webkit-transform: skew(0deg, 20deg) translate3d(0, 100%, 0px); - transform: skew(0deg, 20deg) translate3d(0, 100%, 0px); } - .lg-css3.lg-slide-skew-ver-cross .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -o-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -ms-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -webkit-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - opacity: 1; } - .lg-css3.lg-slide-skew-ver-cross .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew-ver-cross .lg-item.lg-next-slide, .lg-css3.lg-slide-skew-ver-cross .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-slide-skew-ver-cross-rev .lg-item { - opacity: 0; } - .lg-css3.lg-slide-skew-ver-cross-rev .lg-item.lg-prev-slide { - -moz-transform: skew(0deg, -20deg) translate3d(0, -100%, 0px); - -o-transform: skew(0deg, -20deg) translate3d(0, -100%, 0px); - -ms-transform: skew(0deg, -20deg) translate3d(0, -100%, 0px); - -webkit-transform: skew(0deg, -20deg) translate3d(0, -100%, 0px); - transform: skew(0deg, -20deg) translate3d(0, -100%, 0px); } - .lg-css3.lg-slide-skew-ver-cross-rev .lg-item.lg-next-slide { - -moz-transform: skew(0deg, -20deg) translate3d(0, 100%, 0px); - -o-transform: skew(0deg, -20deg) translate3d(0, 100%, 0px); - -ms-transform: skew(0deg, -20deg) translate3d(0, 100%, 0px); - -webkit-transform: skew(0deg, -20deg) translate3d(0, 100%, 0px); - transform: skew(0deg, -20deg) translate3d(0, 100%, 0px); } - .lg-css3.lg-slide-skew-ver-cross-rev .lg-item.lg-current { - -moz-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -o-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -ms-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - -webkit-transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - transform: skew(0deg, 0deg) translate3d(0%, 0%, 0px); - opacity: 1; } - .lg-css3.lg-slide-skew-ver-cross-rev .lg-item.lg-prev-slide, .lg-css3.lg-slide-skew-ver-cross-rev .lg-item.lg-next-slide, .lg-css3.lg-slide-skew-ver-cross-rev .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-lollipop .lg-item { - opacity: 0; } - .lg-css3.lg-lollipop .lg-item.lg-prev-slide { - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); } - .lg-css3.lg-lollipop .lg-item.lg-next-slide { - -moz-transform: translate3d(0, 0, 0) scale(0.5); - -o-transform: translate3d(0, 0, 0) scale(0.5); - -ms-transform: translate3d(0, 0, 0) scale(0.5); - -webkit-transform: translate3d(0, 0, 0) scale(0.5); - transform: translate3d(0, 0, 0) scale(0.5); } - .lg-css3.lg-lollipop .lg-item.lg-current { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-lollipop .lg-item.lg-prev-slide, .lg-css3.lg-lollipop .lg-item.lg-next-slide, .lg-css3.lg-lollipop .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-lollipop-rev .lg-item { - opacity: 0; } - .lg-css3.lg-lollipop-rev .lg-item.lg-prev-slide { - -moz-transform: translate3d(0, 0, 0) scale(0.5); - -o-transform: translate3d(0, 0, 0) scale(0.5); - -ms-transform: translate3d(0, 0, 0) scale(0.5); - -webkit-transform: translate3d(0, 0, 0) scale(0.5); - transform: translate3d(0, 0, 0) scale(0.5); } - .lg-css3.lg-lollipop-rev .lg-item.lg-next-slide { - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); } - .lg-css3.lg-lollipop-rev .lg-item.lg-current { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-lollipop-rev .lg-item.lg-prev-slide, .lg-css3.lg-lollipop-rev .lg-item.lg-next-slide, .lg-css3.lg-lollipop-rev .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-rotate .lg-item { - opacity: 0; } - .lg-css3.lg-rotate .lg-item.lg-prev-slide { - -moz-transform: rotate(-360deg); - -o-transform: rotate(-360deg); - -ms-transform: rotate(-360deg); - -webkit-transform: rotate(-360deg); - transform: rotate(-360deg); } - .lg-css3.lg-rotate .lg-item.lg-next-slide { - -moz-transform: rotate(360deg); - -o-transform: rotate(360deg); - -ms-transform: rotate(360deg); - -webkit-transform: rotate(360deg); - transform: rotate(360deg); } - .lg-css3.lg-rotate .lg-item.lg-current { - -moz-transform: rotate(0deg); - -o-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - opacity: 1; } - .lg-css3.lg-rotate .lg-item.lg-prev-slide, .lg-css3.lg-rotate .lg-item.lg-next-slide, .lg-css3.lg-rotate .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-rotate-rev .lg-item { - opacity: 0; } - .lg-css3.lg-rotate-rev .lg-item.lg-prev-slide { - -moz-transform: rotate(360deg); - -o-transform: rotate(360deg); - -ms-transform: rotate(360deg); - -webkit-transform: rotate(360deg); - transform: rotate(360deg); } - .lg-css3.lg-rotate-rev .lg-item.lg-next-slide { - -moz-transform: rotate(-360deg); - -o-transform: rotate(-360deg); - -ms-transform: rotate(-360deg); - -webkit-transform: rotate(-360deg); - transform: rotate(-360deg); } - .lg-css3.lg-rotate-rev .lg-item.lg-current { - -moz-transform: rotate(0deg); - -o-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - opacity: 1; } - .lg-css3.lg-rotate-rev .lg-item.lg-prev-slide, .lg-css3.lg-rotate-rev .lg-item.lg-next-slide, .lg-css3.lg-rotate-rev .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } -.lg-css3.lg-tube .lg-item { - opacity: 0; } - .lg-css3.lg-tube .lg-item.lg-prev-slide { - -moz-transform: scale3d(1, 0, 1) translate3d(-100%, 0, 0); - -o-transform: scale3d(1, 0, 1) translate3d(-100%, 0, 0); - -ms-transform: scale3d(1, 0, 1) translate3d(-100%, 0, 0); - -webkit-transform: scale3d(1, 0, 1) translate3d(-100%, 0, 0); - transform: scale3d(1, 0, 1) translate3d(-100%, 0, 0); } - .lg-css3.lg-tube .lg-item.lg-next-slide { - -moz-transform: scale3d(1, 0, 1) translate3d(100%, 0, 0); - -o-transform: scale3d(1, 0, 1) translate3d(100%, 0, 0); - -ms-transform: scale3d(1, 0, 1) translate3d(100%, 0, 0); - -webkit-transform: scale3d(1, 0, 1) translate3d(100%, 0, 0); - transform: scale3d(1, 0, 1) translate3d(100%, 0, 0); } - .lg-css3.lg-tube .lg-item.lg-current { - -moz-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -o-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -ms-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - -webkit-transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - transform: scale3d(1, 1, 1) translate3d(0, 0, 0); - opacity: 1; } - .lg-css3.lg-tube .lg-item.lg-prev-slide, .lg-css3.lg-tube .lg-item.lg-next-slide, .lg-css3.lg-tube .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; } - -/*# sourceMappingURL=lg-transitions.css.map */ diff --git a/vendors/lightgallery/src/css/lg-transitions.css.map b/vendors/lightgallery/src/css/lg-transitions.css.map deleted file mode 100644 index 37599e9e0c..0000000000 --- a/vendors/lightgallery/src/css/lg-transitions.css.map +++ /dev/null @@ -1,7 +0,0 @@ -{ -"version": 3, -"mappings": "AAKQ,4BAAS;EACL,OAAO,EAAE,CAAC;EAEV,0CAAgB;ICgLxB,iBAAiB,EAAE,sBAAmB;IACtC,SAAS,EAAE,sBAAmB;ED7KtB,0CAAgB;IC4KxB,iBAAiB,EAAE,sBAAmB;IACtC,SAAS,EAAE,sBAAmB;EDzKtB,uCAAa;ICwKrB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;IDvKlB,OAAO,EAAE,CAAC;EAGd,+HAA+C;ICmSvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;AD/Rf,gCAAS;EACL,OAAO,EAAE,CAAC;EAEV,8CAAgB;ICyJxB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;EDtJtB,8CAAgB;ICqJxB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;EDlJtB,2CAAa;ICiJrB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;IDhJlB,OAAO,EAAE,CAAC;EAGd,2IAA+C;IC4QvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;ADxQf,6BAAS;EACL,OAAO,EAAE,CAAC;EAEV,2CAAgB;ICkIxB,iBAAiB,EAAE,sBAAmB;IACtC,SAAS,EAAE,sBAAmB;ED/HtB,2CAAgB;IC8HxB,iBAAiB,EAAE,sBAAmB;IACtC,SAAS,EAAE,sBAAmB;ED3HtB,wCAAa;IC0HrB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;IDzHlB,OAAO,EAAE,CAAC;EAGd,kIAA+C;ICqPvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;ADlPf,iCAAS;EACL,OAAO,EAAE,CAAC;EAEV,+CAAgB;IC4GxB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;EDzGtB,+CAAgB;ICwGxB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;EDrGtB,4CAAa;ICoGrB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;IDnGlB,OAAO,EAAE,CAAC;EAGd,8IAA+C;IC+NvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;AD3Nf,gCAAS;EACL,OAAO,EAAE,CAAC;EAEV,8CAAgB;ICqFxB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;EDlFtB,8CAAgB;ICiFxB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;ED9EtB,2CAAa;IC6ErB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;ID5ElB,OAAO,EAAE,CAAC;EAGd,2IAA+C;ICwMvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;ADpMf,gCAAS;EACL,OAAO,EAAE,CAAC;EAEV,8CAAgB;IC8DxB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;ED3DtB,8CAAgB;IC0DxB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;EDvDtB,2CAAa;ICsDrB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;IDrDlB,OAAO,EAAE,CAAC;EAGd,2IAA+C;ICiLvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;AD7Kf,8BAAS;EACL,OAAO,EAAE,CAAC;EAEV,4CAAgB;ICuCxB,iBAAiB,EAAE,sBAAmB;IACtC,SAAS,EAAE,sBAAmB;EDpCtB,4CAAgB;ICmCxB,iBAAiB,EAAE,sBAAmB;IACtC,SAAS,EAAE,sBAAmB;EDhCtB,yCAAa;IC+BrB,iBAAiB,EAAE,gBAAmB;IACtC,SAAS,EAAE,gBAAmB;ID9BlB,OAAO,EAAE,CAAC;EAGd,qIAA+C;IC0JvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;ADtJf,6BAAS;EACL,OAAO,EAAE,CAAC;EAEV,2CAAgB;IC6DxB,cAAc,ED5DiB,8CAA8C;IC6D7E,YAAY,ED7DmB,8CAA8C;IC8D7E,aAAa,ED9DkB,8CAA8C;IC+D7E,iBAAiB,ED/Dc,8CAA8C;ICgE7E,SAAS,EDhEsB,8CAA8C;EAGrE,2CAAgB;ICyDxB,cAAc,EDxDiB,8CAA8C;ICyD7E,YAAY,EDzDmB,8CAA8C;IC0D7E,aAAa,ED1DkB,8CAA8C;IC2D7E,iBAAiB,ED3Dc,8CAA8C;IC4D7E,SAAS,ED5DsB,8CAA8C;EAGrE,wCAAa;ICqDrB,cAAc,EDpDiB,qCAAqC;ICqDpE,YAAY,EDrDmB,qCAAqC;ICsDpE,aAAa,EDtDkB,qCAAqC;ICuDpE,iBAAiB,EDvDc,qCAAqC;ICwDpE,SAAS,EDxDsB,qCAAqC;IACxD,OAAO,EAAE,CAAC;EAGd,kIAA+C;ICmIvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;AD/Hf,mCAAS;EACL,OAAO,EAAE,CAAC;EAEV,iDAAgB;ICsCxB,cAAc,EDrCiB,yCAAyC;ICsCxE,YAAY,EDtCmB,yCAAyC;ICuCxE,aAAa,EDvCkB,yCAAyC;ICwCxE,iBAAiB,EDxCc,yCAAyC;ICyCxE,SAAS,EDzCsB,yCAAyC;EAGhE,iDAAgB;ICkCxB,cAAc,EDjCiB,wCAAwC;ICkCvE,YAAY,EDlCmB,wCAAwC;ICmCvE,aAAa,EDnCkB,wCAAwC;ICoCvE,iBAAiB,EDpCc,wCAAwC;ICqCvE,SAAS,EDrCsB,wCAAwC;EAG/D,8CAAa;IC8BrB,cAAc,ED7BiB,qCAAqC;IC8BpE,YAAY,ED9BmB,qCAAqC;IC+BpE,aAAa,ED/BkB,qCAAqC;ICgCpE,iBAAiB,EDhCc,qCAAqC;ICiCpE,SAAS,EDjCsB,qCAAqC;IACxD,OAAO,EAAE,CAAC;EAGd,oJAA+C;IC4GvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;ADvGf,sCAAS;EACL,OAAO,EAAE,CAAC;EAEV,oDAAgB;ICcxB,cAAc,EDbiB,6CAA6C;ICc5E,YAAY,EDdmB,6CAA6C;ICe5E,aAAa,EDfkB,6CAA6C;ICgB5E,iBAAiB,EDhBc,6CAA6C;ICiB5E,SAAS,EDjBsB,6CAA6C;EAGpE,oDAAgB;ICUxB,cAAc,EDTiB,4CAA4C;ICU3E,YAAY,EDVmB,4CAA4C;ICW3E,aAAa,EDXkB,4CAA4C;ICY3E,iBAAiB,EDZc,4CAA4C;ICa3E,SAAS,EDbsB,4CAA4C;EAGnE,iDAAa;ICMrB,cAAc,EDLiB,qCAAqC;ICMpE,YAAY,EDNmB,qCAAqC;ICOpE,aAAa,EDPkB,qCAAqC;ICQpE,iBAAiB,EDRc,qCAAqC;ICSpE,SAAS,EDTsB,qCAAqC;IACxD,OAAO,EAAE,CAAC;EAGd,6JAA+C;ICoFvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;AD/Ef,wCAAS;EACL,OAAO,EAAE,CAAC;EAEV,sDAAgB;ICVxB,cAAc,EDWiB,4CAA4C;ICV3E,YAAY,EDUmB,4CAA4C;ICT3E,aAAa,EDSkB,4CAA4C;ICR3E,iBAAiB,EDQc,4CAA4C;ICP3E,SAAS,EDOsB,4CAA4C;EAGnE,sDAAgB;ICdxB,cAAc,EDeiB,2CAA2C;ICd1E,YAAY,EDcmB,2CAA2C;ICb1E,aAAa,EDakB,2CAA2C;ICZ1E,iBAAiB,EDYc,2CAA2C;ICX1E,SAAS,EDWsB,2CAA2C;EAGlE,mDAAa;IClBrB,cAAc,EDmBiB,qCAAqC;IClBpE,YAAY,EDkBmB,qCAAqC;ICjBpE,aAAa,EDiBkB,qCAAqC;IChBpE,iBAAiB,EDgBc,qCAAqC;ICfpE,SAAS,EDesB,qCAAqC;IACxD,OAAO,EAAE,CAAC;EAGd,mKAA+C;IC4DvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;ADxDf,4CAAS;EACL,OAAO,EAAE,CAAC;EAEV,0DAAgB;ICjCxB,cAAc,EDkCiB,yCAAyC;ICjCxE,YAAY,EDiCmB,yCAAyC;IChCxE,aAAa,EDgCkB,yCAAyC;IC/BxE,iBAAiB,ED+Bc,yCAAyC;IC9BxE,SAAS,ED8BsB,yCAAyC;EAGhE,0DAAgB;ICrCxB,cAAc,EDsCiB,wCAAwC;ICrCvE,YAAY,EDqCmB,wCAAwC;ICpCvE,aAAa,EDoCkB,wCAAwC;ICnCvE,iBAAiB,EDmCc,wCAAwC;IClCvE,SAAS,EDkCsB,wCAAwC;EAG/D,uDAAa;ICzCrB,cAAc,ED0CiB,qCAAqC;ICzCpE,YAAY,EDyCmB,qCAAqC;ICxCpE,aAAa,EDwCkB,qCAAqC;ICvCpE,iBAAiB,EDuCc,qCAAqC;ICtCpE,SAAS,EDsCsB,qCAAqC;IACxD,OAAO,EAAE,CAAC;EAGd,+KAA+C;ICqCvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;ADhCf,iDAAS;EACL,OAAO,EAAE,CAAC;EAEV,+DAAgB;ICzDxB,cAAc,ED0DiB,6CAA6C;ICzD5E,YAAY,EDyDmB,6CAA6C;ICxD5E,aAAa,EDwDkB,6CAA6C;ICvD5E,iBAAiB,EDuDc,6CAA6C;ICtD5E,SAAS,EDsDsB,6CAA6C;EAGpE,+DAAgB;IC7DxB,cAAc,ED8DiB,4CAA4C;IC7D3E,YAAY,ED6DmB,4CAA4C;IC5D3E,aAAa,ED4DkB,4CAA4C;IC3D3E,iBAAiB,ED2Dc,4CAA4C;IC1D3E,SAAS,ED0DsB,4CAA4C;EAGnE,4DAAa;ICjErB,cAAc,EDkEiB,qCAAqC;ICjEpE,YAAY,EDiEmB,qCAAqC;IChEpE,aAAa,EDgEkB,qCAAqC;IC/DpE,iBAAiB,ED+Dc,qCAAqC;IC9DpE,SAAS,ED8DsB,qCAAqC;IACxD,OAAO,EAAE,CAAC;EAGd,8LAA+C;ICavD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;ADRf,iDAAS;EACL,OAAO,EAAE,CAAC;EAEV,+DAAgB;ICjFxB,cAAc,EDkFiB,4CAA4C;ICjF3E,YAAY,EDiFmB,4CAA4C;IChF3E,aAAa,EDgFkB,4CAA4C;IC/E3E,iBAAiB,ED+Ec,4CAA4C;IC9E3E,SAAS,ED8EsB,4CAA4C;EAGnE,+DAAgB;ICrFxB,cAAc,EDsFiB,2CAA2C;ICrF1E,YAAY,EDqFmB,2CAA2C;ICpF1E,aAAa,EDoFkB,2CAA2C;ICnF1E,iBAAiB,EDmFc,2CAA2C;IClF1E,SAAS,EDkFsB,2CAA2C;EAGlE,4DAAa;ICzFrB,cAAc,ED0FiB,qCAAqC;ICzFpE,YAAY,EDyFmB,qCAAqC;ICxFpE,aAAa,EDwFkB,qCAAqC;ICvFpE,iBAAiB,EDuFc,qCAAqC;ICtFpE,SAAS,EDsFsB,qCAAqC;IACxD,OAAO,EAAE,CAAC;EAGd,8LAA+C;ICXvD,kBAAkB,EAnBH,uEAAsD;IAoBrE,eAAe,EApBA,oEAAsD;IAqBrE,aAAa,EArBE,kEAAsD;IAsBrE,UAAU,EAAE,+DAAO;ADef,mCAAS;EACL,OAAO,EAAE,CAAC;EAEV,iDAAgB;IC1JxB,iBAAiB,EAAE,wBAAuB;IAC1C,SAAS,EAAE,wBAAuB;ED6J1B,iDAAgB;IC9JxB,iBAAiB,EAAE,uBAAuB;IAC1C,SAAS,EAAE,uBAAuB;EDiK1B,8CAAa;IClKrB,iBAAiB,EAAE,oBAAuB;IAC1C,SAAS,EAAE,oBAAuB;IDmKtB,OAAO,EAAE,CAAC;EAGd,oJAA+C;IClCvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;ADsCf,0CAAS;EACL,OAAO,EAAE,CAAC;EAEV,wDAAgB;IC/HxB,cAAc,EDgIiB,+CAA+C;IC/H9E,YAAY,ED+HmB,+CAA+C;IC9H9E,aAAa,ED8HkB,+CAA+C;IC7H9E,iBAAiB,ED6Hc,+CAA+C;IC5H9E,SAAS,ED4HsB,+CAA+C;EAGtE,wDAAgB;ICnIxB,cAAc,EDoIiB,8CAA8C;ICnI7E,YAAY,EDmImB,8CAA8C;IClI7E,aAAa,EDkIkB,8CAA8C;ICjI7E,iBAAiB,EDiIc,8CAA8C;IChI7E,SAAS,EDgIsB,8CAA8C;EAGrE,qDAAa;ICvIrB,cAAc,EDwIiB,qCAAqC;ICvIpE,YAAY,EDuImB,qCAAqC;ICtIpE,aAAa,EDsIkB,qCAAqC;ICrIpE,iBAAiB,EDqIc,qCAAqC;ICpIpE,SAAS,EDoIsB,qCAAqC;IACxD,OAAO,EAAE,CAAC;EAGd,yKAA+C;ICzDvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;AD6Df,oCAAS;EACL,OAAO,EAAE,CAAC;EAEV,kDAAgB;ICtJxB,cAAc,EAAE,iBAAW;IAC3B,YAAY,EAAE,iBAAW;IACzB,aAAa,EAAE,iBAAW;IAC1B,iBAAiB,EAAE,iBAAW;IAC9B,SAAS,EAAE,iBAAW;EDsJd,kDAAgB;IC1JxB,cAAc,EAAE,iBAAW;IAC3B,YAAY,EAAE,iBAAW;IACzB,aAAa,EAAE,iBAAW;IAC1B,iBAAiB,EAAE,iBAAW;IAC9B,SAAS,EAAE,iBAAW;ED0Jd,+CAAa;IC9JrB,cAAc,EAAE,gBAAW;IAC3B,YAAY,EAAE,gBAAW;IACzB,aAAa,EAAE,gBAAW;IAC1B,iBAAiB,EAAE,gBAAW;IAC9B,SAAS,EAAE,gBAAW;ID4JV,OAAO,EAAE,CAAC;EAGd,uJAA+C;IChFvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;ADoFf,wCAAS;EACL,OAAO,EAAE,CAAC;EAEV,sDAAgB;IC7KxB,cAAc,EAAE,kBAAW;IAC3B,YAAY,EAAE,kBAAW;IACzB,aAAa,EAAE,kBAAW;IAC1B,iBAAiB,EAAE,kBAAW;IAC9B,SAAS,EAAE,kBAAW;ED6Kd,sDAAgB;ICjLxB,cAAc,EAAE,kBAAW;IAC3B,YAAY,EAAE,kBAAW;IACzB,aAAa,EAAE,kBAAW;IAC1B,iBAAiB,EAAE,kBAAW;IAC9B,SAAS,EAAE,kBAAW;EDiLd,mDAAa;ICrLrB,cAAc,EAAE,gBAAW;IAC3B,YAAY,EAAE,gBAAW;IACzB,aAAa,EAAE,gBAAW;IAC1B,iBAAiB,EAAE,gBAAW;IAC9B,SAAS,EAAE,gBAAW;IDmLV,OAAO,EAAE,CAAC;EAGd,mKAA+C;ICvGvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;AD2Gf,sCAAS;EACL,OAAO,EAAE,CAAC;EAEV,oDAAgB;ICpMxB,cAAc,EAAE,iBAAW;IAC3B,YAAY,EAAE,iBAAW;IACzB,aAAa,EAAE,iBAAW;IAC1B,iBAAiB,EAAE,iBAAW;IAC9B,SAAS,EAAE,iBAAW;EDoMd,oDAAgB;ICxMxB,cAAc,EAAE,iBAAW;IAC3B,YAAY,EAAE,iBAAW;IACzB,aAAa,EAAE,iBAAW;IAC1B,iBAAiB,EAAE,iBAAW;IAC9B,SAAS,EAAE,iBAAW;EDwMd,iDAAa;IC5MrB,cAAc,EAAE,gBAAW;IAC3B,YAAY,EAAE,gBAAW;IACzB,aAAa,EAAE,gBAAW;IAC1B,iBAAiB,EAAE,gBAAW;IAC9B,SAAS,EAAE,gBAAW;ID0MV,OAAO,EAAE,CAAC;EAGd,6JAA+C;IC9HvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;ADkIf,0CAAS;EACL,OAAO,EAAE,CAAC;EAEV,wDAAgB;IC3NxB,cAAc,EAAE,kBAAW;IAC3B,YAAY,EAAE,kBAAW;IACzB,aAAa,EAAE,kBAAW;IAC1B,iBAAiB,EAAE,kBAAW;IAC9B,SAAS,EAAE,kBAAW;ED2Nd,wDAAgB;IC/NxB,cAAc,EAAE,kBAAW;IAC3B,YAAY,EAAE,kBAAW;IACzB,aAAa,EAAE,kBAAW;IAC1B,iBAAiB,EAAE,kBAAW;IAC9B,SAAS,EAAE,kBAAW;ED+Nd,qDAAa;ICnOrB,cAAc,EAAE,gBAAW;IAC3B,YAAY,EAAE,gBAAW;IACzB,aAAa,EAAE,gBAAW;IAC1B,iBAAiB,EAAE,gBAAW;IAC9B,SAAS,EAAE,gBAAW;IDiOV,OAAO,EAAE,CAAC;EAGd,yKAA+C;ICrJvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;ADyJf,+BAAS;EACL,OAAO,EAAE,CAAC;EAEV,6CAAgB;IClPxB,cAAc,EDmPiB,6CAA6C;IClP5E,YAAY,EDkPmB,6CAA6C;ICjP5E,aAAa,EDiPkB,6CAA6C;IChP5E,iBAAiB,EDgPc,6CAA6C;IC/O5E,SAAS,ED+OsB,6CAA6C;EAGpE,6CAAgB;ICtPxB,cAAc,EDuPiB,4CAA4C;ICtP3E,YAAY,EDsPmB,4CAA4C;ICrP3E,aAAa,EDqPkB,4CAA4C;ICpP3E,iBAAiB,EDoPc,4CAA4C;ICnP3E,SAAS,EDmPsB,4CAA4C;EAGnE,0CAAa;IC1PrB,cAAc,ED2PiB,yCAAyC;IC1PxE,YAAY,ED0PmB,yCAAyC;ICzPxE,aAAa,EDyPkB,yCAAyC;ICxPxE,iBAAiB,EDwPc,yCAAyC;ICvPxE,SAAS,EDuPsB,yCAAyC;IAC5D,OAAO,EAAE,CAAC;EAGd,wIAA+C;IC5KvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;ADgLf,mCAAS;EACL,OAAO,EAAE,CAAC;EAEV,iDAAgB;ICzQxB,cAAc,ED0QiB,8CAA8C;ICzQ7E,YAAY,EDyQmB,8CAA8C;ICxQ7E,aAAa,EDwQkB,8CAA8C;ICvQ7E,iBAAiB,EDuQc,8CAA8C;ICtQ7E,SAAS,EDsQsB,8CAA8C;EAGrE,iDAAgB;IC7QxB,cAAc,ED8QiB,6CAA6C;IC7Q5E,YAAY,ED6QmB,6CAA6C;IC5Q5E,aAAa,ED4QkB,6CAA6C;IC3Q5E,iBAAiB,ED2Qc,6CAA6C;IC1Q5E,SAAS,ED0QsB,6CAA6C;EAGpE,8CAAa;ICjRrB,cAAc,EDkRiB,yCAAyC;ICjRxE,YAAY,EDiRmB,yCAAyC;IChRxE,aAAa,EDgRkB,yCAAyC;IC/QxE,iBAAiB,ED+Qc,yCAAyC;IC9QxE,SAAS,ED8QsB,yCAAyC;IAC5D,OAAO,EAAE,CAAC;EAGd,oJAA+C;ICnMvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;ADuMf,qCAAS;EACL,OAAO,EAAE,CAAC;EAEV,mDAAgB;IChSxB,cAAc,EDiSiB,6CAA6C;IChS5E,YAAY,EDgSmB,6CAA6C;IC/R5E,aAAa,ED+RkB,6CAA6C;IC9R5E,iBAAiB,ED8Rc,6CAA6C;IC7R5E,SAAS,ED6RsB,6CAA6C;EAGpE,mDAAgB;ICpSxB,cAAc,EDqSiB,4CAA4C;ICpS3E,YAAY,EDoSmB,4CAA4C;ICnS3E,aAAa,EDmSkB,4CAA4C;IClS3E,iBAAiB,EDkSc,4CAA4C;ICjS3E,SAAS,EDiSsB,4CAA4C;EAGnE,gDAAa;ICxSrB,cAAc,EDySiB,yCAAyC;ICxSxE,YAAY,EDwSmB,yCAAyC;ICvSxE,aAAa,EDuSkB,yCAAyC;ICtSxE,iBAAiB,EDsSc,yCAAyC;ICrSxE,SAAS,EDqSsB,yCAAyC;IAC5D,OAAO,EAAE,CAAC;EAGd,0JAA+C;IC1NvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;AD8Nf,yCAAS;EACL,OAAO,EAAE,CAAC;EAEV,uDAAgB;ICvTxB,cAAc,EDwTiB,8CAA8C;ICvT7E,YAAY,EDuTmB,8CAA8C;ICtT7E,aAAa,EDsTkB,8CAA8C;ICrT7E,iBAAiB,EDqTc,8CAA8C;ICpT7E,SAAS,EDoTsB,8CAA8C;EAGrE,uDAAgB;IC3TxB,cAAc,ED4TiB,6CAA6C;IC3T5E,YAAY,ED2TmB,6CAA6C;IC1T5E,aAAa,ED0TkB,6CAA6C;ICzT5E,iBAAiB,EDyTc,6CAA6C;ICxT5E,SAAS,EDwTsB,6CAA6C;EAGpE,oDAAa;IC/TrB,cAAc,EDgUiB,yCAAyC;IC/TxE,YAAY,ED+TmB,yCAAyC;IC9TxE,aAAa,ED8TkB,yCAAyC;IC7TxE,iBAAiB,ED6Tc,yCAAyC;IC5TxE,SAAS,ED4TsB,yCAAyC;IAC5D,OAAO,EAAE,CAAC;EAGd,sKAA+C;ICjPvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;ADqPf,mCAAS;EACL,OAAO,EAAE,CAAC;EAEV,iDAAgB;IC9UxB,cAAc,ED+UiB,4CAA4C;IC9U3E,YAAY,ED8UmB,4CAA4C;IC7U3E,aAAa,ED6UkB,4CAA4C;IC5U3E,iBAAiB,ED4Uc,4CAA4C;IC3U3E,SAAS,ED2UsB,4CAA4C;EAGnE,iDAAgB;IClVxB,cAAc,EDmViB,2CAA2C;IClV1E,YAAY,EDkVmB,2CAA2C;ICjV1E,aAAa,EDiVkB,2CAA2C;IChV1E,iBAAiB,EDgVc,2CAA2C;IC/U1E,SAAS,ED+UsB,2CAA2C;EAGlE,8CAAa;ICtVrB,cAAc,EDuViB,yCAAyC;ICtVxE,YAAY,EDsVmB,yCAAyC;ICrVxE,aAAa,EDqVkB,yCAAyC;ICpVxE,iBAAiB,EDoVc,yCAAyC;ICnVxE,SAAS,EDmVsB,yCAAyC;IAC5D,OAAO,EAAE,CAAC;EAGd,oJAA+C;ICxQvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;AD4Qf,uCAAS;EACL,OAAO,EAAE,CAAC;EAEV,qDAAgB;ICrWxB,cAAc,EDsWiB,6CAA6C;ICrW5E,YAAY,EDqWmB,6CAA6C;ICpW5E,aAAa,EDoWkB,6CAA6C;ICnW5E,iBAAiB,EDmWc,6CAA6C;IClW5E,SAAS,EDkWsB,6CAA6C;EAGpE,qDAAgB;ICzWxB,cAAc,ED0WiB,4CAA4C;ICzW3E,YAAY,EDyWmB,4CAA4C;ICxW3E,aAAa,EDwWkB,4CAA4C;ICvW3E,iBAAiB,EDuWc,4CAA4C;ICtW3E,SAAS,EDsWsB,4CAA4C;EAGnE,kDAAa;IC7WrB,cAAc,ED8WiB,yCAAyC;IC7WxE,YAAY,ED6WmB,yCAAyC;IC5WxE,aAAa,ED4WkB,yCAAyC;IC3WxE,iBAAiB,ED2Wc,yCAAyC;IC1WxE,SAAS,ED0WsB,yCAAyC;IAC5D,OAAO,EAAE,CAAC;EAGd,gKAA+C;IC/RvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;ADmSf,yCAAS;EACL,OAAO,EAAE,CAAC;EAEV,uDAAgB;IC5XxB,cAAc,ED6XiB,4CAA4C;IC5X3E,YAAY,ED4XmB,4CAA4C;IC3X3E,aAAa,ED2XkB,4CAA4C;IC1X3E,iBAAiB,ED0Xc,4CAA4C;ICzX3E,SAAS,EDyXsB,4CAA4C;EAGnE,uDAAgB;IChYxB,cAAc,EDiYiB,2CAA2C;IChY1E,YAAY,EDgYmB,2CAA2C;IC/X1E,aAAa,ED+XkB,2CAA2C;IC9X1E,iBAAiB,ED8Xc,2CAA2C;IC7X1E,SAAS,ED6XsB,2CAA2C;EAGlE,oDAAa;ICpYrB,cAAc,EDqYiB,yCAAyC;ICpYxE,YAAY,EDoYmB,yCAAyC;ICnYxE,aAAa,EDmYkB,yCAAyC;IClYxE,iBAAiB,EDkYc,yCAAyC;ICjYxE,SAAS,EDiYsB,yCAAyC;IAC5D,OAAO,EAAE,CAAC;EAGd,sKAA+C;ICtTvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;AD0Tf,6CAAS;EACL,OAAO,EAAE,CAAC;EAEV,2DAAgB;ICnZxB,cAAc,EDoZiB,6CAA6C;ICnZ5E,YAAY,EDmZmB,6CAA6C;IClZ5E,aAAa,EDkZkB,6CAA6C;ICjZ5E,iBAAiB,EDiZc,6CAA6C;IChZ5E,SAAS,EDgZsB,6CAA6C;EAGpE,2DAAgB;ICvZxB,cAAc,EDwZiB,4CAA4C;ICvZ3E,YAAY,EDuZmB,4CAA4C;ICtZ3E,aAAa,EDsZkB,4CAA4C;ICrZ3E,iBAAiB,EDqZc,4CAA4C;ICpZ3E,SAAS,EDoZsB,4CAA4C;EAGnE,wDAAa;IC3ZrB,cAAc,ED4ZiB,yCAAyC;IC3ZxE,YAAY,ED2ZmB,yCAAyC;IC1ZxE,aAAa,ED0ZkB,yCAAyC;ICzZxE,iBAAiB,EDyZc,yCAAyC;ICxZxE,SAAS,EDwZsB,yCAAyC;IAC5D,OAAO,EAAE,CAAC;EAGd,kLAA+C;IC7UvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;ADiVf,6BAAS;EACL,OAAO,EAAE,CAAC;EAEV,2CAAgB;IC5dxB,iBAAiB,EAAE,wBAAuB;IAC1C,SAAS,EAAE,wBAAuB;ED+d1B,2CAAgB;IC9axB,cAAc,ED+aiB,+BAA+B;IC9a9D,YAAY,ED8amB,+BAA+B;IC7a9D,aAAa,ED6akB,+BAA+B;IC5a9D,iBAAiB,ED4ac,+BAA+B;IC3a9D,SAAS,ED2asB,+BAA+B;EAGtD,wCAAa;ICperB,iBAAiB,EAAE,oBAAuB;IAC1C,SAAS,EAAE,oBAAuB;IDqetB,OAAO,EAAE,CAAC;EAGd,kIAA+C;ICpWvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;ADwWf,iCAAS;EACL,OAAO,EAAE,CAAC;EAEV,+CAAgB;ICjcxB,cAAc,EDkciB,+BAA+B;ICjc9D,YAAY,EDicmB,+BAA+B;IChc9D,aAAa,EDgckB,+BAA+B;IC/b9D,iBAAiB,ED+bc,+BAA+B;IC9b9D,SAAS,ED8bsB,+BAA+B;EAGtD,+CAAgB;ICvfxB,iBAAiB,EAAE,uBAAuB;IAC1C,SAAS,EAAE,uBAAuB;ED0f1B,4CAAa;IC3frB,iBAAiB,EAAE,oBAAuB;IAC1C,SAAS,EAAE,oBAAuB;ID4ftB,OAAO,EAAE,CAAC;EAGd,8IAA+C;IC3XvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;AD+Xf,2BAAS;EACL,OAAO,EAAE,CAAC;EAEV,yCAAgB;ICxdxB,cAAc,EAAE,eAAW;IAC3B,YAAY,EAAE,eAAW;IACzB,aAAa,EAAE,eAAW;IAC1B,iBAAiB,EAAE,eAAW;IAC9B,SAAS,EAAE,eAAW;EDwdd,yCAAgB;IC5dxB,cAAc,EAAE,cAAW;IAC3B,YAAY,EAAE,cAAW;IACzB,aAAa,EAAE,cAAW;IAC1B,iBAAiB,EAAE,cAAW;IAC9B,SAAS,EAAE,cAAW;ED4dd,sCAAa;ICherB,cAAc,EAAE,YAAW;IAC3B,YAAY,EAAE,YAAW;IACzB,aAAa,EAAE,YAAW;IAC1B,iBAAiB,EAAE,YAAW;IAC9B,SAAS,EAAE,YAAW;ID8dV,OAAO,EAAE,CAAC;EAGd,4HAA+C;IClZvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;ADsZf,+BAAS;EACL,OAAO,EAAE,CAAC;EAEV,6CAAgB;IC/exB,cAAc,EAAE,cAAW;IAC3B,YAAY,EAAE,cAAW;IACzB,aAAa,EAAE,cAAW;IAC1B,iBAAiB,EAAE,cAAW;IAC9B,SAAS,EAAE,cAAW;ED+ed,6CAAgB;ICnfxB,cAAc,EAAE,eAAW;IAC3B,YAAY,EAAE,eAAW;IACzB,aAAa,EAAE,eAAW;IAC1B,iBAAiB,EAAE,eAAW;IAC9B,SAAS,EAAE,eAAW;EDmfd,0CAAa;ICvfrB,cAAc,EAAE,YAAW;IAC3B,YAAY,EAAE,YAAW;IACzB,aAAa,EAAE,YAAW;IAC1B,iBAAiB,EAAE,YAAW;IAC9B,SAAS,EAAE,YAAW;IDqfV,OAAO,EAAE,CAAC;EAGd,wIAA+C;ICzavD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO;AD6af,yBAAS;EACL,OAAO,EAAE,CAAC;EAEV,uCAAgB;ICtgBxB,cAAc,EDugBiB,yCAAyC;ICtgBxE,YAAY,EDsgBmB,yCAAyC;ICrgBxE,aAAa,EDqgBkB,yCAAyC;ICpgBxE,iBAAiB,EDogBc,yCAAyC;ICngBxE,SAAS,EDmgBsB,yCAAyC;EAGhE,uCAAgB;IC1gBxB,cAAc,ED2gBiB,wCAAwC;IC1gBvE,YAAY,ED0gBmB,wCAAwC;ICzgBvE,aAAa,EDygBkB,wCAAwC;ICxgBvE,iBAAiB,EDwgBc,wCAAwC;ICvgBvE,SAAS,EDugBsB,wCAAwC;EAG/D,oCAAa;IC9gBrB,cAAc,ED+gBiB,qCAAqC;IC9gBpE,YAAY,ED8gBmB,qCAAqC;IC7gBpE,aAAa,ED6gBkB,qCAAqC;IC5gBpE,iBAAiB,ED4gBc,qCAAqC;IC3gBpE,SAAS,ED2gBsB,qCAAqC;IACxD,OAAO,EAAE,CAAC;EAGd,sHAA+C;IChcvD,kBAAkB,EAnBH,yEAAsD;IAoBrE,eAAe,EApBA,sEAAsD;IAqBrE,aAAa,EArBE,oEAAsD;IAsBrE,UAAU,EAAE,iEAAO", -"sources": ["../sass/lg-transitions.scss","../sass/lg-mixins.scss"], -"names": [], -"file": "lg-transitions.css" -} diff --git a/vendors/lightgallery/src/css/lightgallery.css b/vendors/lightgallery/src/css/lightgallery.css deleted file mode 100644 index d28f1ce449..0000000000 --- a/vendors/lightgallery/src/css/lightgallery.css +++ /dev/null @@ -1,843 +0,0 @@ -@font-face { - font-family: 'lg'; - src: url("../fonts/lg.eot?n1z373"); - src: url("../fonts/lg.eot?#iefixn1z373") format("embedded-opentype"), url("../fonts/lg.woff?n1z373") format("woff"), url("../fonts/lg.ttf?n1z373") format("truetype"), url("../fonts/lg.svg?n1z373#lg") format("svg"); - font-weight: normal; - font-style: normal; -} -.lg-icon { - font-family: 'lg'; - speak: none; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.lg-actions .lg-next, .lg-actions .lg-prev { - background-color: rgba(0, 0, 0, 0.45); - border-radius: 2px; - color: #999; - cursor: pointer; - display: block; - font-size: 22px; - margin-top: -10px; - padding: 8px 10px 9px; - position: absolute; - top: 50%; - z-index: 1080; -} -.lg-actions .lg-next.disabled, .lg-actions .lg-prev.disabled { - pointer-events: none; - opacity: 0.5; -} -.lg-actions .lg-next:hover, .lg-actions .lg-prev:hover { - color: #FFF; -} -.lg-actions .lg-next { - right: 20px; -} -.lg-actions .lg-next:before { - content: "\e095"; -} -.lg-actions .lg-prev { - left: 20px; -} -.lg-actions .lg-prev:after { - content: "\e094"; -} - -@-webkit-keyframes lg-right-end { - 0% { - left: 0; - } - 50% { - left: -30px; - } - 100% { - left: 0; - } -} -@-moz-keyframes lg-right-end { - 0% { - left: 0; - } - 50% { - left: -30px; - } - 100% { - left: 0; - } -} -@-ms-keyframes lg-right-end { - 0% { - left: 0; - } - 50% { - left: -30px; - } - 100% { - left: 0; - } -} -@keyframes lg-right-end { - 0% { - left: 0; - } - 50% { - left: -30px; - } - 100% { - left: 0; - } -} -@-webkit-keyframes lg-left-end { - 0% { - left: 0; - } - 50% { - left: 30px; - } - 100% { - left: 0; - } -} -@-moz-keyframes lg-left-end { - 0% { - left: 0; - } - 50% { - left: 30px; - } - 100% { - left: 0; - } -} -@-ms-keyframes lg-left-end { - 0% { - left: 0; - } - 50% { - left: 30px; - } - 100% { - left: 0; - } -} -@keyframes lg-left-end { - 0% { - left: 0; - } - 50% { - left: 30px; - } - 100% { - left: 0; - } -} -.lg-outer.lg-right-end .lg-object { - -webkit-animation: lg-right-end 0.3s; - -o-animation: lg-right-end 0.3s; - animation: lg-right-end 0.3s; - position: relative; -} -.lg-outer.lg-left-end .lg-object { - -webkit-animation: lg-left-end 0.3s; - -o-animation: lg-left-end 0.3s; - animation: lg-left-end 0.3s; - position: relative; -} - -.lg-toolbar { - z-index: 1082; - left: 0; - position: absolute; - top: 0; - width: 100%; - background-color: rgba(0, 0, 0, 0.45); -} -.lg-toolbar .lg-icon { - color: #999; - cursor: pointer; - float: right; - font-size: 24px; - height: 47px; - line-height: 27px; - padding: 10px 0; - text-align: center; - width: 50px; - text-decoration: none !important; - outline: medium none; - -webkit-transition: color 0.2s linear; - -o-transition: color 0.2s linear; - transition: color 0.2s linear; -} -.lg-toolbar .lg-icon:hover { - color: #FFF; -} -.lg-toolbar .lg-close:after { - content: "\e070"; -} -.lg-toolbar .lg-download:after { - content: "\e0f2"; -} - -.lg-sub-html { - background-color: rgba(0, 0, 0, 0.45); - bottom: 0; - color: #EEE; - font-size: 16px; - left: 0; - padding: 10px 40px; - position: fixed; - right: 0; - text-align: center; - z-index: 1080; -} -.lg-sub-html h4 { - margin: 0; - font-size: 13px; - font-weight: bold; -} -.lg-sub-html p { - font-size: 12px; - margin: 5px 0 0; -} - -#lg-counter { - color: #999; - display: inline-block; - font-size: 16px; - padding-left: 20px; - padding-top: 12px; - vertical-align: middle; -} - -.lg-toolbar, .lg-prev, .lg-next { - opacity: 1; - -webkit-transition: -webkit-transform 0.35s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.35s cubic-bezier(0, 0, 0.25, 1) 0s, color 0.2s linear; - -moz-transition: -moz-transform 0.35s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.35s cubic-bezier(0, 0, 0.25, 1) 0s, color 0.2s linear; - -o-transition: -o-transform 0.35s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.35s cubic-bezier(0, 0, 0.25, 1) 0s, color 0.2s linear; - transition: transform 0.35s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.35s cubic-bezier(0, 0, 0.25, 1) 0s, color 0.2s linear; -} - -.lg-hide-items .lg-prev { - opacity: 0; - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); -} -.lg-hide-items .lg-next { - opacity: 0; - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); -} -.lg-hide-items .lg-toolbar { - opacity: 0; - -webkit-transform: translate3d(0, -10px, 0); - transform: translate3d(0, -10px, 0); -} - -body:not(.lg-from-hash) .lg-outer.lg-start-zoom .lg-object { - -webkit-transform: scale3d(0.5, 0.5, 0.5); - transform: scale3d(0.5, 0.5, 0.5); - opacity: 0; - -webkit-transition: -webkit-transform 250ms cubic-bezier(0, 0, 0.25, 1) 0s, opacity 250ms cubic-bezier(0, 0, 0.25, 1) !important; - -moz-transition: -moz-transform 250ms cubic-bezier(0, 0, 0.25, 1) 0s, opacity 250ms cubic-bezier(0, 0, 0.25, 1) !important; - -o-transition: -o-transform 250ms cubic-bezier(0, 0, 0.25, 1) 0s, opacity 250ms cubic-bezier(0, 0, 0.25, 1) !important; - transition: transform 250ms cubic-bezier(0, 0, 0.25, 1) 0s, opacity 250ms cubic-bezier(0, 0, 0.25, 1) !important; - -webkit-transform-origin: 50% 50%; - -moz-transform-origin: 50% 50%; - -ms-transform-origin: 50% 50%; - transform-origin: 50% 50%; -} -body:not(.lg-from-hash) .lg-outer.lg-start-zoom .lg-item.lg-complete .lg-object { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - opacity: 1; -} - -.lg-outer .lg-thumb-outer { - background-color: #0D0A0A; - bottom: 0; - position: absolute; - width: 100%; - z-index: 1080; - max-height: 350px; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - -webkit-transition: -webkit-transform 0.25s cubic-bezier(0, 0, 0.25, 1) 0s; - -moz-transition: -moz-transform 0.25s cubic-bezier(0, 0, 0.25, 1) 0s; - -o-transition: -o-transform 0.25s cubic-bezier(0, 0, 0.25, 1) 0s; - transition: transform 0.25s cubic-bezier(0, 0, 0.25, 1) 0s; -} -.lg-outer .lg-thumb-outer.lg-grab .lg-thumb-item { - cursor: -webkit-grab; - cursor: -moz-grab; - cursor: -o-grab; - cursor: -ms-grab; - cursor: grab; -} -.lg-outer .lg-thumb-outer.lg-grabbing .lg-thumb-item { - cursor: move; - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - cursor: -o-grabbing; - cursor: -ms-grabbing; - cursor: grabbing; -} -.lg-outer .lg-thumb-outer.lg-dragging .lg-thumb { - -webkit-transition-duration: 0s !important; - transition-duration: 0s !important; -} -.lg-outer.lg-thumb-open .lg-thumb-outer { - -webkit-transform: translate3d(0, 0%, 0); - transform: translate3d(0, 0%, 0); -} -.lg-outer .lg-thumb { - padding: 10px 0; - height: 100%; - margin-bottom: -5px; -} -.lg-outer .lg-thumb-item { - border-radius: 5px; - cursor: pointer; - float: left; - overflow: hidden; - height: 100%; - border: 2px solid #FFF; - border-radius: 4px; - margin-bottom: 5px; -} -@media (min-width: 1025px) { - .lg-outer .lg-thumb-item { - -webkit-transition: border-color 0.25s ease; - -o-transition: border-color 0.25s ease; - transition: border-color 0.25s ease; - } -} -.lg-outer .lg-thumb-item.active, .lg-outer .lg-thumb-item:hover { - border-color: #a90707; -} -.lg-outer .lg-thumb-item img { - width: 100%; - height: 100%; - object-fit: cover; -} -.lg-outer.lg-has-thumb .lg-item { - padding-bottom: 120px; -} -.lg-outer.lg-can-toggle .lg-item { - padding-bottom: 0; -} -.lg-outer.lg-pull-caption-up .lg-sub-html { - -webkit-transition: bottom 0.25s ease; - -o-transition: bottom 0.25s ease; - transition: bottom 0.25s ease; -} -.lg-outer.lg-pull-caption-up.lg-thumb-open .lg-sub-html { - bottom: 100px; -} -.lg-outer .lg-toogle-thumb { - background-color: #0D0A0A; - border-radius: 2px 2px 0 0; - color: #999; - cursor: pointer; - font-size: 24px; - height: 39px; - line-height: 27px; - padding: 5px 0; - position: absolute; - right: 20px; - text-align: center; - top: -39px; - width: 50px; -} -.lg-outer .lg-toogle-thumb:after { - content: "\e1ff"; -} -.lg-outer .lg-toogle-thumb:hover { - color: #FFF; -} - -.lg-outer .lg-video-cont { - display: inline-block; - vertical-align: middle; - max-width: 1140px; - max-height: 100%; - width: 100%; - padding: 0 5px; -} -.lg-outer .lg-video { - width: 100%; - height: 0; - padding-bottom: 56.25%; - overflow: hidden; - position: relative; -} -.lg-outer .lg-video .lg-object { - display: inline-block; - position: absolute; - top: 0; - left: 0; - width: 100% !important; - height: 100% !important; -} -.lg-outer .lg-video .lg-video-play { - width: 84px; - height: 59px; - position: absolute; - left: 50%; - top: 50%; - margin-left: -42px; - margin-top: -30px; - z-index: 1080; - cursor: pointer; -} -.lg-outer .lg-has-vimeo .lg-video-play { - background: url("../img/vimeo-play.png") no-repeat scroll 0 0 transparent; -} -.lg-outer .lg-has-vimeo:hover .lg-video-play { - background: url("../img/vimeo-play.png") no-repeat scroll 0 -58px transparent; -} -.lg-outer .lg-has-html5 .lg-video-play { - background: transparent url("../img/video-play.png") no-repeat scroll 0 0; - height: 64px; - margin-left: -32px; - margin-top: -32px; - width: 64px; - opacity: 0.8; -} -.lg-outer .lg-has-html5:hover .lg-video-play { - opacity: 1; -} -.lg-outer .lg-has-youtube .lg-video-play { - background: url("../img/youtube-play.png") no-repeat scroll 0 0 transparent; -} -.lg-outer .lg-has-youtube:hover .lg-video-play { - background: url("../img/youtube-play.png") no-repeat scroll 0 -60px transparent; -} -.lg-outer .lg-video-object { - width: 100% !important; - height: 100% !important; - position: absolute; - top: 0; - left: 0; -} -.lg-outer .lg-has-video .lg-video-object { - visibility: hidden; -} -.lg-outer .lg-has-video.lg-video-playing .lg-object, .lg-outer .lg-has-video.lg-video-playing .lg-video-play { - display: none; -} -.lg-outer .lg-has-video.lg-video-playing .lg-video-object { - visibility: visible; -} - -.lg-progress-bar { - background-color: #333; - height: 5px; - left: 0; - position: absolute; - top: 0; - width: 100%; - z-index: 1083; - opacity: 0; - -webkit-transition: opacity 0.08s ease 0s; - -moz-transition: opacity 0.08s ease 0s; - -o-transition: opacity 0.08s ease 0s; - transition: opacity 0.08s ease 0s; -} -.lg-progress-bar .lg-progress { - background-color: #a90707; - height: 5px; - width: 0; -} -.lg-progress-bar.lg-start .lg-progress { - width: 100%; -} -.lg-show-autoplay .lg-progress-bar { - opacity: 1; -} - -.lg-autoplay-button:after { - content: "\e01d"; -} -.lg-show-autoplay .lg-autoplay-button:after { - content: "\e01a"; -} - -.lg-outer.lg-css3.lg-zoom-dragging .lg-item.lg-complete.lg-zoomable .lg-img-wrap, .lg-outer.lg-css3.lg-zoom-dragging .lg-item.lg-complete.lg-zoomable .lg-image { - -webkit-transition-duration: 0s; - transition-duration: 0s; -} -.lg-outer .lg-item.lg-complete.lg-zoomable .lg-img-wrap { - -webkit-transition: left 0.3s cubic-bezier(0, 0, 0.25, 1) 0s, top 0.3s cubic-bezier(0, 0, 0.25, 1) 0s; - -moz-transition: left 0.3s cubic-bezier(0, 0, 0.25, 1) 0s, top 0.3s cubic-bezier(0, 0, 0.25, 1) 0s; - -o-transition: left 0.3s cubic-bezier(0, 0, 0.25, 1) 0s, top 0.3s cubic-bezier(0, 0, 0.25, 1) 0s; - transition: left 0.3s cubic-bezier(0, 0, 0.25, 1) 0s, top 0.3s cubic-bezier(0, 0, 0.25, 1) 0s; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - -webkit-backface-visibility: hidden; - -moz-backface-visibility: hidden; - backface-visibility: hidden; -} -.lg-outer .lg-item.lg-complete.lg-zoomable .lg-image { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - -webkit-transition: -webkit-transform 0.3s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.15s !important; - -moz-transition: -moz-transform 0.3s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.15s !important; - -o-transition: -o-transform 0.3s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.15s !important; - transition: transform 0.3s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.15s !important; - -webkit-transform-origin: 0 0; - -moz-transform-origin: 0 0; - -ms-transform-origin: 0 0; - transform-origin: 0 0; - -webkit-backface-visibility: hidden; - -moz-backface-visibility: hidden; - backface-visibility: hidden; -} - -#lg-zoom-in:after { - content: "\e311"; -} - -#lg-actual-size { - font-size: 20px; -} -#lg-actual-size:after { - content: "\e033"; -} - -#lg-zoom-out { - opacity: 0.5; - pointer-events: none; -} -#lg-zoom-out:after { - content: "\e312"; -} -.lg-zoomed #lg-zoom-out { - opacity: 1; - pointer-events: auto; -} - -.lg-outer .lg-pager-outer { - bottom: 60px; - left: 0; - position: absolute; - right: 0; - text-align: center; - z-index: 1080; - height: 10px; -} -.lg-outer .lg-pager-outer.lg-pager-hover .lg-pager-cont { - overflow: visible; -} -.lg-outer .lg-pager-cont { - cursor: pointer; - display: inline-block; - overflow: hidden; - position: relative; - vertical-align: top; - margin: 0 5px; -} -.lg-outer .lg-pager-cont:hover .lg-pager-thumb-cont { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); -} -.lg-outer .lg-pager-cont.lg-pager-active .lg-pager { - box-shadow: 0 0 0 2px white inset; -} -.lg-outer .lg-pager-thumb-cont { - background-color: #fff; - color: #FFF; - bottom: 100%; - height: 83px; - left: 0; - margin-bottom: 20px; - margin-left: -60px; - opacity: 0; - padding: 5px; - position: absolute; - width: 120px; - border-radius: 3px; - -webkit-transition: opacity 0.15s ease 0s, -webkit-transform 0.15s ease 0s; - -moz-transition: opacity 0.15s ease 0s, -moz-transform 0.15s ease 0s; - -o-transition: opacity 0.15s ease 0s, -o-transform 0.15s ease 0s; - transition: opacity 0.15s ease 0s, transform 0.15s ease 0s; - -webkit-transform: translate3d(0, 5px, 0); - transform: translate3d(0, 5px, 0); -} -.lg-outer .lg-pager-thumb-cont img { - width: 100%; - height: 100%; -} -.lg-outer .lg-pager { - background-color: rgba(255, 255, 255, 0.5); - border-radius: 50%; - box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.7) inset; - display: block; - height: 12px; - -webkit-transition: box-shadow 0.3s ease 0s; - -o-transition: box-shadow 0.3s ease 0s; - transition: box-shadow 0.3s ease 0s; - width: 12px; -} -.lg-outer .lg-pager:hover, .lg-outer .lg-pager:focus { - box-shadow: 0 0 0 8px white inset; -} -.lg-outer .lg-caret { - border-left: 10px solid transparent; - border-right: 10px solid transparent; - border-top: 10px dashed; - bottom: -10px; - display: inline-block; - height: 0; - left: 50%; - margin-left: -5px; - position: absolute; - vertical-align: middle; - width: 0; -} - -.lg-fullscreen:after { - content: "\e20c"; -} -.lg-fullscreen-on .lg-fullscreen:after { - content: "\e20d"; -} - -.group { - *zoom: 1; -} - -.group:before, .group:after { - display: table; - content: ""; - line-height: 0; -} - -.group:after { - clear: both; -} - -.lg-outer { - width: 100%; - height: 100%; - position: fixed; - top: 0; - left: 0; - z-index: 1050; - opacity: 0; - -webkit-transition: opacity 0.15s ease 0s; - -o-transition: opacity 0.15s ease 0s; - transition: opacity 0.15s ease 0s; -} -.lg-outer * { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -.lg-outer.lg-visible { - opacity: 1; -} -.lg-outer.lg-css3 .lg-item.lg-prev-slide, .lg-outer.lg-css3 .lg-item.lg-next-slide, .lg-outer.lg-css3 .lg-item.lg-current { - -webkit-transition-duration: inherit !important; - transition-duration: inherit !important; - -webkit-transition-timing-function: inherit !important; - transition-timing-function: inherit !important; -} -.lg-outer.lg-css3.lg-dragging .lg-item.lg-prev-slide, .lg-outer.lg-css3.lg-dragging .lg-item.lg-next-slide, .lg-outer.lg-css3.lg-dragging .lg-item.lg-current { - -webkit-transition-duration: 0s !important; - transition-duration: 0s !important; - opacity: 1; -} -.lg-outer.lg-grab img.lg-object { - cursor: -webkit-grab; - cursor: -moz-grab; - cursor: -o-grab; - cursor: -ms-grab; - cursor: grab; -} -.lg-outer.lg-grabbing img.lg-object { - cursor: move; - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - cursor: -o-grabbing; - cursor: -ms-grabbing; - cursor: grabbing; -} -.lg-outer .lg { - height: 100%; - width: 100%; - position: relative; - overflow: hidden; - margin-left: auto; - margin-right: auto; - max-width: 100%; - max-height: 100%; -} -.lg-outer .lg-inner { - width: 100%; - height: 100%; - position: absolute; - left: 0; - top: 0; - white-space: nowrap; -} -.lg-outer .lg-item { - background: url("../img/loading.gif") no-repeat scroll center center transparent; - display: none !important; -} -.lg-outer.lg-css3 .lg-prev-slide, .lg-outer.lg-css3 .lg-current, .lg-outer.lg-css3 .lg-next-slide { - display: inline-block !important; -} -.lg-outer.lg-css .lg-current { - display: inline-block !important; -} -.lg-outer .lg-item, .lg-outer .lg-img-wrap { - display: inline-block; - text-align: center; - position: absolute; - width: 100%; - height: 100%; -} -.lg-outer .lg-item:before, .lg-outer .lg-img-wrap:before { - content: ""; - display: inline-block; - height: 50%; - width: 1px; - margin-right: -1px; -} -.lg-outer .lg-img-wrap { - position: absolute; - padding: 0 5px; - left: 0; - right: 0; - top: 0; - bottom: 0; -} -.lg-outer .lg-item.lg-complete { - background-image: none; -} -.lg-outer .lg-item.lg-current { - z-index: 1060; -} -.lg-outer .lg-image { - display: inline-block; - vertical-align: middle; - max-width: 100%; - max-height: 100%; - width: auto !important; - height: auto !important; -} -.lg-outer.lg-show-after-load .lg-item .lg-object, .lg-outer.lg-show-after-load .lg-item .lg-video-play { - opacity: 0; - -webkit-transition: opacity 0.15s ease 0s; - -o-transition: opacity 0.15s ease 0s; - transition: opacity 0.15s ease 0s; -} -.lg-outer.lg-show-after-load .lg-item.lg-complete .lg-object, .lg-outer.lg-show-after-load .lg-item.lg-complete .lg-video-play { - opacity: 1; -} -.lg-outer .lg-empty-html { - display: none; -} -.lg-outer.lg-hide-download #lg-download { - display: none; -} - -.lg-backdrop { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 1040; - background-color: #000; - opacity: 0; - -webkit-transition: opacity 0.15s ease 0s; - -o-transition: opacity 0.15s ease 0s; - transition: opacity 0.15s ease 0s; -} -.lg-backdrop.in { - opacity: 1; -} - -.lg-css3.lg-no-trans .lg-prev-slide, .lg-css3.lg-no-trans .lg-next-slide, .lg-css3.lg-no-trans .lg-current { - -webkit-transition: none 0s ease 0s !important; - -moz-transition: none 0s ease 0s !important; - -o-transition: none 0s ease 0s !important; - transition: none 0s ease 0s !important; -} -.lg-css3.lg-use-css3 .lg-item { - -webkit-backface-visibility: hidden; - -moz-backface-visibility: hidden; - backface-visibility: hidden; -} -.lg-css3.lg-use-left .lg-item { - -webkit-backface-visibility: hidden; - -moz-backface-visibility: hidden; - backface-visibility: hidden; -} -.lg-css3.lg-fade .lg-item { - opacity: 0; -} -.lg-css3.lg-fade .lg-item.lg-current { - opacity: 1; -} -.lg-css3.lg-fade .lg-item.lg-prev-slide, .lg-css3.lg-fade .lg-item.lg-next-slide, .lg-css3.lg-fade .lg-item.lg-current { - -webkit-transition: opacity 0.1s ease 0s; - -moz-transition: opacity 0.1s ease 0s; - -o-transition: opacity 0.1s ease 0s; - transition: opacity 0.1s ease 0s; -} -.lg-css3.lg-slide.lg-use-css3 .lg-item { - opacity: 0; -} -.lg-css3.lg-slide.lg-use-css3 .lg-item.lg-prev-slide { - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); -} -.lg-css3.lg-slide.lg-use-css3 .lg-item.lg-next-slide { - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); -} -.lg-css3.lg-slide.lg-use-css3 .lg-item.lg-current { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; -} -.lg-css3.lg-slide.lg-use-css3 .lg-item.lg-prev-slide, .lg-css3.lg-slide.lg-use-css3 .lg-item.lg-next-slide, .lg-css3.lg-slide.lg-use-css3 .lg-item.lg-current { - -webkit-transition: -webkit-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: -moz-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: -o-transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; -} -.lg-css3.lg-slide.lg-use-left .lg-item { - opacity: 0; - position: absolute; - left: 0; -} -.lg-css3.lg-slide.lg-use-left .lg-item.lg-prev-slide { - left: -100%; -} -.lg-css3.lg-slide.lg-use-left .lg-item.lg-next-slide { - left: 100%; -} -.lg-css3.lg-slide.lg-use-left .lg-item.lg-current { - left: 0; - opacity: 1; -} -.lg-css3.lg-slide.lg-use-left .lg-item.lg-prev-slide, .lg-css3.lg-slide.lg-use-left .lg-item.lg-next-slide, .lg-css3.lg-slide.lg-use-left .lg-item.lg-current { - -webkit-transition: left 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -moz-transition: left 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - -o-transition: left 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; - transition: left 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s; -} - -/*# sourceMappingURL=lightgallery.css.map */ diff --git a/vendors/lightgallery/src/css/lightgallery.css.map b/vendors/lightgallery/src/css/lightgallery.css.map deleted file mode 100644 index c34247efeb..0000000000 --- a/vendors/lightgallery/src/css/lightgallery.css.map +++ /dev/null @@ -1,7 +0,0 @@ -{ -"version": 3, -"mappings": "AACA,UAMC;EALG,WAAW,EAAE,IAAI;EACjB,GAAG,EAAE,6BAAsC;EAC3C,GAAG,EAAE,gNAAoP;EACzP,WAAW,EAAE,MAAM;EACnB,UAAU,EAAE,MAAM;;AAItB,QAAS;EACL,WAAW,EAAE,IAAI;EACjB,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,MAAM;EAClB,WAAW,EAAE,MAAM;EACnB,YAAY,EAAE,MAAM;EACpB,cAAc,EAAE,IAAI;EACpB,WAAW,EAAE,CAAC;EACd,uCAAuC;EACvC,sBAAsB,EAAE,WAAW;EACnC,uBAAuB,EAAE,SAAS;;;AClBlC,0CAAmB;EACf,gBAAgB,ECaN,mBAAW;EDZrB,aAAa,ECFG,GAAG;EDGnB,KAAK,ECqCW,IAAc;EDpC9B,MAAM,EAAE,OAAO;EACf,OAAO,EAAE,KAAK;EACd,SAAS,EAAE,IAAI;EACf,UAAU,EAAE,KAAK;EACjB,OAAO,EAAE,YAAY;EACrB,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,GAAG;EACR,OAAO,ECoCG,IAAI;;ADlCd,4DAAW;EACP,cAAc,EAAE,IAAI;EACpB,OAAO,EAAE,GAAG;;AAGhB,sDAAQ;EACJ,KAAK,ECsBa,IAAoB;;ADlB9C,oBAAS;EACL,KAAK,EAAE,IAAI;;AAEX,2BAAS;EACL,OAAO,EAAE,OAAO;;AAIxB,oBAAS;EACL,IAAI,EAAE,IAAI;;AAEV,0BAAQ;EACJ,OAAO,EAAE,OAAO;;;AEuBxB,+BAEC;EFnBD,EAAG;IACC,IAAI,EAAE,CAAC;;EAGX,GAAI;IACA,IAAI,EAAE,KAAK;;EAGf,IAAK;IACD,IAAI,EAAE,CAAC;;;AEYX,4BAEC;EFvBD,EAAG;IACC,IAAI,EAAE,CAAC;;EAGX,GAAI;IACA,IAAI,EAAE,KAAK;;EAGf,IAAK;IACD,IAAI,EAAE,CAAC;;;AEgBX,2BAEC;EF3BD,EAAG;IACC,IAAI,EAAE,CAAC;;EAGX,GAAI;IACA,IAAI,EAAE,KAAK;;EAGf,IAAK;IACD,IAAI,EAAE,CAAC;;;AEoBX,uBAEC;EF/BD,EAAG;IACC,IAAI,EAAE,CAAC;;EAGX,GAAI;IACA,IAAI,EAAE,KAAK;;EAGf,IAAK;IACD,IAAI,EAAE,CAAC;;;AEQX,8BAEC;EFJD,EAAG;IACC,IAAI,EAAE,CAAC;;EAGX,GAAI;IACA,IAAI,EAAE,IAAI;;EAGd,IAAK;IACD,IAAI,EAAE,CAAC;;;AEHX,2BAEC;EFRD,EAAG;IACC,IAAI,EAAE,CAAC;;EAGX,GAAI;IACA,IAAI,EAAE,IAAI;;EAGd,IAAK;IACD,IAAI,EAAE,CAAC;;;AECX,0BAEC;EFZD,EAAG;IACC,IAAI,EAAE,CAAC;;EAGX,GAAI;IACA,IAAI,EAAE,IAAI;;EAGd,IAAK;IACD,IAAI,EAAE,CAAC;;;AEKX,sBAEC;EFhBD,EAAG;IACC,IAAI,EAAE,CAAC;;EAGX,GAAI;IACA,IAAI,EAAE,IAAI;;EAGd,IAAK;IACD,IAAI,EAAE,CAAC;;;AAOP,iCAAW;EEvDf,iBAAiB,EFwDU,iBAAiB;EEvD5C,YAAY,EFuDe,iBAAiB;EEtD5C,SAAS,EFsDkB,iBAAiB;EACpC,QAAQ,EAAE,QAAQ;;AAKtB,gCAAW;EE9Df,iBAAiB,EF+DU,gBAAgB;EE9D3C,YAAY,EF8De,gBAAgB;EE7D3C,SAAS,EF6DkB,gBAAgB;EACnC,QAAQ,EAAE,QAAQ;;;AAM9B,WAAY;EACR,OAAO,ECxCM,IAAI;EDyCjB,IAAI,EAAE,CAAC;EACP,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,IAAI;EACX,gBAAgB,EC9FJ,mBAAmB;;ADgG/B,oBAAS;EACL,KAAK,ECxDW,IAAc;EDyD9B,MAAM,EAAE,OAAO;EACf,KAAK,EAAE,KAAK;EACZ,SAAS,EAAE,IAAI;EACf,MAAM,EAAE,IAAI;EACZ,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,MAAM;EACf,UAAU,EAAE,MAAM;EAClB,KAAK,EAAE,IAAI;EACX,eAAe,EAAE,eAAe;EAChC,OAAO,EAAE,WAAW;EEiHxB,kBAAkB,EAAE,iBAAW;EAC/B,aAAa,EAAE,iBAAW;EAC1B,UAAU,EAAE,iBAAW;;AFhHnB,0BAAQ;EACJ,KAAK,ECrEa,IAAoB;;AD0E1C,2BAAQ;EACJ,OAAO,EAAE,OAAO;;AAKpB,8BAAQ;EACJ,OAAO,EAAE,OAAO;;;AAM5B,YAAa;EACT,gBAAgB,EC9FH,mBAAmB;ED+FhC,MAAM,EAAE,CAAC;EACT,KAAK,EC/FW,IAAI;EDgGpB,SAAS,EAAE,IAAI;EACf,IAAI,EAAE,CAAC;EACP,OAAO,EAAE,SAAS;EAClB,QAAQ,EAAE,KAAK;EACf,KAAK,EAAE,CAAC;EACR,UAAU,EAAE,MAAM;EAClB,OAAO,ECzFM,IAAI;;AD2FjB,eAAG;EACC,MAAM,EAAE,CAAC;EACT,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;;AAGrB,cAAE;EACE,SAAS,EAAE,IAAI;EACf,MAAM,EAAE,OAAO;;;AAKvB,WAAY;EACR,KAAK,EClHe,IAAc;EDmHlC,OAAO,EAAE,YAAY;EACrB,SAAS,ECjJU,IAAI;EDkJvB,YAAY,EAAE,IAAI;EAClB,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,MAAM;;;AAI1B,+BAAgC;EAC5B,OAAO,EAAE,CAAC;EEkIV,kBAAkB,EArBH,uHAAsD;EAsBrE,eAAe,EAtBA,oHAAsD;EAuBrE,aAAa,EAvBE,kHAAsD;EAwBrE,UAAU,EAAE,+GAAO;;;AFhInB,uBAAS;EACL,OAAO,EAAE,CAAC;EEXd,iBAAiB,EAAE,wBAAuB;EAC1C,SAAS,EAAE,wBAAuB;;AFclC,uBAAS;EACL,OAAO,EAAE,CAAC;EEhBd,iBAAiB,EAAE,uBAAuB;EAC1C,SAAS,EAAE,uBAAuB;;AFmBlC,0BAAY;EACR,OAAO,EAAE,CAAC;EErBd,iBAAiB,EAAE,wBAAuB;EAC1C,SAAS,EAAE,wBAAuB;;;AF6B1B,0DAAU;EEzBlB,iBAAiB,EAAE,sBAAmB;EACtC,SAAS,EAAE,sBAAmB;EF0BlB,OAAO,EAAE,CAAC;EEuGtB,kBAAkB,EArBH,4GAAsD;EAsBrE,eAAe,EAtBA,yGAAsD;EAuBrE,aAAa,EAvBE,uGAAsD;EAwBrE,UAAU,EAAE,oGAAO;EA/FnB,wBAAwB,EFTc,OAAO;EEU7C,qBAAqB,EFViB,OAAO;EEW7C,oBAAoB,EFXkB,OAAO;EEY7C,gBAAgB,EFZsB,OAAO;;AAGjC,+EAAU;EEhCtB,iBAAiB,EAAE,gBAAmB;EACtC,SAAS,EAAE,gBAAmB;EFiCd,OAAO,EAAE,CAAC;;;AGvM1B,yBAAgB;EACZ,gBAAgB,EF0CV,OAAO;EEzCb,MAAM,EAAE,CAAC;EACT,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;EACX,OAAO,EF8CI,IAAI;EE7Cf,UAAU,EAAE,KAAK;ED0JrB,iBAAiB,EAAE,uBAAuB;EAC1C,SAAS,EAAE,uBAAuB;EAsIlC,kBAAkB,EArBH,sDAAsD;EAsBrE,eAAe,EAtBA,mDAAsD;EAuBrE,aAAa,EAvBE,iDAAsD;EAwBrE,UAAU,EAAE,8CAAO;;AC/RX,gDAAe;ED+SvB,MAAM,EAAE,YAAY;EACpB,MAAM,EAAE,SAAS;EACjB,MAAM,EAAE,OAAO;EACf,MAAM,EAAE,QAAQ;EAChB,MAAM,EAAE,IAAI;;AC7SJ,oDAAe;EDiTvB,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,gBAAgB;EACxB,MAAM,EAAE,aAAa;EACrB,MAAM,EAAE,WAAW;EACnB,MAAM,EAAE,YAAY;EACpB,MAAM,EAAE,QAAQ;;AChTR,+CAAU;EDqNlB,2BAA2B,EAAE,aAAoB;EACjD,mBAAmB,EAAE,aAAoB;;AChNrC,uCAAgB;EDmIpB,iBAAiB,EAAE,qBAAuB;EAC1C,SAAS,EAAE,qBAAuB;;AC/HlC,mBAAU;EACN,OAAO,EAAE,MAAM;EACf,MAAM,EAAE,IAAI;EACZ,aAAa,EAAE,IAAI;;AAGvB,wBAAe;EACX,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,OAAO;EACf,KAAK,EAAE,IAAI;EACX,QAAQ,EAAE,MAAM;EAChB,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,cAAc;EACtB,aAAa,EAAE,GAAG;EAClB,aAAa,EAAE,GAAG;;AAClB,0BAA2B;EAT/B,wBAAe;IDoLf,kBAAkB,EAAE,uBAAW;IAC/B,aAAa,EAAE,uBAAW;IAC1B,UAAU,EAAE,uBAAW;;;ACzKnB,+DAAkB;EACd,YAAY,EF7BI,OAAmB;;AEgCvC,4BAAI;EACA,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,KAAK;;AAKrB,+BAAS;EACL,cAAc,EAAE,KAAK;;AAKzB,gCAAS;EACL,cAAc,EAAE,CAAC;;AAIrB,yCAAa;EDgJjB,kBAAkB,EAAE,iBAAW;EAC/B,aAAa,EAAE,iBAAW;EAC1B,UAAU,EAAE,iBAAW;;AC9If,uDAAa;EACT,MAAM,EAAE,KAAK;;AAKzB,0BAAiB;EACb,gBAAgB,EF/CH,OAAO;EEgDpB,aAAa,EAAE,WAAiD;EAChE,KAAK,EFhDW,IAAc;EEiD9B,MAAM,EAAE,OAAO;EACf,SAAS,EAAE,IAAI;EACf,MAAM,EAAE,IAAI;EACZ,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,KAAK;EACd,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,MAAM;EAClB,GAAG,EAAE,KAAK;EACV,KAAK,EAAE,IAAI;;AAEX,gCAAQ;EACJ,OAAO,EAAE,OAAO;;AAGpB,gCAAQ;EACJ,KAAK,EFhEa,IAAoB;;;AG1C9C,wBAAe;EACX,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,MAAM;EACtB,SAAS,EAAE,MAAM;EACjB,UAAU,EAAE,IAAI;EAChB,KAAK,EAAE,IAAI;EACX,OAAO,EAAE,KAAK;;AAGlB,mBAAU;EACN,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,CAAC;EACT,cAAc,EAAE,MAAM;EACtB,QAAQ,EAAE,MAAM;EAChB,QAAQ,EAAE,QAAQ;;AAElB,8BAAW;EACP,OAAO,EAAE,YAAY;EACrB,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,eAAe;EACtB,MAAM,EAAE,eAAe;;AAG3B,kCAAe;EACX,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,QAAQ,EAAE,QAAQ;EAClB,IAAI,EAAE,GAAG;EACT,GAAG,EAAE,GAAG;EACR,WAAW,EAAE,KAAK;EAClB,UAAU,EAAE,KAAK;EACjB,OAAO,EHoBC,IAAI;EGnBZ,MAAM,EAAE,OAAO;;AAKnB,sCAAc;EACV,UAAU,EAAE,6DAAyE;;AAGrF,4CAAc;EACV,UAAU,EAAE,iEAA6E;;AAOjG,sCAAc;EACV,UAAU,EAAE,6DAAyE;EACrF,MAAM,EAAE,IAAI;EACZ,WAAW,EAAE,KAAK;EAClB,UAAU,EAAE,KAAK;EACjB,KAAK,EAAE,IAAI;EACX,OAAO,EAAE,GAAG;;AAGZ,4CAAc;EACV,OAAO,EAAE,CAAC;;AAOlB,wCAAc;EACV,UAAU,EAAE,+DAA2E;;AAGvF,8CAAc;EACV,UAAU,EAAE,mEAA+E;;AAKvG,0BAAiB;EACb,KAAK,EAAE,eAAe;EACtB,MAAM,EAAE,eAAe;EACvB,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;;AAIP,wCAAiB;EACb,UAAU,EAAE,MAAM;;AAIlB,4GAA2B;EACvB,OAAO,EAAE,IAAI;;AAGjB,yDAAiB;EACb,UAAU,EAAE,OAAO;;;AClGnC,gBAAiB;EACb,gBAAgB,EJwBC,IAAI;EIvBrB,MAAM,EJyBe,GAAG;EIxBxB,IAAI,EAAE,CAAC;EACP,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,IAAI;EACX,OAAO,EJyCU,IAAI;EIxCrB,OAAO,EAAE,CAAC;EHgSV,kBAAkB,EArBH,qBAAsD;EAsBrE,eAAe,EAtBA,qBAAsD;EAuBrE,aAAa,EAvBE,qBAAsD;EAwBrE,UAAU,EAAE,qBAAO;;AGhSnB,6BAAa;EACT,gBAAgB,EJcI,OAAmB;EIbvC,MAAM,EJcW,GAAG;EIbpB,KAAK,EAAE,CAAC;;AAIR,sCAAa;EACT,KAAK,EAAE,IAAI;;AAInB,kCAAoB;EAChB,OAAO,EAAE,CAAC;;;AAKd,yBAAQ;EAIJ,OAAO,EAAE,OAAO;;AAHhB,2CAAoB;EAChB,OAAO,EAAE,OAAO;;;AC3BhB,+JAAwB;EJyOhC,2BAA2B,EAAE,EAAoB;EACjD,mBAAmB,EAAE,EAAoB;;AIlOrC,uDAAa;EJ4RjB,kBAAkB,EArBH,iFAAsD;EAsBrE,eAAe,EAtBA,iFAAsD;EAuBrE,aAAa,EAvBE,iFAAsD;EAwBrE,UAAU,EAAE,iFAAO;EA1InB,iBAAiB,EAAE,oBAAuB;EAC1C,SAAS,EAAE,oBAAuB;EAhFlC,2BAA2B,EInEU,MAAM;EJoE3C,wBAAwB,EIpEa,MAAM;EJqE3C,mBAAmB,EIrEkB,MAAM;;AAGvC,oDAAU;EJoJd,iBAAiB,EAAE,gBAAmB;EACtC,SAAS,EAAE,gBAAmB;EAiI9B,kBAAkB,EArBH,+EAAsD;EAsBrE,eAAe,EAtBA,4EAAsD;EAuBrE,aAAa,EAvBE,0EAAsD;EAwBrE,UAAU,EAAE,uEAAO;EA/FnB,wBAAwB,EItLU,GAAG;EJuLrC,qBAAqB,EIvLa,GAAG;EJwLrC,oBAAoB,EIxLc,GAAG;EJyLrC,gBAAgB,EIzLkB,GAAG;EJ4DrC,2BAA2B,EI3DU,MAAM;EJ4D3C,wBAAwB,EI5Da,MAAM;EJ6D3C,mBAAmB,EI7DkB,MAAM;;;AAQ3C,iBAAQ;EACJ,OAAO,EAAE,OAAO;;;AAIxB,eAAgB;EACZ,SAAS,EAAE,IAAI;;AACf,qBAAQ;EACJ,OAAO,EAAE,OAAO;;;AAIxB,YAAa;EACT,OAAO,EAAE,GAAG;EACZ,cAAc,EAAE,IAAI;;AAEpB,kBAAQ;EACJ,OAAO,EAAE,OAAO;;AAGpB,uBAAa;EACT,OAAO,EAAE,CAAC;EACV,cAAc,EAAE,IAAI;;;ACpDxB,yBAAgB;EACZ,MAAM,EAAE,IAAI;EACZ,IAAI,EAAE,CAAC;EACP,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,CAAC;EACR,UAAU,EAAE,MAAM;EAClB,OAAO,EN8CA,IAAI;EM7CX,MAAM,EAAE,IAAI;;AAGR,uDAAe;EACX,QAAQ,EAAE,OAAO;;AAK7B,wBAAe;EACX,MAAM,EAAE,OAAO;EACf,OAAO,EAAE,YAAY;EACrB,QAAQ,EAAE,MAAM;EAChB,QAAQ,EAAE,QAAQ;EAClB,cAAc,EAAE,GAAG;EACnB,MAAM,EAAE,KAAK;;AAGT,mDAAqB;EACjB,OAAO,EAAE,CAAC;ELsItB,iBAAiB,EAAE,oBAAuB;EAC1C,SAAS,EAAE,oBAAuB;;AKjI1B,kDAAU;EACN,UAAU,EAAE,qBAAqB;;AAK7C,8BAAqB;EACjB,gBAAgB,EAAE,IAAI;EACtB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,IAAI;EACZ,IAAI,EAAE,CAAC;EACP,aAAa,EAAE,IAAI;EACnB,WAAW,EAAE,KAAK;EAClB,OAAO,EAAE,CAAC;EACV,OAAO,EAAE,GAAG;EACZ,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,KAAK;EACZ,aAAa,EAAE,GAAG;ELqPtB,kBAAkB,EArBH,sDAAsD;EAsBrE,eAAe,EAtBA,mDAAsD;EAuBrE,aAAa,EAvBE,iDAAsD;EAwBrE,UAAU,EAAE,8CAAO;EA1InB,iBAAiB,EAAE,sBAAuB;EAC1C,SAAS,EAAE,sBAAuB;;AK3G9B,kCAAI;EACA,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;;AAIpB,mBAAU;EACN,gBAAgB,EAAE,wBAAwB;EAC1C,aAAa,EAAE,GAAG;EAClB,UAAU,EAAE,wCAAwC;EACpD,OAAO,EAAE,KAAK;EACd,MAAM,EAAE,IAAI;EL2JhB,kBAAkB,EAAE,uBAAW;EAC/B,aAAa,EAAE,uBAAW;EAC1B,UAAU,EAAE,uBAAW;EK3JnB,KAAK,EAAE,IAAI;;AAEX,oDAAiB;EACb,UAAU,EAAE,qBAAqB;;AAIzC,mBAAU;EACN,WAAW,EAAE,sBAAsB;EACnC,YAAY,EAAE,sBAAsB;EACpC,UAAU,EAAE,WAAW;EACvB,MAAM,EAAE,KAAK;EACb,OAAO,EAAE,YAAY;EACrB,MAAM,EAAE,CAAC;EACT,IAAI,EAAE,GAAG;EACT,WAAW,EAAE,IAAI;EACjB,QAAQ,EAAE,QAAQ;EAClB,cAAc,EAAE,MAAM;EACtB,KAAK,EAAE,CAAC;;;ACrFZ,oBAAQ;EACJ,OAAO,EAAE,OAAO;;AAEhB,sCAAoB;EAChB,OAAO,EAAE,OAAO;;;ACQ5B,MAAO;EACH,KAAK,EAAE,CAAC;;;AAGZ,2BAA4B;EACxB,OAAO,EAAE,KAAK;EACd,OAAO,EAAE,EAAE;EACX,WAAW,EAAE,CAAC;;;AAGlB,YAAa;EACT,KAAK,EAAE,IAAI;;;AAIf,SAAU;EACN,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,OAAO,ERaI,IAAI;EQZf,OAAO,EAAE,CAAC;EP0LV,kBAAkB,EAAE,qBAAW;EAC/B,aAAa,EAAE,qBAAW;EAC1B,UAAU,EAAE,qBAAW;;AOxLvB,WAAE;EP4DF,kBAAkB,EO3DM,UAAU;EP4DlC,eAAe,EO5DS,UAAU;EP6DlC,UAAU,EO7Dc,UAAU;;AAGlC,oBAAa;EACT,OAAO,EAAE,CAAC;;AAMN,yHAA+C;EP2LvD,2BAA2B,EAAE,kBAAoB;EACjD,mBAAmB,EAAE,kBAAoB;EAIzC,kCAAkC,EO9Lc,kBAAkB;EP+LlE,0BAA0B,EO/LsB,kBAAkB;;AAQ1D,6JAA+C;EPiLvD,2BAA2B,EAAE,aAAoB;EACjD,mBAAmB,EAAE,aAAoB;EOhL7B,OAAO,EAAE,CAAC;;AAOlB,+BAAc;EPsPlB,MAAM,EAAE,YAAY;EACpB,MAAM,EAAE,SAAS;EACjB,MAAM,EAAE,OAAO;EACf,MAAM,EAAE,QAAQ;EAChB,MAAM,EAAE,IAAI;;AOpPR,mCAAc;EPwPlB,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,gBAAgB;EACxB,MAAM,EAAE,aAAa;EACrB,MAAM,EAAE,WAAW;EACnB,MAAM,EAAE,YAAY;EACpB,MAAM,EAAE,QAAQ;;AOxPhB,aAAI;EACA,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;EACX,QAAQ,EAAE,QAAQ;EAClB,QAAQ,EAAE,MAAM;EAChB,WAAW,EAAE,IAAI;EACjB,YAAY,EAAE,IAAI;EAClB,SAAS,EAAE,IAAI;EACf,UAAU,EAAE,IAAI;;AAGpB,mBAAU;EACN,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,QAAQ,EAAE,QAAQ;EAClB,IAAI,EAAE,CAAC;EACP,GAAG,EAAE,CAAC;EACN,WAAW,EAAE,MAAM;;AAGvB,kBAAS;EACL,UAAU,EAAE,oEAAgF;EAC5F,OAAO,EAAE,eAAe;;AAGxB,iGAA2C;EACvC,OAAO,EAAE,uBAAuB;;AAIpC,4BAAW;EACP,OAAO,EAAE,uBAAuB;;AAIxC,0CAAuB;EACnB,OAAO,EAAE,YAAY;EACrB,UAAU,EAAE,MAAM;EAClB,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;;AAEZ,wDAAS;EACL,OAAO,EAAE,EAAE;EACX,OAAO,EAAE,YAAY;EACrB,MAAM,EAAE,GAAG;EACX,KAAK,EAAE,GAAG;EACV,YAAY,EAAE,IAAI;;AAI1B,sBAAa;EACT,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,KAAK;EACd,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,GAAG,EAAE,CAAC;EACN,MAAM,EAAE,CAAC;;AAIT,8BAAc;EACV,gBAAgB,EAAE,IAAI;;AAG1B,6BAAa;EACT,OAAO,ER3FL,IAAI;;AQ+Fd,mBAAU;EACN,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,MAAM;EACtB,SAAS,EAAE,IAAI;EACf,UAAU,EAAE,IAAI;EAChB,KAAK,EAAE,eAAe;EACtB,MAAM,EAAE,eAAe;;AAKnB,sGAA2B;EACvB,OAAO,EAAE,CAAC;EP2DtB,kBAAkB,EAAE,qBAAW;EAC/B,aAAa,EAAE,qBAAW;EAC1B,UAAU,EAAE,qBAAW;;AOxDX,8HAA2B;EACvB,OAAO,EAAE,CAAC;;AAO1B,wBAAe;EACX,OAAO,EAAE,IAAI;;AAIb,uCAAY;EACR,OAAO,EAAE,IAAI;;;AAIzB,YAAY;EACR,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,OAAO,ERvIO,IAAI;EQwIlB,gBAAgB,EAAE,IAAI;EACtB,OAAO,EAAE,CAAC;EP4BV,kBAAkB,EAAE,qBAAW;EAC/B,aAAa,EAAE,qBAAW;EAC1B,UAAU,EAAE,qBAAW;;AO5BvB,eAAI;EACA,OAAO,ERpMI,CAAC;;;AQ6MZ,0GAA4C;EP2FhD,kBAAkB,EArBH,0BAAsD;EAsBrE,eAAe,EAtBA,0BAAsD;EAuBrE,aAAa,EAvBE,0BAAsD;EAwBrE,UAAU,EAAE,0BAAO;;AOxFf,6BAAS;EPjIb,2BAA2B,EOkIU,MAAM;EPjI3C,wBAAwB,EOiIa,MAAM;EPhI3C,mBAAmB,EOgIkB,MAAM;;AAKvC,6BAAS;EPvIb,2BAA2B,EOwIU,MAAM;EPvI3C,wBAAwB,EOuIa,MAAM;EPtI3C,mBAAmB,EOsIkB,MAAM;;AAMvC,yBAAS;EACL,OAAO,EAAE,CAAC;;AAEV,oCAAa;EACT,OAAO,EAAE,CAAC;;AAId,sHAA+C;EPgEvD,kBAAkB,EArBH,oBAAsD;EAsBrE,eAAe,EAtBA,oBAAsD;EAuBrE,aAAa,EAvBE,oBAAsD;EAwBrE,UAAU,EAAE,oBAAO;;AO3DX,sCAAS;EACL,OAAO,EAAE,CAAC;;AAEV,oDAAgB;EPlF5B,iBAAiB,EAAE,wBAAuB;EAC1C,SAAS,EAAE,wBAAuB;;AOqFtB,oDAAgB;EPtF5B,iBAAiB,EAAE,uBAAuB;EAC1C,SAAS,EAAE,uBAAuB;;AOyFtB,iDAAa;EP1FzB,iBAAiB,EAAE,oBAAuB;EAC1C,SAAS,EAAE,oBAAuB;EO2FlB,OAAO,EAAE,CAAC;;AAId,6JAA+C;EPuC3D,kBAAkB,EArBH,yEAAsD;EAsBrE,eAAe,EAtBA,sEAAsD;EAuBrE,aAAa,EAvBE,oEAAsD;EAwBrE,UAAU,EAAE,iEAAO;;AOnCX,sCAAS;EACL,OAAO,EAAE,CAAC;EACV,QAAQ,EAAE,QAAQ;EAClB,IAAI,EAAE,CAAC;;AAEP,oDAAgB;EACZ,IAAI,EAAE,KAAK;;AAGf,oDAAgB;EACZ,IAAI,EAAE,IAAI;;AAGd,iDAAa;EACT,IAAI,EAAE,CAAC;EACP,OAAO,EAAE,CAAC;;AAId,6JAA+C;EPa3D,kBAAkB,EArBH,4DAAsD;EAsBrE,eAAe,EAtBA,4DAAsD;EAuBrE,aAAa,EAvBE,4DAAsD;EAwBrE,UAAU,EAAE,4DAAO", -"sources": ["../sass/lg-fonts.scss","../sass/lg-theme-default.scss","../sass/lg-variables.scss","../sass/lg-mixins.scss","../sass/lg-thumbnail.scss","../sass/lg-video.scss","../sass/lg-autoplay.scss","../sass/lg-zoom.scss","../sass/lg-pager.scss","../sass/lg-fullscreen.scss","../sass/lightgallery.scss"], -"names": [], -"file": "lightgallery.css" -} diff --git a/vendors/lightgallery/src/fonts/lg.eot b/vendors/lightgallery/src/fonts/lg.eot deleted file mode 100644 index 1eb39169ca..0000000000 Binary files a/vendors/lightgallery/src/fonts/lg.eot and /dev/null differ diff --git a/vendors/lightgallery/src/fonts/lg.svg b/vendors/lightgallery/src/fonts/lg.svg deleted file mode 100644 index 648412d9dc..0000000000 --- a/vendors/lightgallery/src/fonts/lg.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - -{ - "fontFamily": "lg", - "majorVersion": 1, - "minorVersion": 0, - "fontURL": "https://github.com/sachinchoolur/lightGallery", - "copyright": "sachin", - "license": "MLT", - "licenseURL": "http://opensource.org/licenses/MIT", - "version": "Version 1.0", - "fontId": "lg", - "psName": "lg", - "subFamily": "Regular", - "fullName": "lg", - "description": "Font generated by IcoMoon." -} - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vendors/lightgallery/src/fonts/lg.ttf b/vendors/lightgallery/src/fonts/lg.ttf deleted file mode 100644 index d33b8e5e86..0000000000 Binary files a/vendors/lightgallery/src/fonts/lg.ttf and /dev/null differ diff --git a/vendors/lightgallery/src/fonts/lg.woff b/vendors/lightgallery/src/fonts/lg.woff deleted file mode 100644 index bd370be88d..0000000000 Binary files a/vendors/lightgallery/src/fonts/lg.woff and /dev/null differ diff --git a/vendors/lightgallery/src/img/loading.gif b/vendors/lightgallery/src/img/loading.gif deleted file mode 100644 index 4744c4556c..0000000000 Binary files a/vendors/lightgallery/src/img/loading.gif and /dev/null differ diff --git a/vendors/lightgallery/src/img/video-play.png b/vendors/lightgallery/src/img/video-play.png deleted file mode 100644 index 197672353d..0000000000 Binary files a/vendors/lightgallery/src/img/video-play.png and /dev/null differ diff --git a/vendors/lightgallery/src/img/vimeo-play.png b/vendors/lightgallery/src/img/vimeo-play.png deleted file mode 100644 index b244856fbe..0000000000 Binary files a/vendors/lightgallery/src/img/vimeo-play.png and /dev/null differ diff --git a/vendors/lightgallery/src/img/youtube-play.png b/vendors/lightgallery/src/img/youtube-play.png deleted file mode 100644 index 580d949365..0000000000 Binary files a/vendors/lightgallery/src/img/youtube-play.png and /dev/null differ diff --git a/vendors/lightgallery/src/js/.jshintrc b/vendors/lightgallery/src/js/.jshintrc deleted file mode 100644 index 4dd755879d..0000000000 --- a/vendors/lightgallery/src/js/.jshintrc +++ /dev/null @@ -1,21 +0,0 @@ -{ - "curly": true, - "eqeqeq": true, - "immed": true, - "latedef": true, - "newcap": true, - "noarg": true, - "sub": true, - "undef": true, - "unused": true, - "boss": true, - "eqnull": true, - "browser": true, - "predef": [ - "jQuery", - "console", - "$f", - "picturefill", - "videojs" - ] -} diff --git a/vendors/lightgallery/src/js/lg-autoplay.js b/vendors/lightgallery/src/js/lg-autoplay.js deleted file mode 100644 index 48d1d14812..0000000000 --- a/vendors/lightgallery/src/js/lg-autoplay.js +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Autoplay Plugin - * @version 1.2.0 - * @author Sachin N - @sachinchoolur - * @license MIT License (MIT) - */ - -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - autoplay: false, - pause: 5000, - progressBar: true, - fourceAutoplay: false, - autoplayControls: true, - appendAutoplayControlsTo: '.lg-toolbar' - }; - - /** - * Creates the autoplay plugin. - * @param {object} element - lightGallery element - */ - var Autoplay = function(element) { - - this.core = $(element).data('lightGallery'); - - this.$el = $(element); - - // Execute only if items are above 1 - if (this.core.$items.length < 2) { - return false; - } - - this.core.s = $.extend({}, defaults, this.core.s); - this.interval = false; - - // Identify if slide happened from autoplay - this.fromAuto = true; - - // Identify if autoplay canceled from touch/drag - this.canceledOnTouch = false; - - // save fourceautoplay value - this.fourceAutoplayTemp = this.core.s.fourceAutoplay; - - // do not allow progress bar if browser does not support css3 transitions - if (!this.core.doCss()) { - this.core.s.progressBar = false; - } - - this.init(); - - return this; - }; - - Autoplay.prototype.init = function() { - var _this = this; - - // append autoplay controls - if (_this.core.s.autoplayControls) { - _this.controls(); - } - - // Create progress bar - if (_this.core.s.progressBar) { - _this.core.$outer.find('.lg').append('
    '); - } - - // set progress - _this.progress(); - - // Start autoplay - if (_this.core.s.autoplay) { - _this.startlAuto(); - } - - // cancel interval on touchstart and dragstart - _this.$el.on('onDragstart.lg.tm touchstart.lg.tm', function() { - if (_this.interval) { - _this.cancelAuto(); - _this.canceledOnTouch = true; - } - }); - - // restore autoplay if autoplay canceled from touchstart / dragstart - _this.$el.on('onDragend.lg.tm touchend.lg.tm onSlideClick.lg.tm', function() { - if (!_this.interval && _this.canceledOnTouch) { - _this.startlAuto(); - _this.canceledOnTouch = false; - } - }); - - }; - - Autoplay.prototype.progress = function() { - - var _this = this; - var _$progressBar; - var _$progress; - - _this.$el.on('onBeforeSlide.lg.tm', function() { - - // start progress bar animation - if (_this.core.s.progressBar && _this.fromAuto) { - _$progressBar = _this.core.$outer.find('.lg-progress-bar'); - _$progress = _this.core.$outer.find('.lg-progress'); - if (_this.interval) { - _$progress.removeAttr('style'); - _$progressBar.removeClass('lg-start'); - setTimeout(function() { - _$progress.css('transition', 'width ' + (_this.core.s.speed + _this.core.s.pause) + 'ms ease 0s'); - _$progressBar.addClass('lg-start'); - }, 20); - } - } - - // Remove setinterval if slide is triggered manually and fourceautoplay is false - if (!_this.fromAuto && !_this.core.s.fourceAutoplay) { - _this.cancelAuto(); - } - - _this.fromAuto = false; - - }); - }; - - // Manage autoplay via play/stop buttons - Autoplay.prototype.controls = function() { - var _this = this; - var _html = ''; - - // Append autoplay controls - $(this.core.s.appendAutoplayControlsTo).append(_html); - - _this.core.$outer.find('.lg-autoplay-button').on('click.lg', function() { - if ($(_this.core.$outer).hasClass('lg-show-autoplay')) { - _this.cancelAuto(); - _this.core.s.fourceAutoplay = false; - } else { - if (!_this.interval) { - _this.startlAuto(); - _this.core.s.fourceAutoplay = _this.fourceAutoplayTemp; - } - } - }); - }; - - // Autostart gallery - Autoplay.prototype.startlAuto = function() { - var _this = this; - - _this.core.$outer.find('.lg-progress').css('transition', 'width ' + (_this.core.s.speed + _this.core.s.pause) + 'ms ease 0s'); - _this.core.$outer.addClass('lg-show-autoplay'); - _this.core.$outer.find('.lg-progress-bar').addClass('lg-start'); - - _this.interval = setInterval(function() { - if (_this.core.index + 1 < _this.core.$items.length) { - _this.core.index++; - } else { - _this.core.index = 0; - } - - _this.fromAuto = true; - _this.core.slide(_this.core.index, false, false); - }, _this.core.s.speed + _this.core.s.pause); - }; - - // cancel Autostart - Autoplay.prototype.cancelAuto = function() { - clearInterval(this.interval); - this.interval = false; - this.core.$outer.find('.lg-progress').removeAttr('style'); - this.core.$outer.removeClass('lg-show-autoplay'); - this.core.$outer.find('.lg-progress-bar').removeClass('lg-start'); - }; - - Autoplay.prototype.destroy = function() { - - this.cancelAuto(); - this.core.$outer.find('.lg-progress-bar').remove(); - }; - - $.fn.lightGallery.modules.autoplay = Autoplay; - -})(jQuery, window, document); diff --git a/vendors/lightgallery/src/js/lg-fullscreen.js b/vendors/lightgallery/src/js/lg-fullscreen.js deleted file mode 100644 index bb9e405cf5..0000000000 --- a/vendors/lightgallery/src/js/lg-fullscreen.js +++ /dev/null @@ -1,94 +0,0 @@ -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - fullScreen: true - }; - - var Fullscreen = function(element) { - - // get lightGallery core plugin data - this.core = $(element).data('lightGallery'); - - this.$el = $(element); - - // extend module defalut settings with lightGallery core settings - this.core.s = $.extend({}, defaults, this.core.s); - - this.init(); - - return this; - }; - - Fullscreen.prototype.init = function() { - var fullScreen = ''; - if (this.core.s.fullScreen) { - - // check for fullscreen browser support - if (!document.fullscreenEnabled && !document.webkitFullscreenEnabled && - !document.mozFullScreenEnabled && !document.msFullscreenEnabled) { - return; - } else { - fullScreen = ''; - this.core.$outer.find('.lg-toolbar').append(fullScreen); - this.fullScreen(); - } - } - }; - - Fullscreen.prototype.requestFullscreen = function() { - var el = document.documentElement; - if (el.requestFullscreen) { - el.requestFullscreen(); - } else if (el.msRequestFullscreen) { - el.msRequestFullscreen(); - } else if (el.mozRequestFullScreen) { - el.mozRequestFullScreen(); - } else if (el.webkitRequestFullscreen) { - el.webkitRequestFullscreen(); - } - }; - - Fullscreen.prototype.exitFullscreen = function() { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } - }; - - // https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode - Fullscreen.prototype.fullScreen = function() { - var _this = this; - - $(document).on('fullscreenchange.lg webkitfullscreenchange.lg mozfullscreenchange.lg MSFullscreenChange.lg', function() { - _this.core.$outer.toggleClass('lg-fullscreen-on'); - }); - - this.core.$outer.find('.lg-fullscreen').on('click.lg', function() { - if (!document.fullscreenElement && - !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { - _this.requestFullscreen(); - } else { - _this.exitFullscreen(); - } - }); - - }; - - Fullscreen.prototype.destroy = function() { - - // exit from fullscreen if activated - this.exitFullscreen(); - - $(document).off('fullscreenchange.lg webkitfullscreenchange.lg mozfullscreenchange.lg MSFullscreenChange.lg'); - }; - - $.fn.lightGallery.modules.fullscreen = Fullscreen; - -})(jQuery, window, document); diff --git a/vendors/lightgallery/src/js/lg-hash.js b/vendors/lightgallery/src/js/lg-hash.js deleted file mode 100644 index 9cd458b5f7..0000000000 --- a/vendors/lightgallery/src/js/lg-hash.js +++ /dev/null @@ -1,70 +0,0 @@ -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - hash: true - }; - - var Hash = function(element) { - - this.core = $(element).data('lightGallery'); - - this.core.s = $.extend({}, defaults, this.core.s); - - if (this.core.s.hash) { - this.oldHash = window.location.hash; - this.init(); - } - - return this; - }; - - Hash.prototype.init = function() { - var _this = this; - var _hash; - - // Change hash value on after each slide transition - _this.core.$el.on('onAfterSlide.lg.tm', function(event, prevIndex, index) { - window.location.hash = 'lg=' + _this.core.s.galleryId + '&slide=' + index; - }); - - // Listen hash change and change the slide according to slide value - $(window).on('hashchange.lg.hash', function() { - _hash = window.location.hash; - var _idx = parseInt(_hash.split('&slide=')[1], 10); - - // it galleryId doesn't exist in the url close the gallery - if ((_hash.indexOf('lg=' + _this.core.s.galleryId) > -1)) { - _this.core.slide(_idx, false, false); - } else if (_this.core.lGalleryOn) { - _this.core.destroy(); - } - - }); - }; - - Hash.prototype.destroy = function() { - - if (!this.core.s.hash) { - return; - } - - // Reset to old hash value - if (this.oldHash && this.oldHash.indexOf('lg=' + this.core.s.galleryId) < 0) { - window.location.hash = this.oldHash; - } else { - if (history.pushState) { - history.pushState('', document.title, window.location.pathname + window.location.search); - } else { - window.location.hash = ''; - } - } - - this.core.$el.off('.lg.hash'); - - }; - - $.fn.lightGallery.modules.hash = Hash; - -})(jQuery, window, document); \ No newline at end of file diff --git a/vendors/lightgallery/src/js/lg-pager.js b/vendors/lightgallery/src/js/lg-pager.js deleted file mode 100644 index 324de282f2..0000000000 --- a/vendors/lightgallery/src/js/lg-pager.js +++ /dev/null @@ -1,82 +0,0 @@ -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - pager: false - }; - - var Pager = function(element) { - - this.core = $(element).data('lightGallery'); - - this.$el = $(element); - this.core.s = $.extend({}, defaults, this.core.s); - if (this.core.s.pager && this.core.$items.length > 1) { - this.init(); - } - - return this; - }; - - Pager.prototype.init = function() { - var _this = this; - var pagerList = ''; - var $pagerCont; - var $pagerOuter; - var timeout; - - _this.core.$outer.find('.lg').append('
    '); - - if (_this.core.s.dynamic) { - for (var i = 0; i < _this.core.s.dynamicEl.length; i++) { - pagerList += '
    '; - } - } else { - _this.core.$items.each(function() { - - if (!_this.core.s.exThumbImage) { - pagerList += '
    '; - } else { - pagerList += '
    '; - } - - }); - } - - $pagerOuter = _this.core.$outer.find('.lg-pager-outer'); - - $pagerOuter.html(pagerList); - - $pagerCont = _this.core.$outer.find('.lg-pager-cont'); - $pagerCont.on('click.lg touchend.lg', function() { - var _$this = $(this); - _this.core.index = _$this.index(); - _this.core.slide(_this.core.index, false, false); - }); - - $pagerOuter.on('mouseover.lg', function() { - clearTimeout(timeout); - $pagerOuter.addClass('lg-pager-hover'); - }); - - $pagerOuter.on('mouseout.lg', function() { - timeout = setTimeout(function() { - $pagerOuter.removeClass('lg-pager-hover'); - }); - }); - - _this.core.$el.on('onBeforeSlide.lg.tm', function(e, prevIndex, index) { - $pagerCont.removeClass('lg-pager-active'); - $pagerCont.eq(index).addClass('lg-pager-active'); - }); - - }; - - Pager.prototype.destroy = function() { - - }; - - $.fn.lightGallery.modules.pager = Pager; - -})(jQuery, window, document); diff --git a/vendors/lightgallery/src/js/lg-thumbnail.js b/vendors/lightgallery/src/js/lg-thumbnail.js deleted file mode 100644 index 0331acbeae..0000000000 --- a/vendors/lightgallery/src/js/lg-thumbnail.js +++ /dev/null @@ -1,451 +0,0 @@ -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - thumbnail: true, - - animateThumb: true, - currentPagerPosition: 'middle', - - thumbWidth: 100, - thumbContHeight: 100, - thumbMargin: 5, - - exThumbImage: false, - showThumbByDefault: true, - toogleThumb: true, - pullCaptionUp: true, - - enableThumbDrag: true, - enableThumbSwipe: true, - swipeThreshold: 50, - - loadYoutubeThumbnail: true, - youtubeThumbSize: 1, - - loadVimeoThumbnail: true, - vimeoThumbSize: 'thumbnail_small', - - loadDailymotionThumbnail: true - }; - - var Thumbnail = function(element) { - - // get lightGallery core plugin data - this.core = $(element).data('lightGallery'); - - // extend module default settings with lightGallery core settings - this.core.s = $.extend({}, defaults, this.core.s); - - this.$el = $(element); - this.$thumbOuter = null; - this.thumbOuterWidth = 0; - this.thumbTotalWidth = (this.core.$items.length * (this.core.s.thumbWidth + this.core.s.thumbMargin)); - this.thumbIndex = this.core.index; - - // Thumbnail animation value - this.left = 0; - - this.init(); - - return this; - }; - - Thumbnail.prototype.init = function() { - var _this = this; - if (this.core.s.thumbnail && this.core.$items.length > 1) { - if (this.core.s.showThumbByDefault) { - setTimeout(function(){ - _this.core.$outer.addClass('lg-thumb-open'); - }, 700); - } - - if (this.core.s.pullCaptionUp) { - this.core.$outer.addClass('lg-pull-caption-up'); - } - - this.build(); - if (this.core.s.animateThumb) { - if (this.core.s.enableThumbDrag && !this.core.isTouch && this.core.doCss()) { - this.enableThumbDrag(); - } - - if (this.core.s.enableThumbSwipe && this.core.isTouch && this.core.doCss()) { - this.enableThumbSwipe(); - } - - this.thumbClickable = false; - } else { - this.thumbClickable = true; - } - - this.toogle(); - this.thumbkeyPress(); - } - }; - - Thumbnail.prototype.build = function() { - var _this = this; - var thumbList = ''; - var vimeoErrorThumbSize = ''; - var $thumb; - var html = '
    ' + - '
    ' + - '
    ' + - '
    '; - - switch (this.core.s.vimeoThumbSize) { - case 'thumbnail_large': - vimeoErrorThumbSize = '640'; - break; - case 'thumbnail_medium': - vimeoErrorThumbSize = '200x150'; - break; - case 'thumbnail_small': - vimeoErrorThumbSize = '100x75'; - } - - _this.core.$outer.addClass('lg-has-thumb'); - - _this.core.$outer.find('.lg').append(html); - - _this.$thumbOuter = _this.core.$outer.find('.lg-thumb-outer'); - _this.thumbOuterWidth = _this.$thumbOuter.width(); - - if (_this.core.s.animateThumb) { - _this.core.$outer.find('.lg-thumb').css({ - width: _this.thumbTotalWidth + 'px', - position: 'relative' - }); - } - - if (this.core.s.animateThumb) { - _this.$thumbOuter.css('height', _this.core.s.thumbContHeight + 'px'); - } - - function getThumb(src, thumb, index) { - var isVideo = _this.core.isVideo(src, index) || {}; - var thumbImg; - var vimeoId = ''; - - if (isVideo.youtube || isVideo.vimeo || isVideo.dailymotion) { - if (isVideo.youtube) { - if (_this.core.s.loadYoutubeThumbnail) { - thumbImg = '//img.youtube.com/vi/' + isVideo.youtube[1] + '/' + _this.core.s.youtubeThumbSize + '.jpg'; - } else { - thumbImg = thumb; - } - } else if (isVideo.vimeo) { - if (_this.core.s.loadVimeoThumbnail) { - thumbImg = '//i.vimeocdn.com/video/error_' + vimeoErrorThumbSize + '.jpg'; - vimeoId = isVideo.vimeo[1]; - } else { - thumbImg = thumb; - } - } else if (isVideo.dailymotion) { - if (_this.core.s.loadDailymotionThumbnail) { - thumbImg = '//www.dailymotion.com/thumbnail/video/' + isVideo.dailymotion[1]; - } else { - thumbImg = thumb; - } - } - } else { - thumbImg = thumb; - } - - thumbList += '
    '; - vimeoId = ''; - } - - if (_this.core.s.dynamic) { - for (var i = 0; i < _this.core.s.dynamicEl.length; i++) { - getThumb(_this.core.s.dynamicEl[i].src, _this.core.s.dynamicEl[i].thumb, i); - } - } else { - _this.core.$items.each(function(i) { - - if (!_this.core.s.exThumbImage) { - getThumb($(this).attr('href') || $(this).attr('data-src'), $(this).find('img').attr('src'), i); - } else { - getThumb($(this).attr('href') || $(this).attr('data-src'), $(this).attr(_this.core.s.exThumbImage), i); - } - - }); - } - - _this.core.$outer.find('.lg-thumb').html(thumbList); - - $thumb = _this.core.$outer.find('.lg-thumb-item'); - - // Load vimeo thumbnails - $thumb.each(function() { - var $this = $(this); - var vimeoVideoId = $this.attr('data-vimeo-id'); - - if (vimeoVideoId) { - $.getJSON('//www.vimeo.com/api/v2/video/' + vimeoVideoId + '.json?callback=?', { - format: 'json' - }, function(data) { - $this.find('img').attr('src', data[0][_this.core.s.vimeoThumbSize]); - }); - } - }); - - // manage active class for thumbnail - $thumb.eq(_this.core.index).addClass('active'); - _this.core.$el.on('onBeforeSlide.lg.tm', function() { - $thumb.removeClass('active'); - $thumb.eq(_this.core.index).addClass('active'); - }); - - $thumb.on('click.lg touchend.lg', function() { - var _$this = $(this); - setTimeout(function() { - - // In IE9 and bellow touch does not support - // Go to slide if browser does not support css transitions - if ((_this.thumbClickable && !_this.core.lgBusy) || !_this.core.doCss()) { - _this.core.index = _$this.index(); - _this.core.slide(_this.core.index, false, true); - } - }, 50); - }); - - _this.core.$el.on('onBeforeSlide.lg.tm', function() { - _this.animateThumb(_this.core.index); - }); - - $(window).on('resize.lg.thumb orientationchange.lg.thumb', function() { - setTimeout(function() { - _this.animateThumb(_this.core.index); - _this.thumbOuterWidth = _this.$thumbOuter.width(); - }, 200); - }); - - }; - - Thumbnail.prototype.setTranslate = function(value) { - // jQuery supports Automatic CSS prefixing since jQuery 1.8.0 - this.core.$outer.find('.lg-thumb').css({ - transform: 'translate3d(-' + (value) + 'px, 0px, 0px)' - }); - }; - - Thumbnail.prototype.animateThumb = function(index) { - var $thumb = this.core.$outer.find('.lg-thumb'); - if (this.core.s.animateThumb) { - var position; - switch (this.core.s.currentPagerPosition) { - case 'left': - position = 0; - break; - case 'middle': - position = (this.thumbOuterWidth / 2) - (this.core.s.thumbWidth / 2); - break; - case 'right': - position = this.thumbOuterWidth - this.core.s.thumbWidth; - } - this.left = ((this.core.s.thumbWidth + this.core.s.thumbMargin) * index - 1) - position; - if (this.left > (this.thumbTotalWidth - this.thumbOuterWidth)) { - this.left = this.thumbTotalWidth - this.thumbOuterWidth; - } - - if (this.left < 0) { - this.left = 0; - } - - if (this.core.lGalleryOn) { - if (!$thumb.hasClass('on')) { - this.core.$outer.find('.lg-thumb').css('transition-duration', this.core.s.speed + 'ms'); - } - - if (!this.core.doCss()) { - $thumb.animate({ - left: -this.left + 'px' - }, this.core.s.speed); - } - } else { - if (!this.core.doCss()) { - $thumb.css('left', -this.left + 'px'); - } - } - - this.setTranslate(this.left); - - } - }; - - // Enable thumbnail dragging and swiping - Thumbnail.prototype.enableThumbDrag = function() { - - var _this = this; - var startCoords = 0; - var endCoords = 0; - var isDraging = false; - var isMoved = false; - var tempLeft = 0; - - _this.$thumbOuter.addClass('lg-grab'); - - _this.core.$outer.find('.lg-thumb').on('mousedown.lg.thumb', function(e) { - if (_this.thumbTotalWidth > _this.thumbOuterWidth) { - // execute only on .lg-object - e.preventDefault(); - startCoords = e.pageX; - isDraging = true; - - // ** Fix for webkit cursor issue https://code.google.com/p/chromium/issues/detail?id=26723 - _this.core.$outer.scrollLeft += 1; - _this.core.$outer.scrollLeft -= 1; - - // * - _this.thumbClickable = false; - _this.$thumbOuter.removeClass('lg-grab').addClass('lg-grabbing'); - } - }); - - $(window).on('mousemove.lg.thumb', function(e) { - if (isDraging) { - tempLeft = _this.left; - isMoved = true; - endCoords = e.pageX; - - _this.$thumbOuter.addClass('lg-dragging'); - - tempLeft = tempLeft - (endCoords - startCoords); - - if (tempLeft > (_this.thumbTotalWidth - _this.thumbOuterWidth)) { - tempLeft = _this.thumbTotalWidth - _this.thumbOuterWidth; - } - - if (tempLeft < 0) { - tempLeft = 0; - } - - // move current slide - _this.setTranslate(tempLeft); - - } - }); - - $(window).on('mouseup.lg.thumb', function() { - if (isMoved) { - isMoved = false; - _this.$thumbOuter.removeClass('lg-dragging'); - - _this.left = tempLeft; - - if (Math.abs(endCoords - startCoords) < _this.core.s.swipeThreshold) { - _this.thumbClickable = true; - } - - } else { - _this.thumbClickable = true; - } - - if (isDraging) { - isDraging = false; - _this.$thumbOuter.removeClass('lg-grabbing').addClass('lg-grab'); - } - }); - - }; - - Thumbnail.prototype.enableThumbSwipe = function() { - var _this = this; - var startCoords = 0; - var endCoords = 0; - var isMoved = false; - var tempLeft = 0; - - _this.core.$outer.find('.lg-thumb').on('touchstart.lg', function(e) { - if (_this.thumbTotalWidth > _this.thumbOuterWidth) { - e.preventDefault(); - startCoords = e.originalEvent.targetTouches[0].pageX; - _this.thumbClickable = false; - } - }); - - _this.core.$outer.find('.lg-thumb').on('touchmove.lg', function(e) { - if (_this.thumbTotalWidth > _this.thumbOuterWidth) { - e.preventDefault(); - endCoords = e.originalEvent.targetTouches[0].pageX; - isMoved = true; - - _this.$thumbOuter.addClass('lg-dragging'); - - tempLeft = _this.left; - - tempLeft = tempLeft - (endCoords - startCoords); - - if (tempLeft > (_this.thumbTotalWidth - _this.thumbOuterWidth)) { - tempLeft = _this.thumbTotalWidth - _this.thumbOuterWidth; - } - - if (tempLeft < 0) { - tempLeft = 0; - } - - // move current slide - _this.setTranslate(tempLeft); - - } - }); - - _this.core.$outer.find('.lg-thumb').on('touchend.lg', function() { - if (_this.thumbTotalWidth > _this.thumbOuterWidth) { - - if (isMoved) { - isMoved = false; - _this.$thumbOuter.removeClass('lg-dragging'); - if (Math.abs(endCoords - startCoords) < _this.core.s.swipeThreshold) { - _this.thumbClickable = true; - } - - _this.left = tempLeft; - } else { - _this.thumbClickable = true; - } - } else { - _this.thumbClickable = true; - } - }); - - }; - - Thumbnail.prototype.toogle = function() { - var _this = this; - if (_this.core.s.toogleThumb) { - _this.core.$outer.addClass('lg-can-toggle'); - _this.$thumbOuter.append(''); - _this.core.$outer.find('.lg-toogle-thumb').on('click.lg', function() { - _this.core.$outer.toggleClass('lg-thumb-open'); - }); - } - }; - - Thumbnail.prototype.thumbkeyPress = function() { - var _this = this; - $(window).on('keydown.lg.thumb', function(e) { - if (e.keyCode === 38) { - e.preventDefault(); - _this.core.$outer.addClass('lg-thumb-open'); - } else if (e.keyCode === 40) { - e.preventDefault(); - _this.core.$outer.removeClass('lg-thumb-open'); - } - }); - }; - - Thumbnail.prototype.destroy = function() { - if (this.core.s.thumbnail && this.core.$items.length > 1) { - $(window).off('resize.lg.thumb orientationchange.lg.thumb keydown.lg.thumb'); - this.$thumbOuter.remove(); - this.core.$outer.removeClass('lg-has-thumb'); - } - }; - - $.fn.lightGallery.modules.Thumbnail = Thumbnail; - -})(jQuery, window, document); diff --git a/vendors/lightgallery/src/js/lg-video.js b/vendors/lightgallery/src/js/lg-video.js deleted file mode 100644 index da7706c355..0000000000 --- a/vendors/lightgallery/src/js/lg-video.js +++ /dev/null @@ -1,289 +0,0 @@ -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - videoMaxWidth: '855px', - youtubePlayerParams: false, - vimeoPlayerParams: false, - dailymotionPlayerParams: false, - vkPlayerParams: false, - videojs: false, - videojsOptions: {} - }; - - var Video = function(element) { - - this.core = $(element).data('lightGallery'); - - this.$el = $(element); - this.core.s = $.extend({}, defaults, this.core.s); - this.videoLoaded = false; - - this.init(); - - return this; - }; - - Video.prototype.init = function() { - var _this = this; - - // Event triggered when video url found without poster - _this.core.$el.on('hasVideo.lg.tm', function(event, index, src, html) { - _this.core.$slide.eq(index).find('.lg-video').append(_this.loadVideo(src, 'lg-object', true, index, html)); - if (html) { - if (_this.core.s.videojs) { - try { - videojs(_this.core.$slide.eq(index).find('.lg-html5').get(0), _this.core.s.videojsOptions, function() { - if (!_this.videoLoaded) { - this.play(); - } - }); - } catch (e) { - console.error('Make sure you have included videojs'); - } - } else { - _this.core.$slide.eq(index).find('.lg-html5').get(0).play(); - } - } - }); - - // Set max width for video - _this.core.$el.on('onAferAppendSlide.lg.tm', function(event, index) { - _this.core.$slide.eq(index).find('.lg-video-cont').css('max-width', _this.core.s.videoMaxWidth); - _this.videoLoaded = true; - }); - - var loadOnClick = function($el) { - // check slide has poster - if ($el.find('.lg-object').hasClass('lg-has-poster') && $el.find('.lg-object').is(':visible')) { - - // check already video element present - if (!$el.hasClass('lg-has-video')) { - - $el.addClass('lg-video-playing lg-has-video'); - - var _src; - var _html; - var _loadVideo = function(_src, _html) { - - $el.find('.lg-video').append(_this.loadVideo(_src, '', false, _this.core.index, _html)); - - if (_html) { - if (_this.core.s.videojs) { - try { - videojs(_this.core.$slide.eq(_this.core.index).find('.lg-html5').get(0), _this.core.s.videojsOptions, function() { - this.play(); - }); - } catch (e) { - console.error('Make sure you have included videojs'); - } - } else { - _this.core.$slide.eq(_this.core.index).find('.lg-html5').get(0).play(); - } - } - - }; - - if (_this.core.s.dynamic) { - - _src = _this.core.s.dynamicEl[_this.core.index].src; - _html = _this.core.s.dynamicEl[_this.core.index].html; - - _loadVideo(_src, _html); - - } else { - - _src = _this.core.$items.eq(_this.core.index).attr('href') || _this.core.$items.eq(_this.core.index).attr('data-src'); - _html = _this.core.$items.eq(_this.core.index).attr('data-html'); - - _loadVideo(_src, _html); - - } - - var $tempImg = $el.find('.lg-object'); - $el.find('.lg-video').append($tempImg); - - // @todo loading icon for html5 videos also - // for showing the loading indicator while loading video - if (!$el.find('.lg-video-object').hasClass('lg-html5')) { - $el.removeClass('lg-complete'); - $el.find('.lg-video-object').on('load.lg error.lg', function() { - $el.addClass('lg-complete'); - }); - } - - } else { - - var youtubePlayer = $el.find('.lg-youtube').get(0); - var vimeoPlayer = $el.find('.lg-vimeo').get(0); - var dailymotionPlayer = $el.find('.lg-dailymotion').get(0); - var html5Player = $el.find('.lg-html5').get(0); - if (youtubePlayer) { - youtubePlayer.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*'); - } else if (vimeoPlayer) { - try { - $f(vimeoPlayer).api('play'); - } catch (e) { - console.error('Make sure you have included froogaloop2 js'); - } - } else if (dailymotionPlayer) { - dailymotionPlayer.contentWindow.postMessage('play', '*'); - - } else if (html5Player) { - if (_this.core.s.videojs) { - try { - videojs(html5Player).play(); - } catch (e) { - console.error('Make sure you have included videojs'); - } - } else { - html5Player.play(); - } - } - - $el.addClass('lg-video-playing'); - - } - } - }; - - if (_this.core.doCss() && _this.core.$items.length > 1 && ((_this.core.s.enableSwipe && _this.core.isTouch) || (_this.core.s.enableDrag && !_this.core.isTouch))) { - _this.core.$el.on('onSlideClick.lg.tm', function() { - var $el = _this.core.$slide.eq(_this.core.index); - loadOnClick($el); - }); - } else { - - // For IE 9 and bellow - _this.core.$slide.on('click.lg', function() { - loadOnClick($(this)); - }); - } - - _this.core.$el.on('onBeforeSlide.lg.tm', function(event, prevIndex, index) { - - var $videoSlide = _this.core.$slide.eq(prevIndex); - var youtubePlayer = $videoSlide.find('.lg-youtube').get(0); - var vimeoPlayer = $videoSlide.find('.lg-vimeo').get(0); - var dailymotionPlayer = $videoSlide.find('.lg-dailymotion').get(0); - var vkPlayer = $videoSlide.find('.lg-vk').get(0); - var html5Player = $videoSlide.find('.lg-html5').get(0); - if (youtubePlayer) { - youtubePlayer.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*'); - } else if (vimeoPlayer) { - try { - $f(vimeoPlayer).api('pause'); - } catch (e) { - console.error('Make sure you have included froogaloop2 js'); - } - } else if (dailymotionPlayer) { - dailymotionPlayer.contentWindow.postMessage('pause', '*'); - - } else if (html5Player) { - if (_this.core.s.videojs) { - try { - videojs(html5Player).pause(); - } catch (e) { - console.error('Make sure you have included videojs'); - } - } else { - html5Player.pause(); - } - } if (vkPlayer) { - $(vkPlayer).attr('src', $(vkPlayer).attr('src').replace('&autoplay', '&noplay')); - } - - var _src; - if (_this.core.s.dynamic) { - _src = _this.core.s.dynamicEl[index].src; - } else { - _src = _this.core.$items.eq(index).attr('href') || _this.core.$items.eq(index).attr('data-src'); - - } - - var _isVideo = _this.core.isVideo(_src, index) || {}; - if (_isVideo.youtube || _isVideo.vimeo || _isVideo.dailymotion || _isVideo.vk) { - _this.core.$outer.addClass('lg-hide-download'); - } - - //$videoSlide.addClass('lg-complete'); - - }); - - _this.core.$el.on('onAfterSlide.lg.tm', function(event, prevIndex) { - _this.core.$slide.eq(prevIndex).removeClass('lg-video-playing'); - }); - }; - - Video.prototype.loadVideo = function(src, addClass, noposter, index, html) { - var video = ''; - var autoplay = 1; - var a = ''; - var isVideo = this.core.isVideo(src, index) || {}; - - // Enable autoplay for first video if poster doesn't exist - if (noposter) { - if (this.videoLoaded) { - autoplay = 0; - } else { - autoplay = 1; - } - } - - if (isVideo.youtube) { - - a = '?wmode=opaque&autoplay=' + autoplay + '&enablejsapi=1'; - if (this.core.s.youtubePlayerParams) { - a = a + '&' + $.param(this.core.s.youtubePlayerParams); - } - - video = ''; - - } else if (isVideo.vimeo) { - - a = '?autoplay=' + autoplay + '&api=1'; - if (this.core.s.vimeoPlayerParams) { - a = a + '&' + $.param(this.core.s.vimeoPlayerParams); - } - - video = ''; - - } else if (isVideo.dailymotion) { - - a = '?wmode=opaque&autoplay=' + autoplay + '&api=postMessage'; - if (this.core.s.dailymotionPlayerParams) { - a = a + '&' + $.param(this.core.s.dailymotionPlayerParams); - } - - video = ''; - - } else if (isVideo.html5) { - var fL = html.substring(0, 1); - if (fL === '.' || fL === '#') { - html = $(html).html(); - } - - video = html; - - } else if (isVideo.vk) { - - a = '&autoplay=' + autoplay; - if (this.core.s.vkPlayerParams) { - a = a + '&' + $.param(this.core.s.vkPlayerParams); - } - - video = ''; - - } - - return video; - }; - - Video.prototype.destroy = function() { - this.videoLoaded = false; - }; - - $.fn.lightGallery.modules.video = Video; - -})(jQuery, window, document); diff --git a/vendors/lightgallery/src/js/lg-zoom.js b/vendors/lightgallery/src/js/lg-zoom.js deleted file mode 100644 index 663d82c4b6..0000000000 --- a/vendors/lightgallery/src/js/lg-zoom.js +++ /dev/null @@ -1,474 +0,0 @@ -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - scale: 1, - zoom: true, - actualSize: true, - enableZoomAfter: 300 - }; - - var Zoom = function(element) { - - this.core = $(element).data('lightGallery'); - - this.core.s = $.extend({}, defaults, this.core.s); - - if (this.core.s.zoom && this.core.doCss()) { - this.init(); - - // Store the zoomable timeout value just to clear it while closing - this.zoomabletimeout = false; - - // Set the initial value center - this.pageX = $(window).width() / 2; - this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - } - - return this; - }; - - Zoom.prototype.init = function() { - - var _this = this; - var zoomIcons = ''; - - if (_this.core.s.actualSize) { - zoomIcons += ''; - } - - this.core.$outer.find('.lg-toolbar').append(zoomIcons); - - // Add zoomable class - _this.core.$el.on('onSlideItemLoad.lg.tm.zoom', function(event, index, delay) { - - // delay will be 0 except first time - var _speed = _this.core.s.enableZoomAfter + delay; - - // set _speed value 0 if gallery opened from direct url and if it is first slide - if ($('body').hasClass('lg-from-hash') && delay) { - - // will execute only once - _speed = 0; - } else { - - // Remove lg-from-hash to enable starting animation. - $('body').removeClass('lg-from-hash'); - } - - _this.zoomabletimeout = setTimeout(function() { - _this.core.$slide.eq(index).addClass('lg-zoomable'); - }, _speed + 30); - }); - - var scale = 1; - /** - * @desc Image zoom - * Translate the wrap and scale the image to get better user experience - * - * @param {String} scaleVal - Zoom decrement/increment value - */ - var zoom = function(scaleVal) { - - var $image = _this.core.$outer.find('.lg-current .lg-image'); - var _x; - var _y; - - // Find offset manually to avoid issue after zoom - var offsetX = ($(window).width() - $image.width()) / 2; - var offsetY = (($(window).height() - $image.height()) / 2) + $(window).scrollTop(); - - _x = _this.pageX - offsetX; - _y = _this.pageY - offsetY; - - var x = (scaleVal - 1) * (_x); - var y = (scaleVal - 1) * (_y); - - $image.css('transform', 'scale3d(' + scaleVal + ', ' + scaleVal + ', 1)').attr('data-scale', scaleVal); - - $image.parent().css({ - left: -x + 'px', - top: -y + 'px' - }).attr('data-x', x).attr('data-y', y); - }; - - var callScale = function() { - if (scale > 1) { - _this.core.$outer.addClass('lg-zoomed'); - } else { - _this.resetZoom(); - } - - if (scale < 1) { - scale = 1; - } - - zoom(scale); - }; - - var actualSize = function(event, $image, index, fromIcon) { - var w = $image.width(); - var nw; - if (_this.core.s.dynamic) { - nw = _this.core.s.dynamicEl[index].width || $image[0].naturalWidth || w; - } else { - nw = _this.core.$items.eq(index).attr('data-width') || $image[0].naturalWidth || w; - } - - var _scale; - - if (_this.core.$outer.hasClass('lg-zoomed')) { - scale = 1; - } else { - if (nw > w) { - _scale = nw / w; - scale = _scale || 2; - } - } - - if (fromIcon) { - _this.pageX = $(window).width() / 2; - _this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - } else { - _this.pageX = event.pageX || event.originalEvent.targetTouches[0].pageX; - _this.pageY = event.pageY || event.originalEvent.targetTouches[0].pageY; - } - - callScale(); - setTimeout(function() { - _this.core.$outer.removeClass('lg-grabbing').addClass('lg-grab'); - }, 10); - }; - - var tapped = false; - - // event triggered after appending slide content - _this.core.$el.on('onAferAppendSlide.lg.tm.zoom', function(event, index) { - - // Get the current element - var $image = _this.core.$slide.eq(index).find('.lg-image'); - - $image.on('dblclick', function(event) { - actualSize(event, $image, index); - }); - - $image.on('touchstart', function(event) { - if (!tapped) { - tapped = setTimeout(function() { - tapped = null; - }, 300); - } else { - clearTimeout(tapped); - tapped = null; - actualSize(event, $image, index); - } - - event.preventDefault(); - }); - - }); - - // Update zoom on resize and orientationchange - $(window).on('resize.lg.zoom scroll.lg.zoom orientationchange.lg.zoom', function() { - _this.pageX = $(window).width() / 2; - _this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - zoom(scale); - }); - - $('#lg-zoom-out').on('click.lg', function() { - if (_this.core.$outer.find('.lg-current .lg-image').length) { - scale -= _this.core.s.scale; - callScale(); - } - }); - - $('#lg-zoom-in').on('click.lg', function() { - if (_this.core.$outer.find('.lg-current .lg-image').length) { - scale += _this.core.s.scale; - callScale(); - } - }); - - $('#lg-actual-size').on('click.lg', function(event) { - actualSize(event, _this.core.$slide.eq(_this.core.index).find('.lg-image'), _this.core.index, true); - }); - - // Reset zoom on slide change - _this.core.$el.on('onBeforeSlide.lg.tm', function() { - scale = 1; - _this.resetZoom(); - }); - - // Drag option after zoom - if (!_this.core.isTouch) { - _this.zoomDrag(); - } - - if (_this.core.isTouch) { - _this.zoomSwipe(); - } - - }; - - // Reset zoom effect - Zoom.prototype.resetZoom = function() { - this.core.$outer.removeClass('lg-zoomed'); - this.core.$slide.find('.lg-img-wrap').removeAttr('style data-x data-y'); - this.core.$slide.find('.lg-image').removeAttr('style data-scale'); - - // Reset pagx pagy values to center - this.pageX = $(window).width() / 2; - this.pageY = ($(window).height() / 2) + $(window).scrollTop(); - }; - - Zoom.prototype.zoomSwipe = function() { - var _this = this; - var startCoords = {}; - var endCoords = {}; - var isMoved = false; - - // Allow x direction drag - var allowX = false; - - // Allow Y direction drag - var allowY = false; - - _this.core.$slide.on('touchstart.lg', function(e) { - - if (_this.core.$outer.hasClass('lg-zoomed')) { - var $image = _this.core.$slide.eq(_this.core.index).find('.lg-object'); - - allowY = $image.outerHeight() * $image.attr('data-scale') > _this.core.$outer.find('.lg').height(); - allowX = $image.outerWidth() * $image.attr('data-scale') > _this.core.$outer.find('.lg').width(); - if ((allowX || allowY)) { - e.preventDefault(); - startCoords = { - x: e.originalEvent.targetTouches[0].pageX, - y: e.originalEvent.targetTouches[0].pageY - }; - } - } - - }); - - _this.core.$slide.on('touchmove.lg', function(e) { - - if (_this.core.$outer.hasClass('lg-zoomed')) { - - var _$el = _this.core.$slide.eq(_this.core.index).find('.lg-img-wrap'); - var distanceX; - var distanceY; - - e.preventDefault(); - isMoved = true; - - endCoords = { - x: e.originalEvent.targetTouches[0].pageX, - y: e.originalEvent.targetTouches[0].pageY - }; - - // reset opacity and transition duration - _this.core.$outer.addClass('lg-zoom-dragging'); - - if (allowY) { - distanceY = (-Math.abs(_$el.attr('data-y'))) + (endCoords.y - startCoords.y); - } else { - distanceY = -Math.abs(_$el.attr('data-y')); - } - - if (allowX) { - distanceX = (-Math.abs(_$el.attr('data-x'))) + (endCoords.x - startCoords.x); - } else { - distanceX = -Math.abs(_$el.attr('data-x')); - } - - if ((Math.abs(endCoords.x - startCoords.x) > 15) || (Math.abs(endCoords.y - startCoords.y) > 15)) { - _$el.css({ - left: distanceX + 'px', - top: distanceY + 'px' - }); - } - - } - - }); - - _this.core.$slide.on('touchend.lg', function() { - if (_this.core.$outer.hasClass('lg-zoomed')) { - if (isMoved) { - isMoved = false; - _this.core.$outer.removeClass('lg-zoom-dragging'); - _this.touchendZoom(startCoords, endCoords, allowX, allowY); - - } - } - }); - - }; - - Zoom.prototype.zoomDrag = function() { - - var _this = this; - var startCoords = {}; - var endCoords = {}; - var isDraging = false; - var isMoved = false; - - // Allow x direction drag - var allowX = false; - - // Allow Y direction drag - var allowY = false; - - _this.core.$slide.on('mousedown.lg.zoom', function(e) { - - // execute only on .lg-object - var $image = _this.core.$slide.eq(_this.core.index).find('.lg-object'); - - allowY = $image.outerHeight() * $image.attr('data-scale') > _this.core.$outer.find('.lg').height(); - allowX = $image.outerWidth() * $image.attr('data-scale') > _this.core.$outer.find('.lg').width(); - - if (_this.core.$outer.hasClass('lg-zoomed')) { - if ($(e.target).hasClass('lg-object') && (allowX || allowY)) { - e.preventDefault(); - startCoords = { - x: e.pageX, - y: e.pageY - }; - - isDraging = true; - - // ** Fix for webkit cursor issue https://code.google.com/p/chromium/issues/detail?id=26723 - _this.core.$outer.scrollLeft += 1; - _this.core.$outer.scrollLeft -= 1; - - _this.core.$outer.removeClass('lg-grab').addClass('lg-grabbing'); - } - } - }); - - $(window).on('mousemove.lg.zoom', function(e) { - if (isDraging) { - var _$el = _this.core.$slide.eq(_this.core.index).find('.lg-img-wrap'); - var distanceX; - var distanceY; - - isMoved = true; - endCoords = { - x: e.pageX, - y: e.pageY - }; - - // reset opacity and transition duration - _this.core.$outer.addClass('lg-zoom-dragging'); - - if (allowY) { - distanceY = (-Math.abs(_$el.attr('data-y'))) + (endCoords.y - startCoords.y); - } else { - distanceY = -Math.abs(_$el.attr('data-y')); - } - - if (allowX) { - distanceX = (-Math.abs(_$el.attr('data-x'))) + (endCoords.x - startCoords.x); - } else { - distanceX = -Math.abs(_$el.attr('data-x')); - } - - _$el.css({ - left: distanceX + 'px', - top: distanceY + 'px' - }); - } - }); - - $(window).on('mouseup.lg.zoom', function(e) { - - if (isDraging) { - isDraging = false; - _this.core.$outer.removeClass('lg-zoom-dragging'); - - // Fix for chrome mouse move on click - if (isMoved && ((startCoords.x !== endCoords.x) || (startCoords.y !== endCoords.y))) { - endCoords = { - x: e.pageX, - y: e.pageY - }; - _this.touchendZoom(startCoords, endCoords, allowX, allowY); - - } - - isMoved = false; - } - - _this.core.$outer.removeClass('lg-grabbing').addClass('lg-grab'); - - }); - }; - - Zoom.prototype.touchendZoom = function(startCoords, endCoords, allowX, allowY) { - - var _this = this; - var _$el = _this.core.$slide.eq(_this.core.index).find('.lg-img-wrap'); - var $image = _this.core.$slide.eq(_this.core.index).find('.lg-object'); - var distanceX = (-Math.abs(_$el.attr('data-x'))) + (endCoords.x - startCoords.x); - var distanceY = (-Math.abs(_$el.attr('data-y'))) + (endCoords.y - startCoords.y); - var minY = (_this.core.$outer.find('.lg').height() - $image.outerHeight()) / 2; - var maxY = Math.abs(($image.outerHeight() * Math.abs($image.attr('data-scale'))) - _this.core.$outer.find('.lg').height() + minY); - var minX = (_this.core.$outer.find('.lg').width() - $image.outerWidth()) / 2; - var maxX = Math.abs(($image.outerWidth() * Math.abs($image.attr('data-scale'))) - _this.core.$outer.find('.lg').width() + minX); - - if ((Math.abs(endCoords.x - startCoords.x) > 15) || (Math.abs(endCoords.y - startCoords.y) > 15)) { - if (allowY) { - if (distanceY <= -maxY) { - distanceY = -maxY; - } else if (distanceY >= -minY) { - distanceY = -minY; - } - } - - if (allowX) { - if (distanceX <= -maxX) { - distanceX = -maxX; - } else if (distanceX >= -minX) { - distanceX = -minX; - } - } - - if (allowY) { - _$el.attr('data-y', Math.abs(distanceY)); - } else { - distanceY = -Math.abs(_$el.attr('data-y')); - } - - if (allowX) { - _$el.attr('data-x', Math.abs(distanceX)); - } else { - distanceX = -Math.abs(_$el.attr('data-x')); - } - - _$el.css({ - left: distanceX + 'px', - top: distanceY + 'px' - }); - - } - }; - - Zoom.prototype.destroy = function() { - - var _this = this; - - // Unbind all events added by lightGallery zoom plugin - _this.core.$el.off('.lg.zoom'); - $(window).off('.lg.zoom'); - _this.core.$slide.off('.lg.zoom'); - _this.core.$el.off('.lg.tm.zoom'); - _this.resetZoom(); - clearTimeout(_this.zoomabletimeout); - _this.zoomabletimeout = false; - }; - - $.fn.lightGallery.modules.zoom = Zoom; - -})(jQuery, window, document); \ No newline at end of file diff --git a/vendors/lightgallery/src/js/lightgallery.js b/vendors/lightgallery/src/js/lightgallery.js deleted file mode 100644 index 4c42e7345d..0000000000 --- a/vendors/lightgallery/src/js/lightgallery.js +++ /dev/null @@ -1,1314 +0,0 @@ -(function($, window, document, undefined) { - - 'use strict'; - - var defaults = { - - mode: 'lg-slide', - - // Ex : 'ease' - cssEasing: 'ease', - - //'for jquery animation' - easing: 'linear', - speed: 600, - height: '100%', - width: '100%', - addClass: '', - startClass: 'lg-start-zoom', - backdropDuration: 150, - hideBarsDelay: 6000, - - useLeft: false, - - closable: true, - loop: true, - escKey: true, - keyPress: true, - controls: true, - slideEndAnimatoin: true, - hideControlOnEnd: false, - mousewheel: true, - - getCaptionFromTitleOrAlt: true, - - // .lg-item || '.lg-sub-html' - appendSubHtmlTo: '.lg-sub-html', - - subHtmlSelectorRelative: false, - - /** - * @desc number of preload slides - * will exicute only after the current slide is fully loaded. - * - * @ex you clicked on 4th image and if preload = 1 then 3rd slide and 5th - * slide will be loaded in the background after the 4th slide is fully loaded.. - * if preload is 2 then 2nd 3rd 5th 6th slides will be preloaded.. ... ... - * - */ - preload: 1, - showAfterLoad: true, - selector: '', - selectWithin: '', - nextHtml: '', - prevHtml: '', - - // 0, 1 - index: false, - - iframeMaxWidth: '100%', - - download: true, - counter: true, - appendCounterTo: '.lg-toolbar', - - swipeThreshold: 50, - enableSwipe: true, - enableDrag: true, - - dynamic: false, - dynamicEl: [], - galleryId: 1 - }; - - function Plugin(element, options) { - - // Current lightGallery element - this.el = element; - - // Current jquery element - this.$el = $(element); - - // lightGallery settings - this.s = $.extend({}, defaults, options); - - // When using dynamic mode, ensure dynamicEl is an array - if (this.s.dynamic && this.s.dynamicEl !== 'undefined' && this.s.dynamicEl.constructor === Array && !this.s.dynamicEl.length) { - throw ('When using dynamic mode, you must also define dynamicEl as an Array.'); - } - - // lightGallery modules - this.modules = {}; - - // false when lightgallery complete first slide; - this.lGalleryOn = false; - - this.lgBusy = false; - - // Timeout function for hiding controls; - this.hideBartimeout = false; - - // To determine browser supports for touch events; - this.isTouch = ('ontouchstart' in document.documentElement); - - // Disable hideControlOnEnd if sildeEndAnimation is true - if (this.s.slideEndAnimatoin) { - this.s.hideControlOnEnd = false; - } - - // Gallery items - if (this.s.dynamic) { - this.$items = this.s.dynamicEl; - } else { - if (this.s.selector === 'this') { - this.$items = this.$el; - } else if (this.s.selector !== '') { - if (this.s.selectWithin) { - this.$items = $(this.s.selectWithin).find(this.s.selector); - } else { - this.$items = this.$el.find($(this.s.selector)); - } - } else { - this.$items = this.$el.children(); - } - } - - // .lg-item - this.$slide = ''; - - // .lg-outer - this.$outer = ''; - - this.init(); - - return this; - } - - Plugin.prototype.init = function() { - - var _this = this; - - // s.preload should not be more than $item.length - if (_this.s.preload > _this.$items.length) { - _this.s.preload = _this.$items.length; - } - - // if dynamic option is enabled execute immediately - var _hash = window.location.hash; - if (_hash.indexOf('lg=' + this.s.galleryId) > 0) { - - _this.index = parseInt(_hash.split('&slide=')[1], 10); - - $('body').addClass('lg-from-hash'); - if (!$('body').hasClass('lg-on')) { - setTimeout(function() { - _this.build(_this.index); - $('body').addClass('lg-on'); - }); - } - } - - if (_this.s.dynamic) { - - _this.$el.trigger('onBeforeOpen.lg'); - - _this.index = _this.s.index || 0; - - // prevent accidental double execution - if (!$('body').hasClass('lg-on')) { - setTimeout(function() { - _this.build(_this.index); - $('body').addClass('lg-on'); - }); - } - } else { - - // Using different namespace for click because click event should not unbind if selector is same object('this') - _this.$items.on('click.lgcustom', function(event) { - - // For IE8 - try { - event.preventDefault(); - event.preventDefault(); - } catch (er) { - event.returnValue = false; - } - - _this.$el.trigger('onBeforeOpen.lg'); - - _this.index = _this.s.index || _this.$items.index(this); - - // prevent accidental double execution - if (!$('body').hasClass('lg-on')) { - _this.build(_this.index); - $('body').addClass('lg-on'); - } - }); - } - - }; - - Plugin.prototype.build = function(index) { - - var _this = this; - - _this.structure(); - - // module constructor - $.each($.fn.lightGallery.modules, function(key) { - _this.modules[key] = new $.fn.lightGallery.modules[key](_this.el); - }); - - // initiate slide function - _this.slide(index, false, false); - - if (_this.s.keyPress) { - _this.keyPress(); - } - - if (_this.$items.length > 1) { - - _this.arrow(); - - setTimeout(function() { - _this.enableDrag(); - _this.enableSwipe(); - }, 50); - - if (_this.s.mousewheel) { - _this.mousewheel(); - } - } - - _this.counter(); - - _this.closeGallery(); - - _this.$el.trigger('onAfterOpen.lg'); - - // Hide controllers if mouse doesn't move for some period - _this.$outer.on('mousemove.lg click.lg touchstart.lg', function() { - - _this.$outer.removeClass('lg-hide-items'); - - clearTimeout(_this.hideBartimeout); - - // Timeout will be cleared on each slide movement also - _this.hideBartimeout = setTimeout(function() { - _this.$outer.addClass('lg-hide-items'); - }, _this.s.hideBarsDelay); - - }); - - }; - - Plugin.prototype.structure = function() { - var list = ''; - var controls = ''; - var i = 0; - var subHtmlCont = ''; - var template; - var _this = this; - - $('body').append('
    '); - $('.lg-backdrop').css('transition-duration', this.s.backdropDuration + 'ms'); - - // Create gallery items - for (i = 0; i < this.$items.length; i++) { - list += '
    '; - } - - // Create controlls - if (this.s.controls && this.$items.length > 1) { - controls = '
    ' + - '
    ' + this.s.prevHtml + '
    ' + - '
    ' + this.s.nextHtml + '
    ' + - '
    '; - } - - if (this.s.appendSubHtmlTo === '.lg-sub-html') { - subHtmlCont = '
    '; - } - - template = '
    ' + - '
    ' + - '
    ' + list + '
    ' + - '
    ' + - '' + - '
    ' + - controls + - subHtmlCont + - '
    ' + - '
    '; - - $('body').append(template); - this.$outer = $('.lg-outer'); - this.$slide = this.$outer.find('.lg-item'); - - if (this.s.useLeft) { - this.$outer.addClass('lg-use-left'); - - // Set mode lg-slide if use left is true; - this.s.mode = 'lg-slide'; - } else { - this.$outer.addClass('lg-use-css3'); - } - - // For fixed height gallery - _this.setTop(); - $(window).on('resize.lg orientationchange.lg', function() { - setTimeout(function() { - _this.setTop(); - }, 100); - }); - - // add class lg-current to remove initial transition - this.$slide.eq(this.index).addClass('lg-current'); - - // add Class for css support and transition mode - if (this.doCss()) { - this.$outer.addClass('lg-css3'); - } else { - this.$outer.addClass('lg-css'); - - // Set speed 0 because no animation will happen if browser doesn't support css3 - this.s.speed = 0; - } - - this.$outer.addClass(this.s.mode); - - if (this.s.enableDrag && this.$items.length > 1) { - this.$outer.addClass('lg-grab'); - } - - if (this.s.showAfterLoad) { - this.$outer.addClass('lg-show-after-load'); - } - - if (this.doCss()) { - var $inner = this.$outer.find('.lg-inner'); - $inner.css('transition-timing-function', this.s.cssEasing); - $inner.css('transition-duration', this.s.speed + 'ms'); - } - - $('.lg-backdrop').addClass('in'); - - setTimeout(function() { - _this.$outer.addClass('lg-visible'); - }, this.s.backdropDuration); - - if (this.s.download) { - this.$outer.find('.lg-toolbar').append(''); - } - - // Store the current scroll top value to scroll back after closing the gallery.. - this.prevScrollTop = $(window).scrollTop(); - - }; - - // For fixed height gallery - Plugin.prototype.setTop = function() { - if (this.s.height !== '100%') { - var wH = $(window).height(); - var top = (wH - parseInt(this.s.height, 10)) / 2; - var $lGallery = this.$outer.find('.lg'); - if (wH >= parseInt(this.s.height, 10)) { - $lGallery.css('top', top + 'px'); - } else { - $lGallery.css('top', '0px'); - } - } - }; - - // Find css3 support - Plugin.prototype.doCss = function() { - // check for css animation support - var support = function() { - var transition = ['transition', 'MozTransition', 'WebkitTransition', 'OTransition', 'msTransition', 'KhtmlTransition']; - var root = document.documentElement; - var i = 0; - for (i = 0; i < transition.length; i++) { - if (transition[i] in root.style) { - return true; - } - } - }; - - if (support()) { - return true; - } - - return false; - }; - - /** - * @desc Check the given src is video - * @param {String} src - * @return {Object} video type - * Ex:{ youtube : ["//www.youtube.com/watch?v=c0asJgSyxcY", "c0asJgSyxcY"] } - */ - Plugin.prototype.isVideo = function(src, index) { - - var html; - if (this.s.dynamic) { - html = this.s.dynamicEl[index].html; - } else { - html = this.$items.eq(index).attr('data-html'); - } - - if (!src && html) { - return { - html5: true - }; - } - - var youtube = src.match(/\/\/(?:www\.)?youtu(?:\.be|be\.com)\/(?:watch\?v=|embed\/)?([a-z0-9\-\_\%]+)/i); - var vimeo = src.match(/\/\/(?:www\.)?vimeo.com\/([0-9a-z\-_]+)/i); - var dailymotion = src.match(/\/\/(?:www\.)?dai.ly\/([0-9a-z\-_]+)/i); - var vk = src.match(/\/\/(?:www\.)?(?:vk\.com|vkontakte\.ru)\/(?:video_ext\.php\?)(.*)/i); - - if (youtube) { - return { - youtube: youtube - }; - } else if (vimeo) { - return { - vimeo: vimeo - }; - } else if (dailymotion) { - return { - dailymotion: dailymotion - }; - } else if (vk) { - return { - vk: vk - }; - } - }; - - /** - * @desc Create image counter - * Ex: 1/10 - */ - Plugin.prototype.counter = function() { - if (this.s.counter) { - $(this.s.appendCounterTo).append('
    ' + (parseInt(this.index, 10) + 1) + ' / ' + this.$items.length + '
    '); - } - }; - - /** - * @desc add sub-html into the slide - * @param {Number} index - index of the slide - */ - Plugin.prototype.addHtml = function(index) { - var subHtml = null; - var subHtmlUrl; - var $currentEle; - if (this.s.dynamic) { - if (this.s.dynamicEl[index].subHtmlUrl) { - subHtmlUrl = this.s.dynamicEl[index].subHtmlUrl; - } else { - subHtml = this.s.dynamicEl[index].subHtml; - } - } else { - $currentEle = this.$items.eq(index); - if ($currentEle.attr('data-sub-html-url')) { - subHtmlUrl = $currentEle.attr('data-sub-html-url'); - } else { - subHtml = $currentEle.attr('data-sub-html'); - if (this.s.getCaptionFromTitleOrAlt && !subHtml) { - subHtml = $currentEle.attr('title') || $currentEle.find('img').first().attr('alt'); - } - } - } - - if (!subHtmlUrl) { - if (typeof subHtml !== 'undefined' && subHtml !== null) { - - // get first letter of subhtml - // if first letter starts with . or # get the html form the jQuery object - var fL = subHtml.substring(0, 1); - if (fL === '.' || fL === '#') { - if (this.s.subHtmlSelectorRelative && !this.s.dynamic) { - subHtml = $currentEle.find(subHtml).html(); - } else { - subHtml = $(subHtml).html(); - } - } - } else { - subHtml = ''; - } - } - - if (this.s.appendSubHtmlTo === '.lg-sub-html') { - - if (subHtmlUrl) { - this.$outer.find(this.s.appendSubHtmlTo).load(subHtmlUrl); - } else { - this.$outer.find(this.s.appendSubHtmlTo).html(subHtml); - } - - } else { - - if (subHtmlUrl) { - this.$slide.eq(index).load(subHtmlUrl); - } else { - this.$slide.eq(index).append(subHtml); - } - } - - // Add lg-empty-html class if title doesn't exist - if (typeof subHtml !== 'undefined' && subHtml !== null) { - if (subHtml === '') { - this.$outer.find(this.s.appendSubHtmlTo).addClass('lg-empty-html'); - } else { - this.$outer.find(this.s.appendSubHtmlTo).removeClass('lg-empty-html'); - } - } - - this.$el.trigger('onAfterAppendSubHtml.lg', [index]); - }; - - /** - * @desc Preload slides - * @param {Number} index - index of the slide - */ - Plugin.prototype.preload = function(index) { - var i = 1; - var j = 1; - for (i = 1; i <= this.s.preload; i++) { - if (i >= this.$items.length - index) { - break; - } - - this.loadContent(index + i, false, 0); - } - - for (j = 1; j <= this.s.preload; j++) { - if (index - j < 0) { - break; - } - - this.loadContent(index - j, false, 0); - } - }; - - /** - * @desc Load slide content into slide. - * @param {Number} index - index of the slide. - * @param {Boolean} rec - if true call loadcontent() function again. - * @param {Boolean} delay - delay for adding complete class. it is 0 except first time. - */ - Plugin.prototype.loadContent = function(index, rec, delay) { - - var _this = this; - var _hasPoster = false; - var _$img; - var _src; - var _poster; - var _srcset; - var _sizes; - var _html; - var getResponsiveSrc = function(srcItms) { - var rsWidth = []; - var rsSrc = []; - for (var i = 0; i < srcItms.length; i++) { - var __src = srcItms[i].split(' '); - - // Manage empty space - if (__src[0] === '') { - __src.splice(0, 1); - } - - rsSrc.push(__src[0]); - rsWidth.push(__src[1]); - } - - var wWidth = $(window).width(); - for (var j = 0; j < rsWidth.length; j++) { - if (parseInt(rsWidth[j], 10) > wWidth) { - _src = rsSrc[j]; - break; - } - } - }; - - if (_this.s.dynamic) { - - if (_this.s.dynamicEl[index].poster) { - _hasPoster = true; - _poster = _this.s.dynamicEl[index].poster; - } - - _html = _this.s.dynamicEl[index].html; - _src = _this.s.dynamicEl[index].src; - - if (_this.s.dynamicEl[index].responsive) { - var srcDyItms = _this.s.dynamicEl[index].responsive.split(','); - getResponsiveSrc(srcDyItms); - } - - _srcset = _this.s.dynamicEl[index].srcset; - _sizes = _this.s.dynamicEl[index].sizes; - - } else { - - if (_this.$items.eq(index).attr('data-poster')) { - _hasPoster = true; - _poster = _this.$items.eq(index).attr('data-poster'); - } - - _html = _this.$items.eq(index).attr('data-html'); - _src = _this.$items.eq(index).attr('href') || _this.$items.eq(index).attr('data-src'); - - if (_this.$items.eq(index).attr('data-responsive')) { - var srcItms = _this.$items.eq(index).attr('data-responsive').split(','); - getResponsiveSrc(srcItms); - } - - _srcset = _this.$items.eq(index).attr('data-srcset'); - _sizes = _this.$items.eq(index).attr('data-sizes'); - - } - - //if (_src || _srcset || _sizes || _poster) { - - var iframe = false; - if (_this.s.dynamic) { - if (_this.s.dynamicEl[index].iframe) { - iframe = true; - } - } else { - if (_this.$items.eq(index).attr('data-iframe') === 'true') { - iframe = true; - } - } - - var _isVideo = _this.isVideo(_src, index); - if (!_this.$slide.eq(index).hasClass('lg-loaded')) { - if (iframe) { - _this.$slide.eq(index).prepend('
    '); - } else if (_hasPoster) { - var videoClass = ''; - if (_isVideo && _isVideo.youtube) { - videoClass = 'lg-has-youtube'; - } else if (_isVideo && _isVideo.vimeo) { - videoClass = 'lg-has-vimeo'; - } else { - videoClass = 'lg-has-html5'; - } - - _this.$slide.eq(index).prepend('
    '); - - } else if (_isVideo) { - _this.$slide.eq(index).prepend('
    '); - _this.$el.trigger('hasVideo.lg', [index, _src, _html]); - } else { - _this.$slide.eq(index).prepend('
    '); - } - - _this.$el.trigger('onAferAppendSlide.lg', [index]); - - _$img = _this.$slide.eq(index).find('.lg-object'); - if (_sizes) { - _$img.attr('sizes', _sizes); - } - - if (_srcset) { - _$img.attr('srcset', _srcset); - try { - picturefill({ - elements: [_$img[0]] - }); - } catch (e) { - console.error('Make sure you have included Picturefill version 2'); - } - } - - if (this.s.appendSubHtmlTo !== '.lg-sub-html') { - _this.addHtml(index); - } - - _this.$slide.eq(index).addClass('lg-loaded'); - } - - _this.$slide.eq(index).find('.lg-object').on('load.lg error.lg', function() { - - // For first time add some delay for displaying the start animation. - var _speed = 0; - - // Do not change the delay value because it is required for zoom plugin. - // If gallery opened from direct url (hash) speed value should be 0 - if (delay && !$('body').hasClass('lg-from-hash')) { - _speed = delay; - } - - setTimeout(function() { - _this.$slide.eq(index).addClass('lg-complete'); - _this.$el.trigger('onSlideItemLoad.lg', [index, delay || 0]); - }, _speed); - - }); - - // @todo check load state for html5 videos - if (_isVideo && _isVideo.html5 && !_hasPoster) { - _this.$slide.eq(index).addClass('lg-complete'); - } - - if (rec === true) { - if (!_this.$slide.eq(index).hasClass('lg-complete')) { - _this.$slide.eq(index).find('.lg-object').on('load.lg error.lg', function() { - _this.preload(index); - }); - } else { - _this.preload(index); - } - } - - //} - }; - - /** - * @desc slide function for lightgallery - ** Slide() gets call on start - ** ** Set lg.on true once slide() function gets called. - ** Call loadContent() on slide() function inside setTimeout - ** ** On first slide we do not want any animation like slide of fade - ** ** So on first slide( if lg.on if false that is first slide) loadContent() should start loading immediately - ** ** Else loadContent() should wait for the transition to complete. - ** ** So set timeout s.speed + 50 - <=> ** loadContent() will load slide content in to the particular slide - ** ** It has recursion (rec) parameter. if rec === true loadContent() will call preload() function. - ** ** preload will execute only when the previous slide is fully loaded (images iframe) - ** ** avoid simultaneous image load - <=> ** Preload() will check for s.preload value and call loadContent() again accoring to preload value - ** loadContent() <====> Preload(); - - * @param {Number} index - index of the slide - * @param {Boolean} fromTouch - true if slide function called via touch event or mouse drag - * @param {Boolean} fromThumb - true if slide function called via thumbnail click - */ - Plugin.prototype.slide = function(index, fromTouch, fromThumb) { - - var _prevIndex = this.$outer.find('.lg-current').index(); - var _this = this; - - // Prevent if multiple call - // Required for hsh plugin - if (_this.lGalleryOn && (_prevIndex === index)) { - return; - } - - var _length = this.$slide.length; - var _time = _this.lGalleryOn ? this.s.speed : 0; - var _next = false; - var _prev = false; - - if (!_this.lgBusy) { - - if (this.s.download) { - var _src; - if (_this.s.dynamic) { - _src = _this.s.dynamicEl[index].downloadUrl !== false && (_this.s.dynamicEl[index].downloadUrl || _this.s.dynamicEl[index].src); - } else { - _src = _this.$items.eq(index).attr('data-download-url') !== 'false' && (_this.$items.eq(index).attr('data-download-url') || _this.$items.eq(index).attr('href') || _this.$items.eq(index).attr('data-src')); - - } - - if (_src) { - $('#lg-download').attr('href', _src); - _this.$outer.removeClass('lg-hide-download'); - } else { - _this.$outer.addClass('lg-hide-download'); - } - } - - this.$el.trigger('onBeforeSlide.lg', [_prevIndex, index, fromTouch, fromThumb]); - - _this.lgBusy = true; - - clearTimeout(_this.hideBartimeout); - - // Add title if this.s.appendSubHtmlTo === lg-sub-html - if (this.s.appendSubHtmlTo === '.lg-sub-html') { - - // wait for slide animation to complete - setTimeout(function() { - _this.addHtml(index); - }, _time); - } - - this.arrowDisable(index); - - if (!fromTouch) { - - // remove all transitions - _this.$outer.addClass('lg-no-trans'); - - this.$slide.removeClass('lg-prev-slide lg-next-slide'); - - if (index < _prevIndex) { - _prev = true; - if ((index === 0) && (_prevIndex === _length - 1) && !fromThumb) { - _prev = false; - _next = true; - } - } else if (index > _prevIndex) { - _next = true; - if ((index === _length - 1) && (_prevIndex === 0) && !fromThumb) { - _prev = true; - _next = false; - } - } - - if (_prev) { - - //prevslide - this.$slide.eq(index).addClass('lg-prev-slide'); - this.$slide.eq(_prevIndex).addClass('lg-next-slide'); - } else if (_next) { - - // next slide - this.$slide.eq(index).addClass('lg-next-slide'); - this.$slide.eq(_prevIndex).addClass('lg-prev-slide'); - } - - // give 50 ms for browser to add/remove class - setTimeout(function() { - _this.$slide.removeClass('lg-current'); - - //_this.$slide.eq(_prevIndex).removeClass('lg-current'); - _this.$slide.eq(index).addClass('lg-current'); - - // reset all transitions - _this.$outer.removeClass('lg-no-trans'); - }, 50); - } else { - - var touchPrev = index - 1; - var touchNext = index + 1; - - if ((index === 0) && (_prevIndex === _length - 1)) { - - // next slide - touchNext = 0; - touchPrev = _length - 1; - } else if ((index === _length - 1) && (_prevIndex === 0)) { - - // prev slide - touchNext = 0; - touchPrev = _length - 1; - } - - this.$slide.removeClass('lg-prev-slide lg-current lg-next-slide'); - _this.$slide.eq(touchPrev).addClass('lg-prev-slide'); - _this.$slide.eq(touchNext).addClass('lg-next-slide'); - _this.$slide.eq(index).addClass('lg-current'); - } - - if (_this.lGalleryOn) { - setTimeout(function() { - _this.loadContent(index, true, 0); - }, this.s.speed + 50); - - setTimeout(function() { - _this.lgBusy = false; - _this.$el.trigger('onAfterSlide.lg', [_prevIndex, index, fromTouch, fromThumb]); - }, this.s.speed); - - } else { - _this.loadContent(index, true, _this.s.backdropDuration); - - _this.lgBusy = false; - _this.$el.trigger('onAfterSlide.lg', [_prevIndex, index, fromTouch, fromThumb]); - } - - _this.lGalleryOn = true; - - if (this.s.counter) { - $('#lg-counter-current').text(index + 1); - } - - } - - }; - - /** - * @desc Go to next slide - * @param {Boolean} fromTouch - true if slide function called via touch event - */ - Plugin.prototype.goToNextSlide = function(fromTouch) { - var _this = this; - if (!_this.lgBusy) { - if ((_this.index + 1) < _this.$slide.length) { - _this.index++; - _this.$el.trigger('onBeforeNextSlide.lg', [_this.index]); - _this.slide(_this.index, fromTouch, false); - } else { - if (_this.s.loop) { - _this.index = 0; - _this.$el.trigger('onBeforeNextSlide.lg', [_this.index]); - _this.slide(_this.index, fromTouch, false); - } else if (_this.s.slideEndAnimatoin) { - _this.$outer.addClass('lg-right-end'); - setTimeout(function() { - _this.$outer.removeClass('lg-right-end'); - }, 400); - } - } - } - }; - - /** - * @desc Go to previous slide - * @param {Boolean} fromTouch - true if slide function called via touch event - */ - Plugin.prototype.goToPrevSlide = function(fromTouch) { - var _this = this; - if (!_this.lgBusy) { - if (_this.index > 0) { - _this.index--; - _this.$el.trigger('onBeforePrevSlide.lg', [_this.index, fromTouch]); - _this.slide(_this.index, fromTouch, false); - } else { - if (_this.s.loop) { - _this.index = _this.$items.length - 1; - _this.$el.trigger('onBeforePrevSlide.lg', [_this.index, fromTouch]); - _this.slide(_this.index, fromTouch, false); - } else if (_this.s.slideEndAnimatoin) { - _this.$outer.addClass('lg-left-end'); - setTimeout(function() { - _this.$outer.removeClass('lg-left-end'); - }, 400); - } - } - } - }; - - Plugin.prototype.keyPress = function() { - var _this = this; - if (this.$items.length > 1) { - $(window).on('keyup.lg', function(e) { - if (_this.$items.length > 1) { - if (e.keyCode === 37) { - e.preventDefault(); - _this.goToPrevSlide(); - } - - if (e.keyCode === 39) { - e.preventDefault(); - _this.goToNextSlide(); - } - } - }); - } - - $(window).on('keydown.lg', function(e) { - if (_this.s.escKey === true && e.keyCode === 27) { - e.preventDefault(); - if (!_this.$outer.hasClass('lg-thumb-open')) { - _this.destroy(); - } else { - _this.$outer.removeClass('lg-thumb-open'); - } - } - }); - }; - - Plugin.prototype.arrow = function() { - var _this = this; - this.$outer.find('.lg-prev').on('click.lg', function() { - _this.goToPrevSlide(); - }); - - this.$outer.find('.lg-next').on('click.lg', function() { - _this.goToNextSlide(); - }); - }; - - Plugin.prototype.arrowDisable = function(index) { - - // Disable arrows if s.hideControlOnEnd is true - if (!this.s.loop && this.s.hideControlOnEnd) { - if ((index + 1) < this.$slide.length) { - this.$outer.find('.lg-next').removeAttr('disabled').removeClass('disabled'); - } else { - this.$outer.find('.lg-next').attr('disabled', 'disabled').addClass('disabled'); - } - - if (index > 0) { - this.$outer.find('.lg-prev').removeAttr('disabled').removeClass('disabled'); - } else { - this.$outer.find('.lg-prev').attr('disabled', 'disabled').addClass('disabled'); - } - } - }; - - Plugin.prototype.setTranslate = function($el, xValue, yValue) { - // jQuery supports Automatic CSS prefixing since jQuery 1.8.0 - if (this.s.useLeft) { - $el.css('left', xValue); - } else { - $el.css({ - transform: 'translate3d(' + (xValue) + 'px, ' + yValue + 'px, 0px)' - }); - } - }; - - Plugin.prototype.touchMove = function(startCoords, endCoords) { - - var distance = endCoords - startCoords; - - if (Math.abs(distance) > 15) { - // reset opacity and transition duration - this.$outer.addClass('lg-dragging'); - - // move current slide - this.setTranslate(this.$slide.eq(this.index), distance, 0); - - // move next and prev slide with current slide - this.setTranslate($('.lg-prev-slide'), -this.$slide.eq(this.index).width() + distance, 0); - this.setTranslate($('.lg-next-slide'), this.$slide.eq(this.index).width() + distance, 0); - } - }; - - Plugin.prototype.touchEnd = function(distance) { - var _this = this; - - // keep slide animation for any mode while dragg/swipe - if (_this.s.mode !== 'lg-slide') { - _this.$outer.addClass('lg-slide'); - } - - this.$slide.not('.lg-current, .lg-prev-slide, .lg-next-slide').css('opacity', '0'); - - // set transition duration - setTimeout(function() { - _this.$outer.removeClass('lg-dragging'); - if ((distance < 0) && (Math.abs(distance) > _this.s.swipeThreshold)) { - _this.goToNextSlide(true); - } else if ((distance > 0) && (Math.abs(distance) > _this.s.swipeThreshold)) { - _this.goToPrevSlide(true); - } else if (Math.abs(distance) < 5) { - - // Trigger click if distance is less than 5 pix - _this.$el.trigger('onSlideClick.lg'); - } - - _this.$slide.removeAttr('style'); - }); - - // remove slide class once drag/swipe is completed if mode is not slide - setTimeout(function() { - if (!_this.$outer.hasClass('lg-dragging') && _this.s.mode !== 'lg-slide') { - _this.$outer.removeClass('lg-slide'); - } - }, _this.s.speed + 100); - - }; - - Plugin.prototype.enableSwipe = function() { - var _this = this; - var startCoords = 0; - var endCoords = 0; - var isMoved = false; - - if (_this.s.enableSwipe && _this.isTouch && _this.doCss()) { - - _this.$slide.on('touchstart.lg', function(e) { - if (!_this.$outer.hasClass('lg-zoomed') && !_this.lgBusy) { - e.preventDefault(); - _this.manageSwipeClass(); - startCoords = e.originalEvent.targetTouches[0].pageX; - } - }); - - _this.$slide.on('touchmove.lg', function(e) { - if (!_this.$outer.hasClass('lg-zoomed')) { - e.preventDefault(); - endCoords = e.originalEvent.targetTouches[0].pageX; - _this.touchMove(startCoords, endCoords); - isMoved = true; - } - }); - - _this.$slide.on('touchend.lg', function() { - if (!_this.$outer.hasClass('lg-zoomed')) { - if (isMoved) { - isMoved = false; - _this.touchEnd(endCoords - startCoords); - } else { - _this.$el.trigger('onSlideClick.lg'); - } - } - }); - } - - }; - - Plugin.prototype.enableDrag = function() { - var _this = this; - var startCoords = 0; - var endCoords = 0; - var isDraging = false; - var isMoved = false; - if (_this.s.enableDrag && !_this.isTouch && _this.doCss()) { - _this.$slide.on('mousedown.lg', function(e) { - // execute only on .lg-object - if (!_this.$outer.hasClass('lg-zoomed')) { - if ($(e.target).hasClass('lg-object') || $(e.target).hasClass('lg-video-play')) { - e.preventDefault(); - - if (!_this.lgBusy) { - _this.manageSwipeClass(); - startCoords = e.pageX; - isDraging = true; - - // ** Fix for webkit cursor issue https://code.google.com/p/chromium/issues/detail?id=26723 - _this.$outer.scrollLeft += 1; - _this.$outer.scrollLeft -= 1; - - // * - - _this.$outer.removeClass('lg-grab').addClass('lg-grabbing'); - - _this.$el.trigger('onDragstart.lg'); - } - - } - } - }); - - $(window).on('mousemove.lg', function(e) { - if (isDraging) { - isMoved = true; - endCoords = e.pageX; - _this.touchMove(startCoords, endCoords); - _this.$el.trigger('onDragmove.lg'); - } - }); - - $(window).on('mouseup.lg', function(e) { - if (isMoved) { - isMoved = false; - _this.touchEnd(endCoords - startCoords); - _this.$el.trigger('onDragend.lg'); - } else if ($(e.target).hasClass('lg-object') || $(e.target).hasClass('lg-video-play')) { - _this.$el.trigger('onSlideClick.lg'); - } - - // Prevent execution on click - if (isDraging) { - isDraging = false; - _this.$outer.removeClass('lg-grabbing').addClass('lg-grab'); - } - }); - - } - }; - - Plugin.prototype.manageSwipeClass = function() { - var touchNext = this.index + 1; - var touchPrev = this.index - 1; - var length = this.$slide.length; - if (this.s.loop) { - if (this.index === 0) { - touchPrev = length - 1; - } else if (this.index === length - 1) { - touchNext = 0; - } - } - - this.$slide.removeClass('lg-next-slide lg-prev-slide'); - if (touchPrev > -1) { - this.$slide.eq(touchPrev).addClass('lg-prev-slide'); - } - - this.$slide.eq(touchNext).addClass('lg-next-slide'); - }; - - Plugin.prototype.mousewheel = function() { - var _this = this; - _this.$outer.on('mousewheel.lg', function(e) { - - if (!e.deltaY) { - return; - } - - if (e.deltaY > 0) { - _this.goToPrevSlide(); - } else { - _this.goToNextSlide(); - } - - e.preventDefault(); - }); - - }; - - Plugin.prototype.closeGallery = function() { - - var _this = this; - var mousedown = false; - this.$outer.find('.lg-close').on('click.lg', function() { - _this.destroy(); - }); - - if (_this.s.closable) { - - // If you drag the slide and release outside gallery gets close on chrome - // for preventing this check mousedown and mouseup happened on .lg-item or lg-outer - _this.$outer.on('mousedown.lg', function(e) { - - if ($(e.target).is('.lg-outer') || $(e.target).is('.lg-item ') || $(e.target).is('.lg-img-wrap')) { - mousedown = true; - } else { - mousedown = false; - } - - }); - - _this.$outer.on('mouseup.lg', function(e) { - - if ($(e.target).is('.lg-outer') || $(e.target).is('.lg-item ') || $(e.target).is('.lg-img-wrap') && mousedown) { - if (!_this.$outer.hasClass('lg-dragging')) { - _this.destroy(); - } - } - - }); - - } - - }; - - Plugin.prototype.destroy = function(d) { - - var _this = this; - - if (!d) { - _this.$el.trigger('onBeforeClose.lg'); - } - - $(window).scrollTop(_this.prevScrollTop); - - /** - * if d is false or undefined destroy will only close the gallery - * plugins instance remains with the element - * - * if d is true destroy will completely remove the plugin - */ - - if (d) { - if (!_this.s.dynamic) { - // only when not using dynamic mode is $items a jquery collection - this.$items.off('click.lg click.lgcustom'); - } - - $.removeData(_this.el, 'lightGallery'); - } - - // Unbind all events added by lightGallery - this.$el.off('.lg.tm'); - - // Distroy all lightGallery modules - $.each($.fn.lightGallery.modules, function(key) { - if (_this.modules[key]) { - _this.modules[key].destroy(); - } - }); - - this.lGalleryOn = false; - - clearTimeout(_this.hideBartimeout); - this.hideBartimeout = false; - $(window).off('.lg'); - $('body').removeClass('lg-on lg-from-hash'); - - if (_this.$outer) { - _this.$outer.removeClass('lg-visible'); - } - - $('.lg-backdrop').removeClass('in'); - - setTimeout(function() { - if (_this.$outer) { - _this.$outer.remove(); - } - - $('.lg-backdrop').remove(); - - if (!d) { - _this.$el.trigger('onCloseAfter.lg'); - } - - }, _this.s.backdropDuration + 50); - }; - - $.fn.lightGallery = function(options) { - return this.each(function() { - if (!$.data(this, 'lightGallery')) { - $.data(this, 'lightGallery', new Plugin(this, options)); - } else { - try { - $(this).data('lightGallery').init(); - } catch (err) { - console.error('lightGallery has not initiated properly'); - } - } - }); - }; - - $.fn.lightGallery.modules = {}; - -})(jQuery, window, document); diff --git a/vendors/lightgallery/src/sass/lg-animations.scss b/vendors/lightgallery/src/sass/lg-animations.scss deleted file mode 100644 index 9a47e417ea..0000000000 --- a/vendors/lightgallery/src/sass/lg-animations.scss +++ /dev/null @@ -1,714 +0,0 @@ -.lg-css3 { - // Remove all transition effects - &.lg-no-trans { - .lg-prev-slide, .lg-next-slide, .lg-current { - @include transitionCustom(none 0s ease 0s !important); - } - } - - &.lg-use-css3 { - .lg-item { - will-change: transform, opacity; - } - } - - &.lg-use-left { - .lg-item { - will-change: left, opacity; - } - } - - &.lg-zoom-in { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(2, 2, 2); - } - - &.lg-next-slide { - @include scale3d(2, 2, 2); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-zoom-out { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(0, 0, 0); - } - - &.lg-next-slide { - @include scale3d(0, 0, 0); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-zoom-out-in { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(0, 0, 0); - } - - &.lg-next-slide { - @include scale3d(2, 2, 2); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-zoom-in-out { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(2, 2, 2); - } - - &.lg-next-slide { - @include scale3d(0, 0, 0); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-soft-zoom { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(1.2, 1.2, 1.2); - } - - &.lg-next-slide { - @include scale3d(0.8, 0.8, 0.8); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-slide-circular { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(-100%, 0, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(100%, 0, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - // sec - &.lg-slide-circular-up { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(-100%, -100%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(100%, -100%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - // sec - &.lg-slide-circular-down { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(-100%, 100%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(100%, 100%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-slide-circular-vertical { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(0, -100%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(0, 100%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - // sec - &.lg-slide-circular-vertical-left { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(-100%, -100%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(-100%, 100%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - // sec - &.lg-slide-circular-vertical-down { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(100%, -100%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(100%, 100%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-slide-vertical { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include translate3d(0, -100%, 0); - } - - &.lg-next-slide { - @include translate3d(0, 100%, 0); - } - - &.lg-current { - @include translate3d(0, 0, 0); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-vertical-growth { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0.5, 0.5, 0.5) translate3d(0, -150%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0.5, 0.5, 0.5) translate3d(0, 150%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-only { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(60deg, 0deg)); - } - - &.lg-next-slide { - @include transform(skew(60deg, 0deg)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-only-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(-60deg, 0deg)); - } - - &.lg-next-slide { - @include transform(skew(-60deg, 0deg)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-only-y { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, 60deg)); - } - - &.lg-next-slide { - @include transform(skew(0deg, 60deg)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-only-y-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, -60deg)); - } - - &.lg-next-slide { - @include transform(skew(0deg, -60deg)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(60deg, 0deg) translate3d(-100%, 0%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(60deg, 0deg) translate3d(100%, 0%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(-60deg, 0deg) translate3d(-100%, 0%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(-60deg, 0deg) translate3d(100%, 0%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-cross { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, 60deg) translate3d(-100%, 0%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(0deg, 60deg) translate3d(100%, 0%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-cross-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, -60deg) translate3d(-100%, 0%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(0deg, -60deg) translate3d(100%, 0%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-ver { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(60deg, 0deg) translate3d(0, -100%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(60deg, 0deg) translate3d(0, 100%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-ver-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(-60deg, 0deg) translate3d(0, -100%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(-60deg, 0deg) translate3d(0, 100%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-ver-cross { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, 60deg) translate3d(0, -100%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(0deg, 60deg) translate3d(0, 100%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-ver-cross-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, -60deg) translate3d(0, -100%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(0deg, -60deg) translate3d(0, 100%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-lollipop { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include translate3d(-100%, 0, 0); - } - - &.lg-next-slide { - @include transform(translate3d(0, 0, 0) scale(0.5)); - } - - &.lg-current { - @include translate3d(0, 0, 0); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-lollipop-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(translate3d(0, 0, 0) scale(0.5)); - } - - &.lg-next-slide { - @include translate3d(100%, 0, 0); - } - - &.lg-current { - @include translate3d(0, 0, 0); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-rotate { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(rotate(-360deg)); - } - - &.lg-next-slide { - @include transform(rotate(360deg)); - } - - &.lg-current { - @include transform(rotate(0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-rotate-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(rotate(360deg)); - } - - &.lg-next-slide { - @include transform(rotate(-360deg)); - } - - &.lg-current { - @include transform(rotate(0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-tube { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(1, 0, 1) translate3d(-100%, 0, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(1, 0, 1) translate3d(100%, 0, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } -} \ No newline at end of file diff --git a/vendors/lightgallery/src/sass/lg-autoplay.scss b/vendors/lightgallery/src/sass/lg-autoplay.scss deleted file mode 100644 index 0a2da002e6..0000000000 --- a/vendors/lightgallery/src/sass/lg-autoplay.scss +++ /dev/null @@ -1,36 +0,0 @@ -.lg-progress-bar { - background-color: $lg-progress-bar-bg; - height: $lg-progress-bar-height; - left: 0; - position: absolute; - top: 0; - width: 100%; - z-index: $zindex-progressbar; - opacity: 0; - @include transitionCustom(opacity 0.08s ease 0s); - - .lg-progress { - background-color: $lg-progress-bar-active-bg; - height: $lg-progress-bar-height; - width: 0; - } - - &.lg-start { - .lg-progress { - width: 100%; - } - } - - .lg-show-autoplay & { - opacity: 1; - } -} - -.lg-autoplay-button { - &:after { - .lg-show-autoplay & { - content: "\e01a"; - } - content: "\e01d"; - } -} \ No newline at end of file diff --git a/vendors/lightgallery/src/sass/lg-fb-comment-box.scss b/vendors/lightgallery/src/sass/lg-fb-comment-box.scss deleted file mode 100644 index 96aaab0d25..0000000000 --- a/vendors/lightgallery/src/sass/lg-fb-comment-box.scss +++ /dev/null @@ -1,46 +0,0 @@ -@import "lg-variables"; -@import "lg-mixins"; -.lg-outer.fb-comments{ - .lg-img-wrap { - padding-right: 400px !important; - } - .fb-comments { - height: 100%; - overflow-y: auto; - position: absolute; - right: 0; - top: 0; - width: 420px; - z-index: 99999; - background: #fff url("#{$lg-path-images}/loading.gif") no-repeat scroll center center; - &.fb_iframe_widget { - background-image: none; - &.fb_iframe_widget_loader{ - background: #fff url("#{$lg-path-images}/loading.gif") no-repeat scroll center center; - } - } - } - .lg-toolbar { - right: 420px; - width: auto; - } - .lg-actions .lg-next { - right: 420px; - } - .lg-item { - background-image: none; - &.lg-complete{ - .lg-img-wrap{ - background-image: none; - } - } - } - .lg-img-wrap { - background: url("#{$lg-path-images}/loading.gif") no-repeat scroll center center transparent; - } - - .lg-sub-html { - padding: 0; - position: static; - } -} diff --git a/vendors/lightgallery/src/sass/lg-fonts.scss b/vendors/lightgallery/src/sass/lg-fonts.scss deleted file mode 100644 index 091e6f91e6..0000000000 --- a/vendors/lightgallery/src/sass/lg-fonts.scss +++ /dev/null @@ -1,22 +0,0 @@ -// font icons support -@font-face { - font-family: 'lg'; - src: url("#{$lg-path-fonts}/lg.eot?n1z373"); - src: url("#{$lg-path-fonts}/lg.eot?#iefixn1z373") format("embedded-opentype"), url("#{$lg-path-fonts}/lg.woff?n1z373") format("woff"), url("#{$lg-path-fonts}/lg.ttf?n1z373") format("truetype"), url("#{$lg-path-fonts}/lg.svg?n1z373#lg") format("svg"); - font-weight: normal; - font-style: normal; -} - - -.lg-icon { - font-family: 'lg'; - speak: none; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} \ No newline at end of file diff --git a/vendors/lightgallery/src/sass/lg-fullscreen.scss b/vendors/lightgallery/src/sass/lg-fullscreen.scss deleted file mode 100644 index fd710357e1..0000000000 --- a/vendors/lightgallery/src/sass/lg-fullscreen.scss +++ /dev/null @@ -1,9 +0,0 @@ -.lg-fullscreen { - &:after { - content: "\e20c"; - - .lg-fullscreen-on & { - content: "\e20d"; - } - } -} \ No newline at end of file diff --git a/vendors/lightgallery/src/sass/lg-mixins.scss b/vendors/lightgallery/src/sass/lg-mixins.scss deleted file mode 100644 index b0260d3a82..0000000000 --- a/vendors/lightgallery/src/sass/lg-mixins.scss +++ /dev/null @@ -1,330 +0,0 @@ -// Vendor Prefixes -// -// All vendor mixins are deprecated as of v3.2.0 due to the introduction of -// Autoprefixer in our Gruntfile. They will be removed in v4. - -// - Animations -// - Backface visibility -// - Box shadow -// - Box sizing -// - Content columns -// - Hyphens -// - Placeholder text -// - Transformations -// - Transitions -// - User Select -// - cursor grab - -// Animations -@mixin animation($animation) { - -webkit-animation: $animation; - -o-animation: $animation; - animation: $animation; -} - -@mixin animation-name($name) { - -webkit-animation-name: $name; - animation-name: $name; -} - -@mixin animation-duration($duration) { - -webkit-animation-duration: $duration; - animation-duration: $duration; -} - -@mixin animation-timing-function($timing-function) { - -webkit-animation-timing-function: $timing-function; - animation-timing-function: $timing-function; -} - -@mixin animation-delay($delay) { - -webkit-animation-delay: $delay; - animation-delay: $delay; -} - -@mixin animation-iteration-count($iteration-count) { - -webkit-animation-iteration-count: $iteration-count; - animation-iteration-count: $iteration-count; -} - -@mixin animation-direction($direction) { - -webkit-animation-direction: $direction; - animation-direction: $direction; -} - -@mixin animation-fill-mode($fill-mode) { - -webkit-animation-fill-mode: $fill-mode; - animation-fill-mode: $fill-mode; -} - -@mixin keyframes($name) { - @-webkit-keyframes #{$name} { - @content; - } - - @-moz-keyframes #{$name} { - @content; - } - - @-ms-keyframes #{$name} { - @content; - } - - @keyframes #{$name} { - @content; - } -} - -// Backface visibility -// Prevent browsers from flickering when using CSS 3D transforms. -// Default value is `visible`, but can be changed to `hidden` - -@mixin backface-visibility($visibility) { - -webkit-backface-visibility: $visibility; - -moz-backface-visibility: $visibility; - backface-visibility: $visibility; -} - -// Drop shadows -// -// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's -// supported browsers that have box shadow capabilities now support it. - -@mixin box-shadow($shadow...) { - -webkit-box-shadow: $shadow; // iOS <4.3 & Android <4.1 - box-shadow: $shadow; -} - -// Box sizing -@mixin box-sizing($boxmodel) { - -webkit-box-sizing: $boxmodel; - -moz-box-sizing: $boxmodel; - box-sizing: $boxmodel; -} - -// CSS3 Content Columns -@mixin content-columns($column-count, $column-gap: $grid-gutter-width) { - -webkit-column-count: $column-count; - -moz-column-count: $column-count; - column-count: $column-count; - -webkit-column-gap: $column-gap; - -moz-column-gap: $column-gap; - column-gap: $column-gap; -} - -// Optional hyphenation -@mixin hyphens($mode: auto) { - word-wrap: break-word; - -webkit-hyphens: $mode; - -moz-hyphens: $mode; - -ms-hyphens: $mode; // IE10+ - -o-hyphens: $mode; - hyphens: $mode; -} - -// Transformations -@mixin scale($ratio...) { - -webkit-transform: scale($ratio); - -ms-transform: scale($ratio); // IE9 only - -o-transform: scale($ratio); - transform: scale($ratio); -} - -@mixin scaleX($ratio) { - -webkit-transform: scaleX($ratio); - -ms-transform: scaleX($ratio); // IE9 only - -o-transform: scaleX($ratio); - transform: scaleX($ratio); -} - -@mixin scaleY($ratio) { - -webkit-transform: scaleY($ratio); - -ms-transform: scaleY($ratio); // IE9 only - -o-transform: scaleY($ratio); - transform: scaleY($ratio); -} - -@mixin skew($x, $y) { - -webkit-transform: skewX($x) skewY($y); - -ms-transform: skewX($x) skewY($y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+ - -o-transform: skewX($x) skewY($y); - transform: skewX($x) skewY($y); -} - -@mixin translate($x, $y) { - -webkit-transform: translate($x, $y); - -ms-transform: translate($x, $y); // IE9 only - -o-transform: translate($x, $y); - transform: translate($x, $y); -} - -@mixin translate3d($x, $y, $z) { - -webkit-transform: translate3d($x, $y, $z); - transform: translate3d($x, $y, $z); -} - -@mixin scale3d($x, $y, $z) { - -webkit-transform: scale3d($x, $y, $z); - transform: scale3d($x, $y, $z); -} - -@mixin rotate($degrees) { - -webkit-transform: rotate($degrees); - -ms-transform: rotate($degrees); // IE9 only - -o-transform: rotate($degrees); - transform: rotate($degrees); -} - -@mixin rotateX($degrees) { - -webkit-transform: rotateX($degrees); - -ms-transform: rotateX($degrees); // IE9 only - -o-transform: rotateX($degrees); - transform: rotateX($degrees); -} - -@mixin rotateY($degrees) { - -webkit-transform: rotateY($degrees); - -ms-transform: rotateY($degrees); // IE9 only - -o-transform: rotateY($degrees); - transform: rotateY($degrees); -} - -@mixin perspective($perspective) { - -webkit-perspective: $perspective; - -moz-perspective: $perspective; - perspective: $perspective; -} - -@mixin perspective-origin($perspective) { - -webkit-perspective-origin: $perspective; - -moz-perspective-origin: $perspective; - perspective-origin: $perspective; -} - -@mixin transform-origin($origin) { - -webkit-transform-origin: $origin; - -moz-transform-origin: $origin; - -ms-transform-origin: $origin; // IE9 only - transform-origin: $origin; -} - -@mixin transform($transforms) { - -moz-transform: $transforms; - -o-transform: $transforms; - -ms-transform: $transforms; - -webkit-transform: $transforms; - transform: $transforms; -} - -// Transitions - -@mixin transition($transition...) { - -webkit-transition: $transition; - -o-transition: $transition; - transition: $transition; -} - -@mixin transition-property($transition-property...) { - -webkit-transition-property: $transition-property; - transition-property: $transition-property; -} - -@mixin transition-delay($transition-delay) { - -webkit-transition-delay: $transition-delay; - transition-delay: $transition-delay; -} - -@mixin transition-duration($transition-duration...) { - -webkit-transition-duration: $transition-duration; - transition-duration: $transition-duration; -} - -@mixin transition-timing-function($timing-function) { - -webkit-transition-timing-function: $timing-function; - transition-timing-function: $timing-function; -} - -@mixin transition-transform($transition...) { - -webkit-transition: -webkit-transform $transition; - -moz-transition: -moz-transform $transition; - -o-transition: -o-transform $transition; - transition: transform $transition; -} - -// transition custom - -@function prefix($property, $prefixes: webkit moz o ms) { - $vendor-prefixed-properties: transform background-clip background-size; - $result: (); - - @each $prefix in $prefixes { - @if index($vendor-prefixed-properties, $property) { - $property: -#{$prefix}-#{$property}; - } - $result: append($result, $property); - } - @return $result; -} - -@function trans-prefix($transition, $prefix: moz) { - $prefixed: (); - - @each $trans in $transition { - $prop-name: nth($trans, 1); - $vendor-prop-name: prefix($prop-name, $prefix); - $prop-vals: nth($trans, 2); - $prefixed: append($prefixed, $vendor-prop-name $prop-vals, comma); - } - @return $prefixed; -} - -@mixin transitionCustom($values...) { - $transitions: (); - - @each $declaration in $values { - $prop: nth($declaration, 1); - $prop-opts: (); - $length: length($declaration); - - @if $length >= 2 { - @for $i from 2 through $length { - $prop-opts: append($prop-opts, nth($declaration, $i)); - } - } - $trans: $prop, $prop-opts; - $transitions: append($transitions, $trans, comma); - } - -webkit-transition: trans-prefix($transitions, webkit); - -moz-transition: trans-prefix($transitions, moz); - -o-transition: trans-prefix($transitions, o); - transition: $values; -} - -// User select -// For selecting text on the page - -@mixin user-select($select) { - -webkit-user-select: $select; - -moz-user-select: $select; - -ms-user-select: $select; // IE10+ - user-select: $select; -} - -// mouse grab - -@mixin grab-cursor { - cursor: -webkit-grab; - cursor: -moz-grab; - cursor: -o-grab; - cursor: -ms-grab; - cursor: grab; -} - -@mixin grabbing-cursor { - cursor: move; - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - cursor: -o-grabbing; - cursor: -ms-grabbing; - cursor: grabbing; -} diff --git a/vendors/lightgallery/src/sass/lg-pager.scss b/vendors/lightgallery/src/sass/lg-pager.scss deleted file mode 100644 index 5138b5520f..0000000000 --- a/vendors/lightgallery/src/sass/lg-pager.scss +++ /dev/null @@ -1,89 +0,0 @@ -.lg-outer { - .lg-pager-outer { - bottom: 60px; - left: 0; - position: absolute; - right: 0; - text-align: center; - z-index: $zindex-pager; - height: 10px; - - &.lg-pager-hover { - .lg-pager-cont { - overflow: visible; - } - } - } - - .lg-pager-cont { - cursor: pointer; - display: inline-block; - overflow: hidden; - position: relative; - vertical-align: top; - margin: 0 5px; - - &:hover { - .lg-pager-thumb-cont { - opacity: 1; - @include translate3d(0, 0, 0); - } - } - - &.lg-pager-active { - .lg-pager { - box-shadow: 0 0 0 2px white inset; - } - } - } - - .lg-pager-thumb-cont { - background-color: #fff; - color: #FFF; - bottom: 100%; - height: 83px; - left: 0; - margin-bottom: 20px; - margin-left: -60px; - opacity: 0; - padding: 5px; - position: absolute; - width: 120px; - border-radius: 3px; - @include transitionCustom(opacity 0.15s ease 0s, transform 0.15s ease 0s); - @include translate3d(0, 5px, 0); - - img { - width: 100%; - height: 100%; - } - } - - .lg-pager { - background-color: rgba(255, 255, 255, 0.5); - border-radius: 50%; - box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.7) inset; - display: block; - height: 12px; - @include transition(box-shadow 0.3s ease 0s); - width: 12px; - - &:hover, &:focus { - box-shadow: 0 0 0 8px white inset; - } - } - - .lg-caret { - border-left: 10px solid transparent; - border-right: 10px solid transparent; - border-top: 10px dashed; - bottom: -10px; - display: inline-block; - height: 0; - left: 50%; - margin-left: -5px; - position: absolute; - vertical-align: middle; - width: 0; - } -} \ No newline at end of file diff --git a/vendors/lightgallery/src/sass/lg-theme-default.scss b/vendors/lightgallery/src/sass/lg-theme-default.scss deleted file mode 100644 index 4052bea100..0000000000 --- a/vendors/lightgallery/src/sass/lg-theme-default.scss +++ /dev/null @@ -1,206 +0,0 @@ -// default theme -.lg-actions { - .lg-next, .lg-prev { - background-color: $lg-next-prev-bg; - border-radius: $lg-border-radius-base; - color: $lg-next-prev-color; - cursor: pointer; - display: block; - font-size: 22px; - margin-top: -10px; - padding: 8px 10px 9px; - position: absolute; - top: 50%; - z-index: $zindex-controls; - - &.disabled { - pointer-events: none; - opacity: 0.5; - } - - &:hover { - color: $lg-next-prev-hover-color; - } - } - - .lg-next { - right: 20px; - - &:before { - content: "\e095"; - } - } - - .lg-prev { - left: 20px; - - &:after { - content: "\e094"; - } - } -} - -@include keyframes(lg-right-end) { - 0% { - left: 0; - } - - 50% { - left: -30px; - } - - 100% { - left: 0; - } -} - - -@include keyframes(lg-left-end) { - 0% { - left: 0; - } - - 50% { - left: 30px; - } - - 100% { - left: 0; - } -} - - -.lg-outer { - &.lg-right-end { - .lg-object { - @include animation(lg-right-end 0.3s); - position: relative; - } - } - - &.lg-left-end { - .lg-object { - @include animation(lg-left-end 0.3s); - position: relative; - } - } -} - -// lg toolbar -.lg-toolbar { - z-index: $zindex-toolbar; - left: 0; - position: absolute; - top: 0; - width: 100%; - background-color: $lg-toolbar-bg; - - .lg-icon { - color: $lg-toolbar-icon-color; - cursor: pointer; - float: right; - font-size: 24px; - height: 47px; - line-height: 27px; - padding: 10px 0; - text-align: center; - width: 50px; - text-decoration: none !important; - outline: medium none; - @include transition(color 0.2s linear); - - &:hover { - color: $lg-toolbar-icon-hover-color; - } - } - - .lg-close { - &:after { - content: "\e070"; - } - } - - .lg-download { - &:after { - content: "\e0f2"; - } - } -} - -// lightGallery title -.lg-sub-html { - background-color: $lg-sub-html-bg; - bottom: 0; - color: $lg-sub-html-color; - font-size: 16px; - left: 0; - padding: 10px 40px; - position: fixed; - right: 0; - text-align: center; - z-index: $zindex-subhtml; - - h4 { - margin: 0; - font-size: 13px; - font-weight: bold; - } - - p { - font-size: 12px; - margin: 5px 0 0; - } -} - -// lg image counter -#lg-counter { - color: $lg-icon-color; - display: inline-block; - font-size: $lg-counter-font-size; - padding-left: 20px; - padding-top: 12px; - vertical-align: middle; -} - -// for idle state -.lg-toolbar, .lg-prev, .lg-next { - opacity: 1; - @include transitionCustom(transform 0.35s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.35s cubic-bezier(0, 0, 0.25, 1) 0s, color 0.2s linear); -} - -.lg-hide-items { - .lg-prev { - opacity: 0; - @include translate3d(-10px, 0, 0); - } - - .lg-next { - opacity: 0; - @include translate3d(10px, 0, 0); - } - - .lg-toolbar { - opacity: 0; - @include translate3d(0, -10px, 0); - } -} - -// Starting effect -body:not(.lg-from-hash){ - .lg-outer { - &.lg-start-zoom{ - .lg-object{ - @include scale3d(0.5, 0.5, 0.5); - opacity: 0; - @include transitionCustom(transform 250ms cubic-bezier(0, 0, 0.25, 1) 0s, opacity 250ms cubic-bezier(0, 0, 0.25, 1) !important); - @include transform-origin(50% 50%); - } - .lg-item.lg-complete{ - .lg-object{ - @include scale3d(1, 1, 1); - opacity: 1; - } - } - } - } -} \ No newline at end of file diff --git a/vendors/lightgallery/src/sass/lg-thumbnail.scss b/vendors/lightgallery/src/sass/lg-thumbnail.scss deleted file mode 100644 index 5746945986..0000000000 --- a/vendors/lightgallery/src/sass/lg-thumbnail.scss +++ /dev/null @@ -1,111 +0,0 @@ -.lg-outer { - .lg-thumb-outer { - background-color: $lg-thumb-bg; - bottom: 0; - position: absolute; - width: 100%; - z-index: $zindex-thumbnail; - max-height: 350px; - @include translate3d(0, 100%, 0); - @include transitionCustom(transform 0.25s cubic-bezier(0, 0, 0.25, 1) 0s); - - &.lg-grab { - .lg-thumb-item { - @include grab-cursor; - } - } - - &.lg-grabbing { - .lg-thumb-item { - @include grabbing-cursor; - } - } - - &.lg-dragging { - .lg-thumb { - @include transition-duration(0s !important); - } - } - } - &.lg-thumb-open{ - .lg-thumb-outer { - @include translate3d(0, 0%, 0); - } - } - - .lg-thumb { - padding: 10px 0; - height: 100%; - margin-bottom: -5px; - } - - .lg-thumb-item { - border-radius: 5px; - cursor: pointer; - float: left; - overflow: hidden; - height: 100%; - border: 2px solid #FFF; - border-radius: 4px; - margin-bottom: 5px; - @media (min-width: 1025px) { - @include transition(border-color 0.25s ease); - } - - &.active, &:hover { - border-color: $lg-theme-highlight; - } - - img { - width: 100%; - height: 100%; - object-fit: cover; - } - } - - &.lg-has-thumb { - .lg-item { - padding-bottom: 120px; - } - } - - &.lg-can-toggle { - .lg-item { - padding-bottom: 0; - } - } - &.lg-pull-caption-up{ - .lg-sub-html { - @include transition(bottom 0.25s ease); - } - &.lg-thumb-open{ - .lg-sub-html { - bottom: 100px; - } - } - } - - .lg-toogle-thumb { - background-color: $lg-thumb-toggle-bg; - border-radius: $lg-border-radius-base $lg-border-radius-base 0 0; - color: $lg-thumb-toggle-color; - cursor: pointer; - font-size: 24px; - height: 39px; - line-height: 27px; - padding: 5px 0; - position: absolute; - right: 20px; - text-align: center; - top: -39px; - width: 50px; - - &:after { - content: "\e1ff"; - } - - &:hover { - color: $lg-thumb-toggle-hover-color; - } - } -} \ No newline at end of file diff --git a/vendors/lightgallery/src/sass/lg-transitions.scss b/vendors/lightgallery/src/sass/lg-transitions.scss deleted file mode 100644 index f8d9b71fe9..0000000000 --- a/vendors/lightgallery/src/sass/lg-transitions.scss +++ /dev/null @@ -1,766 +0,0 @@ -@import "lg-variables"; -@import "lg-mixins"; - -.lg-css3 { - &.lg-zoom-in { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(1.3, 1.3, 1.3); - } - - &.lg-next-slide { - @include scale3d(1.3, 1.3, 1.3); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-zoom-in-big { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(2, 2, 2); - } - - &.lg-next-slide { - @include scale3d(2, 2, 2); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-zoom-out { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(0.7, 0.7, 0.7); - } - - &.lg-next-slide { - @include scale3d(0.7, 0.7, 0.7); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - &.lg-zoom-out-big { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(0, 0, 0); - } - - &.lg-next-slide { - @include scale3d(0, 0, 0); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-zoom-out-in { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(0, 0, 0); - } - - &.lg-next-slide { - @include scale3d(2, 2, 2); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-zoom-in-out { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(2, 2, 2); - } - - &.lg-next-slide { - @include scale3d(0, 0, 0); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-soft-zoom { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include scale3d(1.1, 1.1, 1.1); - } - - &.lg-next-slide { - @include scale3d(0.9, 0.9, 0.9); - } - - &.lg-current { - @include scale3d(1, 1, 1); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-scale-up { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0.8, 0.8, 0.8) translate3d(0%, 10%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-slide-circular { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(-100%, 0, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(100%, 0, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - // sec - &.lg-slide-circular-up { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(-100%, -100%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(100%, -100%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - // sec - &.lg-slide-circular-down { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(-100%, 100%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(100%, 100%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-slide-circular-vertical { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(0, -100%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(0, 100%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - // sec - &.lg-slide-circular-vertical-left { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(-100%, -100%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(-100%, 100%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - // sec - &.lg-slide-circular-vertical-down { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0, 0, 0) translate3d(100%, -100%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0, 0, 0) translate3d(100%, 100%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 1s ease 0s); - } - } - } - - &.lg-slide-vertical { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include translate3d(0, -100%, 0); - } - - &.lg-next-slide { - @include translate3d(0, 100%, 0); - } - - &.lg-current { - @include translate3d(0, 0, 0); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-vertical-growth { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(0.5, 0.5, 0.5) translate3d(0, -150%, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(0.5, 0.5, 0.5) translate3d(0, 150%, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-only { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(10deg, 0deg)); - } - - &.lg-next-slide { - @include transform(skew(10deg, 0deg)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-only-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(-10deg, 0deg)); - } - - &.lg-next-slide { - @include transform(skew(-10deg, 0deg)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-only-y { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, 10deg)); - } - - &.lg-next-slide { - @include transform(skew(0deg, 10deg)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-only-y-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, -10deg)); - } - - &.lg-next-slide { - @include transform(skew(0deg, -10deg)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(20deg, 0deg) translate3d(-100%, 0%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(20deg, 0deg) translate3d(100%, 0%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(-20deg, 0deg) translate3d(-100%, 0%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(-20deg, 0deg) translate3d(100%, 0%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-cross { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, 60deg) translate3d(-100%, 0%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(0deg, 60deg) translate3d(100%, 0%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-cross-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, -60deg) translate3d(-100%, 0%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(0deg, -60deg) translate3d(100%, 0%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-ver { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(60deg, 0deg) translate3d(0, -100%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(60deg, 0deg) translate3d(0, 100%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-ver-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(-60deg, 0deg) translate3d(0, -100%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(-60deg, 0deg) translate3d(0, 100%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-ver-cross { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, 20deg) translate3d(0, -100%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(0deg, 20deg) translate3d(0, 100%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-slide-skew-ver-cross-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(skew(0deg, -20deg) translate3d(0, -100%, 0px)); - } - - &.lg-next-slide { - @include transform(skew(0deg, -20deg) translate3d(0, 100%, 0px)); - } - - &.lg-current { - @include transform(skew(0deg, 0deg) translate3d(0%, 0%, 0px)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-lollipop { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include translate3d(-100%, 0, 0); - } - - &.lg-next-slide { - @include transform(translate3d(0, 0, 0) scale(0.5)); - } - - &.lg-current { - @include translate3d(0, 0, 0); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-lollipop-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(translate3d(0, 0, 0) scale(0.5)); - } - - &.lg-next-slide { - @include translate3d(100%, 0, 0); - } - - &.lg-current { - @include translate3d(0, 0, 0); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-rotate { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(rotate(-360deg)); - } - - &.lg-next-slide { - @include transform(rotate(360deg)); - } - - &.lg-current { - @include transform(rotate(0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-rotate-rev { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(rotate(360deg)); - } - - &.lg-next-slide { - @include transform(rotate(-360deg)); - } - - &.lg-current { - @include transform(rotate(0deg)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-tube { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include transform(scale3d(1, 0, 1) translate3d(-100%, 0, 0)); - } - - &.lg-next-slide { - @include transform(scale3d(1, 0, 1) translate3d(100%, 0, 0)); - } - - &.lg-current { - @include transform(scale3d(1, 1, 1) translate3d(0, 0, 0)); - opacity: 1; - } - - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } -} \ No newline at end of file diff --git a/vendors/lightgallery/src/sass/lg-variables.scss b/vendors/lightgallery/src/sass/lg-variables.scss deleted file mode 100644 index bfa8ba6cab..0000000000 --- a/vendors/lightgallery/src/sass/lg-variables.scss +++ /dev/null @@ -1,57 +0,0 @@ -$backdrop-opacity: 1 !default; -$lg-toolbar-bg: rgba(0, 0, 0, 0.45) !default; -$lg-border-radius-base: 2px !default; -$lg-theme-highlight: rgb(169, 7, 7) !default; -$lg-theme: #0D0A0A !default; - -// basic icon colours -$lg-icon-bg: rgba(0, 0, 0, 0.45) !default; -$lg-icon-color: #999 !default; -$lg-icon-hover-color: #FFF !default; - -// counter -$lg-counter-color: #e6e6e6 !default; -$lg-counter-font-size: 16px !default; - -// Next prev icons -$lg-next-prev-bg: $lg-icon-bg !default; -$lg-next-prev-color: $lg-icon-color !default; -$lg-next-prev-hover-color: $lg-icon-hover-color !default; - -// toolbar icons -$lg-toolbar-icon-color: $lg-icon-color !default; -$lg-toolbar-icon-hover-color: $lg-icon-hover-color !default; - -// autoplay progress bar -$lg-progress-bar-bg: #333 !default; -$lg-progress-bar-active-bg: $lg-theme-highlight !default; -$lg-progress-bar-height: 5px !default; - -// paths -$lg-path-fonts: '../fonts'!default; -$lg-path-images: '../img'!default; - -// Zoom plugin -$zoom-transition-duration: 0.3s !default; - -// Sub html - titile -$lg-sub-html-bg: rgba(0, 0, 0, 0.45) !default; -$lg-sub-html-color: #EEE !default; - -// thumbnail toggle button -$lg-thumb-toggle-bg: #0D0A0A !default; -$lg-thumb-toggle-color: $lg-icon-color !default; -$lg-thumb-toggle-hover-color: $lg-icon-hover-color !default; -$lg-thumb-bg: #0D0A0A !default; - -// z-index -$zindex-outer: 1050 !default; -$zindex-progressbar: 1083 !default; -$zindex-controls: 1080 !default; -$zindex-toolbar: 1082 !default; -$zindex-subhtml: 1080 !default; -$zindex-thumbnail: 1080 !default; -$zindex-pager: 1080 !default; -$zindex-playbutton: 1080 !default; -$zindex-item: 1060 !default; -$zindex-backdrop: 1040 !default; diff --git a/vendors/lightgallery/src/sass/lg-video.scss b/vendors/lightgallery/src/sass/lg-video.scss deleted file mode 100644 index 48e68e2eb1..0000000000 --- a/vendors/lightgallery/src/sass/lg-video.scss +++ /dev/null @@ -1,103 +0,0 @@ -.lg-outer { - .lg-video-cont { - display: inline-block; - vertical-align: middle; - max-width: 1140px; - max-height: 100%; - width: 100%; - padding: 0 5px; - } - - .lg-video { - width: 100%; - height: 0; - padding-bottom: 56.25%; - overflow: hidden; - position: relative; - - .lg-object { - display: inline-block; - position: absolute; - top: 0; - left: 0; - width: 100% !important; - height: 100% !important; - } - - .lg-video-play { - width: 84px; - height: 59px; - position: absolute; - left: 50%; - top: 50%; - margin-left: -42px; - margin-top: -30px; - z-index: $zindex-playbutton; - cursor: pointer; - } - } - - .lg-has-vimeo{ - .lg-video-play{ - background: url("#{$lg-path-images}/vimeo-play.png") no-repeat scroll 0 0 transparent; - } - &:hover{ - .lg-video-play{ - background: url("#{$lg-path-images}/vimeo-play.png") no-repeat scroll 0 -58px transparent; - } - - } - } - - .lg-has-html5{ - .lg-video-play{ - background: transparent url("#{$lg-path-images}/video-play.png") no-repeat scroll 0 0; - height: 64px; - margin-left: -32px; - margin-top: -32px; - width: 64px; - opacity: 0.8; - } - &:hover{ - .lg-video-play{ - opacity: 1 - } - - } - } - - .lg-has-youtube{ - .lg-video-play{ - background: url("#{$lg-path-images}/youtube-play.png") no-repeat scroll 0 0 transparent; - } - &:hover{ - .lg-video-play{ - background: url("#{$lg-path-images}/youtube-play.png") no-repeat scroll 0 -60px transparent; - } - - } - } - .lg-video-object { - width: 100% !important; - height: 100% !important; - position: absolute; - top: 0; - left: 0; - } - - .lg-has-video { - .lg-video-object { - visibility: hidden; - } - - &.lg-video-playing { - .lg-object, .lg-video-play { - display: none; - } - - .lg-video-object { - visibility: visible; - } - } - } -} \ No newline at end of file diff --git a/vendors/lightgallery/src/sass/lg-zoom.scss b/vendors/lightgallery/src/sass/lg-zoom.scss deleted file mode 100644 index 3d5d1fce64..0000000000 --- a/vendors/lightgallery/src/sass/lg-zoom.scss +++ /dev/null @@ -1,56 +0,0 @@ -.lg-outer { - // reset transition duration - &.lg-css3.lg-zoom-dragging { - .lg-item.lg-complete.lg-zoomable { - .lg-img-wrap, .lg-image { - @include transition-duration(0s); - } - } - } - - .lg-item.lg-complete.lg-zoomable{ - - .lg-img-wrap { - @include transitionCustom(left $zoom-transition-duration cubic-bezier(0, 0, 0.25, 1) 0s, top $zoom-transition-duration cubic-bezier(0, 0, 0.25, 1) 0s); - @include translate3d(0, 0, 0); - @include backface-visibility(hidden); - } - - .lg-image { - // Translate required for zoom - @include scale3d(1, 1, 1); - @include transitionCustom(transform $zoom-transition-duration cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.15s !important); - @include transform-origin(0 0); - @include backface-visibility(hidden); - } - } - -} - -// zoom buttons -#lg-zoom-in { - &:after { - content: "\e311"; - } -} - -#lg-actual-size { - font-size: 20px; - &:after { - content: "\e033"; - } -} - -#lg-zoom-out { - opacity: 0.5; - pointer-events: none; - - &:after { - content: "\e312"; - } - - .lg-zoomed & { - opacity: 1; - pointer-events: auto; - } -} \ No newline at end of file diff --git a/vendors/lightgallery/src/sass/lightgallery.scss b/vendors/lightgallery/src/sass/lightgallery.scss deleted file mode 100644 index 94ad014faa..0000000000 --- a/vendors/lightgallery/src/sass/lightgallery.scss +++ /dev/null @@ -1,290 +0,0 @@ -// Core variables and mixins -@import "lg-variables"; -@import "lg-mixins"; -@import "lg-fonts"; -@import "lg-theme-default"; -@import "lg-thumbnail"; -@import "lg-video"; -@import "lg-autoplay"; -@import "lg-zoom"; -@import "lg-pager"; -@import "lg-fullscreen"; - -// Clearfix -.group { - *zoom: 1; -} - -.group:before, .group:after { - display: table; - content: ""; - line-height: 0; -} - -.group:after { - clear: both; -} - -// lightgallery core -.lg-outer { - width: 100%; - height: 100%; - position: fixed; - top: 0; - left: 0; - z-index: $zindex-outer; - opacity: 0; - // For start/end transition - @include transition(opacity 0.15s ease 0s); - - * { - @include box-sizing(border-box); - } - - &.lg-visible { - opacity: 1; - } - - // Set transition speed and timing function - &.lg-css3 { - .lg-item { - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transition-duration(inherit !important); - @include transition-timing-function(inherit !important); - } - } - } - - // Remove transition while dragging - &.lg-css3.lg-dragging { - .lg-item { - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transition-duration(0s !important); - opacity: 1; - } - } - } - - // Set cursor grab while dragging - &.lg-grab { - img.lg-object { - @include grab-cursor; - } - } - - &.lg-grabbing { - img.lg-object { - @include grabbing-cursor; - } - } - - .lg { - height: 100%; - width: 100%; - position: relative; - overflow: hidden; - margin-left: auto; - margin-right: auto; - max-width: 100%; - max-height: 100%; - } - - .lg-inner { - width: 100%; - height: 100%; - position: absolute; - left: 0; - top: 0; - white-space: nowrap; - } - - .lg-item { - background: url("#{$lg-path-images}/loading.gif") no-repeat scroll center center transparent; - display: none !important; - } - &.lg-css3{ - .lg-prev-slide, .lg-current, .lg-next-slide{ - display: inline-block !important; - } - } - &.lg-css{ - .lg-current{ - display: inline-block !important; - } - } - - .lg-item, .lg-img-wrap { - display: inline-block; - text-align: center; - position: absolute; - width: 100%; - height: 100%; - - &:before { - content: ""; - display: inline-block; - height: 50%; - width: 1px; - margin-right: -1px; - } - } - - .lg-img-wrap { - position: absolute; - padding: 0 5px; - left: 0; - right: 0; - top: 0; - bottom: 0 - } - - .lg-item { - &.lg-complete { - background-image: none; - } - - &.lg-current { - z-index: $zindex-item; - } - } - - .lg-image { - display: inline-block; - vertical-align: middle; - max-width: 100%; - max-height: 100%; - width: auto !important; - height: auto !important; - } - - &.lg-show-after-load { - .lg-item { - .lg-object, .lg-video-play { - opacity: 0; - @include transition(opacity 0.15s ease 0s); - } - - &.lg-complete { - .lg-object, .lg-video-play { - opacity: 1; - } - } - } - } - - // Hide title div if empty - .lg-empty-html { - display: none; - } - - &.lg-hide-download{ - #lg-download{ - display: none; - } - } -} -.lg-backdrop{ - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: $zindex-backdrop; - background-color: #000; - opacity: 0; - @include transition(opacity 0.15s ease 0s); - &.in{ - opacity: $backdrop-opacity; - } -} - -// Default slide animations. Should be placed at the bottom of the animation css -.lg-css3 { - - // Remove all transition effects - &.lg-no-trans { - .lg-prev-slide, .lg-next-slide, .lg-current { - @include transitionCustom(none 0s ease 0s !important); - } - } - - &.lg-use-css3 { - .lg-item { - @include backface-visibility(hidden); - } - } - - &.lg-use-left { - .lg-item { - @include backface-visibility(hidden); - } - } - - // Fade mode - &.lg-fade { - .lg-item { - opacity: 0; - - &.lg-current { - opacity: 1; - } - - // transition timing property and duration will be over written from javascript - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(opacity 0.1s ease 0s); - } - } - } - - &.lg-slide { - &.lg-use-css3 { - .lg-item { - opacity: 0; - - &.lg-prev-slide { - @include translate3d(-100%, 0, 0); - } - - &.lg-next-slide { - @include translate3d(100%, 0, 0); - } - - &.lg-current { - @include translate3d(0, 0, 0); - opacity: 1; - } - - // transition timing property and duration will be over written from javascript - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(transform 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - - &.lg-use-left { - .lg-item { - opacity: 0; - position: absolute; - left: 0; - - &.lg-prev-slide { - left: -100%; - } - - &.lg-next-slide { - left: 100%; - } - - &.lg-current { - left: 0; - opacity: 1; - } - - // transition timing property and duration will be over written from javascript - &.lg-prev-slide, &.lg-next-slide, &.lg-current { - @include transitionCustom(left 1s cubic-bezier(0, 0, 0.25, 1) 0s, opacity 0.1s ease 0s); - } - } - } - } -} \ No newline at end of file diff --git a/vendors/lightgallery/src/sass/prepros.cfg b/vendors/lightgallery/src/sass/prepros.cfg deleted file mode 100644 index efca44505e..0000000000 --- a/vendors/lightgallery/src/sass/prepros.cfg +++ /dev/null @@ -1,490 +0,0 @@ -[ - { - "About This File": "This is Prepros config file, https://prepros.io . Please do not edit this file, doing so can crash Prepros." - }, - { - "data": { - "id": "", - "cfgVersion": 2, - "name": "LG", - "path": "", - "files": { - "8ff90eb8": { - "id": "8ff90eb8", - "path": "lg-animations.scss", - "output": "lg-animations.css", - "name": "lg-animations.scss", - "category": "CSS", - "autoCompile": true, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": false, - "parents": [], - "type": "sass" - }, - "26f18653": { - "id": "26f18653", - "path": "lg-autoplay.scss", - "output": "lg-autoplay.css", - "name": "lg-autoplay.scss", - "category": "CSS", - "autoCompile": false, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": true, - "parents": [ - "f80b85a1" - ], - "type": "sass" - }, - "94edd1e7": { - "id": "94edd1e7", - "path": "lg-fb-comment-box.scss", - "output": "lg-fb-comment-box.css", - "name": "lg-fb-comment-box.scss", - "category": "CSS", - "autoCompile": true, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": false, - "parents": [], - "type": "sass" - }, - "fb5b0eff": { - "id": "fb5b0eff", - "path": "lg-fonts.scss", - "output": "lg-fonts.css", - "name": "lg-fonts.scss", - "category": "CSS", - "autoCompile": false, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": true, - "parents": [ - "f80b85a1" - ], - "type": "sass" - }, - "9e150360": { - "id": "9e150360", - "path": "lg-fullscreen.scss", - "output": "lg-fullscreen.css", - "name": "lg-fullscreen.scss", - "category": "CSS", - "autoCompile": false, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": true, - "parents": [ - "f80b85a1" - ], - "type": "sass" - }, - "11b5aed8": { - "id": "11b5aed8", - "path": "lg-mixins.scss", - "output": "lg-mixins.css", - "name": "lg-mixins.scss", - "category": "CSS", - "autoCompile": false, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": true, - "parents": [ - "94edd1e7", - "6018a35b", - "f80b85a1" - ], - "type": "sass" - }, - "06548c3d": { - "id": "06548c3d", - "path": "lg-pager.scss", - "output": "lg-pager.css", - "name": "lg-pager.scss", - "category": "CSS", - "autoCompile": false, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": true, - "parents": [ - "f80b85a1" - ], - "type": "sass" - }, - "7dbddfad": { - "id": "7dbddfad", - "path": "lg-theme-default.scss", - "output": "lg-theme-default.css", - "name": "lg-theme-default.scss", - "category": "CSS", - "autoCompile": false, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": true, - "parents": [ - "f80b85a1" - ], - "type": "sass" - }, - "ccc7c5d4": { - "id": "ccc7c5d4", - "path": "lg-thumbnail.scss", - "output": "lg-thumbnail.css", - "name": "lg-thumbnail.scss", - "category": "CSS", - "autoCompile": false, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": true, - "parents": [ - "f80b85a1" - ], - "type": "sass" - }, - "6018a35b": { - "id": "6018a35b", - "path": "lg-transitions.scss", - "output": "lg-transitions.css", - "name": "lg-transitions.scss", - "category": "CSS", - "autoCompile": true, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": false, - "parents": [], - "type": "sass" - }, - "36a5e0f7": { - "id": "36a5e0f7", - "path": "lg-variables.scss", - "output": "lg-variables.css", - "name": "lg-variables.scss", - "category": "CSS", - "autoCompile": false, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": true, - "parents": [ - "94edd1e7", - "6018a35b", - "f80b85a1" - ], - "type": "sass" - }, - "70ef8df4": { - "id": "70ef8df4", - "path": "lg-video.scss", - "output": "lg-video.css", - "name": "lg-video.scss", - "category": "CSS", - "autoCompile": false, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": true, - "parents": [ - "f80b85a1" - ], - "type": "sass" - }, - "0690cbf9": { - "id": "0690cbf9", - "path": "lg-zoom.scss", - "output": "lg-zoom.css", - "name": "lg-zoom.scss", - "category": "CSS", - "autoCompile": false, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": false, - "imported": true, - "parents": [ - "f80b85a1" - ], - "type": "sass" - }, - "f80b85a1": { - "id": "f80b85a1", - "path": "lightgallery.scss", - "output": "C:/wamp/www/lightgallery-github/lightGallery/lightgallery/css/lightgallery.css", - "name": "lightgallery.scss", - "category": "CSS", - "autoCompile": true, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": true, - "fullCompass": false, - "outputStyle": "expanded", - "customOutput": true, - "imported": false, - "parents": [], - "type": "sass" - } - }, - "deploymentHistory": {}, - "config": { - "watch": "", - "liveRefresh": true, - "useCustomServer": false, - "port": 0, - "useCustomPort": false, - "customServerUrl": "", - "watchedFileExtensions": "less, sass, scss, styl, md, markdown, coffee, js, jade, haml, slim, ls, html,htm, css, rb, php, asp, aspx, cfm, chm, cms, do, erb, jsp, mhtml, mspx, pl, py, shtml, cshtml, cs,vb, vbs, tpl, ctp, kit, png, jpg, jpeg", - "excludePatterns": "Prepros Build, node_modules, .git, .idea, .sass-cache, .hg, .svn, .cache, config.rb, prepros.cfg, .DS_Store, bower_components", - "autoprefixerBrowsers": "last 4 versions", - "liveRefreshDelay": 0, - "disableImportAutoCompile": true, - "browserFlow": { - "enabled": false, - "mouseSync": true, - "scrollSync": true, - "keyboardSync": true, - "animateCss": true - }, - "deployment": { - "ftpHost": "", - "ftpPort": "21", - "ftpUsePrivateKey": false, - "ftpPrivateKey": "", - "ftpRemotePath": "", - "ftpUserName": "", - "ftpPassword": "", - "ftpType": "FTP", - "ftpSecure": false, - "ftpUploadOnModify": false, - "ftpRefreshAfterUpload": false, - "ignorePreprocessableFiles": true, - "copyPath": "Prepros Build", - "excludePatterns": ".map, Prepros Build, config.rb, prepros.cfg, node_modules, .git, .idea, .sass-cache, .hg, .svn, .cache, .DS_Store, bower_components" - }, - "css": { - "path": "css/", - "outputType": "REPLACE_SEGMENT", - "segmentToReplace": "less, sass, stylus, scss, styl", - "segmentToReplaceWith": "css", - "preprocessableFilesDirs": "less/\nsass/\nstylus/\nscss/\nstyl/\n" - }, - "minCss": { - "path": "", - "outputType": "RELATIVE_FILEDIR", - "segmentToReplace": "", - "segmentToReplaceWith": "", - "types": "", - "preprocessableFilesDirs": "", - "filePrefix": "-dist" - }, - "html": { - "segmentToReplace": "jade, haml, slim, markdown, md, kit", - "segmentToReplaceWith": "html", - "path": "html/", - "extension": ".html", - "outputType": "REPLACE_SEGMENT", - "preprocessableFilesDirs": "jade/\nhaml/\nslim/\nmarkdown/\nmd/\nkit" - }, - "js": { - "segmentToReplace": "coffee, coffeescript, coffeescripts, ls, livescript, livescripts", - "segmentToReplaceWith": "html", - "extension": ".html", - "outputType": "REPLACE_SEGMENT", - "preprocessableFilesDirs": "coffee/\ncoffeescript/\ncoffeescripts/\nls/\nlivescript/\nlivescripts", - "path": "js/" - }, - "minJs": { - "path": "", - "outputType": "RELATIVE_FILEDIR", - "segmentToReplace": "", - "segmentToReplaceWith": "", - "types": "", - "preprocessableFilesDirs": "", - "filePrefix": "-dist" - }, - "compilers": { - "less": { - "autoCompile": true, - "autoprefixer": false, - "compress": false, - "sourceMaps": false - }, - "sass": { - "autoCompile": true, - "autoprefixer": false, - "sourceMaps": false, - "libSass": true, - "compass": false, - "fullCompass": false, - "outputStyle": "expanded" - }, - "stylus": { - "autoCompile": true, - "sourceMaps": false, - "autoprefixer": false, - "nib": false, - "compress": false - }, - "markdown": { - "autoCompile": true, - "sanitize": false, - "gfm": true, - "wrapWithHtml": false - }, - "coffee": { - "autoCompile": true, - "bare": false, - "uglify": false, - "mangle": true, - "iced": false, - "sourceMaps": false - }, - "livescript": { - "autoCompile": true, - "bare": false, - "uglify": false, - "mangle": true - }, - "javascript": { - "autoCompile": false, - "uglify": true, - "mangle": true, - "babel": false, - "sourceMaps": false - }, - "jade": { - "autoCompile": true, - "pretty": true - }, - "haml": { - "autoCompile": true, - "pretty": true, - "doubleQuotes": false - }, - "kit": { - "autoCompile": true, - "minifyHtml": false - }, - "slim": { - "autoCompile": true, - "pretty": true, - "indent": "default" - }, - "css": { - "autoCompile": false, - "sourceMaps": false, - "compress": true, - "cssnext": false, - "autoprefixer": false - }, - "uglify": { - "compress": { - "sequences": true, - "properties": true, - "dead_code": true, - "drop_debugger": true, - "unsafe": false, - "unsafe_comps": false, - "conditionals": true, - "comparisons": true, - "evaluate": true, - "booleans": true, - "loops": true, - "unused": true, - "hoist_funs": true, - "keep_fargs": false, - "hoist_vars": false, - "if_return": true, - "join_vars": true, - "cascade": true, - "side_effects": true, - "pure_getters": false, - "negate_iife": true, - "screw_ie8": false, - "drop_console": false, - "angular": false, - "warnings": true, - "pure_funcs": null, - "global_defs": null - }, - "output": { - "quote_keys": false, - "space_colon": true, - "ascii_only": false, - "unescape_regexps": false, - "inline_script": false, - "beautify": false, - "bracketize": false, - "semicolons": true, - "comments": false, - "preserve_line": false, - "screw_ie8": false, - "preamble": null - } - } - } - } - } - } -] \ No newline at end of file diff --git a/vendors/marked/marked.js b/vendors/marked/marked.js new file mode 100644 index 0000000000..46ed213024 --- /dev/null +++ b/vendors/marked/marked.js @@ -0,0 +1,2483 @@ +/*eslint max-len: "off"*/ +/*eslint no-control-regex: "off"*/ +/** + * marked v14.0.0 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + * + * modified for SnappyMail + */ + +const marked = (() => { + + /** + * Gets the original marked default options. + */ + const _getDefaults = () => ({ + async: false, + breaks: false, + extensions: null, + gfm: true, + hooks: null, + pedantic: false, + renderer: null, + silent: false, + tokenizer: null, + walkTokens: null, + }); + var exports_defaults = _getDefaults(); + + /** + * Helpers + */ + const escapeTest = /[&<>"']/; + const escapeReplace = new RegExp(escapeTest.source, 'g'); + const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/; + const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g'); + const escapeReplacements = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + const getEscapeReplacement = (ch) => escapeReplacements[ch]; + const escape$1 = (html, encode) => { + if (encode) { + if (escapeTest.test(html)) { + return html.replace(escapeReplace, getEscapeReplacement); + } + } + else { + if (escapeTestNoEncode.test(html)) { + return html.replace(escapeReplaceNoEncode, getEscapeReplacement); + } + } + return html; + }; + const caret = /(^|[^[])\^/g; + const edit = (regex, opt) => { + let source = typeof regex === 'string' ? regex : regex.source; + opt = opt || ''; + const obj = { + replace: (name, val) => { + let valSource = typeof val === 'string' ? val : val.source; + valSource = valSource.replace(caret, '$1'); + source = source.replace(name, valSource); + return obj; + }, + getRegex: () => { + return new RegExp(source, opt); + }, + }; + return obj; + }; + const cleanUrl = (href) => { + try { + href = encodeURI(href).replace(/%25/g, '%'); + } + catch { + return null; + } + return href; + }; + const noopTest = { exec: () => null }; + const splitCells = (tableRow, count) => { + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + const row = tableRow.replace(/\|/g, (match, offset, str) => { + let escaped = false; + let curr = offset; + while (--curr >= 0 && str[curr] === '\\') + escaped = !escaped; + if (escaped) { + // odd number of slashes means | is escaped + // so we leave it alone + return '|'; + } + else { + // add space before unescaped | + return ' |'; + } + }), cells = row.split(/ \|/); + let i = 0; + // First/last cell in a row cannot be empty if it has no leading/trailing pipe + if (!cells[0].trim()) { + cells.shift(); + } + if (cells.length > 0 && !cells[cells.length - 1].trim()) { + cells.pop(); + } + if (count) { + if (cells.length > count) { + cells.splice(count); + } + else { + while (cells.length < count) + cells.push(''); + } + } + for (; i < cells.length; i++) { + // leading or trailing whitespace is ignored per the gfm spec + cells[i] = cells[i].trim().replace(/\\\|/g, '|'); + } + return cells; + }; + /** + * Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). + * /c*$/ is vulnerable to REDOS. + * + * @param str + * @param c + * @param invert Remove suffix of non-c chars instead. Default falsey. + */ + const rtrim = (str, c, invert) => { + const l = str.length; + if (l === 0) { + return ''; + } + // Length of suffix matching the invert condition. + let suffLen = 0; + // Step left until we fail to match the invert condition. + while (suffLen < l) { + const currChar = str.charAt(l - suffLen - 1); + if (currChar === c && !invert) { + suffLen++; + } + else if (currChar !== c && invert) { + suffLen++; + } + else { + break; + } + } + return str.slice(0, l - suffLen); + }; + const findClosingBracket = (str, b) => { + if (str.indexOf(b[1]) === -1) { + return -1; + } + let level = 0; + for (let i = 0; i < str.length; i++) { + if (str[i] === '\\') { + i++; + } + else if (str[i] === b[0]) { + level++; + } + else if (str[i] === b[1]) { + level--; + if (level < 0) { + return i; + } + } + } + return -1; + }; + + const outputLink = (cap, link, raw, lexer) => { + const href = link.href; + const title = link.title ? escape$1(link.title) : null; + const text = cap[1].replace(/\\([[\]])/g, '$1'); + if (cap[0].charAt(0) !== '!') { + lexer.state.inLink = true; + const token = { + type: 'link', + raw, + href, + title, + text, + tokens: lexer.inlineTokens(text), + }; + lexer.state.inLink = false; + return token; + } + return { + type: 'image', + raw, + href, + title, + text: escape$1(text), + }; + }; + const indentCodeCompensation = (raw, text) => { + const matchIndentToCode = raw.match(/^(\s+)(?:```)/); + if (matchIndentToCode === null) { + return text; + } + const indentToCode = matchIndentToCode[1]; + return text + .split('\n') + .map(node => { + const matchIndentInNode = node.match(/^\s+/); + if (matchIndentInNode === null) { + return node; + } + const [indentInNode] = matchIndentInNode; + if (indentInNode.length >= indentToCode.length) { + return node.slice(indentToCode.length); + } + return node; + }) + .join('\n'); + }; + + /** + * Tokenizer + */ + class _Tokenizer { + constructor(options) { + this.options = options || exports_defaults; +// this.rules; // set by the lexer +// this.lexer; // set by the lexer + } + space(src) { + const cap = this.rules.block.newline.exec(src); + if (cap && cap[0].length > 0) { + return { + type: 'space', + raw: cap[0], + }; + } + } + code(src) { + const cap = this.rules.block.code.exec(src); + if (cap) { + const text = cap[0].replace(/^ {1,4}/gm, ''); + return { + type: 'code', + raw: cap[0], + codeBlockStyle: 'indented', + text: !this.options.pedantic + ? rtrim(text, '\n') + : text, + }; + } + } + fences(src) { + const cap = this.rules.block.fences.exec(src); + if (cap) { + const raw = cap[0]; + const text = indentCodeCompensation(raw, cap[3] || ''); + return { + type: 'code', + raw, + lang: cap[2] ? cap[2].trim().replace(this.rules.inline.anyPunctuation, '$1') : cap[2], + text, + }; + } + } + heading(src) { + const cap = this.rules.block.heading.exec(src); + if (cap) { + let text = cap[2].trim(); + // remove trailing #s + if (/#$/.test(text)) { + const trimmed = rtrim(text, '#'); + if (this.options.pedantic) { + text = trimmed.trim(); + } + else if (!trimmed || / $/.test(trimmed)) { + // CommonMark requires space before trailing #s + text = trimmed.trim(); + } + } + return { + type: 'heading', + raw: cap[0], + depth: cap[1].length, + text, + tokens: this.lexer.inline(text), + }; + } + } + hr(src) { + const cap = this.rules.block.hr.exec(src); + if (cap) { + return { + type: 'hr', + raw: rtrim(cap[0], '\n'), + }; + } + } + blockquote(src) { + const cap = this.rules.block.blockquote.exec(src); + if (cap) { + let lines = rtrim(cap[0], '\n').split('\n'); + let raw = ''; + let text = ''; + const tokens = []; + while (lines.length > 0) { + let inBlockquote = false; + const currentLines = []; + let i; + for (i = 0; i < lines.length; i++) { + // get lines up to a continuation + if (/^ {0,3}>/.test(lines[i])) { + currentLines.push(lines[i]); + inBlockquote = true; + } + else if (!inBlockquote) { + currentLines.push(lines[i]); + } + else { + break; + } + } + lines = lines.slice(i); + const currentRaw = currentLines.join('\n'); + const currentText = currentRaw + // precede setext continuation with 4 spaces so it isn't a setext + .replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g, '\n $1') + .replace(/^ {0,3}>[ \t]?/gm, ''); + raw = raw ? `${raw}\n${currentRaw}` : currentRaw; + text = text ? `${text}\n${currentText}` : currentText; + // parse blockquote lines as top level tokens + // merge paragraphs if this is a continuation + const top = this.lexer.state.top; + this.lexer.state.top = true; + this.lexer.blockTokens(currentText, tokens, true); + this.lexer.state.top = top; + // if there is no continuation then we are done + if (lines.length === 0) { + break; + } + const lastToken = tokens[tokens.length - 1]; + if (lastToken?.type === 'code') { + // blockquote continuation cannot be preceded by a code block + break; + } + else if (lastToken?.type === 'blockquote') { + // include continuation in nested blockquote + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.blockquote(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - oldToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.text.length) + newToken.text; + break; + } + else if (lastToken?.type === 'list') { + // include continuation in nested list + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.list(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - lastToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.raw.length) + newToken.raw; + lines = newText.substring(tokens[tokens.length - 1].raw.length).split('\n'); + continue; + } + } + return { + type: 'blockquote', + raw, + tokens, + text, + }; + } + } + list(src) { + let cap = this.rules.block.list.exec(src); + if (cap) { + let bull = cap[1].trim(); + const isordered = bull.length > 1; + const list = { + type: 'list', + raw: '', + ordered: isordered, + start: isordered ? +bull.slice(0, -1) : '', + loose: false, + items: [], + }; + bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`; + if (this.options.pedantic) { + bull = isordered ? bull : '[*+-]'; + } + // Get next list item + const itemRegex = new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`); + let endsWithBlankLine = false; + // Check if current bullet point can start a new List Item + while (src) { + let endEarly = false; + let raw = ''; + let itemContents = ''; + if (!(cap = itemRegex.exec(src))) { + break; + } + if (this.rules.block.hr.test(src)) { // End list if bullet was actually HR (possibly move into itemRegex?) + break; + } + raw = cap[0]; + src = src.substring(raw.length); + let line = cap[2].split('\n', 1)[0].replace(/^\t+/, (t) => ' '.repeat(3 * t.length)); + let nextLine = src.split('\n', 1)[0]; + let blankLine = !line.trim(); + let indent = 0; + if (this.options.pedantic) { + indent = 2; + itemContents = line.trimStart(); + } + else if (blankLine) { + indent = cap[1].length + 1; + } + else { + indent = cap[2].search(/[^ ]/); // Find first non-space char + indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent + itemContents = line.slice(indent); + indent += cap[1].length; + } + if (blankLine && /^ *$/.test(nextLine)) { // Items begin with at most one blank line + raw += nextLine + '\n'; + src = src.substring(nextLine.length + 1); + endEarly = true; + } + if (!endEarly) { + const nextBulletRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`); + const hrRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`); + const fencesBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`); //` + const headingBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`); + // Check if following lines should be included in List Item + while (src) { + const rawLine = src.split('\n', 1)[0]; + nextLine = rawLine; + // Re-align to follow commonmark nesting rules + if (this.options.pedantic) { + nextLine = nextLine.replace(/^ {1,4}(?=( {4})*[^ ])/g, ' '); + } + // End list item if found code fences + if (fencesBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new heading + if (headingBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new bullet + if (nextBulletRegex.test(nextLine)) { + break; + } + // Horizontal rule found + if (hrRegex.test(src)) { + break; + } + if (nextLine.search(/[^ ]/) >= indent || !nextLine.trim()) { // Dedent if possible + itemContents += '\n' + nextLine.slice(indent); + } + else { + // not enough indentation + if (blankLine) { + break; + } + // paragraph continuation unless last line was a different block level element + if (line.search(/[^ ]/) >= 4) { // indented code block + break; + } + if (fencesBeginRegex.test(line)) { + break; + } + if (headingBeginRegex.test(line)) { + break; + } + if (hrRegex.test(line)) { + break; + } + itemContents += '\n' + nextLine; + } + if (!blankLine && !nextLine.trim()) { // Check if current line is blank + blankLine = true; + } + raw += rawLine + '\n'; + src = src.substring(rawLine.length + 1); + line = nextLine.slice(indent); + } + } + if (!list.loose) { + // If the previous item ended with a blank line, the list is loose + if (endsWithBlankLine) { + list.loose = true; + } + else if (/\n *\n *$/.test(raw)) { + endsWithBlankLine = true; + } + } + let istask = null; + let ischecked; + // Check for task list items + if (this.options.gfm) { + istask = /^\[[ xX]\] /.exec(itemContents); + if (istask) { + ischecked = istask[0] !== '[ ] '; + itemContents = itemContents.replace(/^\[[ xX]\] +/, ''); + } + } + list.items.push({ + type: 'list_item', + raw, + task: !!istask, + checked: ischecked, + loose: false, + text: itemContents, + tokens: [], + }); + list.raw += raw; + } + // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic + list.items[list.items.length - 1].raw = list.items[list.items.length - 1].raw.trimEnd(); + list.items[list.items.length - 1].text = list.items[list.items.length - 1].text.trimEnd(); + list.raw = list.raw.trimEnd(); + // Item child tokens handled here at end because we needed to have the final item to trim it first + for (let i = 0; i < list.items.length; i++) { + this.lexer.state.top = false; + list.items[i].tokens = this.lexer.blockTokens(list.items[i].text, []); + if (!list.loose) { + // Check if list should be loose + const spacers = list.items[i].tokens.filter(t => t.type === 'space'); + const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => /\n.*\n/.test(t.raw)); + list.loose = hasMultipleLineBreaks; + } + } + // Set all items to loose if list is loose + if (list.loose) { + for (let i = 0; i < list.items.length; i++) { + list.items[i].loose = true; + } + } + return list; + } + } + html(src) { + const cap = this.rules.block.html.exec(src); + if (cap) { + const token = { + type: 'html', + block: true, + raw: cap[0], + pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', + text: cap[0], + }; + return token; + } + } + def(src) { + const cap = this.rules.block.def.exec(src); + if (cap) { + const tag = cap[1].toLowerCase().replace(/\s+/g, ' '); + const href = cap[2] ? cap[2].replace(/^<(.*)>$/, '$1').replace(this.rules.inline.anyPunctuation, '$1') : ''; + const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline.anyPunctuation, '$1') : cap[3]; + return { + type: 'def', + tag, + raw: cap[0], + href, + title, + }; + } + } + table(src) { + const cap = this.rules.block.table.exec(src); + if (!cap) { + return; + } + if (!/[:|]/.test(cap[2])) { + // delimiter row must have a pipe (|) or colon (:) otherwise it is a setext heading + return; + } + const headers = splitCells(cap[1]); + const aligns = cap[2].replace(/^\||\| *$/g, '').split('|'); + const rows = cap[3] && cap[3].trim() ? cap[3].replace(/\n[ \t]*$/, '').split('\n') : []; + const item = { + type: 'table', + raw: cap[0], + header: [], + align: [], + rows: [], + }; + if (headers.length !== aligns.length) { + // header and align columns must be equal, rows can be different. + return; + } + for (const align of aligns) { + if (/^ *-+: *$/.test(align)) { + item.align.push('right'); + } + else if (/^ *:-+: *$/.test(align)) { + item.align.push('center'); + } + else if (/^ *:-+ *$/.test(align)) { + item.align.push('left'); + } + else { + item.align.push(null); + } + } + for (let i = 0; i < headers.length; i++) { + item.header.push({ + text: headers[i], + tokens: this.lexer.inline(headers[i]), + header: true, + align: item.align[i], + }); + } + for (const row of rows) { + item.rows.push(splitCells(row, item.header.length).map((cell, i) => { + return { + text: cell, + tokens: this.lexer.inline(cell), + header: false, + align: item.align[i], + }; + })); + } + return item; + } + lheading(src) { + const cap = this.rules.block.lheading.exec(src); + if (cap) { + return { + type: 'heading', + raw: cap[0], + depth: cap[2].charAt(0) === '=' ? 1 : 2, + text: cap[1], + tokens: this.lexer.inline(cap[1]), + }; + } + } + paragraph(src) { + const cap = this.rules.block.paragraph.exec(src); + if (cap) { + const text = cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1]; + return { + type: 'paragraph', + raw: cap[0], + text, + tokens: this.lexer.inline(text), + }; + } + } + text(src) { + const cap = this.rules.block.text.exec(src); + if (cap) { + return { + type: 'text', + raw: cap[0], + text: cap[0], + tokens: this.lexer.inline(cap[0]), + }; + } + } + escape(src) { + const cap = this.rules.inline.escape.exec(src); + if (cap) { + return { + type: 'escape', + raw: cap[0], + text: escape$1(cap[1]), + }; + } + } + tag(src) { + const cap = this.rules.inline.tag.exec(src); + if (cap) { + if (!this.lexer.state.inLink && /^/i.test(cap[0])) { + this.lexer.state.inLink = false; + } + if (!this.lexer.state.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.lexer.state.inRawBlock = true; + } + else if (this.lexer.state.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.lexer.state.inRawBlock = false; + } + return { + type: 'html', + raw: cap[0], + inLink: this.lexer.state.inLink, + inRawBlock: this.lexer.state.inRawBlock, + block: false, + text: cap[0], + }; + } + } + link(src) { + const cap = this.rules.inline.link.exec(src); + if (cap) { + const trimmedUrl = cap[2].trim(); + if (!this.options.pedantic && /^$/.test(trimmedUrl))) { + return; + } + // ending angle bracket cannot be escaped + const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\'); + if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { + return; + } + } + else { + // find closing parenthesis + const lastParenIndex = findClosingBracket(cap[2], '()'); + if (lastParenIndex > -1) { + const start = cap[0].indexOf('!') === 0 ? 5 : 4; + const linkLen = start + cap[1].length + lastParenIndex; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, linkLen).trim(); + cap[3] = ''; + } + } + let href = cap[2]; + let title = ''; + if (this.options.pedantic) { + // split pedantic href and title + const link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href); + if (link) { + href = link[1]; + title = link[3]; + } + } + else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + href = href.trim(); + if (/^$/.test(trimmedUrl))) { + // pedantic allows starting angle bracket without ending angle bracket + href = href.slice(1); + } + else { + href = href.slice(1, -1); + } + } + return outputLink(cap, { + href: href ? href.replace(this.rules.inline.anyPunctuation, '$1') : href, + title: title ? title.replace(this.rules.inline.anyPunctuation, '$1') : title, + }, cap[0], this.lexer); + } + } + reflink(src, links) { + let cap; + if ((cap = this.rules.inline.reflink.exec(src)) + || (cap = this.rules.inline.nolink.exec(src))) { + const linkString = (cap[2] || cap[1]).replace(/\s+/g, ' '); + const link = links[linkString.toLowerCase()]; + if (!link) { + const text = cap[0].charAt(0); + return { + type: 'text', + raw: text, + text, + }; + } + return outputLink(cap, link, cap[0], this.lexer); + } + } + emStrong(src, maskedSrc, prevChar = '') { + let match = this.rules.inline.emStrongLDelim.exec(src); + if (!match) + return; + // _ can't be between two alphanumerics. \p{L}\p{N} includes non-english alphabet/numbers as well + if (match[3] && prevChar.match(/[\p{L}\p{N}]/u)) + return; + const nextChar = match[1] || match[2] || ''; + if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) { + // unicode Regex counts emoji as 1 char; spread into array for proper count (used multiple times below) + const lLength = [...match[0]].length - 1; + let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0; + const endReg = match[0][0] === '*' ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd; + endReg.lastIndex = 0; + // Clip maskedSrc to same section of string as src (move to lexer?) + maskedSrc = maskedSrc.slice(-1 * src.length + lLength); + while ((match = endReg.exec(maskedSrc)) != null) { + rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6]; + if (!rDelim) + continue; // skip single * in __abc*abc__ + rLength = [...rDelim].length; + if (match[3] || match[4]) { // found another Left Delim + delimTotal += rLength; + continue; + } + else if (match[5] || match[6]) { // either Left or Right Delim + if (lLength % 3 && !((lLength + rLength) % 3)) { + midDelimTotal += rLength; + continue; // CommonMark Emphasis Rules 9-10 + } + } + delimTotal -= rLength; + if (delimTotal > 0) + continue; // Haven't found enough closing delimiters + // Remove extra characters. *a*** -> *a* + rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal); + // char length can be >1 for unicode characters; + const lastCharLength = [...match[0]][0].length; + const raw = src.slice(0, lLength + match.index + lastCharLength + rLength); + // Create `em` if smallest delimiter has odd char count. *a*** + if (Math.min(lLength, rLength) % 2) { + const text = raw.slice(1, -1); + return { + type: 'em', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + // Create 'strong' if smallest delimiter has even char count. **a*** + const text = raw.slice(2, -2); + return { + type: 'strong', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + } + } + codespan(src) { + const cap = this.rules.inline.code.exec(src); + if (cap) { + let text = cap[2].replace(/\n/g, ' '); + const hasNonSpaceChars = /[^ ]/.test(text); + const hasSpaceCharsOnBothEnds = /^ /.test(text) && / $/.test(text); + if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { + text = text.substring(1, text.length - 1); + } + text = escape$1(text, true); + return { + type: 'codespan', + raw: cap[0], + text, + }; + } + } + br(src) { + const cap = this.rules.inline.br.exec(src); + if (cap) { + return { + type: 'br', + raw: cap[0], + }; + } + } + del(src) { + const cap = this.rules.inline.del.exec(src); + if (cap) { + return { + type: 'del', + raw: cap[0], + text: cap[2], + tokens: this.lexer.inlineTokens(cap[2]), + }; + } + } + autolink(src) { + const cap = this.rules.inline.autolink.exec(src); + if (cap) { + let text, href; + if (cap[2] === '@') { + text = escape$1(cap[1]); + href = 'mailto:' + text; + } + else { + text = escape$1(cap[1]); + href = text; + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + url(src) { + let cap; + if (cap = this.rules.inline.url.exec(src)) { + let text, href; + if (cap[2] === '@') { + text = escape$1(cap[0]); + href = 'mailto:' + text; + } + else { + // do extended autolink path validation + let prevCapZero; + do { + prevCapZero = cap[0]; + cap[0] = this.rules.inline._backpedal.exec(cap[0])?.[0] ?? ''; + } while (prevCapZero !== cap[0]); + text = escape$1(cap[0]); + if (cap[1] === 'www.') { + href = 'http://' + cap[0]; + } + else { + href = cap[0]; + } + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + inlineText(src) { + const cap = this.rules.inline.text.exec(src); + if (cap) { + let text; + if (this.lexer.state.inRawBlock) { + text = cap[0]; + } + else { + text = escape$1(cap[0]); + } + return { + type: 'text', + raw: cap[0], + text, + }; + } + } + } + + /** + * Block-Level Grammar + */ + const newline = /^(?: *(?:\n|$))+/; + const blockCode = /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/; + const fences = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/; + const hr = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/; + const heading = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/; + const bullet = /(?:[*+-]|\d{1,9}[.)])/; + const lheading = edit(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/) + .replace(/bull/g, bullet) // lists can interrupt + .replace(/blockCode/g, / {4}/) // indented code blocks can interrupt + .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt + .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt + .replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt + .replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt + .getRegex(); + const _paragraph = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/; + const blockText = /^[^\n]+/; + const _blockLabel = /(?!\s*\])(?:\\.|[^[\]\\])+/; + const def = edit(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/) + .replace('label', _blockLabel) + .replace('title', /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/) + .getRegex(); + const list = edit(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/) + .replace(/bull/g, bullet) + .getRegex(); + const _tag = 'address|article|aside|base|basefont|blockquote|body|caption' + + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + + '|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title' + + '|tr|track|ul'; + const _comment = /|$))/; + const html = edit('^ {0,3}(?:' // optional indentation + + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3) + + '|\\n*|$)' // (4) + + '|\\n*|$)' // (5) + + '|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (6) + + '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) open tag + + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) closing tag + + ')', 'i') + .replace('comment', _comment) + .replace('tag', _tag) + .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) + .getRegex(); + const paragraph = edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(); + const blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/) + .replace('paragraph', paragraph) + .getRegex(); + /** + * Normal Block Grammar + */ + const blockNormal = { + blockquote, + code: blockCode, + def, + fences, + heading, + hr, + html, + lheading, + list, + newline, + paragraph, + table: noopTest, + text: blockText, + }; + /** + * GFM Block Grammar + */ + const gfmTable = edit('^ *([^\\n ].*)\\n' // Header + + ' {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)' // Align + + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)') // Cells + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('blockquote', ' {0,3}>') + .replace('code', ' {4}[^\\n]') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // tables can be interrupted by type (6) html blocks + .getRegex(); + const blockGfm = { + ...blockNormal, + table: gfmTable, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('table', gfmTable) // interrupt paragraphs with table + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(), + }; + /** + * Pedantic grammar (original John Gruber's loose markdown specification) + */ + const blockPedantic = { + ...blockNormal, + html: edit('^ *(?:comment *(?:\\n|\\s*$)' + + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') + .replace('comment', _comment) + .replace(/tag/g, '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') + .getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^(#{1,6})(.*)(?:\n+|$)/, + fences: noopTest, // fences not supported + lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' *#{1,6} *[^\n]') + .replace('lheading', lheading) + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('|fences', '') + .replace('|list', '') + .replace('|html', '') + .replace('|tag', '') + .getRegex(), + }; + /** + * Inline-Level Grammar + */ + const escape = /^\\([!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~])/; + const inlineCode = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/; + const br = /^( {2,}|\\)\n(?!\s*$)/; + const inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ + const blockSkip = /\[[^[\]]*?\]\([^()]*?\)|`[^`]*?`|<[^<>]*?>/g; + const emStrongLDelim = edit(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/, 'u') + .replace(/punct/g, _punctuation) + .getRegex(); + const emStrongRDelimAst = edit('^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)' // Skip orphan inside strong + + '|[^*]+(?=[^*])' // Consume to delim + + '|(?!\\*)[punct](\\*+)(?=[\\s]|$)' // (1) #*** can only be a Right Delimiter + + '|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)' // (2) a***#, a*** can only be a Right Delimiter + + '|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])' // (3) #***a, ***a can only be Left Delimiter + + '|[\\s](\\*+)(?!\\*)(?=[punct])' // (4) ***# can only be Left Delimiter + + '|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])' // (5) #***# can be either Left or Right Delimiter + + '|[^punct\\s](\\*+)(?=[^punct\\s])', 'gu') // (6) a***a can be either Left or Right Delimiter + .replace(/punct/g, _punctuation) + .getRegex(); + // (6) Not allowed for _ + const emStrongRDelimUnd = edit('^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)' // Skip orphan inside strong + + '|[^_]+(?=[^_])' // Consume to delim + + '|(?!_)[punct](_+)(?=[\\s]|$)' // (1) #___ can only be a Right Delimiter + + '|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)' // (2) a___#, a___ can only be a Right Delimiter + + '|(?!_)[punct\\s](_+)(?=[^punct\\s])' // (3) #___a, ___a can only be Left Delimiter + + '|[\\s](_+)(?!_)(?=[punct])' // (4) ___# can only be Left Delimiter + + '|(?!_)[punct](_+)(?!_)(?=[punct])', 'gu') // (5) #___# can be either Left or Right Delimiter + .replace(/punct/g, _punctuation) + .getRegex(); + const anyPunctuation = edit(/\\([punct])/, 'gu') + .replace(/punct/g, _punctuation) + .getRegex(); + const autolink = edit(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/) + .replace('scheme', /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/) + .replace('email', /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/) + .getRegex(); + const _inlineComment = edit(_comment).replace('(?:-->|$)', '-->').getRegex(); + const tag = edit('^comment' + + '|^' // self-closing tag + + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + + '|^' // declaration, e.g. + + '|^') // CDATA section + .replace('comment', _inlineComment) + .replace('attribute', /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/) + .getRegex(); + const _inlineLabel = /(?:\[(?:\\.|[^[\]\\])*\]|\\.|`[^`]*`|[^[\]\\`])*?/; + const link = edit(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/) + .replace('label', _inlineLabel) + .replace('href', /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/) + .replace('title', /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/) + .getRegex(); + const reflink = edit(/^!?\[(label)\]\[(ref)\]/) + .replace('label', _inlineLabel) + .replace('ref', _blockLabel) + .getRegex(); + const nolink = edit(/^!?\[(ref)\](?:\[\])?/) + .replace('ref', _blockLabel) + .getRegex(); + const reflinkSearch = edit('reflink|nolink(?!\\()', 'g') + .replace('reflink', reflink) + .replace('nolink', nolink) + .getRegex(); + /** + * Normal Inline Grammar + */ + const inlineNormal = { + _backpedal: noopTest, // only used for GFM url + anyPunctuation, + autolink, + blockSkip, + br, + code: inlineCode, + del: noopTest, + emStrongLDelim, + emStrongRDelimAst, + emStrongRDelimUnd, + escape, + link, + nolink, + punctuation, + reflink, + reflinkSearch, + tag, + text: inlineText, + url: noopTest, + }; + /** + * Pedantic Inline Grammar + */ + const inlinePedantic = { + ...inlineNormal, + link: edit(/^!?\[(label)\]\((.*?)\)/) + .replace('label', _inlineLabel) + .getRegex(), + reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace('label', _inlineLabel) + .getRegex(), + }; + /** + * GFM Inline Grammar + */ + const inlineGfm = { + ...inlineNormal, + escape: edit(escape).replace('])', '~|])').getRegex(), + url: edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9-]+\.?)+[^\s<]*|^email/, 'i') + .replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/) + .getRegex(), + _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, + del: /^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/, + text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+/=?_`{|}~-]+@)|[\s\S]*?(?:(?=[\\ { + return leading + ' '.repeat(tabs.length); + }); + } + let token; + let lastToken; + let cutSrc; + while (src) { + if (this.options.extensions + && this.options.extensions.block + && this.options.extensions.block.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // newline + if (token = this.tokenizer.space(src)) { + src = src.substring(token.raw.length); + if (token.raw.length === 1 && tokens.length > 0) { + // if there's a single \n as a spacer, it's terminating the last line, + // so move it there so that we don't get unnecessary paragraph tags + tokens[tokens.length - 1].raw += '\n'; + } + else { + tokens.push(token); + } + continue; + } + // code + if (token = this.tokenizer.code(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + // An indented code block cannot interrupt a paragraph. + if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + // fences + if (token = this.tokenizer.fences(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // heading + if (token = this.tokenizer.heading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // hr + if (token = this.tokenizer.hr(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // blockquote + if (token = this.tokenizer.blockquote(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // list + if (token = this.tokenizer.list(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // html + if (token = this.tokenizer.html(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // def + if (token = this.tokenizer.def(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.raw; + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else if (!this.tokens.links[token.tag]) { + this.tokens.links[token.tag] = { + href: token.href, + title: token.title, + }; + } + continue; + } + // table (gfm) + if (token = this.tokenizer.table(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // lheading + if (token = this.tokenizer.lheading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // top-level paragraph + // prevent paragraph consuming extensions by clipping 'src' to extension start + cutSrc = src; + if (this.options.extensions && this.options.extensions.startBlock) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startBlock.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { + lastToken = tokens[tokens.length - 1]; + if (lastParagraphClipped && lastToken?.type === 'paragraph') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else { + tokens.push(token); + } + lastParagraphClipped = (cutSrc.length !== src.length); + src = src.substring(token.raw.length); + continue; + } + // text + if (token = this.tokenizer.text(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && lastToken.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + this.state.top = true; + return tokens; + } + inline(src, tokens = []) { + this.inlineQueue.push({ src, tokens }); + return tokens; + } + /** + * Lexing/Compiling + */ + inlineTokens(src, tokens = []) { + let token, lastToken, cutSrc; + // String with links masked to avoid interference with em and strong + let maskedSrc = src; + let match; + let keepPrevChar, prevChar; + // Mask out reflinks + if (this.tokens.links) { + const links = Object.keys(this.tokens.links); + if (links.length > 0) { + while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { + if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); + } + } + } + } + // Mask out other blocks + while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); + } + // Mask out escaped characters + while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex); + } + while (src) { + if (!keepPrevChar) { + prevChar = ''; + } + keepPrevChar = false; + // extensions + if (this.options.extensions + && this.options.extensions.inline + && this.options.extensions.inline.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // escape + if (token = this.tokenizer.escape(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // tag + if (token = this.tokenizer.tag(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && token.type === 'text' && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + // link + if (token = this.tokenizer.link(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // reflink, nolink + if (token = this.tokenizer.reflink(src, this.tokens.links)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && token.type === 'text' && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + // em & strong + if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // code + if (token = this.tokenizer.codespan(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // br + if (token = this.tokenizer.br(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // del (gfm) + if (token = this.tokenizer.del(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // autolink + if (token = this.tokenizer.autolink(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // url (gfm) + if (!this.state.inLink && (token = this.tokenizer.url(src))) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // text + // prevent inlineText consuming extensions by clipping 'src' to extension start + cutSrc = src; + if (this.options.extensions && this.options.extensions.startInline) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startInline.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (token = this.tokenizer.inlineText(cutSrc)) { + src = src.substring(token.raw.length); + if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started + prevChar = token.raw.slice(-1); + } + keepPrevChar = true; + lastToken = tokens[tokens.length - 1]; + if (lastToken && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + return tokens; + } + } + + /** + * Renderer + */ + class _Renderer { + constructor(options) { + this.options = options || exports_defaults; +// this.parser = null; // set by the parser + } + space() { + return ''; + } + code({ text, lang, escaped }) { + const langString = (lang || '').match(/^\S*/)?.[0]; + const code = text.replace(/\n$/, '') + '\n'; + if (!langString) { + return '
    '
    +					+ (escaped ? code : escape$1(code, true))
    +					+ '
    \n'; + } + return '
    '
    +				+ (escaped ? code : escape$1(code, true))
    +				+ '
    \n'; + } + blockquote({ tokens }) { + const body = this.parser.parse(tokens); + return `
    \n${body}
    \n`; + } + html({ text }) { + return text; + } + heading({ tokens, depth }) { + return `${this.parser.parseInline(tokens)}\n`; + } + hr() { + return '
    \n'; + } + list(token) { + const ordered = token.ordered; + const start = token.start; + let body = ''; + for (let j = 0; j < token.items.length; j++) { + const item = token.items[j]; + body += this.listitem(item); + } + const type = ordered ? 'ol' : 'ul'; + const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : ''; + return '<' + type + startAttr + '>\n' + body + '\n'; + } + listitem(item) { + let itemBody = ''; + if (item.task) { + const checkbox = this.checkbox({ checked: !!item.checked }); + if (item.loose) { + if (item.tokens.length > 0 && item.tokens[0].type === 'paragraph') { + item.tokens[0].text = checkbox + ' ' + item.tokens[0].text; + if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { + item.tokens[0].tokens[0].text = checkbox + ' ' + item.tokens[0].tokens[0].text; + } + } + else { + item.tokens.unshift({ + type: 'text', + raw: checkbox + ' ', + text: checkbox + ' ', + }); + } + } + else { + itemBody += checkbox + ' '; + } + } + itemBody += this.parser.parse(item.tokens, !!item.loose); + return `
  • ${itemBody}
  • \n`; + } + checkbox({ checked }) { + return ''; + } + paragraph({ tokens }) { + return `

    ${this.parser.parseInline(tokens)}

    \n`; + } + table(token) { + let header = ''; + // header + let cell = ''; + for (let j = 0; j < token.header.length; j++) { + cell += this.tablecell(token.header[j]); + } + header += this.tablerow({ text: cell }); + let body = ''; + for (let j = 0; j < token.rows.length; j++) { + const row = token.rows[j]; + cell = ''; + for (let k = 0; k < row.length; k++) { + cell += this.tablecell(row[k]); + } + body += this.tablerow({ text: cell }); + } + if (body) + body = `${body}`; + return '\n' + + '\n' + + header + + '\n' + + body + + '
    \n'; + } + tablerow({ text }) { + return `\n${text}\n`; + } + tablecell(token) { + const content = this.parser.parseInline(token.tokens); + const type = token.header ? 'th' : 'td'; + const tag = token.align + ? `<${type} align="${token.align}">` + : `<${type}>`; + return tag + content + `\n`; + } + /** + * span level renderer + */ + strong({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + em({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + codespan({ text }) { + return `${text}`; + } + br() { + return '
    '; + } + del({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + link({ href, title, tokens }) { + const text = this.parser.parseInline(tokens); + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return text; + } + href = cleanHref; + let out = '
    '; + return out; + } + image({ href, title, text }) { + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return text; + } + href = cleanHref; + let out = `${text} { + const tokens = genericToken[childTokens].flat(Infinity); + values = values.concat(this.walkTokens(tokens, callback)); + }); + } + else if (genericToken.tokens) { + values = values.concat(this.walkTokens(genericToken.tokens, callback)); + } + } + } + } + return values; + } + use(...args) { + const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} }; + args.forEach((pack) => { + // copy options to new object + const opts = { ...pack }; + // set async to true if it was set to true before + opts.async = this.defaults.async || opts.async || false; + // ==-- Parse "addon" extensions --== // + if (pack.extensions) { + pack.extensions.forEach((ext) => { + if (!ext.name) { + throw new Error('extension name required'); + } + if ('renderer' in ext) { // Renderer extensions + const prevRenderer = extensions.renderers[ext.name]; + if (prevRenderer) { + // Replace extension with func to run new extension but fall back if false + extensions.renderers[ext.name] = function (...args) { + let ret = ext.renderer.apply(this, args); + if (ret === false) { + ret = prevRenderer.apply(this, args); + } + return ret; + }; + } + else { + extensions.renderers[ext.name] = ext.renderer; + } + } + if ('tokenizer' in ext) { // Tokenizer Extensions + if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) { + throw new Error("extension level must be 'block' or 'inline'"); + } + const extLevel = extensions[ext.level]; + if (extLevel) { + extLevel.unshift(ext.tokenizer); + } + else { + extensions[ext.level] = [ext.tokenizer]; + } + if (ext.start) { // Function to check for start of token + if (ext.level === 'block') { + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } + else { + extensions.startBlock = [ext.start]; + } + } + else if (ext.level === 'inline') { + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } + else { + extensions.startInline = [ext.start]; + } + } + } + } + if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens + extensions.childTokens[ext.name] = ext.childTokens; + } + }); + opts.extensions = extensions; + } + // ==-- Parse "overwrite" extensions --== // + if (pack.renderer) { + const renderer = this.defaults.renderer || new _Renderer(this.defaults); + for (const prop in pack.renderer) { + if (!(prop in renderer)) { + throw new Error(`renderer '${prop}' does not exist`); + } + if (['options', 'parser'].includes(prop)) { + // ignore options property + continue; + } + const rendererProp = prop; + const rendererFunc = pack.renderer[rendererProp]; + const prevRenderer = renderer[rendererProp]; + // Replace renderer with func to run extension, but fall back if false + renderer[rendererProp] = (...args) => { + let ret = rendererFunc.apply(renderer, args); + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } + return ret || ''; + }; + } + opts.renderer = renderer; + } + if (pack.tokenizer) { + const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults); + for (const prop in pack.tokenizer) { + if (!(prop in tokenizer)) { + throw new Error(`tokenizer '${prop}' does not exist`); + } + if (['options', 'rules', 'lexer'].includes(prop)) { + // ignore options, rules, and lexer properties + continue; + } + const tokenizerProp = prop; + const tokenizerFunc = pack.tokenizer[tokenizerProp]; + const prevTokenizer = tokenizer[tokenizerProp]; + // Replace tokenizer with func to run extension, but fall back if false + // @ts-expect-error cannot type tokenizer function dynamically + tokenizer[tokenizerProp] = (...args) => { + let ret = tokenizerFunc.apply(tokenizer, args); + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + return ret; + }; + } + opts.tokenizer = tokenizer; + } + // ==-- Parse Hooks extensions --== // + if (pack.hooks) { + const hooks = this.defaults.hooks || new _Hooks(); + for (const prop in pack.hooks) { + if (!(prop in hooks)) { + throw new Error(`hook '${prop}' does not exist`); + } + if (prop === 'options') { + // ignore options property + continue; + } + const hooksProp = prop; + const hooksFunc = pack.hooks[hooksProp]; + const prevHook = hooks[hooksProp]; + if (_Hooks.passThroughHooks.has(prop)) { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (arg) => { + if (this.defaults.async) { + return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => { + return prevHook.call(hooks, ret); + }); + } + const ret = hooksFunc.call(hooks, arg); + return prevHook.call(hooks, ret); + }; + } + else { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (...args) => { + let ret = hooksFunc.apply(hooks, args); + if (ret === false) { + ret = prevHook.apply(hooks, args); + } + return ret; + }; + } + } + opts.hooks = hooks; + } + // ==-- Parse WalkTokens extensions --== // + if (pack.walkTokens) { + const walkTokens = this.defaults.walkTokens; + const packWalktokens = pack.walkTokens; + opts.walkTokens = function (token) { + let values = []; + values.push(packWalktokens.call(this, token)); + if (walkTokens) { + values = values.concat(walkTokens.call(this, token)); + } + return values; + }; + } + this.defaults = { ...this.defaults, ...opts }; + }); + return this; + } + setOptions(opt) { + this.defaults = { ...this.defaults, ...opt }; + return this; + } + lexer(src, options) { + return _Lexer.lex(src, options ?? this.defaults); + } + parser(tokens, options) { + return _Parser.parse(tokens, options ?? this.defaults); + } + parseMarkdown(lexer, parser) { + // eslint-disable-next-line + const parse = (src, options) => { + const origOpt = { ...options }; + const opt = { ...this.defaults, ...origOpt }; + const throwError = this.onError(!!opt.silent, !!opt.async); + // throw error if an extension set async to true but parse was called with async: false + if (this.defaults.async === true && origOpt.async === false) { + return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.')); + } + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + return throwError(new Error('marked(): input parameter is undefined or null')); + } + if (typeof src !== 'string') { + return throwError(new Error('marked(): input parameter is of type ' + + Object.prototype.toString.call(src) + ', string expected')); + } + if (opt.hooks) { + opt.hooks.options = opt; + } + if (opt.async) { + return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) + .then(src => lexer(src, opt)) + .then(tokens => opt.hooks ? opt.hooks.processAllTokens(tokens) : tokens) + .then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) + .then(tokens => parser(tokens, opt)) + .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) + .catch(throwError); + } + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + let tokens = lexer(src, opt); + if (opt.hooks) { + tokens = opt.hooks.processAllTokens(tokens); + } + if (opt.walkTokens) { + this.walkTokens(tokens, opt.walkTokens); + } + let html = parser(tokens, opt); + if (opt.hooks) { + html = opt.hooks.postprocess(html); + } + return html; + } + catch (e) { + return throwError(e); + } + }; + return parse; + } + onError(silent, async) { + return (e) => { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + if (silent) { + const msg = '

    An error occurred:

    '
    +						+ escape$1(e.message + '', true)
    +						+ '
    '; + if (async) { + return Promise.resolve(msg); + } + return msg; + } + if (async) { + return Promise.reject(e); + } + throw e; + }; + } + } + + const markedInstance = new Marked(); +/* + function changeDefaults(newDefaults) { + exports_defaults = newDefaults; + } + + function marked(src, opt) { + return markedInstance.parse(src, opt); + } + /** + * Sets the default options. + * + * @param options Hash of options + * / + marked.options = + marked.setOptions = (options) => { + markedInstance.setOptions(options); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; + }; + /** + * Gets the original marked default options. + * / + marked.defaults = exports_defaults; + /** + * Run callback for every token + * / + marked.walkTokens = (tokens, callback) => markedInstance.walkTokens(tokens, callback); + /** + * Expose + * / + marked.Parser = _Parser; + marked.parser = _Parser.parse; + marked.Renderer = _Renderer; + marked.TextRenderer = _TextRenderer; + marked.Lexer = _Lexer; + marked.lexer = _Lexer.lex; + marked.Tokenizer = _Tokenizer; + marked.Hooks = _Hooks; + marked.parse = marked; +*/ + + return { +/* + Hooks: _Hooks, + Lexer: _Lexer, + Marked: Marked, + Parser: _Parser, + Renderer: _Renderer, + TextRenderer: _TextRenderer, + Tokenizer: _Tokenizer, + getDefaults: _getDefaults, + lexer: _Lexer.lex, + marked: marked, + options: marked.options, +*/ + parse: (src, opt) => markedInstance.parse(src, opt), +/* + /** + * Compiles markdown to HTML without enclosing `p` tag. + * + * @param src String of markdown source to be compiled + * @param options Hash of options + * @return String of compiled HTML + * / + parseInline: markedInstance.parseInline, + parser: _Parser.parse, + setOptions: marked.setOptions, + /** + * Use Extension + * / + use: (...args) => { + markedInstance.use(...args); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; + }, + walkTokens: marked.walkTokens +*/ + }; +})(); diff --git a/vendors/mathiasbynens/punycode.js b/vendors/mathiasbynens/punycode.js new file mode 100644 index 0000000000..7afa382587 --- /dev/null +++ b/vendors/mathiasbynens/punycode.js @@ -0,0 +1,395 @@ +/** + * Modified version of https://github.com/mathiasbynens/punycode.js + */ + +(() => { + +'use strict'; + +const + /** Highest positive signed 32-bit float value */ + maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1 + + /** Bootstring parameters */ + base = 36, + tMin = 1, + tMax = 26, + skew = 38, + damp = 700, + initialBias = 72, + initialN = 128, // 0x80 + delimiter = '-', // '\x2D' + + /** Regular expressions */ + regexPunycode = /^xn--/, + regexNonASCII = /[^\0-\x7F]/, // Note: U+007F DEL is excluded too. + regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g, // RFC 3490 separators + + /** Error messages */ + errors = { + 'overflow': 'Overflow: input needs wider integers to process', + 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', + 'invalid-input': 'Invalid input' + }, + + /** Convenience shortcuts */ + baseMinusTMin = base - tMin, + floor = Math.floor, + stringFromCharCode = String.fromCharCode, + + /*--------------------------------------------------------------------------*/ + + /** + * A generic error utility function. + * @private + * @param {String} type The error type. + * @returns {Error} Throws a `RangeError` with the applicable error message. + */ + error = type => { + throw new RangeError(errors[type]) + }, + + /** + * A simple `Array#map`-like wrapper to work with domain name strings or email + * addresses. + * @private + * @param {String} domain The domain name or email address. + * @param {Function} callback The function that gets called for every + * character. + * @returns {String} A new string of characters returned by the callback + * function. + */ + mapDomain = (domain, callback) => { + // In email addresses, only the domain name should be punycoded. + // Leave the local part (i.e. everything up to `@`) intact. + const parts = (domain || '').split('@'); + parts.push( + parts.pop() + .split(regexSeparators) + .map(label => callback(label)) + .join('.') + ); + return parts.join('@'); + }, + + /** + * Creates an array containing the numeric code points of each Unicode + * character in the string. While JavaScript uses UCS-2 internally, + * this function will convert a pair of surrogate halves (each of which + * UCS-2 exposes as separate characters) into a single code point, + * matching UTF-16. + * @see + * @name decode + * @param {String} string The Unicode input string (UCS-2). + * @returns {Array} The new array of code points. + */ + ucs2decode = string => { + const output = []; + let counter = 0; + const length = string.length; + while (counter < length) { + const value = string.charCodeAt(counter++); + if (value >= 0xD800 && value <= 0xDBFF && counter < length) { + // It's a high surrogate, and there is a next character. + const extra = string.charCodeAt(counter++); + if ((extra & 0xFC00) == 0xDC00) { // Low surrogate. + output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); + } else { + // It's an unmatched surrogate; only append this code unit, in case the + // next code unit is the high surrogate of a surrogate pair. + output.push(value); + counter--; + } + } else { + output.push(value); + } + } + return output; + }, + + /** + * Converts a basic code point into a digit/integer. + * @see `digitToBasic()` + * @private + * @param {Number} codePoint The basic numeric code point value. + * @returns {Number} The numeric value of a basic code point (for use in + * representing integers) in the range `0` to `base - 1`, or `base` if + * the code point does not represent a value. + */ + basicToDigit = codePoint => { + if (codePoint >= 0x30 && codePoint < 0x3A) { + return 26 + (codePoint - 0x30); + } + if (codePoint >= 0x41 && codePoint < 0x5B) { + return codePoint - 0x41; + } + if (codePoint >= 0x61 && codePoint < 0x7B) { + return codePoint - 0x61; + } + return base; + }, + + /** + * Converts a digit/integer into a basic code point. + * @see `basicToDigit()` + * @private + * @param {Number} digit The numeric value of a basic code point. + * @returns {Number} The basic code point whose value (when used for + * representing integers) is `digit`, which needs to be in the range + * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is + * used; else, the lowercase form is used. The behavior is undefined + * if `flag` is non-zero and `digit` has no uppercase form. + */ + digitToBasic = (digit, flag) => + // 0..25 map to ASCII a..z or A..Z + // 26..35 map to ASCII 0..9 + digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5), + + /** + * Bias adaptation function as per section 3.4 of RFC 3492. + * https://tools.ietf.org/html/rfc3492#section-3.4 + * @private + */ + adapt = (delta, numPoints, firstTime) => { + let k = 0; + delta = firstTime ? floor(delta / damp) : delta >> 1; + delta += floor(delta / numPoints); + for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) { + delta = floor(delta / baseMinusTMin); + } + return floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); + }, + + /** + * Converts a Punycode string of ASCII-only symbols to a string of Unicode + * symbols. + * @memberOf punycode + * @param {String} input The Punycode string of ASCII-only symbols. + * @returns {String} The resulting string of Unicode symbols. + */ + decode = input => { + // Don't use UCS-2. + const output = []; + const inputLength = input.length; + let i = 0; + let n = initialN; + let bias = initialBias; + + // Handle the basic code points: let `basic` be the number of input code + // points before the last delimiter, or `0` if there is none, then copy + // the first basic code points to the output. + + let basic = input.lastIndexOf(delimiter); + if (basic < 0) { + basic = 0; + } + + for (let j = 0; j < basic; ++j) { + // if it's not a basic code point + if (input.charCodeAt(j) >= 0x80) { + error('not-basic'); + } + output.push(input.charCodeAt(j)); + } + + // Main decoding loop: start just after the last delimiter if any basic code + // points were copied; start at the beginning otherwise. + + for (let index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) { + + // `index` is the index of the next character to be consumed. + // Decode a generalized variable-length integer into `delta`, + // which gets added to `i`. The overflow checking is easier + // if we increase `i` as we go, then subtract off its starting + // value at the end to obtain `delta`. + const oldi = i; + for (let w = 1, k = base; /* no condition */; k += base) { + + if (index >= inputLength) { + error('invalid-input'); + } + + const digit = basicToDigit(input.charCodeAt(index++)); + + if (digit >= base) { + error('invalid-input'); + } + if (digit > floor((maxInt - i) / w)) { + error('overflow'); + } + + i += digit * w; + const t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); + + if (digit < t) { + break; + } + + const baseMinusT = base - t; + if (w > floor(maxInt / baseMinusT)) { + error('overflow'); + } + + w *= baseMinusT; + + } + + const out = output.length + 1; + bias = adapt(i - oldi, out, oldi == 0); + + // `i` was supposed to wrap around from `out` to `0`, + // incrementing `n` each time, so we'll fix that now: + if (floor(i / out) > maxInt - n) { + error('overflow'); + } + + n += floor(i / out); + i %= out; + + // Insert `n` at position `i` of the output. + output.splice(i++, 0, n); + + } + + return String.fromCodePoint(...output); + }, + + /** + * Converts a string of Unicode symbols (e.g. a domain name label) to a + * Punycode string of ASCII-only symbols. + * @memberOf punycode + * @param {String} input The string of Unicode symbols. + * @returns {String} The resulting Punycode string of ASCII-only symbols. + */ + encode = input => { + const output = []; + + // Convert the input in UCS-2 to an array of Unicode code points. + input = ucs2decode(input); + + // Cache the length. + const inputLength = input.length; + + // Initialize the state. + let n = initialN; + let delta = 0; + let bias = initialBias; + + // Handle the basic code points. + for (const currentValue of input) { + if (currentValue < 0x80) { + output.push(stringFromCharCode(currentValue)); + } + } + + const basicLength = output.length; + let handledCPCount = basicLength; + + // `handledCPCount` is the number of code points that have been handled; + // `basicLength` is the number of basic code points. + + // Finish the basic string with a delimiter unless it's empty. + if (basicLength) { + output.push(delimiter); + } + + // Main encoding loop: + while (handledCPCount < inputLength) { + + // All non-basic code points < n have been handled already. Find the next + // larger one: + let m = maxInt; + for (const currentValue of input) { + if (currentValue >= n && currentValue < m) { + m = currentValue; + } + } + + // Increase `delta` enough to advance the decoder's state to , + // but guard against overflow. + const handledCPCountPlusOne = handledCPCount + 1; + if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) { + error('overflow'); + } + + delta += (m - n) * handledCPCountPlusOne; + n = m; + + for (const currentValue of input) { + if (currentValue < n && ++delta > maxInt) { + error('overflow'); + } + if (currentValue === n) { + // Represent delta as a generalized variable-length integer. + let q = delta; + for (let k = base; /* no condition */; k += base) { + const t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); + if (q < t) { + break; + } + const qMinusT = q - t; + const baseMinusT = base - t; + output.push( + stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0)) + ); + q = floor(qMinusT / baseMinusT); + } + + output.push(stringFromCharCode(digitToBasic(q, 0))); + bias = adapt(delta, handledCPCountPlusOne, handledCPCount === basicLength); + delta = 0; + ++handledCPCount; + } + } + + ++delta; + ++n; + + } + return output.join(''); + }; + + /*--------------------------------------------------------------------------*/ + + /** Define the public API */ + window.IDN = { + /** + * A string representing the current Punycode.js version number. + * @memberOf punycode + * @type String + */ + version: '2.3.1', + + /** + * Converts a Punycode string representing a domain name or an email address + * to Unicode. Only the Punycoded parts of the input will be converted, i.e. + * it doesn't matter if you call it on a string that has already been + * converted to Unicode. + * @memberOf punycode + * @param {String} input The Punycoded domain name or email address to + * convert to Unicode. + * @returns {String} The Unicode representation of the given Punycode + * string. + */ + toUnicode: input => mapDomain( + input, + string => regexPunycode.test(string) ? decode(string.slice(4).toLowerCase()) : string + ), + + /** + * Converts a Unicode string representing a domain name or an email address to + * Punycode. Only the non-ASCII parts of the domain name will be converted, + * i.e. it doesn't matter if you call it with a domain that's already in + * ASCII. + * @memberOf punycode + * @param {String} input The domain name or email address to convert, as a + * Unicode string. + * @returns {String} The Punycode representation of the given domain name or + * email address. + */ + toASCII: input => mapDomain( + input, + string => (regexNonASCII.test(string) ? 'xn--' + encode(string) : string).toLowerCase() + ) + }; +})(); diff --git a/vendors/modernizr/config.json b/vendors/modernizr/config.json deleted file mode 100644 index cd532988fa..0000000000 --- a/vendors/modernizr/config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "minify": false, - "options": [ - "setClasses" - ], - "feature-detects": [ - "test/css/animations", - "test/css/backgroundsize", - "test/css/boxshadow", - "test/css/rgba", - "test/css/textshadow", - "test/css/transitions" - ] -} \ No newline at end of file diff --git a/vendors/modernizr/modernizr-custom.js b/vendors/modernizr/modernizr-custom.js deleted file mode 100644 index 059a6194a1..0000000000 --- a/vendors/modernizr/modernizr-custom.js +++ /dev/null @@ -1,905 +0,0 @@ -/*! - * modernizr v3.3.1 - * Build http://modernizr.com/download?-backgroundsize-boxshadow-cssanimations-csstransitions-rgba-textshadow-setclasses-dontmin - * - * Copyright (c) - * Faruk Ates - * Paul Irish - * Alex Sexton - * Ryan Seddon - * Patrick Kettner - * Stu Cox - * Richard Herrera - - * MIT License - */ - -/* - * Modernizr tests which native CSS3 and HTML5 features are available in the - * current UA and makes the results available to you in two ways: as properties on - * a global `Modernizr` object, and as classes on the `` element. This - * information allows you to progressively enhance your pages with a granular level - * of control over the experience. -*/ - -;(function(window, document, undefined){ - var classes = []; - - - var tests = []; - - - /** - * - * ModernizrProto is the constructor for Modernizr - * - * @class - * @access public - */ - - var ModernizrProto = { - // The current version, dummy - _version: '3.3.1', - - // Any settings that don't work as separate modules - // can go in here as configuration. - _config: { - 'classPrefix': '', - 'enableClasses': true, - 'enableJSClass': true, - 'usePrefixes': true - }, - - // Queue of tests - _q: [], - - // Stub these for people who are listening - on: function(test, cb) { - // I don't really think people should do this, but we can - // safe guard it a bit. - // -- NOTE:: this gets WAY overridden in src/addTest for actual async tests. - // This is in case people listen to synchronous tests. I would leave it out, - // but the code to *disallow* sync tests in the real version of this - // function is actually larger than this. - var self = this; - setTimeout(function() { - cb(self[test]); - }, 0); - }, - - addTest: function(name, fn, options) { - tests.push({name: name, fn: fn, options: options}); - }, - - addAsyncTest: function(fn) { - tests.push({name: null, fn: fn}); - } - }; - - - - // Fake some of Object.create so we can force non test results to be non "own" properties. - var Modernizr = function() {}; - Modernizr.prototype = ModernizrProto; - - // Leak modernizr globally when you `require` it rather than force it here. - // Overwrite name so constructor name is nicer :D - Modernizr = new Modernizr(); - - - - /** - * is returns a boolean if the typeof an obj is exactly type. - * - * @access private - * @function is - * @param {*} obj - A thing we want to check the type of - * @param {string} type - A string to compare the typeof against - * @returns {boolean} - */ - - function is(obj, type) { - return typeof obj === type; - } - ; - - /** - * Run through all tests and detect their support in the current UA. - * - * @access private - */ - - function testRunner() { - var featureNames; - var feature; - var aliasIdx; - var result; - var nameIdx; - var featureName; - var featureNameSplit; - - for (var featureIdx in tests) { - if (tests.hasOwnProperty(featureIdx)) { - featureNames = []; - feature = tests[featureIdx]; - // run the test, throw the return value into the Modernizr, - // then based on that boolean, define an appropriate className - // and push it into an array of classes we'll join later. - // - // If there is no name, it's an 'async' test that is run, - // but not directly added to the object. That should - // be done with a post-run addTest call. - if (feature.name) { - featureNames.push(feature.name.toLowerCase()); - - if (feature.options && feature.options.aliases && feature.options.aliases.length) { - // Add all the aliases into the names list - for (aliasIdx = 0; aliasIdx < feature.options.aliases.length; aliasIdx++) { - featureNames.push(feature.options.aliases[aliasIdx].toLowerCase()); - } - } - } - - // Run the test, or use the raw value if it's not a function - result = is(feature.fn, 'function') ? feature.fn() : feature.fn; - - - // Set each of the names on the Modernizr object - for (nameIdx = 0; nameIdx < featureNames.length; nameIdx++) { - featureName = featureNames[nameIdx]; - // Support dot properties as sub tests. We don't do checking to make sure - // that the implied parent tests have been added. You must call them in - // order (either in the test, or make the parent test a dependency). - // - // Cap it to TWO to make the logic simple and because who needs that kind of subtesting - // hashtag famous last words - featureNameSplit = featureName.split('.'); - - if (featureNameSplit.length === 1) { - Modernizr[featureNameSplit[0]] = result; - } else { - // cast to a Boolean, if not one already - /* jshint -W053 */ - if (Modernizr[featureNameSplit[0]] && !(Modernizr[featureNameSplit[0]] instanceof Boolean)) { - Modernizr[featureNameSplit[0]] = new Boolean(Modernizr[featureNameSplit[0]]); - } - - Modernizr[featureNameSplit[0]][featureNameSplit[1]] = result; - } - - classes.push((result ? '' : 'no-') + featureNameSplit.join('-')); - } - } - } - } - ; - - /** - * docElement is a convenience wrapper to grab the root element of the document - * - * @access private - * @returns {HTMLElement|SVGElement} The root element of the document - */ - - var docElement = document.documentElement; - - - /** - * A convenience helper to check if the document we are running in is an SVG document - * - * @access private - * @returns {boolean} - */ - - var isSVG = docElement.nodeName.toLowerCase() === 'svg'; - - - /** - * setClasses takes an array of class names and adds them to the root element - * - * @access private - * @function setClasses - * @param {string[]} classes - Array of class names - */ - - // Pass in an and array of class names, e.g.: - // ['no-webp', 'borderradius', ...] - function setClasses(classes) { - var className = docElement.className; - var classPrefix = Modernizr._config.classPrefix || ''; - - if (isSVG) { - className = className.baseVal; - } - - // Change `no-js` to `js` (independently of the `enableClasses` option) - // Handle classPrefix on this too - if (Modernizr._config.enableJSClass) { - var reJS = new RegExp('(^|\\s)' + classPrefix + 'no-js(\\s|$)'); - className = className.replace(reJS, '$1' + classPrefix + 'js$2'); - } - - if (Modernizr._config.enableClasses) { - // Add the new classes - className += ' ' + classPrefix + classes.join(' ' + classPrefix); - isSVG ? docElement.className.baseVal = className : docElement.className = className; - } - - } - - ; - - /** - * createElement is a convenience wrapper around document.createElement. Since we - * use createElement all over the place, this allows for (slightly) smaller code - * as well as abstracting away issues with creating elements in contexts other than - * HTML documents (e.g. SVG documents). - * - * @access private - * @function createElement - * @returns {HTMLElement|SVGElement} An HTML or SVG element - */ - - function createElement() { - if (typeof document.createElement !== 'function') { - // This is the case in IE7, where the type of createElement is "object". - // For this reason, we cannot call apply() as Object is not a Function. - return document.createElement(arguments[0]); - } else if (isSVG) { - return document.createElementNS.call(document, 'http://www.w3.org/2000/svg', arguments[0]); - } else { - return document.createElement.apply(document, arguments); - } - } - - ; -/*! -{ - "name": "CSS rgba", - "caniuse": "css3-colors", - "property": "rgba", - "tags": ["css"], - "notes": [{ - "name": "CSSTricks Tutorial", - "href": "https://css-tricks.com/rgba-browser-support/" - }] -} -!*/ - - Modernizr.addTest('rgba', function() { - var style = createElement('a').style; - style.cssText = 'background-color:rgba(150,255,150,.5)'; - - return ('' + style.backgroundColor).indexOf('rgba') > -1; - }); - - - - /** - * contains checks to see if a string contains another string - * - * @access private - * @function contains - * @param {string} str - The string we want to check for substrings - * @param {string} substr - The substring we want to search the first string for - * @returns {boolean} - */ - - function contains(str, substr) { - return !!~('' + str).indexOf(substr); - } - - ; - - /** - * cssToDOM takes a kebab-case string and converts it to camelCase - * e.g. box-sizing -> boxSizing - * - * @access private - * @function cssToDOM - * @param {string} name - String name of kebab-case prop we want to convert - * @returns {string} The camelCase version of the supplied name - */ - - function cssToDOM(name) { - return name.replace(/([a-z])-([a-z])/g, function(str, m1, m2) { - return m1 + m2.toUpperCase(); - }).replace(/^-/, ''); - } - ; - - /** - * If the browsers follow the spec, then they would expose vendor-specific style as: - * elem.style.WebkitBorderRadius - * instead of something like the following, which would be technically incorrect: - * elem.style.webkitBorderRadius - - * Webkit ghosts their properties in lowercase but Opera & Moz do not. - * Microsoft uses a lowercase `ms` instead of the correct `Ms` in IE8+ - * erik.eae.net/archives/2008/03/10/21.48.10/ - - * More here: github.com/Modernizr/Modernizr/issues/issue/21 - * - * @access private - * @returns {string} The string representing the vendor-specific style properties - */ - - var omPrefixes = 'Moz O ms Webkit'; - - - var cssomPrefixes = (ModernizrProto._config.usePrefixes ? omPrefixes.split(' ') : []); - ModernizrProto._cssomPrefixes = cssomPrefixes; - - - /** - * List of JavaScript DOM values used for tests - * - * @memberof Modernizr - * @name Modernizr._domPrefixes - * @optionName Modernizr._domPrefixes - * @optionProp domPrefixes - * @access public - * @example - * - * Modernizr._domPrefixes is exactly the same as [_prefixes](#modernizr-_prefixes), but rather - * than kebab-case properties, all properties are their Capitalized variant - * - * ```js - * Modernizr._domPrefixes === [ "Moz", "O", "ms", "Webkit" ]; - * ``` - */ - - var domPrefixes = (ModernizrProto._config.usePrefixes ? omPrefixes.toLowerCase().split(' ') : []); - ModernizrProto._domPrefixes = domPrefixes; - - - /** - * fnBind is a super small [bind](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind) polyfill. - * - * @access private - * @function fnBind - * @param {function} fn - a function you want to change `this` reference to - * @param {object} that - the `this` you want to call the function with - * @returns {function} The wrapped version of the supplied function - */ - - function fnBind(fn, that) { - return function() { - return fn.apply(that, arguments); - }; - } - - ; - - /** - * testDOMProps is a generic DOM property test; if a browser supports - * a certain property, it won't return undefined for it. - * - * @access private - * @function testDOMProps - * @param {array.} props - An array of properties to test for - * @param {object} obj - An object or Element you want to use to test the parameters again - * @param {boolean|object} elem - An Element to bind the property lookup again. Use `false` to prevent the check - */ - function testDOMProps(props, obj, elem) { - var item; - - for (var i in props) { - if (props[i] in obj) { - - // return the property name as a string - if (elem === false) { - return props[i]; - } - - item = obj[props[i]]; - - // let's bind a function - if (is(item, 'function')) { - // bind to obj unless overriden - return fnBind(item, elem || obj); - } - - // return the unbound function or obj or value - return item; - } - } - return false; - } - - ; - - /** - * Create our "modernizr" element that we do most feature tests on. - * - * @access private - */ - - var modElem = { - elem: createElement('modernizr') - }; - - // Clean up this element - Modernizr._q.push(function() { - delete modElem.elem; - }); - - - - var mStyle = { - style: modElem.elem.style - }; - - // kill ref for gc, must happen before mod.elem is removed, so we unshift on to - // the front of the queue. - Modernizr._q.unshift(function() { - delete mStyle.style; - }); - - - - /** - * domToCSS takes a camelCase string and converts it to kebab-case - * e.g. boxSizing -> box-sizing - * - * @access private - * @function domToCSS - * @param {string} name - String name of camelCase prop we want to convert - * @returns {string} The kebab-case version of the supplied name - */ - - function domToCSS(name) { - return name.replace(/([A-Z])/g, function(str, m1) { - return '-' + m1.toLowerCase(); - }).replace(/^ms-/, '-ms-'); - } - ; - - /** - * getBody returns the body of a document, or an element that can stand in for - * the body if a real body does not exist - * - * @access private - * @function getBody - * @returns {HTMLElement|SVGElement} Returns the real body of a document, or an - * artificially created element that stands in for the body - */ - - function getBody() { - // After page load injecting a fake body doesn't work so check if body exists - var body = document.body; - - if (!body) { - // Can't use the real body create a fake one. - body = createElement(isSVG ? 'svg' : 'body'); - body.fake = true; - } - - return body; - } - - ; - - /** - * injectElementWithStyles injects an element with style element and some CSS rules - * - * @access private - * @function injectElementWithStyles - * @param {string} rule - String representing a css rule - * @param {function} callback - A function that is used to test the injected element - * @param {number} [nodes] - An integer representing the number of additional nodes you want injected - * @param {string[]} [testnames] - An array of strings that are used as ids for the additional nodes - * @returns {boolean} - */ - - function injectElementWithStyles(rule, callback, nodes, testnames) { - var mod = 'modernizr'; - var style; - var ret; - var node; - var docOverflow; - var div = createElement('div'); - var body = getBody(); - - if (parseInt(nodes, 10)) { - // In order not to give false positives we create a node for each test - // This also allows the method to scale for unspecified uses - while (nodes--) { - node = createElement('div'); - node.id = testnames ? testnames[nodes] : mod + (nodes + 1); - div.appendChild(node); - } - } - - style = createElement('style'); - style.type = 'text/css'; - style.id = 's' + mod; - - // IE6 will false positive on some tests due to the style element inside the test div somehow interfering offsetHeight, so insert it into body or fakebody. - // Opera will act all quirky when injecting elements in documentElement when page is served as xml, needs fakebody too. #270 - (!body.fake ? div : body).appendChild(style); - body.appendChild(div); - - if (style.styleSheet) { - style.styleSheet.cssText = rule; - } else { - style.appendChild(document.createTextNode(rule)); - } - div.id = mod; - - if (body.fake) { - //avoid crashing IE8, if background image is used - body.style.background = ''; - //Safari 5.13/5.1.4 OSX stops loading if ::-webkit-scrollbar is used and scrollbars are visible - body.style.overflow = 'hidden'; - docOverflow = docElement.style.overflow; - docElement.style.overflow = 'hidden'; - docElement.appendChild(body); - } - - ret = callback(div, rule); - // If this is done after page load we don't want to remove the body so check if body exists - if (body.fake) { - body.parentNode.removeChild(body); - docElement.style.overflow = docOverflow; - // Trigger layout so kinetic scrolling isn't disabled in iOS6+ - docElement.offsetHeight; - } else { - div.parentNode.removeChild(div); - } - - return !!ret; - - } - - ; - - /** - * nativeTestProps allows for us to use native feature detection functionality if available. - * some prefixed form, or false, in the case of an unsupported rule - * - * @access private - * @function nativeTestProps - * @param {array} props - An array of property names - * @param {string} value - A string representing the value we want to check via @supports - * @returns {boolean|undefined} A boolean when @supports exists, undefined otherwise - */ - - // Accepts a list of property names and a single value - // Returns `undefined` if native detection not available - function nativeTestProps(props, value) { - var i = props.length; - // Start with the JS API: http://www.w3.org/TR/css3-conditional/#the-css-interface - if ('CSS' in window && 'supports' in window.CSS) { - // Try every prefixed variant of the property - while (i--) { - if (window.CSS.supports(domToCSS(props[i]), value)) { - return true; - } - } - return false; - } - // Otherwise fall back to at-rule (for Opera 12.x) - else if ('CSSSupportsRule' in window) { - // Build a condition string for every prefixed variant - var conditionText = []; - while (i--) { - conditionText.push('(' + domToCSS(props[i]) + ':' + value + ')'); - } - conditionText = conditionText.join(' or '); - return injectElementWithStyles('@supports (' + conditionText + ') { #modernizr { position: absolute; } }', function(node) { - return getComputedStyle(node, null).position == 'absolute'; - }); - } - return undefined; - } - ; - - // testProps is a generic CSS / DOM property test. - - // In testing support for a given CSS property, it's legit to test: - // `elem.style[styleName] !== undefined` - // If the property is supported it will return an empty string, - // if unsupported it will return undefined. - - // We'll take advantage of this quick test and skip setting a style - // on our modernizr element, but instead just testing undefined vs - // empty string. - - // Property names can be provided in either camelCase or kebab-case. - - function testProps(props, prefixed, value, skipValueTest) { - skipValueTest = is(skipValueTest, 'undefined') ? false : skipValueTest; - - // Try native detect first - if (!is(value, 'undefined')) { - var result = nativeTestProps(props, value); - if (!is(result, 'undefined')) { - return result; - } - } - - // Otherwise do it properly - var afterInit, i, propsLength, prop, before; - - // If we don't have a style element, that means we're running async or after - // the core tests, so we'll need to create our own elements to use - - // inside of an SVG element, in certain browsers, the `style` element is only - // defined for valid tags. Therefore, if `modernizr` does not have one, we - // fall back to a less used element and hope for the best. - var elems = ['modernizr', 'tspan']; - while (!mStyle.style) { - afterInit = true; - mStyle.modElem = createElement(elems.shift()); - mStyle.style = mStyle.modElem.style; - } - - // Delete the objects if we created them. - function cleanElems() { - if (afterInit) { - delete mStyle.style; - delete mStyle.modElem; - } - } - - propsLength = props.length; - for (i = 0; i < propsLength; i++) { - prop = props[i]; - before = mStyle.style[prop]; - - if (contains(prop, '-')) { - prop = cssToDOM(prop); - } - - if (mStyle.style[prop] !== undefined) { - - // If value to test has been passed in, do a set-and-check test. - // 0 (integer) is a valid property value, so check that `value` isn't - // undefined, rather than just checking it's truthy. - if (!skipValueTest && !is(value, 'undefined')) { - - // Needs a try catch block because of old IE. This is slow, but will - // be avoided in most cases because `skipValueTest` will be used. - try { - mStyle.style[prop] = value; - } catch (e) {} - - // If the property value has changed, we assume the value used is - // supported. If `value` is empty string, it'll fail here (because - // it hasn't changed), which matches how browsers have implemented - // CSS.supports() - if (mStyle.style[prop] != before) { - cleanElems(); - return prefixed == 'pfx' ? prop : true; - } - } - // Otherwise just return true, or the property name if this is a - // `prefixed()` call - else { - cleanElems(); - return prefixed == 'pfx' ? prop : true; - } - } - } - cleanElems(); - return false; - } - - ; - - /** - * testProp() investigates whether a given style property is recognized - * Property names can be provided in either camelCase or kebab-case. - * - * @memberof Modernizr - * @name Modernizr.testProp - * @access public - * @optionName Modernizr.testProp() - * @optionProp testProp - * @function testProp - * @param {string} prop - Name of the CSS property to check - * @param {string} [value] - Name of the CSS value to check - * @param {boolean} [useValue] - Whether or not to check the value if @supports isn't supported - * @returns {boolean} - * @example - * - * Just like [testAllProps](#modernizr-testallprops), only it does not check any vendor prefixed - * version of the string. - * - * Note that the property name must be provided in camelCase (e.g. boxSizing not box-sizing) - * - * ```js - * Modernizr.testProp('pointerEvents') // true - * ``` - * - * You can also provide a value as an optional second argument to check if a - * specific value is supported - * - * ```js - * Modernizr.testProp('pointerEvents', 'none') // true - * Modernizr.testProp('pointerEvents', 'penguin') // false - * ``` - */ - - var testProp = ModernizrProto.testProp = function(prop, value, useValue) { - return testProps([prop], undefined, value, useValue); - }; - -/*! -{ - "name": "CSS textshadow", - "property": "textshadow", - "caniuse": "css-textshadow", - "tags": ["css"], - "knownBugs": ["FF3.0 will false positive on this test"] -} -!*/ - - Modernizr.addTest('textshadow', testProp('textShadow', '1px 1px')); - - - /** - * testPropsAll tests a list of DOM properties we want to check against. - * We specify literally ALL possible (known and/or likely) properties on - * the element including the non-vendor prefixed one, for forward- - * compatibility. - * - * @access private - * @function testPropsAll - * @param {string} prop - A string of the property to test for - * @param {string|object} [prefixed] - An object to check the prefixed properties on. Use a string to skip - * @param {HTMLElement|SVGElement} [elem] - An element used to test the property and value against - * @param {string} [value] - A string of a css value - * @param {boolean} [skipValueTest] - An boolean representing if you want to test if value sticks when set - */ - function testPropsAll(prop, prefixed, elem, value, skipValueTest) { - - var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1), - props = (prop + ' ' + cssomPrefixes.join(ucProp + ' ') + ucProp).split(' '); - - // did they call .prefixed('boxSizing') or are we just testing a prop? - if (is(prefixed, 'string') || is(prefixed, 'undefined')) { - return testProps(props, prefixed, value, skipValueTest); - - // otherwise, they called .prefixed('requestAnimationFrame', window[, elem]) - } else { - props = (prop + ' ' + (domPrefixes).join(ucProp + ' ') + ucProp).split(' '); - return testDOMProps(props, prefixed, elem); - } - } - - // Modernizr.testAllProps() investigates whether a given style property, - // or any of its vendor-prefixed variants, is recognized - // - // Note that the property names must be provided in the camelCase variant. - // Modernizr.testAllProps('boxSizing') - ModernizrProto.testAllProps = testPropsAll; - - - - /** - * testAllProps determines whether a given CSS property is supported in the browser - * - * @memberof Modernizr - * @name Modernizr.testAllProps - * @optionName Modernizr.testAllProps() - * @optionProp testAllProps - * @access public - * @function testAllProps - * @param {string} prop - String naming the property to test (either camelCase or kebab-case) - * @param {string} [value] - String of the value to test - * @param {boolean} [skipValueTest=false] - Whether to skip testing that the value is supported when using non-native detection - * @example - * - * testAllProps determines whether a given CSS property, in some prefixed form, - * is supported by the browser. - * - * ```js - * testAllProps('boxSizing') // true - * ``` - * - * It can optionally be given a CSS value in string form to test if a property - * value is valid - * - * ```js - * testAllProps('display', 'block') // true - * testAllProps('display', 'penguin') // false - * ``` - * - * A boolean can be passed as a third parameter to skip the value check when - * native detection (@supports) isn't available. - * - * ```js - * testAllProps('shapeOutside', 'content-box', true); - * ``` - */ - - function testAllProps(prop, value, skipValueTest) { - return testPropsAll(prop, undefined, undefined, value, skipValueTest); - } - ModernizrProto.testAllProps = testAllProps; - -/*! -{ - "name": "CSS Animations", - "property": "cssanimations", - "caniuse": "css-animation", - "polyfills": ["transformie", "csssandpaper"], - "tags": ["css"], - "warnings": ["Android < 4 will pass this test, but can only animate a single property at a time"], - "notes": [{ - "name" : "Article: 'Dispelling the Android CSS animation myths'", - "href": "https://goo.gl/OGw5Gm" - }] -} -!*/ -/* DOC -Detects whether or not elements can be animated using CSS -*/ - - Modernizr.addTest('cssanimations', testAllProps('animationName', 'a', true)); - -/*! -{ - "name": "Background Size", - "property": "backgroundsize", - "tags": ["css"], - "knownBugs": ["This will false positive in Opera Mini - https://github.com/Modernizr/Modernizr/issues/396"], - "notes": [{ - "name": "Related Issue", - "href": "https://github.com/Modernizr/Modernizr/issues/396" - }] -} -!*/ - - Modernizr.addTest('backgroundsize', testAllProps('backgroundSize', '100%', true)); - -/*! -{ - "name": "CSS Transitions", - "property": "csstransitions", - "caniuse": "css-transitions", - "tags": ["css"] -} -!*/ - - Modernizr.addTest('csstransitions', testAllProps('transition', 'all', true)); - -/*! -{ - "name": "Box Shadow", - "property": "boxshadow", - "caniuse": "css-boxshadow", - "tags": ["css"], - "knownBugs": [ - "WebOS false positives on this test.", - "The Kindle Silk browser false positives" - ] -} -!*/ - - Modernizr.addTest('boxshadow', testAllProps('boxShadow', '1px 1px', true)); - - - // Run each test - testRunner(); - - // Remove the "no-js" class if it exists - setClasses(classes); - - delete ModernizrProto.addTest; - delete ModernizrProto.addAsyncTest; - - // Run the things that are supposed to run after the tests - for (var i = 0; i < Modernizr._q.length; i++) { - Modernizr._q[i](); - } - - // Leak Modernizr namespace - window.Modernizr = Modernizr; - - -; - -})(window, document); \ No newline at end of file diff --git a/vendors/normalize.css/CHANGELOG.md b/vendors/normalize.css/CHANGELOG.md new file mode 100644 index 0000000000..922f6e38c8 --- /dev/null +++ b/vendors/normalize.css/CHANGELOG.md @@ -0,0 +1,175 @@ +# Changes to normalize.css + +### 8.0.1 (November 4, 2018) + +* Fix regression in IE rendering of `main` element. + +### 8.0.0 (February 2, 2018) + +* Remove support for older browsers Android 4, lte IE 9, lte Safari 7. +* Don't remove search input cancel button in Chrome/Safari. +* Form inputs inherit `font-family`. +* Fix text decoration in Safari 8+. + +### 7.0.0 (May 2, 2017) + +* Revert changes in `body` and form elements styles introduced by v6 + +### 6.0.0 (March 26, 2017) + +* Remove all opinionated rules +* Correct document heading comment +* Update `abbr[title]` support + +### 5.0.0 (October 3, 2016) + +* Add normalized sections not already present from + https://html.spec.whatwg.org/multipage/. +* Move unsorted rules into their respective sections. +* Update the `summary` style in all browsers. +* Remove `::placeholder` styles due to a bug in Edge. +* More explicitly define font resets on form controls. +* Remove the `optgroup` normalization needed by the previous font reset. +* Update text-size-adjust documentation
 for IE on Windows Phone +* Update OS X reference to macOS +* Update the semver strategy. + +### 4.2.0 (June 30, 2016) + +* Correct the `line-height` in all browsers. +* Restore `optgroup` font inheritance. +* Update normalize.css heading. + +### 4.1.1 (April 12, 2016) + +* Update normalize.css heading. + +### 4.1.0 (April 11, 2016) + +* Normalize placeholders in Chrome, Edge, and Safari. +* Normalize `text-decoration-skip` property in Safari. +* Normalize file select buttons. +* Normalize search input outlines in Safari. +* Limit Firefox focus normalizations to buttons. +* Restore `main` to package.json. +* Restore proper overflow to certain `select` elements. +* Remove opinionated cursor styles on buttons. +* Update stylelint configuration. +* Update tests. + +### 4.0.0 (March 19, 2016) + +* Add the correct font weight for `b` and `strong` in Chrome, Edge, and Safari. +* Correct inconsistent `overflow` for `hr` in Edge and IE. +* Correct inconsistent `box-sizing` for `hr` in Firefox. +* Correct inconsistent `text-decoration` and `border-bottom` for `abbr[title]` + in Chrome, Edge, Firefox IE, Opera, and Safari. +* Correct inheritance and scaling of `font-size` for preformatted text. +* Correct `legend` text wrapping not present in Edge and IE. +* Remove unnecessary normalization of `line-height` for `input`. +* Remove unnecessary normalization of `color` for form controls. +* Remove unnecessary `box-sizing` for `input[type="search"]` in Chrome, Edge, + Firefox, IE, and Safari. +* Remove opinionated table resets. +* Remove opinionated `pre` overflow. +* Remove selector weight from some input selectors. +* Update normalization of `border-style` for `img`. +* Update normalization of `color` inheritance for `legend`. +* Update normalization of `background-color` for `mark`. +* Update normalization of `outline` for `:-moz-focusring` removed by a previous + normalization in Firefox. +* Update opinionated style of `outline-width` for `a:active` and `a:hover`. +* Update comments to identify opinionated styles. +* Update comments to specify browser/versions affected by all changes. +* Update comments to use one voice. + +--- + +### 3.0.3 (March 30, 2015) + +* Remove unnecessary vendor prefixes. +* Add `main` property. + +### 3.0.2 (October 4, 2014) + +* Only alter `background-color` of links in IE 10. +* Add `menu` element to HTML5 display definitions. + +### 3.0.1 (March 27, 2014) + +* Add package.json for npm support. + +### 3.0.0 (January 28, 2014) + +### 3.0.0-rc.1 (January 26, 2014) + +* Explicit tests for each normalization. +* Fix i18n for `q` element. +* Fix `pre` text formatting and overflow. +* Fix vertical alignment of `progress`. +* Address `button` overflow in IE 8/9/10. +* Revert `textarea` alignment modification. +* Fix number input button cursor in Chrome on OS X. +* Remove `a:focus` outline normalization. +* Fix `figure` margin normalization. +* Normalize `optgroup`. +* Remove default table cell padding. +* Set correct display for `progress` in IE 8/9. +* Fix `font` and `color` inheritance for forms. + +--- + +### 2.1.3 (August 26, 2013) + +* Fix component.json. +* Remove the gray background color from active links in IE 10. + +### 2.1.2 (May 11, 2013) + +* Revert root `color` and `background` normalizations. + +### 2.1.1 (April 8, 2013) + +* Normalize root `color` and `background` to counter the effects of system + color schemes. + +### 2.1.0 (January 21, 2013) + +* Normalize `text-transform` for `button` and `select`. +* Normalize `h1` margin when within HTML5 sectioning elements. +* Normalize `hr` element. +* Remove unnecessary `pre` styles. +* Add `main` element to HTML5 display definitions. +* Fix cursor style for disabled button `input`. + +### 2.0.1 (August 20, 2012) + +* Remove stray IE 6/7 `inline-block` hack from HTML5 display settings. + +### 2.0.0 (August 19, 2012) + +* Remove legacy browser form normalizations. +* Remove all list normalizations. +* Add `quotes` normalizations. +* Remove all heading normalizations except `h1` font size. +* Form elements automatically inherit `font-family` from ancestor. +* Drop support for IE 6/7, Firefox < 4, and Safari < 5. + +--- + +### 1.0.1 (August 19, 2012) + +* Adjust `small` font size normalization. + +### 1.0.0 (August 14, 2012) + +(Only the notable changes since public release) + +* Add MIT License. +* Hide `audio` elements without controls in iOS 5. +* Normalize heading margins and font size. +* Move font-family normalization from `body` to `html`. +* Remove scrollbar normalization. +* Remove excess padding from checkbox and radio inputs in IE 7. +* Add IE9 correction for SVG overflow. +* Add fix for legend not inheriting color in IE 6/7/8/9. diff --git a/vendors/normalize.css/LICENSE.md b/vendors/normalize.css/LICENSE.md new file mode 100644 index 0000000000..43b5ddcc90 --- /dev/null +++ b/vendors/normalize.css/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright © Nicolas Gallagher and Jonathan Neal + +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/vendors/normalize.css/README.md b/vendors/normalize.css/README.md new file mode 100644 index 0000000000..71954f2306 --- /dev/null +++ b/vendors/normalize.css/README.md @@ -0,0 +1,102 @@ +# normalize.css + +
    Normalize Logo + +> A modern alternative to CSS resets + +[![npm][npm-image]][npm-url] [![license][license-image]][license-url] +[![changelog][changelog-image]][changelog-url] +[![gitter][gitter-image]][gitter-url] + + +**NPM** + +```sh +npm install --save normalize.css +``` + +**CDN** + +See https://yarnpkg.com/en/package/normalize.css + +**Download** + +See https://necolas.github.io/normalize.css/latest/normalize.css + + +## What does it do? + +* Preserves useful defaults, unlike many CSS resets. +* Normalizes styles for a wide range of elements. +* Corrects bugs and common browser inconsistencies. +* Improves usability with subtle modifications. +* Explains what code does using detailed comments. + + +## Browser support + +* Chrome +* Edge +* Firefox ESR+ +* Internet Explorer 10+ +* Safari 8+ +* Opera + + +## Extended details and known issues + +Additional detail and explanation of the esoteric parts of normalize.css. + +#### `pre, code, kbd, samp` + +The `font-family: monospace, monospace` hack fixes the inheritance and scaling +of font-size for preformatted text. The duplication of `monospace` is +intentional. [Source](https://en.wikipedia.org/wiki/User:Davidgothberg/Test59). + +#### `sub, sup` + +Normally, using `sub` or `sup` affects the line-box height of text in all +browsers. [Source](https://gist.github.com/413930). + +#### `select` + +By default, Chrome on OS X and Safari on OS X allow very limited styling of +`select`, unless a border property is set. The default font weight on `optgroup` +elements cannot safely be changed in Chrome on OSX and Safari on OS X. + +#### `[type="checkbox"]` + +It is recommended that you do not style checkbox and radio inputs as Firefox's +implementation does not respect box-sizing, padding, or width. + +#### `[type="number"]` + +Certain font size values applied to number inputs cause the cursor style of the +decrement button to change from `default` to `text`. + +#### `[type="search"]` + +The search input is not fully stylable by default. In Chrome and Safari on +OSX/iOS you can't control `font`, `padding`, `border`, or `background`. In +Chrome and Safari on Windows you can't control `border` properly. It will apply +`border-width` but will only show a border color (which cannot be controlled) +for the outer 1px of that border. Applying `-webkit-appearance: textfield` +addresses these issues without removing the benefits of search inputs (e.g. +showing past searches). + +## Contributing + +Please read the [contribution guidelines](CONTRIBUTING.md) in order to make the +contribution process easy and effective for everyone involved. + + +[changelog-image]: https://img.shields.io/badge/changelog-md-blue.svg?style=flat-square +[changelog-url]: CHANGELOG.md +[license-image]: https://img.shields.io/npm/l/normalize.css.svg?style=flat-square +[license-url]: LICENSE.md +[npm-image]: https://img.shields.io/npm/v/normalize.css.svg?style=flat-square +[npm-url]: https://www.npmjs.com/package/normalize.css +[gitter-image]: https://img.shields.io/badge/chat-gitter-blue.svg?style=flat-square +[gitter-url]: https://gitter.im/necolas/normalize.css diff --git a/vendors/normalize.css/normalize.css b/vendors/normalize.css/normalize.css new file mode 100644 index 0000000000..92aaf10c12 --- /dev/null +++ b/vendors/normalize.css/normalize.css @@ -0,0 +1,90 @@ +/* normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Text-level semantics + ========================================================================== */ + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Forms + ========================================================================== */ + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} diff --git a/vendors/openpgp-5/README.md b/vendors/openpgp-5/README.md new file mode 100644 index 0000000000..1f719ee4e3 --- /dev/null +++ b/vendors/openpgp-5/README.md @@ -0,0 +1,683 @@ +OpenPGP.js [![BrowserStack Status](https://automate.browserstack.com/badge.svg?badge_key=N1l2eHFOanVBMU9wYWxJM3ZnWERnc1lidkt5UkRqa3BralV3SWVhOGpGTT0tLVljSjE4Z3dzVmdiQjl6RWgxb2c3T2c9PQ==--5864052cd523f751b6b907d547ac9c4c5f88c8a3)](https://automate.browserstack.com/public-build/N1l2eHFOanVBMU9wYWxJM3ZnWERnc1lidkt5UkRqa3BralV3SWVhOGpGTT0tLVljSjE4Z3dzVmdiQjl6RWgxb2c3T2c9PQ==--5864052cd523f751b6b907d547ac9c4c5f88c8a3) [![Join the chat on Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/openpgpjs/openpgpjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +========== + +[OpenPGP.js](https://openpgpjs.org/) is a JavaScript implementation of the OpenPGP protocol. It implements [RFC4880](https://tools.ietf.org/html/rfc4880) and parts of [RFC4880bis](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10). + +**Table of Contents** + +- [OpenPGP.js](#openpgpjs) + - [Platform Support](#platform-support) + - [Performance](#performance) + - [Getting started](#getting-started) + - [Node.js](#nodejs) + - [Deno (experimental)](#deno-experimental) + - [Browser (webpack)](#browser-webpack) + - [Browser (plain files)](#browser-plain-files) + - [Examples](#examples) + - [Encrypt and decrypt *Uint8Array* data with a password](#encrypt-and-decrypt-uint8array-data-with-a-password) + - [Encrypt and decrypt *String* data with PGP keys](#encrypt-and-decrypt-string-data-with-pgp-keys) + - [Encrypt symmetrically with compression](#encrypt-symmetrically-with-compression) + - [Streaming encrypt *Uint8Array* data with a password](#streaming-encrypt-uint8array-data-with-a-password) + - [Streaming encrypt and decrypt *String* data with PGP keys](#streaming-encrypt-and-decrypt-string-data-with-pgp-keys) + - [Generate new key pair](#generate-new-key-pair) + - [Revoke a key](#revoke-a-key) + - [Sign and verify cleartext messages](#sign-and-verify-cleartext-messages) + - [Create and verify *detached* signatures](#create-and-verify-detached-signatures) + - [Streaming sign and verify *Uint8Array* data](#streaming-sign-and-verify-uint8array-data) + - [Documentation](#documentation) + - [Security Audit](#security-audit) + - [Security recommendations](#security-recommendations) + - [Development](#development) + - [How do I get involved?](#how-do-i-get-involved) + - [License](#license) + +### Platform Support + +* The `dist/openpgp.min.js` bundle works well with recent versions of Chrome, Firefox, Safari and Edge. + +* The `dist/node/openpgp.min.js` bundle works well in Node.js. It is used by default when you `require('openpgp')` in Node.js. + +* Currently, Chrome, Safari and Edge have partial implementations of the +[Streams specification](https://streams.spec.whatwg.org/), and Firefox +has a partial implementation behind feature flags. Chrome is the only +browser that implements `TransformStream`s, which we need, so we include +a [polyfill](https://github.com/MattiasBuelens/web-streams-polyfill) for +all other browsers. Please note that in those browsers, the global +`ReadableStream` property gets overwritten with the polyfill version if +it exists. In some edge cases, you might need to use the native +`ReadableStream` (for example when using it to create a `Response` +object), in which case you should store a reference to it before loading +OpenPGP.js. There is also the +[web-streams-adapter](https://github.com/MattiasBuelens/web-streams-adapter) +library to convert back and forth between them. + +### Performance + +* Version 3.0.0 of the library introduces support for public-key cryptography using [elliptic curves](https://wiki.gnupg.org/ECC). We use native implementations on browsers and Node.js when available. Elliptic curve cryptography provides stronger security per bits of key, which allows for much faster operations. Currently the following curves are supported: + + | Curve | Encryption | Signature | NodeCrypto | WebCrypto | Constant-Time | + |:---------------:|:----------:|:---------:|:----------:|:---------:|:-----------------:| + | curve25519 | ECDH | N/A | No | No | Algorithmically** | + | ed25519 | N/A | EdDSA | No | No | Algorithmically** | + | p256 | ECDH | ECDSA | Yes* | Yes* | If native*** | + | p384 | ECDH | ECDSA | Yes* | Yes* | If native*** | + | p521 | ECDH | ECDSA | Yes* | Yes* | If native*** | + | brainpoolP256r1 | ECDH | ECDSA | Yes* | No | If native*** | + | brainpoolP384r1 | ECDH | ECDSA | Yes* | No | If native*** | + | brainpoolP512r1 | ECDH | ECDSA | Yes* | No | If native*** | + | secp256k1 | ECDH | ECDSA | Yes* | No | If native*** | + + \* when available + \** the curve25519 and ed25519 implementations are algorithmically constant-time, but may not be constant-time after optimizations of the JavaScript compiler + \*** these curves are only constant-time if the underlying native implementation is available and constant-time + +* Version 2.x of the library has been built from the ground up with Uint8Arrays. This allows for much better performance and memory usage than strings. + +* If the user's browser supports [native WebCrypto](https://caniuse.com/#feat=cryptography) via the `window.crypto.subtle` API, this will be used. Under Node.js the native [crypto module](https://nodejs.org/api/crypto.html#crypto_crypto) is used. + +* The library implements the [RFC4880bis proposal](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10) for authenticated encryption using native AES-EAX, OCB, or GCM. This makes symmetric encryption up to 30x faster on supported platforms. Since the specification has not been finalized and other OpenPGP implementations haven't adopted it yet, the feature is currently behind a flag. **Note: activating this setting can break compatibility with other OpenPGP implementations, and also with future versions of OpenPGP.js. Don't use it with messages you want to store on disk or in a database.** You can enable it by setting `openpgp.config.aeadProtect = true`. + + You can change the AEAD mode by setting one of the following options: + + ``` + openpgp.config.preferredAEADAlgorithm = openpgp.enums.aead.eax // Default, native + openpgp.config.preferredAEADAlgorithm = openpgp.enums.aead.ocb // Non-native + openpgp.config.preferredAEADAlgorithm = openpgp.enums.aead.experimentalGCM // **Non-standard**, fastest + ``` + +* For environments that don't provide native crypto, the library falls back to [asm.js](https://caniuse.com/#feat=asmjs) implementations of AES, SHA-1, and SHA-256. + + +### Getting started + +#### Node.js + +Install OpenPGP.js using npm and save it in your dependencies: + +```sh +npm install --save openpgp +``` + +And import it as a CommonJS module: + +```js +const openpgp = require('openpgp'); +``` + +Or as an ES6 module, from an .mjs file: + +```js +import * as openpgp from 'openpgp'; +``` + +#### Deno (experimental) + +Import as an ES6 module, using /dist/openpgp.mjs. + +```js +import * as openpgp from './openpgpjs/dist/openpgp.mjs'; +``` + +#### Browser (webpack) + +Install OpenPGP.js using npm and save it in your devDependencies: + +```sh +npm install --save-dev openpgp +``` + +And import it as an ES6 module: + +```js +import * as openpgp from 'openpgp'; +``` + +You can also only import the functions you need, as follows: + +```js +import { readMessage, decrypt } from 'openpgp'; +``` + +Or, if you want to use the lightweight build (which is smaller, and lazily loads non-default curves on demand): + +```js +import * as openpgp from 'openpgp/lightweight'; +``` + +To test whether the lazy loading works, try to generate a key with a non-standard curve: + +```js +import { generateKey } from 'openpgp/lightweight'; +await generateKey({ curve: 'brainpoolP512r1', userIDs: [{ name: 'Test', email: 'test@test.com' }] }); +``` + +For more examples of how to generate a key, see [Generate new key pair](#generate-new-key-pair). It is recommended to use `curve25519` instead of `brainpoolP512r1` by default. + + +#### Browser (plain files) + +Grab `openpgp.min.js` from [unpkg.com/openpgp/dist](https://unpkg.com/openpgp/dist/), and load it in a script tag: + +```html + +``` + +Or, to load OpenPGP.js as an ES6 module, grab `openpgp.min.mjs` from [unpkg.com/openpgp/dist](https://unpkg.com/openpgp/dist/), and import it as follows: + +```html + +``` + +To offload cryptographic operations off the main thread, you can implement a Web Worker in your application and load OpenPGP.js from there. For an example Worker implementation, see `test/worker/worker_example.js`. + +#### TypeScript + +Since TS is not fully integrated in the library, TS-only dependencies are currently listed as `devDependencies`, so to compile the project you’ll need to add `@openpgp/web-stream-tools` manually (NB: only versions below v0.12 are compatible with OpenPGP.js v5): + +```sh +npm install --save-dev @openpgp/web-stream-tools@0.0.11-patch-0 +``` + +If you notice missing or incorrect type definitions, feel free to open a PR. + +### Examples + +Here are some examples of how to use OpenPGP.js v5. For more elaborate examples and working code, please check out the [public API unit tests](https://github.com/openpgpjs/openpgpjs/blob/main/test/general/openpgp.js). If you're upgrading from v4 it might help to check out the [changelog](https://github.com/openpgpjs/openpgpjs/wiki/V5-Changelog) and [documentation](https://github.com/openpgpjs/openpgpjs#documentation). + +#### Encrypt and decrypt *Uint8Array* data with a password + +Encryption will use the algorithm specified in config.preferredSymmetricAlgorithm (defaults to aes256), and decryption will use the algorithm used for encryption. + +```js +(async () => { + const message = await openpgp.createMessage({ binary: new Uint8Array([0x01, 0x01, 0x01]) }); + const encrypted = await openpgp.encrypt({ + message, // input as Message object + passwords: ['secret stuff'], // multiple passwords possible + format: 'binary' // don't ASCII armor (for Uint8Array output) + }); + console.log(encrypted); // Uint8Array + + const encryptedMessage = await openpgp.readMessage({ + binaryMessage: encrypted // parse encrypted bytes + }); + const { data: decrypted } = await openpgp.decrypt({ + message: encryptedMessage, + passwords: ['secret stuff'], // decrypt with password + format: 'binary' // output as Uint8Array + }); + console.log(decrypted); // Uint8Array([0x01, 0x01, 0x01]) +})(); +``` + +#### Encrypt and decrypt *String* data with PGP keys + +Encryption will use the algorithm preferred by the public (encryption) key (defaults to aes256 for keys generated in OpenPGP.js), and decryption will use the algorithm used for encryption. + +```js +const openpgp = require('openpgp'); // use as CommonJS, AMD, ES6 module or via window.openpgp + +(async () => { + // put keys in backtick (``) to avoid errors caused by spaces or tabs + const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK----- +... +-----END PGP PUBLIC KEY BLOCK-----`; + const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- +... +-----END PGP PRIVATE KEY BLOCK-----`; // encrypted private key + const passphrase = `yourPassphrase`; // what the private key is encrypted with + + const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); + + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }), + passphrase + }); + + const encrypted = await openpgp.encrypt({ + message: await openpgp.createMessage({ text: 'Hello, World!' }), // input as Message object + encryptionKeys: publicKey, + signingKeys: privateKey // optional + }); + console.log(encrypted); // '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----' + + const message = await openpgp.readMessage({ + armoredMessage: encrypted // parse armored message + }); + const { data: decrypted, signatures } = await openpgp.decrypt({ + message, + verificationKeys: publicKey, // optional + decryptionKeys: privateKey + }); + console.log(decrypted); // 'Hello, World!' + // check signature validity (signed messages only) + try { + await signatures[0].verified; // throws on invalid signature + console.log('Signature is valid'); + } catch (e) { + throw new Error('Signature could not be verified: ' + e.message); + } +})(); +``` + +Encrypt to multiple public keys: + +```js +(async () => { + const publicKeysArmored = [ + `-----BEGIN PGP PUBLIC KEY BLOCK----- +... +-----END PGP PUBLIC KEY BLOCK-----`, + `-----BEGIN PGP PUBLIC KEY BLOCK----- +... +-----END PGP PUBLIC KEY BLOCK-----` + ]; + const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- +... +-----END PGP PRIVATE KEY BLOCK-----`; // encrypted private key + const passphrase = `yourPassphrase`; // what the private key is encrypted with + const plaintext = 'Hello, World!'; + + const publicKeys = await Promise.all(publicKeysArmored.map(armoredKey => openpgp.readKey({ armoredKey }))); + + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readKey({ armoredKey: privateKeyArmored }), + passphrase + }); + + const message = await openpgp.createMessage({ text: plaintext }); + const encrypted = await openpgp.encrypt({ + message, // input as Message object + encryptionKeys: publicKeys, + signingKeys: privateKey // optional + }); + console.log(encrypted); // '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----' +})(); +``` + +If you expect an encrypted message to be signed with one of the public keys you have, and do not want to trust the decrypted data otherwise, you can pass the decryption option `expectSigned = true`, so that the decryption operation will fail if no valid signature is found: +```js +(async () => { + // put keys in backtick (``) to avoid errors caused by spaces or tabs + const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK----- +... +-----END PGP PUBLIC KEY BLOCK-----`; + const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- +... +-----END PGP PRIVATE KEY BLOCK-----`; // encrypted private key + const passphrase = `yourPassphrase`; // what the private key is encrypted with + + const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); + + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }), + passphrase + }); + + const encryptedAndSignedMessage = `-----BEGIN PGP MESSAGE----- +... +-----END PGP MESSAGE-----`; + + const message = await openpgp.readMessage({ + armoredMessage: encryptedAndSignedMessage // parse armored message + }); + // decryption will fail if all signatures are invalid or missing + const { data: decrypted, signatures } = await openpgp.decrypt({ + message, + decryptionKeys: privateKey, + expectSigned: true, + verificationKeys: publicKey, // mandatory with expectSigned=true + }); + console.log(decrypted); // 'Hello, World!' +})(); +``` + +#### Encrypt symmetrically with compression + +By default, `encrypt` will not use any compression when encrypting symmetrically only (i.e. when no `encryptionKeys` are given). +It's possible to change that behaviour by enabling compression through the config, either for the single encryption: + +```js +(async () => { + const message = await openpgp.createMessage({ binary: new Uint8Array([0x01, 0x02, 0x03]) }); // or createMessage({ text: 'string' }) + const encrypted = await openpgp.encrypt({ + message, + passwords: ['secret stuff'], // multiple passwords possible + config: { preferredCompressionAlgorithm: openpgp.enums.compression.zlib } // compress the data with zlib + }); +})(); +``` + +or by changing the default global configuration: +```js +openpgp.config.preferredCompressionAlgorithm = openpgp.enums.compression.zlib +``` + +Where the value can be any of: + * `openpgp.enums.compression.zip` + * `openpgp.enums.compression.zlib` + * `openpgp.enums.compression.uncompressed` (default) + + + +#### Streaming encrypt *Uint8Array* data with a password + +```js +(async () => { + const readableStream = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array([0x01, 0x02, 0x03])); + controller.close(); + } + }); + + const message = await openpgp.createMessage({ binary: readableStream }); + const encrypted = await openpgp.encrypt({ + message, // input as Message object + passwords: ['secret stuff'], // multiple passwords possible + format: 'binary' // don't ASCII armor (for Uint8Array output) + }); + console.log(encrypted); // raw encrypted packets as ReadableStream + + // Either pipe the above stream somewhere, pass it to another function, + // or read it manually as follows: + for await (const chunk of encrypted) { + console.log('new chunk:', chunk); // Uint8Array + } +})(); +``` + +For more information on using ReadableStreams, see [the MDN Documentation on the +Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API). + +You can also pass a [Node.js `Readable` +stream](https://nodejs.org/api/stream.html#stream_class_stream_readable), in +which case OpenPGP.js will return a Node.js `Readable` stream as well, which you +can `.pipe()` to a `Writable` stream, for example. + + +#### Streaming encrypt and decrypt *String* data with PGP keys + +```js +(async () => { + const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK----- +... +-----END PGP PUBLIC KEY BLOCK-----`; // Public key + const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- +... +-----END PGP PRIVATE KEY BLOCK-----`; // Encrypted private key + const passphrase = `yourPassphrase`; // Password that private key is encrypted with + + const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); + + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }), + passphrase + }); + + const readableStream = new ReadableStream({ + start(controller) { + controller.enqueue('Hello, world!'); + controller.close(); + } + }); + + const encrypted = await openpgp.encrypt({ + message: await openpgp.createMessage({ text: readableStream }), // input as Message object + encryptionKeys: publicKey, + signingKeys: privateKey // optional + }); + console.log(encrypted); // ReadableStream containing '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----' + + const message = await openpgp.readMessage({ + armoredMessage: encrypted // parse armored message + }); + const decrypted = await openpgp.decrypt({ + message, + verificationKeys: publicKey, // optional + decryptionKeys: privateKey + }); + const chunks = []; + for await (const chunk of decrypted.data) { + chunks.push(chunk); + } + const plaintext = chunks.join(''); + console.log(plaintext); // 'Hello, World!' +})(); +``` + + +#### Generate new key pair + +ECC keys (smaller and faster to generate): + +Possible values for `curve` are: `curve25519`, `ed25519`, `p256`, `p384`, `p521`, +`brainpoolP256r1`, `brainpoolP384r1`, `brainpoolP512r1`, and `secp256k1`. +Note that both the `curve25519` and `ed25519` options generate a primary key for signing using Ed25519 +and a subkey for encryption using Curve25519. + +```js +(async () => { + const { privateKey, publicKey, revocationCertificate } = await openpgp.generateKey({ + type: 'ecc', // Type of the key, defaults to ECC + curve: 'curve25519', // ECC curve name, defaults to curve25519 + userIDs: [{ name: 'Jon Smith', email: 'jon@example.com' }], // you can pass multiple user IDs + passphrase: 'super long and hard to guess secret', // protects the private key + format: 'armored' // output key format, defaults to 'armored' (other options: 'binary' or 'object') + }); + + console.log(privateKey); // '-----BEGIN PGP PRIVATE KEY BLOCK ... ' + console.log(publicKey); // '-----BEGIN PGP PUBLIC KEY BLOCK ... ' + console.log(revocationCertificate); // '-----BEGIN PGP PUBLIC KEY BLOCK ... ' +})(); +``` + +RSA keys (increased compatibility): + +```js +(async () => { + const { privateKey, publicKey } = await openpgp.generateKey({ + type: 'rsa', // Type of the key + rsaBits: 4096, // RSA key size (defaults to 4096 bits) + userIDs: [{ name: 'Jon Smith', email: 'jon@example.com' }], // you can pass multiple user IDs + passphrase: 'super long and hard to guess secret' // protects the private key + }); +})(); +``` + +#### Revoke a key + +Using a revocation certificate: +```js +(async () => { + const { publicKey: revokedKeyArmored } = await openpgp.revokeKey({ + key: await openpgp.readKey({ armoredKey: publicKeyArmored }), + revocationCertificate, + format: 'armored' // output armored keys + }); + console.log(revokedKeyArmored); // '-----BEGIN PGP PUBLIC KEY BLOCK ... ' +})(); +``` + +Using the private key: +```js +(async () => { + const { publicKey: revokedKeyArmored } = await openpgp.revokeKey({ + key: await openpgp.readKey({ armoredKey: privateKeyArmored }), + format: 'armored' // output armored keys + }); + console.log(revokedKeyArmored); // '-----BEGIN PGP PUBLIC KEY BLOCK ... ' +})(); +``` + +#### Sign and verify cleartext messages + +```js +(async () => { + const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK----- +... +-----END PGP PUBLIC KEY BLOCK-----`; + const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- +... +-----END PGP PRIVATE KEY BLOCK-----`; // encrypted private key + const passphrase = `yourPassphrase`; // what the private key is encrypted with + + const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); + + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }), + passphrase + }); + + const unsignedMessage = await openpgp.createCleartextMessage({ text: 'Hello, World!' }); + const cleartextMessage = await openpgp.sign({ + message: unsignedMessage, // CleartextMessage or Message object + signingKeys: privateKey + }); + console.log(cleartextMessage); // '-----BEGIN PGP SIGNED MESSAGE ... END PGP SIGNATURE-----' + + const signedMessage = await openpgp.readCleartextMessage({ + cleartextMessage // parse armored message + }); + const verificationResult = await openpgp.verify({ + message: signedMessage, + verificationKeys: publicKey + }); + const { verified, keyID } = verificationResult.signatures[0]; + try { + await verified; // throws on invalid signature + console.log('Signed by key id ' + keyID.toHex()); + } catch (e) { + throw new Error('Signature could not be verified: ' + e.message); + } +})(); +``` + +#### Create and verify *detached* signatures + +```js +(async () => { + const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK----- +... +-----END PGP PUBLIC KEY BLOCK-----`; + const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- +... +-----END PGP PRIVATE KEY BLOCK-----`; // encrypted private key + const passphrase = `yourPassphrase`; // what the private key is encrypted with + + const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); + + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }), + passphrase + }); + + const message = await openpgp.createMessage({ text: 'Hello, World!' }); + const detachedSignature = await openpgp.sign({ + message, // Message object + signingKeys: privateKey, + detached: true + }); + console.log(detachedSignature); + + const signature = await openpgp.readSignature({ + armoredSignature: detachedSignature // parse detached signature + }); + const verificationResult = await openpgp.verify({ + message, // Message object + signature, + verificationKeys: publicKey + }); + const { verified, keyID } = verificationResult.signatures[0]; + try { + await verified; // throws on invalid signature + console.log('Signed by key id ' + keyID.toHex()); + } catch (e) { + throw new Error('Signature could not be verified: ' + e.message); + } +})(); +``` + +#### Streaming sign and verify *Uint8Array* data + +```js +(async () => { + var readableStream = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array([0x01, 0x02, 0x03])); + controller.close(); + } + }); + + const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK----- +... +-----END PGP PUBLIC KEY BLOCK-----`; + const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- +... +-----END PGP PRIVATE KEY BLOCK-----`; // encrypted private key + const passphrase = `yourPassphrase`; // what the private key is encrypted with + + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }), + passphrase + }); + + const message = await openpgp.createMessage({ binary: readableStream }); // or createMessage({ text: ReadableStream }) + const signatureArmored = await openpgp.sign({ + message, + signingKeys: privateKey + }); + console.log(signatureArmored); // ReadableStream containing '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----' + + const verificationResult = await openpgp.verify({ + message: await openpgp.readMessage({ armoredMessage: signatureArmored }), // parse armored signature + verificationKeys: await openpgp.readKey({ armoredKey: publicKeyArmored }) + }); + + for await (const chunk of verificationResult.data) {} + // Note: you *have* to read `verificationResult.data` in some way or other, + // even if you don't need it, as that is what triggers the + // verification of the data. + + try { + await verificationResult.signatures[0].verified; // throws on invalid signature + console.log('Signed by key id ' + verificationResult.signatures[0].keyID.toHex()); + } catch (e) { + throw new Error('Signature could not be verified: ' + e.message); + } +})(); +``` + +### Documentation + +The full documentation is available at [openpgpjs.org](https://docs.openpgpjs.org/). + +### Security Audit + +To date the OpenPGP.js code base has undergone two complete security audits from [Cure53](https://cure53.de). The first audit's report has been published [here](https://github.com/openpgpjs/openpgpjs/wiki/Cure53-security-audit). + +### Security recommendations + +It should be noted that js crypto apps deployed via regular web hosting (a.k.a. [**host-based security**](https://www.schneier.com/blog/archives/2012/08/cryptocat.html)) provide users with less security than installable apps with auditable static versions. Installable apps can be deployed as a [Firefox](https://developer.mozilla.org/en-US/Marketplace/Options/Packaged_apps) or [Chrome](https://developer.chrome.com/apps/about_apps.html) packaged app. These apps are basically signed zip files and their runtimes typically enforce a strict [Content Security Policy (CSP)](https://www.html5rocks.com/en/tutorials/security/content-security-policy/) to protect users against [XSS](https://en.wikipedia.org/wiki/Cross-site_scripting). This [blogpost](https://tankredhase.com/2014/04/13/heartbleed-and-javascript-crypto/) explains the trust model of the web quite well. + +It is also recommended to set a strong passphrase that protects the user's private key on disk. + +### Development + +To create your own build of the library, just run the following command after cloning the git repo. This will download all dependencies, run the tests and create a minified bundle under `dist/openpgp.min.js` to use in your project: + + npm install && npm test + +For debugging browser errors, you can run `npm start` and open [`http://localhost:8080/test/unittests.html`](http://localhost:8080/test/unittests.html) in a browser, or run the following command: + + npm run browsertest + +### How do I get involved? + +You want to help, great! It's probably best to send us a message on [Gitter](https://gitter.im/openpgpjs/openpgpjs) before you start your undertaking, to make sure nobody else is working on it, and so we can discuss the best course of action. Other than that, just go ahead and fork our repo, make your changes and send us a pull request! :) + +### License + +[GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl-3.0.en.html) (3.0 or any later version). Please take a look at the [LICENSE](LICENSE) file for more information. diff --git a/vendors/openpgp-5/dist/openpgp.js b/vendors/openpgp-5/dist/openpgp.js new file mode 100644 index 0000000000..6eb7d9e013 --- /dev/null +++ b/vendors/openpgp-5/dist/openpgp.js @@ -0,0 +1,43508 @@ +/*! OpenPGP.js v5.11.1 - 2024-03-15 - this is LGPL licensed code, see LICENSE/our website https://openpgpjs.org/ for more information. */ +var openpgp = (function (exports) { + 'use strict'; + + const doneWritingPromise = Symbol('doneWritingPromise'); + const doneWritingResolve = Symbol('doneWritingResolve'); + const doneWritingReject = Symbol('doneWritingReject'); + + const readingIndex = Symbol('readingIndex'); + + class ArrayStream extends Array { + constructor() { + super(); + this[doneWritingPromise] = new Promise((resolve, reject) => { + this[doneWritingResolve] = resolve; + this[doneWritingReject] = reject; + }); + this[doneWritingPromise].catch(() => {}); + } + } + + ArrayStream.prototype.getReader = function() { + if (this[readingIndex] === undefined) { + this[readingIndex] = 0; + } + return { + read: async () => { + await this[doneWritingPromise]; + if (this[readingIndex] === this.length) { + return { value: undefined, done: true }; + } + return { value: this[this[readingIndex]++], done: false }; + } + }; + }; + + ArrayStream.prototype.readToEnd = async function(join) { + await this[doneWritingPromise]; + const result = join(this.slice(this[readingIndex])); + this.length = 0; + return result; + }; + + ArrayStream.prototype.clone = function() { + const clone = new ArrayStream(); + clone[doneWritingPromise] = this[doneWritingPromise].then(() => { + clone.push(...this); + }); + return clone; + }; + + /** + * Check whether data is an ArrayStream + * @param {Any} input data to check + * @returns {boolean} + */ + function isArrayStream(input) { + return input && input.getReader && Array.isArray(input); + } + + /** + * A wrapper class over the native WritableStreamDefaultWriter. + * It also lets you "write data to" array streams instead of streams. + * @class + */ + function Writer(input) { + if (!isArrayStream(input)) { + const writer = input.getWriter(); + const releaseLock = writer.releaseLock; + writer.releaseLock = () => { + writer.closed.catch(function() {}); + releaseLock.call(writer); + }; + return writer; + } + this.stream = input; + } + + /** + * Write a chunk of data. + * @returns {Promise} + * @async + */ + Writer.prototype.write = async function(chunk) { + this.stream.push(chunk); + }; + + /** + * Close the stream. + * @returns {Promise} + * @async + */ + Writer.prototype.close = async function() { + this.stream[doneWritingResolve](); + }; + + /** + * Error the stream. + * @returns {Promise} + * @async + */ + Writer.prototype.abort = async function(reason) { + this.stream[doneWritingReject](reason); + return reason; + }; + + /** + * Release the writer's lock. + * @returns {undefined} + * @async + */ + Writer.prototype.releaseLock = function() {}; + + const isNode = typeof globalThis.process === 'object' && + typeof globalThis.process.versions === 'object'; + + const NodeReadableStream$1 = isNode && void('stream').Readable; + + /** + * Check whether data is a Stream, and if so of which type + * @param {Any} input data to check + * @returns {'web'|'ponyfill'|'node'|'array'|'web-like'|false} + */ + function isStream(input) { + if (isArrayStream(input)) { + return 'array'; + } + if (globalThis.ReadableStream && globalThis.ReadableStream.prototype.isPrototypeOf(input)) { + return 'web'; + } + if (ReadableStream$1 && ReadableStream$1.prototype.isPrototypeOf(input)) { + return 'ponyfill'; + } + if (NodeReadableStream$1 && NodeReadableStream$1.prototype.isPrototypeOf(input)) { + return 'node'; + } + if (input && input.getReader) { + return 'web-like'; + } + return false; + } + + /** + * Check whether data is a Uint8Array + * @param {Any} input data to check + * @returns {Boolean} + */ + function isUint8Array(input) { + return Uint8Array.prototype.isPrototypeOf(input); + } + + /** + * Concat Uint8Arrays + * @param {Array} Array of Uint8Arrays to concatenate + * @returns {Uint8array} Concatenated array + */ + function concatUint8Array(arrays) { + if (arrays.length === 1) return arrays[0]; + + let totalLength = 0; + for (let i = 0; i < arrays.length; i++) { + if (!isUint8Array(arrays[i])) { + throw Error('concatUint8Array: Data must be in the form of a Uint8Array'); + } + + totalLength += arrays[i].length; + } + + const result = new Uint8Array(totalLength); + let pos = 0; + arrays.forEach(function (element) { + result.set(element, pos); + pos += element.length; + }); + + return result; + } + + const NodeBuffer$1 = isNode && void('buffer').Buffer; + const NodeReadableStream = isNode && void('stream').Readable; + + /** + * Web / node stream conversion functions + * From https://github.com/gwicke/node-web-streams + */ + + let nodeToWeb; + + if (NodeReadableStream) { + + /** + * Convert a Node Readable Stream to a Web ReadableStream + * @param {Readable} nodeStream + * @returns {ReadableStream} + */ + nodeToWeb = function(nodeStream) { + let canceled = false; + return new ReadableStream$1({ + start(controller) { + nodeStream.pause(); + nodeStream.on('data', chunk => { + if (canceled) { + return; + } + if (NodeBuffer$1.isBuffer(chunk)) { + chunk = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); + } + controller.enqueue(chunk); + nodeStream.pause(); + }); + nodeStream.on('end', () => { + if (canceled) { + return; + } + controller.close(); + }); + nodeStream.on('error', e => controller.error(e)); + }, + pull() { + nodeStream.resume(); + }, + cancel(reason) { + canceled = true; + nodeStream.destroy(reason); + } + }); + }; + + } + + const doneReadingSet = new WeakSet(); + const externalBuffer = Symbol('externalBuffer'); + + /** + * A wrapper class over the native ReadableStreamDefaultReader. + * This additionally implements pushing back data on the stream, which + * lets us implement peeking and a host of convenience functions. + * It also lets you read data other than streams, such as a Uint8Array. + * @class + */ + function Reader(input) { + this.stream = input; + if (input[externalBuffer]) { + this[externalBuffer] = input[externalBuffer].slice(); + } + if (isArrayStream(input)) { + const reader = input.getReader(); + this._read = reader.read.bind(reader); + this._releaseLock = () => {}; + this._cancel = () => {}; + return; + } + let streamType = isStream(input); + if (streamType === 'node') { + input = nodeToWeb(input); + } + if (streamType) { + const reader = input.getReader(); + this._read = reader.read.bind(reader); + this._releaseLock = () => { + reader.closed.catch(function() {}); + reader.releaseLock(); + }; + this._cancel = reader.cancel.bind(reader); + return; + } + let doneReading = false; + this._read = async () => { + if (doneReading || doneReadingSet.has(input)) { + return { value: undefined, done: true }; + } + doneReading = true; + return { value: input, done: false }; + }; + this._releaseLock = () => { + if (doneReading) { + try { + doneReadingSet.add(input); + } catch(e) {} + } + }; + } + + /** + * Read a chunk of data. + * @returns {Promise} Either { done: false, value: Uint8Array | String } or { done: true, value: undefined } + * @async + */ + Reader.prototype.read = async function() { + if (this[externalBuffer] && this[externalBuffer].length) { + const value = this[externalBuffer].shift(); + return { done: false, value }; + } + return this._read(); + }; + + /** + * Allow others to read the stream. + */ + Reader.prototype.releaseLock = function() { + if (this[externalBuffer]) { + this.stream[externalBuffer] = this[externalBuffer]; + } + this._releaseLock(); + }; + + /** + * Cancel the stream. + */ + Reader.prototype.cancel = function(reason) { + return this._cancel(reason); + }; + + /** + * Read up to and including the first \n character. + * @returns {Promise} + * @async + */ + Reader.prototype.readLine = async function() { + let buffer = []; + let returnVal; + while (!returnVal) { + let { done, value } = await this.read(); + value += ''; + if (done) { + if (buffer.length) return concat(buffer); + return; + } + const lineEndIndex = value.indexOf('\n') + 1; + if (lineEndIndex) { + returnVal = concat(buffer.concat(value.substr(0, lineEndIndex))); + buffer = []; + } + if (lineEndIndex !== value.length) { + buffer.push(value.substr(lineEndIndex)); + } + } + this.unshift(...buffer); + return returnVal; + }; + + /** + * Read a single byte/character. + * @returns {Promise} + * @async + */ + Reader.prototype.readByte = async function() { + const { done, value } = await this.read(); + if (done) return; + const byte = value[0]; + this.unshift(slice(value, 1)); + return byte; + }; + + /** + * Read a specific amount of bytes/characters, unless the stream ends before that amount. + * @returns {Promise} + * @async + */ + Reader.prototype.readBytes = async function(length) { + const buffer = []; + let bufferLength = 0; + while (true) { + const { done, value } = await this.read(); + if (done) { + if (buffer.length) return concat(buffer); + return; + } + buffer.push(value); + bufferLength += value.length; + if (bufferLength >= length) { + const bufferConcat = concat(buffer); + this.unshift(slice(bufferConcat, length)); + return slice(bufferConcat, 0, length); + } + } + }; + + /** + * Peek (look ahead) a specific amount of bytes/characters, unless the stream ends before that amount. + * @returns {Promise} + * @async + */ + Reader.prototype.peekBytes = async function(length) { + const bytes = await this.readBytes(length); + this.unshift(bytes); + return bytes; + }; + + /** + * Push data to the front of the stream. + * Data must have been read in the last call to read*. + * @param {...(Uint8Array|String|Undefined)} values + */ + Reader.prototype.unshift = function(...values) { + if (!this[externalBuffer]) { + this[externalBuffer] = []; + } + if ( + values.length === 1 && isUint8Array(values[0]) && + this[externalBuffer].length && values[0].length && + this[externalBuffer][0].byteOffset >= values[0].length + ) { + this[externalBuffer][0] = new Uint8Array( + this[externalBuffer][0].buffer, + this[externalBuffer][0].byteOffset - values[0].length, + this[externalBuffer][0].byteLength + values[0].length + ); + return; + } + this[externalBuffer].unshift(...values.filter(value => value && value.length)); + }; + + /** + * Read the stream to the end and return its contents, concatenated by the join function (defaults to streams.concat). + * @param {Function} join + * @returns {Promise} the return value of join() + * @async + */ + Reader.prototype.readToEnd = async function(join=concat) { + const result = []; + while (true) { + const { done, value } = await this.read(); + if (done) break; + result.push(value); + } + return join(result); + }; + + let { ReadableStream: ReadableStream$1, WritableStream: WritableStream$1, TransformStream: TransformStream$1 } = globalThis; + + let toPonyfillReadable, toNativeReadable; + + async function loadStreamsPonyfill() { + if (TransformStream$1) { + return; + } + + const [ponyfill, adapter] = await Promise.all([ + Promise.resolve().then(function () { return ponyfill_es6; }), + Promise.resolve().then(function () { return webStreamsAdapter; }) + ]); + + ({ ReadableStream: ReadableStream$1, WritableStream: WritableStream$1, TransformStream: TransformStream$1 } = ponyfill); + + const { createReadableStreamWrapper } = adapter; + + if (globalThis.ReadableStream && ReadableStream$1 !== globalThis.ReadableStream) { + toPonyfillReadable = createReadableStreamWrapper(ReadableStream$1); + toNativeReadable = createReadableStreamWrapper(globalThis.ReadableStream); + } + } + + const NodeBuffer = isNode && void('buffer').Buffer; + + /** + * Convert data to Stream + * @param {ReadableStream|Uint8array|String} input data to convert + * @returns {ReadableStream} Converted data + */ + function toStream(input) { + let streamType = isStream(input); + if (streamType === 'node') { + return nodeToWeb(input); + } + if (streamType === 'web' && toPonyfillReadable) { + return toPonyfillReadable(input); + } + if (streamType) { + return input; + } + return new ReadableStream$1({ + start(controller) { + controller.enqueue(input); + controller.close(); + } + }); + } + + /** + * Convert data to ArrayStream + * @param {Object} input data to convert + * @returns {ArrayStream} Converted data + */ + function toArrayStream(input) { + if (isStream(input)) { + return input; + } + const stream = new ArrayStream(); + (async () => { + const writer = getWriter(stream); + await writer.write(input); + await writer.close(); + })(); + return stream; + } + + /** + * Concat a list of Uint8Arrays, Strings or Streams + * The caller should not mix Uint8Arrays with Strings, but may mix Streams with non-Streams. + * @param {Array} Array of Uint8Arrays/Strings/Streams to concatenate + * @returns {Uint8array|String|ReadableStream} Concatenated array + */ + function concat(list) { + if (list.some(stream => isStream(stream) && !isArrayStream(stream))) { + return concatStream(list); + } + if (list.some(stream => isArrayStream(stream))) { + return concatArrayStream(list); + } + if (typeof list[0] === 'string') { + return list.join(''); + } + if (NodeBuffer && NodeBuffer.isBuffer(list[0])) { + return NodeBuffer.concat(list); + } + return concatUint8Array(list); + } + + /** + * Concat a list of Streams + * @param {Array} list Array of Uint8Arrays/Strings/Streams to concatenate + * @returns {ReadableStream} Concatenated list + */ + function concatStream(list) { + list = list.map(toStream); + const transform = transformWithCancel(async function(reason) { + await Promise.all(transforms.map(stream => cancel(stream, reason))); + }); + let prev = Promise.resolve(); + const transforms = list.map((stream, i) => transformPair(stream, (readable, writable) => { + prev = prev.then(() => pipe(readable, transform.writable, { + preventClose: i !== list.length - 1 + })); + return prev; + })); + return transform.readable; + } + + /** + * Concat a list of ArrayStreams + * @param {Array} list Array of Uint8Arrays/Strings/ArrayStreams to concatenate + * @returns {ArrayStream} Concatenated streams + */ + function concatArrayStream(list) { + const result = new ArrayStream(); + let prev = Promise.resolve(); + list.forEach((stream, i) => { + prev = prev.then(() => pipe(stream, result, { + preventClose: i !== list.length - 1 + })); + return prev; + }); + return result; + } + + /** + * Get a Reader + * @param {ReadableStream|Uint8array|String} input + * @returns {Reader} + */ + function getReader(input) { + return new Reader(input); + } + + /** + * Get a Writer + * @param {WritableStream} input + * @returns {Writer} + */ + function getWriter(input) { + return new Writer(input); + } + + /** + * Pipe a readable stream to a writable stream. Don't throw on input stream errors, but forward them to the output stream. + * @param {ReadableStream|Uint8array|String} input + * @param {WritableStream} target + * @param {Object} (optional) options + * @returns {Promise} Promise indicating when piping has finished (input stream closed or errored) + * @async + */ + async function pipe(input, target, { + preventClose = false, + preventAbort = false, + preventCancel = false + } = {}) { + if (isStream(input) && !isArrayStream(input)) { + input = toStream(input); + try { + if (input[externalBuffer]) { + const writer = getWriter(target); + for (let i = 0; i < input[externalBuffer].length; i++) { + await writer.ready; + await writer.write(input[externalBuffer][i]); + } + writer.releaseLock(); + } + await input.pipeTo(target, { + preventClose, + preventAbort, + preventCancel + }); + } catch(e) {} + return; + } + input = toArrayStream(input); + const reader = getReader(input); + const writer = getWriter(target); + try { + while (true) { + await writer.ready; + const { done, value } = await reader.read(); + if (done) { + if (!preventClose) await writer.close(); + break; + } + await writer.write(value); + } + } catch (e) { + if (!preventAbort) await writer.abort(e); + } finally { + reader.releaseLock(); + writer.releaseLock(); + } + } + + /** + * Pipe a readable stream through a transform stream. + * @param {ReadableStream|Uint8array|String} input + * @param {Object} (optional) options + * @returns {ReadableStream} transformed stream + */ + function transformRaw(input, options) { + const transformStream = new TransformStream$1(options); + pipe(input, transformStream.writable); + return transformStream.readable; + } + + /** + * Create a cancelable TransformStream. + * @param {Function} cancel + * @returns {TransformStream} + */ + function transformWithCancel(cancel) { + let pulled = false; + let backpressureChangePromiseResolve; + let outputController; + return { + readable: new ReadableStream$1({ + start(controller) { + outputController = controller; + }, + pull() { + if (backpressureChangePromiseResolve) { + backpressureChangePromiseResolve(); + } else { + pulled = true; + } + }, + cancel + }, {highWaterMark: 0}), + writable: new WritableStream$1({ + write: async function(chunk) { + outputController.enqueue(chunk); + if (!pulled) { + await new Promise(resolve => { + backpressureChangePromiseResolve = resolve; + }); + backpressureChangePromiseResolve = null; + } else { + pulled = false; + } + }, + close: outputController.close.bind(outputController), + abort: outputController.error.bind(outputController) + }) + }; + } + + /** + * Transform a stream using helper functions which are called on each chunk, and on stream close, respectively. + * @param {ReadableStream|Uint8array|String} input + * @param {Function} process + * @param {Function} finish + * @returns {ReadableStream|Uint8array|String} + */ + function transform(input, process = () => undefined, finish = () => undefined) { + if (isArrayStream(input)) { + const output = new ArrayStream(); + (async () => { + const data = await readToEnd(input); + const result1 = process(data); + const result2 = finish(); + let result; + if (result1 !== undefined && result2 !== undefined) result = concat([result1, result2]); + else result = result1 !== undefined ? result1 : result2; + const writer = getWriter(output); + await writer.write(result); + await writer.close(); + })(); + return output; + } + if (isStream(input)) { + return transformRaw(input, { + async transform(value, controller) { + try { + const result = await process(value); + if (result !== undefined) controller.enqueue(result); + } catch(e) { + controller.error(e); + } + }, + async flush(controller) { + try { + const result = await finish(); + if (result !== undefined) controller.enqueue(result); + } catch(e) { + controller.error(e); + } + } + }); + } + const result1 = process(input); + const result2 = finish(); + if (result1 !== undefined && result2 !== undefined) return concat([result1, result2]); + return result1 !== undefined ? result1 : result2; + } + + /** + * Transform a stream using a helper function which is passed a readable and a writable stream. + * This function also maintains the possibility to cancel the input stream, + * and does so on cancelation of the output stream, despite cancelation + * normally being impossible when the input stream is being read from. + * @param {ReadableStream|Uint8array|String} input + * @param {Function} fn + * @returns {ReadableStream} + */ + function transformPair(input, fn) { + if (isStream(input) && !isArrayStream(input)) { + let incomingTransformController; + const incoming = new TransformStream$1({ + start(controller) { + incomingTransformController = controller; + } + }); + + const pipeDonePromise = pipe(input, incoming.writable); + + const outgoing = transformWithCancel(async function(reason) { + incomingTransformController.error(reason); + await pipeDonePromise; + await new Promise(setTimeout); + }); + fn(incoming.readable, outgoing.writable); + return outgoing.readable; + } + input = toArrayStream(input); + const output = new ArrayStream(); + fn(input, output); + return output; + } + + /** + * Parse a stream using a helper function which is passed a Reader. + * The reader additionally has a remainder() method which returns a + * stream pointing to the remainder of input, and is linked to input + * for cancelation. + * @param {ReadableStream|Uint8array|String} input + * @param {Function} fn + * @returns {Any} the return value of fn() + */ + function parse(input, fn) { + let returnValue; + const transformed = transformPair(input, (readable, writable) => { + const reader = getReader(readable); + reader.remainder = () => { + reader.releaseLock(); + pipe(readable, writable); + return transformed; + }; + returnValue = fn(reader); + }); + return returnValue; + } + + /** + * Tee a Stream for reading it twice. The input stream can no longer be read after tee()ing. + * Reading either of the two returned streams will pull from the input stream. + * The input stream will only be canceled if both of the returned streams are canceled. + * @param {ReadableStream|Uint8array|String} input + * @returns {Array} array containing two copies of input + */ + function tee(input) { + if (isArrayStream(input)) { + throw Error('ArrayStream cannot be tee()d, use clone() instead'); + } + if (isStream(input)) { + const teed = toStream(input).tee(); + teed[0][externalBuffer] = teed[1][externalBuffer] = input[externalBuffer]; + return teed; + } + return [slice(input), slice(input)]; + } + + /** + * Clone a Stream for reading it twice. The input stream can still be read after clone()ing. + * Reading from the clone will pull from the input stream. + * The input stream will only be canceled if both the clone and the input stream are canceled. + * @param {ReadableStream|Uint8array|String} input + * @returns {ReadableStream|Uint8array|String} cloned input + */ + function clone(input) { + if (isArrayStream(input)) { + return input.clone(); + } + if (isStream(input)) { + const teed = tee(input); + overwrite(input, teed[0]); + return teed[1]; + } + return slice(input); + } + + /** + * Clone a Stream for reading it twice. Data will arrive at the same rate as the input stream is being read. + * Reading from the clone will NOT pull from the input stream. Data only arrives when reading the input stream. + * The input stream will NOT be canceled if the clone is canceled, only if the input stream are canceled. + * If the input stream is canceled, the clone will be errored. + * @param {ReadableStream|Uint8array|String} input + * @returns {ReadableStream|Uint8array|String} cloned input + */ + function passiveClone(input) { + if (isArrayStream(input)) { + return clone(input); + } + if (isStream(input)) { + return new ReadableStream$1({ + start(controller) { + const transformed = transformPair(input, async (readable, writable) => { + const reader = getReader(readable); + const writer = getWriter(writable); + try { + while (true) { + await writer.ready; + const { done, value } = await reader.read(); + if (done) { + try { controller.close(); } catch(e) {} + await writer.close(); + return; + } + try { controller.enqueue(value); } catch(e) {} + await writer.write(value); + } + } catch(e) { + controller.error(e); + await writer.abort(e); + } + }); + overwrite(input, transformed); + } + }); + } + return slice(input); + } + + /** + * Modify a stream object to point to a different stream object. + * This is used internally by clone() and passiveClone() to provide an abstraction over tee(). + * @param {ReadableStream} input + * @param {ReadableStream} clone + */ + function overwrite(input, clone) { + // Overwrite input.getReader, input.locked, etc to point to clone + Object.entries(Object.getOwnPropertyDescriptors(input.constructor.prototype)).forEach(([name, descriptor]) => { + if (name === 'constructor') { + return; + } + if (descriptor.value) { + descriptor.value = descriptor.value.bind(clone); + } else { + descriptor.get = descriptor.get.bind(clone); + } + Object.defineProperty(input, name, descriptor); + }); + } + + /** + * Return a stream pointing to a part of the input stream. + * @param {ReadableStream|Uint8array|String} input + * @returns {ReadableStream|Uint8array|String} clone + */ + function slice(input, begin=0, end=Infinity) { + if (isArrayStream(input)) { + throw Error('Not implemented'); + } + if (isStream(input)) { + if (begin >= 0 && end >= 0) { + let bytesRead = 0; + return transformRaw(input, { + transform(value, controller) { + if (bytesRead < end) { + if (bytesRead + value.length >= begin) { + controller.enqueue(slice(value, Math.max(begin - bytesRead, 0), end - bytesRead)); + } + bytesRead += value.length; + } else { + controller.terminate(); + } + } + }); + } + if (begin < 0 && (end < 0 || end === Infinity)) { + let lastBytes = []; + return transform(input, value => { + if (value.length >= -begin) lastBytes = [value]; + else lastBytes.push(value); + }, () => slice(concat(lastBytes), begin, end)); + } + if (begin === 0 && end < 0) { + let lastBytes; + return transform(input, value => { + const returnValue = lastBytes ? concat([lastBytes, value]) : value; + if (returnValue.length >= -end) { + lastBytes = slice(returnValue, end); + return slice(returnValue, begin, end); + } else { + lastBytes = returnValue; + } + }); + } + console.warn(`stream.slice(input, ${begin}, ${end}) not implemented efficiently.`); + return fromAsync(async () => slice(await readToEnd(input), begin, end)); + } + if (input[externalBuffer]) { + input = concat(input[externalBuffer].concat([input])); + } + if (isUint8Array(input) && !(NodeBuffer && NodeBuffer.isBuffer(input))) { + if (end === Infinity) end = input.length; + return input.subarray(begin, end); + } + return input.slice(begin, end); + } + + /** + * Read a stream to the end and return its contents, concatenated by the join function (defaults to concat). + * @param {ReadableStream|Uint8array|String} input + * @param {Function} join + * @returns {Promise} the return value of join() + * @async + */ + async function readToEnd(input, join=concat) { + if (isArrayStream(input)) { + return input.readToEnd(join); + } + if (isStream(input)) { + return getReader(input).readToEnd(join); + } + return input; + } + + /** + * Cancel a stream. + * @param {ReadableStream|Uint8array|String} input + * @param {Any} reason + * @returns {Promise} indicates when the stream has been canceled + * @async + */ + async function cancel(input, reason) { + if (isStream(input)) { + if (input.cancel) { + return input.cancel(reason); + } + if (input.destroy) { + input.destroy(reason); + await new Promise(setTimeout); + return reason; + } + } + } + + /** + * Convert an async function to an ArrayStream. When the function returns, its return value is written to the stream. + * @param {Function} fn + * @returns {ArrayStream} + */ + function fromAsync(fn) { + const arrayStream = new ArrayStream(); + (async () => { + const writer = getWriter(arrayStream); + try { + await writer.write(await fn()); + await writer.close(); + } catch (e) { + await writer.abort(e); + } + })(); + return arrayStream; + } + + /* eslint-disable new-cap */ + + /** + * @fileoverview + * BigInteger implementation of basic operations + * that wraps the native BigInt library. + * Operations are not constant time, + * but we try and limit timing leakage where we can + * @module biginteger/native + * @private + */ + + /** + * @private + */ + class BigInteger$1 { + /** + * Get a BigInteger (input must be big endian for strings and arrays) + * @param {Number|String|Uint8Array} n - Value to convert + * @throws {Error} on null or undefined input + */ + constructor(n) { + if (n === undefined) { + throw Error('Invalid BigInteger input'); + } + + if (n instanceof Uint8Array) { + const bytes = n; + const hex = new Array(bytes.length); + for (let i = 0; i < bytes.length; i++) { + const hexByte = bytes[i].toString(16); + hex[i] = (bytes[i] <= 0xF) ? ('0' + hexByte) : hexByte; + } + this.value = BigInt('0x0' + hex.join('')); + } else { + this.value = BigInt(n); + } + } + + clone() { + return new BigInteger$1(this.value); + } + + /** + * BigInteger increment in place + */ + iinc() { + this.value++; + return this; + } + + /** + * BigInteger increment + * @returns {BigInteger} this + 1. + */ + inc() { + return this.clone().iinc(); + } + + /** + * BigInteger decrement in place + */ + idec() { + this.value--; + return this; + } + + /** + * BigInteger decrement + * @returns {BigInteger} this - 1. + */ + dec() { + return this.clone().idec(); + } + + /** + * BigInteger addition in place + * @param {BigInteger} x - Value to add + */ + iadd(x) { + this.value += x.value; + return this; + } + + /** + * BigInteger addition + * @param {BigInteger} x - Value to add + * @returns {BigInteger} this + x. + */ + add(x) { + return this.clone().iadd(x); + } + + /** + * BigInteger subtraction in place + * @param {BigInteger} x - Value to subtract + */ + isub(x) { + this.value -= x.value; + return this; + } + + /** + * BigInteger subtraction + * @param {BigInteger} x - Value to subtract + * @returns {BigInteger} this - x. + */ + sub(x) { + return this.clone().isub(x); + } + + /** + * BigInteger multiplication in place + * @param {BigInteger} x - Value to multiply + */ + imul(x) { + this.value *= x.value; + return this; + } + + /** + * BigInteger multiplication + * @param {BigInteger} x - Value to multiply + * @returns {BigInteger} this * x. + */ + mul(x) { + return this.clone().imul(x); + } + + /** + * Compute value modulo m, in place + * @param {BigInteger} m - Modulo + */ + imod(m) { + this.value %= m.value; + if (this.isNegative()) { + this.iadd(m); + } + return this; + } + + /** + * Compute value modulo m + * @param {BigInteger} m - Modulo + * @returns {BigInteger} this mod m. + */ + mod(m) { + return this.clone().imod(m); + } + + /** + * Compute modular exponentiation using square and multiply + * @param {BigInteger} e - Exponent + * @param {BigInteger} n - Modulo + * @returns {BigInteger} this ** e mod n. + */ + modExp(e, n) { + if (n.isZero()) throw Error('Modulo cannot be zero'); + if (n.isOne()) return new BigInteger$1(0); + if (e.isNegative()) throw Error('Unsopported negative exponent'); + + let exp = e.value; + let x = this.value; + + x %= n.value; + let r = BigInt(1); + while (exp > BigInt(0)) { + const lsb = exp & BigInt(1); + exp >>= BigInt(1); // e / 2 + // Always compute multiplication step, to reduce timing leakage + const rx = (r * x) % n.value; + // Update r only if lsb is 1 (odd exponent) + r = lsb ? rx : r; + x = (x * x) % n.value; // Square + } + return new BigInteger$1(r); + } + + + /** + * Compute the inverse of this value modulo n + * Note: this and and n must be relatively prime + * @param {BigInteger} n - Modulo + * @returns {BigInteger} x such that this*x = 1 mod n + * @throws {Error} if the inverse does not exist + */ + modInv(n) { + const { gcd, x } = this._egcd(n); + if (!gcd.isOne()) { + throw Error('Inverse does not exist'); + } + return x.add(n).mod(n); + } + + /** + * Extended Eucleadian algorithm (http://anh.cs.luc.edu/331/notes/xgcd.pdf) + * Given a = this and b, compute (x, y) such that ax + by = gdc(a, b) + * @param {BigInteger} b - Second operand + * @returns {{ gcd, x, y: BigInteger }} + */ + _egcd(b) { + let x = BigInt(0); + let y = BigInt(1); + let xPrev = BigInt(1); + let yPrev = BigInt(0); + + let a = this.value; + b = b.value; + + while (b !== BigInt(0)) { + const q = a / b; + let tmp = x; + x = xPrev - q * x; + xPrev = tmp; + + tmp = y; + y = yPrev - q * y; + yPrev = tmp; + + tmp = b; + b = a % b; + a = tmp; + } + + return { + x: new BigInteger$1(xPrev), + y: new BigInteger$1(yPrev), + gcd: new BigInteger$1(a) + }; + } + + /** + * Compute greatest common divisor between this and n + * @param {BigInteger} b - Operand + * @returns {BigInteger} gcd + */ + gcd(b) { + let a = this.value; + b = b.value; + while (b !== BigInt(0)) { + const tmp = b; + b = a % b; + a = tmp; + } + return new BigInteger$1(a); + } + + /** + * Shift this to the left by x, in place + * @param {BigInteger} x - Shift value + */ + ileftShift(x) { + this.value <<= x.value; + return this; + } + + /** + * Shift this to the left by x + * @param {BigInteger} x - Shift value + * @returns {BigInteger} this << x. + */ + leftShift(x) { + return this.clone().ileftShift(x); + } + + /** + * Shift this to the right by x, in place + * @param {BigInteger} x - Shift value + */ + irightShift(x) { + this.value >>= x.value; + return this; + } + + /** + * Shift this to the right by x + * @param {BigInteger} x - Shift value + * @returns {BigInteger} this >> x. + */ + rightShift(x) { + return this.clone().irightShift(x); + } + + /** + * Whether this value is equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + equal(x) { + return this.value === x.value; + } + + /** + * Whether this value is less than x + * @param {BigInteger} x + * @returns {Boolean} + */ + lt(x) { + return this.value < x.value; + } + + /** + * Whether this value is less than or equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + lte(x) { + return this.value <= x.value; + } + + /** + * Whether this value is greater than x + * @param {BigInteger} x + * @returns {Boolean} + */ + gt(x) { + return this.value > x.value; + } + + /** + * Whether this value is greater than or equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + gte(x) { + return this.value >= x.value; + } + + isZero() { + return this.value === BigInt(0); + } + + isOne() { + return this.value === BigInt(1); + } + + isNegative() { + return this.value < BigInt(0); + } + + isEven() { + return !(this.value & BigInt(1)); + } + + abs() { + const res = this.clone(); + if (this.isNegative()) { + res.value = -res.value; + } + return res; + } + + /** + * Get this value as a string + * @returns {String} this value. + */ + toString() { + return this.value.toString(); + } + + /** + * Get this value as an exact Number (max 53 bits) + * Fails if this value is too large + * @returns {Number} + */ + toNumber() { + const number = Number(this.value); + if (number > Number.MAX_SAFE_INTEGER) { + // We throw and error to conform with the bn.js implementation + throw Error('Number can only safely store up to 53 bits'); + } + return number; + } + + /** + * Get value of i-th bit + * @param {Number} i - Bit index + * @returns {Number} Bit value. + */ + getBit(i) { + const bit = (this.value >> BigInt(i)) & BigInt(1); + return (bit === BigInt(0)) ? 0 : 1; + } + + /** + * Compute bit length + * @returns {Number} Bit length. + */ + bitLength() { + const zero = new BigInteger$1(0); + const one = new BigInteger$1(1); + const negOne = new BigInteger$1(-1); + + // -1n >> -1n is -1n + // 1n >> 1n is 0n + const target = this.isNegative() ? negOne : zero; + let bitlen = 1; + const tmp = this.clone(); + while (!tmp.irightShift(one).equal(target)) { + bitlen++; + } + return bitlen; + } + + /** + * Compute byte length + * @returns {Number} Byte length. + */ + byteLength() { + const zero = new BigInteger$1(0); + const negOne = new BigInteger$1(-1); + + const target = this.isNegative() ? negOne : zero; + const eight = new BigInteger$1(8); + let len = 1; + const tmp = this.clone(); + while (!tmp.irightShift(eight).equal(target)) { + len++; + } + return len; + } + + /** + * Get Uint8Array representation of this number + * @param {String} endian - Endianess of output array (defaults to 'be') + * @param {Number} length - Of output array + * @returns {Uint8Array} + */ + toUint8Array(endian = 'be', length) { + // we get and parse the hex string (https://coolaj86.com/articles/convert-js-bigints-to-typedarrays/) + // this is faster than shift+mod iterations + let hex = this.value.toString(16); + if (hex.length % 2 === 1) { + hex = '0' + hex; + } + + const rawLength = hex.length / 2; + const bytes = new Uint8Array(length || rawLength); + // parse hex + const offset = length ? (length - rawLength) : 0; + let i = 0; + while (i < rawLength) { + bytes[i + offset] = parseInt(hex.slice(2 * i, 2 * i + 2), 16); + i++; + } + + if (endian !== 'be') { + bytes.reverse(); + } + + return bytes; + } + } + + const detectBigInt = () => typeof BigInt !== 'undefined'; + + async function getBigInteger() { + if (detectBigInt()) { + return BigInteger$1; + } else { + const { default: BigInteger } = await Promise.resolve().then(function () { return bn_interface; }); + return BigInteger; + } + } + + /** + * @module enums + */ + + const byValue = Symbol('byValue'); + + var enums = { + + /** Maps curve names under various standards to one + * @see {@link https://wiki.gnupg.org/ECC|ECC - GnuPG wiki} + * @enum {String} + * @readonly + */ + curve: { + /** NIST P-256 Curve */ + 'p256': 'p256', + 'P-256': 'p256', + 'secp256r1': 'p256', + 'prime256v1': 'p256', + '1.2.840.10045.3.1.7': 'p256', + '2a8648ce3d030107': 'p256', + '2A8648CE3D030107': 'p256', + + /** NIST P-384 Curve */ + 'p384': 'p384', + 'P-384': 'p384', + 'secp384r1': 'p384', + '1.3.132.0.34': 'p384', + '2b81040022': 'p384', + '2B81040022': 'p384', + + /** NIST P-521 Curve */ + 'p521': 'p521', + 'P-521': 'p521', + 'secp521r1': 'p521', + '1.3.132.0.35': 'p521', + '2b81040023': 'p521', + '2B81040023': 'p521', + + /** SECG SECP256k1 Curve */ + 'secp256k1': 'secp256k1', + '1.3.132.0.10': 'secp256k1', + '2b8104000a': 'secp256k1', + '2B8104000A': 'secp256k1', + + /** Ed25519 - deprecated by crypto-refresh (replaced by standaone Ed25519 algo) */ + 'ed25519Legacy': 'ed25519', + 'ED25519': 'ed25519', + /** @deprecated use `ed25519Legacy` instead */ + 'ed25519': 'ed25519', + 'Ed25519': 'ed25519', + '1.3.6.1.4.1.11591.15.1': 'ed25519', + '2b06010401da470f01': 'ed25519', + '2B06010401DA470F01': 'ed25519', + + /** Curve25519 - deprecated by crypto-refresh (replaced by standaone X25519 algo) */ + 'curve25519Legacy': 'curve25519', + 'X25519': 'curve25519', + 'cv25519': 'curve25519', + /** @deprecated use `curve25519Legacy` instead */ + 'curve25519': 'curve25519', + 'Curve25519': 'curve25519', + '1.3.6.1.4.1.3029.1.5.1': 'curve25519', + '2b060104019755010501': 'curve25519', + '2B060104019755010501': 'curve25519', + + /** BrainpoolP256r1 Curve */ + 'brainpoolP256r1': 'brainpoolP256r1', + '1.3.36.3.3.2.8.1.1.7': 'brainpoolP256r1', + '2b2403030208010107': 'brainpoolP256r1', + '2B2403030208010107': 'brainpoolP256r1', + + /** BrainpoolP384r1 Curve */ + 'brainpoolP384r1': 'brainpoolP384r1', + '1.3.36.3.3.2.8.1.1.11': 'brainpoolP384r1', + '2b240303020801010b': 'brainpoolP384r1', + '2B240303020801010B': 'brainpoolP384r1', + + /** BrainpoolP512r1 Curve */ + 'brainpoolP512r1': 'brainpoolP512r1', + '1.3.36.3.3.2.8.1.1.13': 'brainpoolP512r1', + '2b240303020801010d': 'brainpoolP512r1', + '2B240303020801010D': 'brainpoolP512r1' + }, + + /** A string to key specifier type + * @enum {Integer} + * @readonly + */ + s2k: { + simple: 0, + salted: 1, + iterated: 3, + gnu: 101 + }, + + /** {@link https://tools.ietf.org/html/draft-ietf-openpgp-crypto-refresh-08.html#section-9.1|crypto-refresh RFC, section 9.1} + * @enum {Integer} + * @readonly + */ + publicKey: { + /** RSA (Encrypt or Sign) [HAC] */ + rsaEncryptSign: 1, + /** RSA (Encrypt only) [HAC] */ + rsaEncrypt: 2, + /** RSA (Sign only) [HAC] */ + rsaSign: 3, + /** Elgamal (Encrypt only) [ELGAMAL] [HAC] */ + elgamal: 16, + /** DSA (Sign only) [FIPS186] [HAC] */ + dsa: 17, + /** ECDH (Encrypt only) [RFC6637] */ + ecdh: 18, + /** ECDSA (Sign only) [RFC6637] */ + ecdsa: 19, + /** EdDSA (Sign only) - deprecated by crypto-refresh (replaced by `ed25519` identifier below) + * [{@link https://tools.ietf.org/html/draft-koch-eddsa-for-openpgp-04|Draft RFC}] */ + eddsaLegacy: 22, // NB: this is declared before `eddsa` to translate 22 to 'eddsa' for backwards compatibility + /** @deprecated use `eddsaLegacy` instead */ + ed25519Legacy: 22, + /** @deprecated use `eddsaLegacy` instead */ + eddsa: 22, + /** Reserved for AEDH */ + aedh: 23, + /** Reserved for AEDSA */ + aedsa: 24, + /** X25519 (Encrypt only) */ + x25519: 25, + /** X448 (Encrypt only) */ + x448: 26, + /** Ed25519 (Sign only) */ + ed25519: 27, + /** Ed448 (Sign only) */ + ed448: 28 + }, + + /** {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC4880, section 9.2} + * @enum {Integer} + * @readonly + */ + symmetric: { + plaintext: 0, + /** Not implemented! */ + idea: 1, + tripledes: 2, + cast5: 3, + blowfish: 4, + aes128: 7, + aes192: 8, + aes256: 9, + twofish: 10 + }, + + /** {@link https://tools.ietf.org/html/rfc4880#section-9.3|RFC4880, section 9.3} + * @enum {Integer} + * @readonly + */ + compression: { + uncompressed: 0, + /** RFC1951 */ + zip: 1, + /** RFC1950 */ + zlib: 2, + bzip2: 3 + }, + + /** {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC4880, section 9.4} + * @enum {Integer} + * @readonly + */ + hash: { + md5: 1, + sha1: 2, + ripemd: 3, + sha256: 8, + sha384: 9, + sha512: 10, + sha224: 11 + }, + + /** A list of hash names as accepted by webCrypto functions. + * {@link https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest|Parameters, algo} + * @enum {String} + */ + webHash: { + 'SHA-1': 2, + 'SHA-256': 8, + 'SHA-384': 9, + 'SHA-512': 10 + }, + + /** {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-9.6|RFC4880bis-04, section 9.6} + * @enum {Integer} + * @readonly + */ + aead: { + eax: 1, + ocb: 2, + experimentalGCM: 100 // Private algorithm + }, + + /** A list of packet types and numeric tags associated with them. + * @enum {Integer} + * @readonly + */ + packet: { + publicKeyEncryptedSessionKey: 1, + signature: 2, + symEncryptedSessionKey: 3, + onePassSignature: 4, + secretKey: 5, + publicKey: 6, + secretSubkey: 7, + compressedData: 8, + symmetricallyEncryptedData: 9, + marker: 10, + literalData: 11, + trust: 12, + userID: 13, + publicSubkey: 14, + userAttribute: 17, + symEncryptedIntegrityProtectedData: 18, + modificationDetectionCode: 19, + aeadEncryptedData: 20 // see IETF draft: https://tools.ietf.org/html/draft-ford-openpgp-format-00#section-2.1 + }, + + /** Data types in the literal packet + * @enum {Integer} + * @readonly + */ + literal: { + /** Binary data 'b' */ + binary: 'b'.charCodeAt(), + /** Text data 't' */ + text: 't'.charCodeAt(), + /** Utf8 data 'u' */ + utf8: 'u'.charCodeAt(), + /** MIME message body part 'm' */ + mime: 'm'.charCodeAt() + }, + + + /** One pass signature packet type + * @enum {Integer} + * @readonly + */ + signature: { + /** 0x00: Signature of a binary document. */ + binary: 0, + /** 0x01: Signature of a canonical text document. + * + * Canonicalyzing the document by converting line endings. */ + text: 1, + /** 0x02: Standalone signature. + * + * This signature is a signature of only its own subpacket contents. + * It is calculated identically to a signature over a zero-lengh + * binary document. Note that it doesn't make sense to have a V3 + * standalone signature. */ + standalone: 2, + /** 0x10: Generic certification of a User ID and Public-Key packet. + * + * The issuer of this certification does not make any particular + * assertion as to how well the certifier has checked that the owner + * of the key is in fact the person described by the User ID. */ + certGeneric: 16, + /** 0x11: Persona certification of a User ID and Public-Key packet. + * + * The issuer of this certification has not done any verification of + * the claim that the owner of this key is the User ID specified. */ + certPersona: 17, + /** 0x12: Casual certification of a User ID and Public-Key packet. + * + * The issuer of this certification has done some casual + * verification of the claim of identity. */ + certCasual: 18, + /** 0x13: Positive certification of a User ID and Public-Key packet. + * + * The issuer of this certification has done substantial + * verification of the claim of identity. + * + * Most OpenPGP implementations make their "key signatures" as 0x10 + * certifications. Some implementations can issue 0x11-0x13 + * certifications, but few differentiate between the types. */ + certPositive: 19, + /** 0x30: Certification revocation signature + * + * This signature revokes an earlier User ID certification signature + * (signature class 0x10 through 0x13) or direct-key signature + * (0x1F). It should be issued by the same key that issued the + * revoked signature or an authorized revocation key. The signature + * is computed over the same data as the certificate that it + * revokes, and should have a later creation date than that + * certificate. */ + certRevocation: 48, + /** 0x18: Subkey Binding Signature + * + * This signature is a statement by the top-level signing key that + * indicates that it owns the subkey. This signature is calculated + * directly on the primary key and subkey, and not on any User ID or + * other packets. A signature that binds a signing subkey MUST have + * an Embedded Signature subpacket in this binding signature that + * contains a 0x19 signature made by the signing subkey on the + * primary key and subkey. */ + subkeyBinding: 24, + /** 0x19: Primary Key Binding Signature + * + * This signature is a statement by a signing subkey, indicating + * that it is owned by the primary key and subkey. This signature + * is calculated the same way as a 0x18 signature: directly on the + * primary key and subkey, and not on any User ID or other packets. + * + * When a signature is made over a key, the hash data starts with the + * octet 0x99, followed by a two-octet length of the key, and then body + * of the key packet. (Note that this is an old-style packet header for + * a key packet with two-octet length.) A subkey binding signature + * (type 0x18) or primary key binding signature (type 0x19) then hashes + * the subkey using the same format as the main key (also using 0x99 as + * the first octet). */ + keyBinding: 25, + /** 0x1F: Signature directly on a key + * + * This signature is calculated directly on a key. It binds the + * information in the Signature subpackets to the key, and is + * appropriate to be used for subpackets that provide information + * about the key, such as the Revocation Key subpacket. It is also + * appropriate for statements that non-self certifiers want to make + * about the key itself, rather than the binding between a key and a + * name. */ + key: 31, + /** 0x20: Key revocation signature + * + * The signature is calculated directly on the key being revoked. A + * revoked key is not to be used. Only revocation signatures by the + * key being revoked, or by an authorized revocation key, should be + * considered valid revocation signatures.a */ + keyRevocation: 32, + /** 0x28: Subkey revocation signature + * + * The signature is calculated directly on the subkey being revoked. + * A revoked subkey is not to be used. Only revocation signatures + * by the top-level signature key that is bound to this subkey, or + * by an authorized revocation key, should be considered valid + * revocation signatures. + * + * Key revocation signatures (types 0x20 and 0x28) + * hash only the key being revoked. */ + subkeyRevocation: 40, + /** 0x40: Timestamp signature. + * This signature is only meaningful for the timestamp contained in + * it. */ + timestamp: 64, + /** 0x50: Third-Party Confirmation signature. + * + * This signature is a signature over some other OpenPGP Signature + * packet(s). It is analogous to a notary seal on the signed data. + * A third-party signature SHOULD include Signature Target + * subpacket(s) to give easy identification. Note that we really do + * mean SHOULD. There are plausible uses for this (such as a blind + * party that only sees the signature, not the key or source + * document) that cannot include a target subpacket. */ + thirdParty: 80 + }, + + /** Signature subpacket type + * @enum {Integer} + * @readonly + */ + signatureSubpacket: { + signatureCreationTime: 2, + signatureExpirationTime: 3, + exportableCertification: 4, + trustSignature: 5, + regularExpression: 6, + revocable: 7, + keyExpirationTime: 9, + placeholderBackwardsCompatibility: 10, + preferredSymmetricAlgorithms: 11, + revocationKey: 12, + issuer: 16, + notationData: 20, + preferredHashAlgorithms: 21, + preferredCompressionAlgorithms: 22, + keyServerPreferences: 23, + preferredKeyServer: 24, + primaryUserID: 25, + policyURI: 26, + keyFlags: 27, + signersUserID: 28, + reasonForRevocation: 29, + features: 30, + signatureTarget: 31, + embeddedSignature: 32, + issuerFingerprint: 33, + preferredAEADAlgorithms: 34 + }, + + /** Key flags + * @enum {Integer} + * @readonly + */ + keyFlags: { + /** 0x01 - This key may be used to certify other keys. */ + certifyKeys: 1, + /** 0x02 - This key may be used to sign data. */ + signData: 2, + /** 0x04 - This key may be used to encrypt communications. */ + encryptCommunication: 4, + /** 0x08 - This key may be used to encrypt storage. */ + encryptStorage: 8, + /** 0x10 - The private component of this key may have been split + * by a secret-sharing mechanism. */ + splitPrivateKey: 16, + /** 0x20 - This key may be used for authentication. */ + authentication: 32, + /** 0x80 - The private component of this key may be in the + * possession of more than one person. */ + sharedPrivateKey: 128 + }, + + /** Armor type + * @enum {Integer} + * @readonly + */ + armor: { + multipartSection: 0, + multipartLast: 1, + signed: 2, + message: 3, + publicKey: 4, + privateKey: 5, + signature: 6 + }, + + /** {@link https://tools.ietf.org/html/rfc4880#section-5.2.3.23|RFC4880, section 5.2.3.23} + * @enum {Integer} + * @readonly + */ + reasonForRevocation: { + /** No reason specified (key revocations or cert revocations) */ + noReason: 0, + /** Key is superseded (key revocations) */ + keySuperseded: 1, + /** Key material has been compromised (key revocations) */ + keyCompromised: 2, + /** Key is retired and no longer used (key revocations) */ + keyRetired: 3, + /** User ID information is no longer valid (cert revocations) */ + userIDInvalid: 32 + }, + + /** {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.2.3.25|RFC4880bis-04, section 5.2.3.25} + * @enum {Integer} + * @readonly + */ + features: { + /** 0x01 - Modification Detection (packets 18 and 19) */ + modificationDetection: 1, + /** 0x02 - AEAD Encrypted Data Packet (packet 20) and version 5 + * Symmetric-Key Encrypted Session Key Packets (packet 3) */ + aead: 2, + /** 0x04 - Version 5 Public-Key Packet format and corresponding new + * fingerprint format */ + v5Keys: 4 + }, + + /** + * Asserts validity of given value and converts from string/integer to integer. + * @param {Object} type target enum type + * @param {String|Integer} e value to check and/or convert + * @returns {Integer} enum value if it exists + * @throws {Error} if the value is invalid + */ + write(type, e) { + if (typeof e === 'number') { + e = this.read(type, e); + } + + if (type[e] !== undefined) { + return type[e]; + } + + throw Error('Invalid enum value.'); + }, + + /** + * Converts enum integer value to the corresponding string, if it exists. + * @param {Object} type target enum type + * @param {Integer} e value to convert + * @returns {String} name of enum value if it exists + * @throws {Error} if the value is invalid + */ + read(type, e) { + if (!type[byValue]) { + type[byValue] = []; + Object.entries(type).forEach(([key, value]) => { + type[byValue][value] = key; + }); + } + + if (type[byValue][e] !== undefined) { + return type[byValue][e]; + } + + throw Error('Invalid enum value.'); + } + }; + + // GPG4Browsers - An OpenPGP implementation in javascript + + const util = { + isString: data => typeof data === 'string' || data instanceof String, + + isArray: data => Array.isArray(data), + + isUint8Array: isUint8Array, + + isStream: isStream, + + readNumber(bytes) { + let n = 0; + for (let i = 0; i < bytes.length; i++) { + n += (256 ** i) * bytes[bytes.length - 1 - i]; + } + return n; + }, + + writeNumber(n, bytes) { + const b = new Uint8Array(bytes); + for (let i = 0; i < bytes; i++) { + b[i] = (n >> (8 * (bytes - i - 1))) & 0xFF; + } + + return b; + }, + + readDate: bytes => new Date(util.readNumber(bytes) * 1000), + + writeDate: time => util.writeNumber(Math.floor(time.getTime() / 1000), 4), + + normalizeDate: (time = Date.now()) => + time === null || time === Infinity ? time : new Date(Math.floor(+time / 1000) * 1000), + + /** + * Read one MPI from bytes in input + * @param {Uint8Array} bytes - Input data to parse + * @returns {Uint8Array} Parsed MPI. + */ + readMPI(bytes) { + const bits = (bytes[0] << 8) | bytes[1]; + const bytelen = (bits + 7) >>> 3; + return bytes.subarray(2, 2 + bytelen); + }, + + /** + * Left-pad Uint8Array to length by adding 0x0 bytes + * @param {Uint8Array} bytes - Data to pad + * @param {Number} length - Padded length + * @returns {Uint8Array} Padded bytes. + */ + leftPad(bytes, length) { + const padded = new Uint8Array(length); + padded.set(bytes, length - bytes.length); + return padded; + }, + + /** + * Convert a Uint8Array to an MPI-formatted Uint8Array. + * @param {Uint8Array} bin - An array of 8-bit integers to convert + * @returns {Uint8Array} MPI-formatted Uint8Array. + */ + uint8ArrayToMPI(bin) { + const bitSize = util.uint8ArrayBitLength(bin); + if (bitSize === 0) { + throw Error('Zero MPI'); + } + const stripped = bin.subarray(bin.length - Math.ceil(bitSize / 8)); + const prefix = new Uint8Array([(bitSize & 0xFF00) >> 8, bitSize & 0xFF]); + return util.concatUint8Array([prefix, stripped]); + }, + + /** + * Return bit length of the input data + * @param {Uint8Array} bin input data (big endian) + * @returns bit length + */ + uint8ArrayBitLength(bin) { + let i; // index of leading non-zero byte + for (i = 0; i < bin.length; i++) if (bin[i] !== 0) break; + if (i === bin.length) { + return 0; + } + const stripped = bin.subarray(i); + return (stripped.length - 1) * 8 + util.nbits(stripped[0]); + }, + + /** + * Convert a hex string to an array of 8-bit integers + * @param {String} hex - A hex string to convert + * @returns {Uint8Array} An array of 8-bit integers. + */ + hexToUint8Array(hex) { + const result = new Uint8Array(hex.length >> 1); + for (let k = 0; k < hex.length >> 1; k++) { + result[k] = parseInt(hex.substr(k << 1, 2), 16); + } + return result; + }, + + /** + * Convert an array of 8-bit integers to a hex string + * @param {Uint8Array} bytes - Array of 8-bit integers to convert + * @returns {String} Hexadecimal representation of the array. + */ + uint8ArrayToHex(bytes) { + const r = []; + const e = bytes.length; + let c = 0; + let h; + while (c < e) { + h = bytes[c++].toString(16); + while (h.length < 2) { + h = '0' + h; + } + r.push('' + h); + } + return r.join(''); + }, + + /** + * Convert a string to an array of 8-bit integers + * @param {String} str - String to convert + * @returns {Uint8Array} An array of 8-bit integers. + */ + stringToUint8Array: str => + transform(str, str => { + if (!util.isString(str)) { + throw Error('stringToUint8Array: Data must be in the form of a string'); + } + + const result = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + result[i] = str.charCodeAt(i); + } + return result; + }), + + /** + * Convert an array of 8-bit integers to a string + * @param {Uint8Array} bytes - An array of 8-bit integers to convert + * @returns {String} String representation of the array. + */ + uint8ArrayToString(bytes) { + bytes = new Uint8Array(bytes); + const result = []; + const bs = 1 << 14; + const j = bytes.length; + + for (let i = 0; i < j; i += bs) { + result.push(String.fromCharCode.apply(String, bytes.subarray(i, i + bs < j ? i + bs : j))); + } + return result.join(''); + }, + + /** + * Convert a native javascript string to a Uint8Array of utf8 bytes + * @param {String|ReadableStream} str - The string to convert + * @returns {Uint8Array|ReadableStream} A valid squence of utf8 bytes. + */ + encodeUTF8(str) { + const encoder = new TextEncoder('utf-8'); + // eslint-disable-next-line no-inner-declarations + function process(value, lastChunk = false) { + return encoder.encode(value, { stream: !lastChunk }); + } + return transform(str, process, () => process('', true)); + }, + + /** + * Convert a Uint8Array of utf8 bytes to a native javascript string + * @param {Uint8Array|ReadableStream} utf8 - A valid squence of utf8 bytes + * @returns {String|ReadableStream} A native javascript string. + */ + decodeUTF8(utf8) { + const decoder = new TextDecoder('utf-8'); + // eslint-disable-next-line no-inner-declarations + function process(value, lastChunk = false) { + return decoder.decode(value, { stream: !lastChunk }); + } + return transform(utf8, process, () => process(new Uint8Array(), true)); + }, + + /** + * Concat a list of Uint8Arrays, Strings or Streams + * The caller must not mix Uint8Arrays with Strings, but may mix Streams with non-Streams. + * @param {Array} Array - Of Uint8Arrays/Strings/Streams to concatenate + * @returns {Uint8Array|String|ReadableStream} Concatenated array. + */ + concat: concat, + + /** + * Concat Uint8Arrays + * @param {Array} Array - Of Uint8Arrays to concatenate + * @returns {Uint8Array} Concatenated array. + */ + concatUint8Array: concatUint8Array, + + /** + * Check Uint8Array equality + * @param {Uint8Array} array1 - First array + * @param {Uint8Array} array2 - Second array + * @returns {Boolean} Equality. + */ + equalsUint8Array(array1, array2) { + if (!util.isUint8Array(array1) || !util.isUint8Array(array2)) { + throw Error('Data must be in the form of a Uint8Array'); + } + + if (array1.length !== array2.length) { + return false; + } + + for (let i = 0; i < array1.length; i++) { + if (array1[i] !== array2[i]) { + return false; + } + } + return true; + }, + + /** + * Calculates a 16bit sum of a Uint8Array by adding each character + * codes modulus 65535 + * @param {Uint8Array} Uint8Array - To create a sum of + * @returns {Uint8Array} 2 bytes containing the sum of all charcodes % 65535. + */ + writeChecksum(text) { + let s = 0; + for (let i = 0; i < text.length; i++) { + s = (s + text[i]) & 0xFFFF; + } + return util.writeNumber(s, 2); + }, + + // returns bit length of the integer x + nbits(x) { + let r = 1; + let t = x >>> 16; + if (t !== 0) { + x = t; + r += 16; + } + t = x >> 8; + if (t !== 0) { + x = t; + r += 8; + } + t = x >> 4; + if (t !== 0) { + x = t; + r += 4; + } + t = x >> 2; + if (t !== 0) { + x = t; + r += 2; + } + t = x >> 1; + if (t !== 0) { + x = t; + r += 1; + } + return r; + }, + + /** + * If S[1] == 0, then double(S) == (S[2..128] || 0); + * otherwise, double(S) == (S[2..128] || 0) xor + * (zeros(120) || 10000111). + * + * Both OCB and EAX (through CMAC) require this function to be constant-time. + * + * @param {Uint8Array} data + */ + double(data) { + const doubleVar = new Uint8Array(data.length); + const last = data.length - 1; + for (let i = 0; i < last; i++) { + doubleVar[i] = (data[i] << 1) ^ (data[i + 1] >> 7); + } + doubleVar[last] = (data[last] << 1) ^ ((data[0] >> 7) * 0x87); + return doubleVar; + }, + + /** + * Shift a Uint8Array to the right by n bits + * @param {Uint8Array} array - The array to shift + * @param {Integer} bits - Amount of bits to shift (MUST be smaller + * than 8) + * @returns {String} Resulting array. + */ + shiftRight(array, bits) { + if (bits) { + for (let i = array.length - 1; i >= 0; i--) { + array[i] >>= bits; + if (i > 0) { + array[i] |= (array[i - 1] << (8 - bits)); + } + } + } + return array; + }, + + /** + * Get native Web Cryptography api, only the current version of the spec. + * @returns {Object} The SubtleCrypto api or 'undefined'. + */ + getWebCrypto: () => crypto.subtle, + + /** + * Get BigInteger class + * It wraps the native BigInt type if it's available + * Otherwise it relies on bn.js + * @returns {BigInteger} + * @async + */ + getBigInteger, + + getHardwareConcurrency: () => navigator.hardwareConcurrency || 1, + + isEmailAddress(data) { + if (!util.isString(data)) { + return false; + } + const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+([a-zA-Z]{2,}[0-9]*|xn--[a-zA-Z\-0-9]+)))$/; + return re.test(data); + }, + + /** + * Normalize line endings to + * Support any encoding where CR=0x0D, LF=0x0A + */ + canonicalizeEOL(data) { + const CR = 13; + const LF = 10; + let carryOverCR = false; + + return transform(data, bytes => { + if (carryOverCR) { + bytes = util.concatUint8Array([new Uint8Array([CR]), bytes]); + } + + if (bytes[bytes.length - 1] === CR) { + carryOverCR = true; + bytes = bytes.subarray(0, -1); + } else { + carryOverCR = false; + } + + let index; + const indices = []; + for (let i = 0; ; i = index) { + index = bytes.indexOf(LF, i) + 1; + if (index) { + if (bytes[index - 2] !== CR) indices.push(index); + } else { + break; + } + } + if (!indices.length) { + return bytes; + } + + const normalized = new Uint8Array(bytes.length + indices.length); + let j = 0; + for (let i = 0; i < indices.length; i++) { + const sub = bytes.subarray(indices[i - 1] || 0, indices[i]); + normalized.set(sub, j); + j += sub.length; + normalized[j - 1] = CR; + normalized[j] = LF; + j++; + } + normalized.set(bytes.subarray(indices[indices.length - 1] || 0), j); + return normalized; + }, () => (carryOverCR ? new Uint8Array([CR]) : undefined)); + }, + + /** + * Convert line endings from canonicalized to native + * Support any encoding where CR=0x0D, LF=0x0A + */ + nativeEOL(data) { + const CR = 13; + const LF = 10; + let carryOverCR = false; + + return transform(data, bytes => { + if (carryOverCR && bytes[0] !== LF) { + bytes = util.concatUint8Array([new Uint8Array([CR]), bytes]); + } else { + bytes = new Uint8Array(bytes); // Don't mutate passed bytes + } + + if (bytes[bytes.length - 1] === CR) { + carryOverCR = true; + bytes = bytes.subarray(0, -1); + } else { + carryOverCR = false; + } + + let index; + let j = 0; + for (let i = 0; i !== bytes.length; i = index) { + index = bytes.indexOf(CR, i) + 1; + if (!index) index = bytes.length; + const last = index - (bytes[index] === LF ? 1 : 0); + if (i) bytes.copyWithin(j, i, last); + j += last - i; + } + return bytes.subarray(0, j); + }, () => (carryOverCR ? new Uint8Array([CR]) : undefined)); + }, + + /** + * Remove trailing spaces, carriage returns and tabs from each line + */ + removeTrailingSpaces(text) { + return text.split('\n').map(line => { + let i = line.length - 1; + for (; i >= 0 && (line[i] === ' ' || line[i] === '\t' || line[i] === '\r'); i--); + return line.substr(0, i + 1); + }).join('\n'); + }, + + wrapError(message, error) { + if (!error) { + return Error(message); + } + + // update error message + try { + error.message = message + ': ' + error.message; + } catch (e) {} + + return error; + }, + + /** + * Map allowed packet tags to corresponding classes + * Meant to be used to format `allowedPacket` for Packetlist.read + * @param {Array} allowedClasses + * @returns {Object} map from enum.packet to corresponding *Packet class + */ + constructAllowedPackets(allowedClasses) { + const map = {}; + allowedClasses.forEach(PacketClass => { + if (!PacketClass.tag) { + throw Error('Invalid input: expected a packet class'); + } + map[PacketClass.tag] = PacketClass; + }); + return map; + }, + + /** + * Return a Promise that will resolve as soon as one of the promises in input resolves + * or will reject if all input promises all rejected + * (similar to Promise.any, but with slightly different error handling) + * @param {Array} promises + * @return {Promise} Promise resolving to the result of the fastest fulfilled promise + * or rejected with the Error of the last resolved Promise (if all promises are rejected) + */ + anyPromise(promises) { + return new Promise(async (resolve, reject) => { + let exception; + await Promise.all(promises.map(async promise => { + try { + resolve(await promise); + } catch (e) { + exception = e; + } + })); + reject(exception); + }); + }, + + /** + * Return either `a` or `b` based on `cond`, in algorithmic constant time. + * @param {Boolean} cond + * @param {Uint8Array} a + * @param {Uint8Array} b + * @returns `a` if `cond` is true, `b` otherwise + */ + selectUint8Array(cond, a, b) { + const length = Math.max(a.length, b.length); + const result = new Uint8Array(length); + let end = 0; + for (let i = 0; i < result.length; i++) { + result[i] = (a[i] & (256 - cond)) | (b[i] & (255 + cond)); + end += (cond & i < a.length) | ((1 - cond) & i < b.length); + } + return result.subarray(0, end); + }, + /** + * Return either `a` or `b` based on `cond`, in algorithmic constant time. + * NB: it only supports `a, b` with values between 0-255. + * @param {Boolean} cond + * @param {Uint8} a + * @param {Uint8} b + * @returns `a` if `cond` is true, `b` otherwise + */ + selectUint8: (cond, a, b) => (a & (256 - cond)) | (b & (255 + cond)), + /** + * @param {module:enums.symmetric} cipherAlgo + */ + isAES: cipherAlgo => cipherAlgo === enums.symmetric.aes128 || cipherAlgo === enums.symmetric.aes192 || cipherAlgo === enums.symmetric.aes256 + }; + + /* OpenPGP radix-64/base64 string encoding/decoding + * Copyright 2005 Herbert Hanewinkel, www.haneWIN.de + * version 1.0, check www.haneWIN.de for the latest version + * + * This software is provided as-is, without express or implied warranty. + * Permission to use, copy, modify, distribute or sell this software, with or + * without fee, for any purpose and by any individual or organization, is hereby + * granted, provided that the above copyright notice and this paragraph appear + * in all copies. Distribution as a part of an application or binary must + * include the above copyright notice in the documentation and/or other materials + * provided with the application or distribution. + */ + + let encodeChunk = buf => btoa(util.uint8ArrayToString(buf)); + let decodeChunk = str => util.stringToUint8Array(atob(str)); + + /** + * Convert binary array to radix-64 + * @param {Uint8Array | ReadableStream} data - Uint8Array to convert + * @returns {String | ReadableStream} Radix-64 version of input string. + * @static + */ + function encode$1(data) { + let buf = new Uint8Array(); + return transform(data, value => { + buf = util.concatUint8Array([buf, value]); + const r = []; + const bytesPerLine = 45; // 60 chars per line * (3 bytes / 4 chars of base64). + const lines = Math.floor(buf.length / bytesPerLine); + const bytes = lines * bytesPerLine; + const encoded = encodeChunk(buf.subarray(0, bytes)); + for (let i = 0; i < lines; i++) { + r.push(encoded.substr(i * 60, 60)); + r.push('\n'); + } + buf = buf.subarray(bytes); + return r.join(''); + }, () => (buf.length ? encodeChunk(buf) + '\n' : '')); + } + + /** + * Convert radix-64 to binary array + * @param {String | ReadableStream} data - Radix-64 string to convert + * @returns {Uint8Array | ReadableStream} Binary array version of input string. + * @static + */ + function decode$2(data) { + let buf = ''; + return transform(data, value => { + buf += value; + + // Count how many whitespace characters there are in buf + let spaces = 0; + const spacechars = [' ', '\t', '\r', '\n']; + for (let i = 0; i < spacechars.length; i++) { + const spacechar = spacechars[i]; + for (let pos = buf.indexOf(spacechar); pos !== -1; pos = buf.indexOf(spacechar, pos + 1)) { + spaces++; + } + } + + // Backtrack until we have 4n non-whitespace characters + // that we can safely base64-decode + let length = buf.length; + for (; length > 0 && (length - spaces) % 4 !== 0; length--) { + if (spacechars.includes(buf[length])) spaces--; + } + + const decoded = decodeChunk(buf.substr(0, length)); + buf = buf.substr(length); + return decoded; + }, () => decodeChunk(buf)); + } + + /** + * Convert a Base-64 encoded string an array of 8-bit integer + * + * Note: accepts both Radix-64 and URL-safe strings + * @param {String} base64 - Base-64 encoded string to convert + * @returns {Uint8Array} An array of 8-bit integers. + */ + function b64ToUint8Array(base64) { + return decode$2(base64.replace(/-/g, '+').replace(/_/g, '/')); + } + + /** + * Convert an array of 8-bit integer to a Base-64 encoded string + * @param {Uint8Array} bytes - An array of 8-bit integers to convert + * @param {bool} url - If true, output is URL-safe + * @returns {String} Base-64 encoded string. + */ + function uint8ArrayToB64(bytes, url) { + let encoded = encode$1(bytes).replace(/[\r\n]/g, ''); + if (url) { + encoded = encoded.replace(/[+]/g, '-').replace(/[/]/g, '_').replace(/[=]/g, ''); + } + return encoded; + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + var config = { + /** + * @memberof module:config + * @property {Integer} preferredHashAlgorithm Default hash algorithm {@link module:enums.hash} + */ + preferredHashAlgorithm: enums.hash.sha256, + /** + * @memberof module:config + * @property {Integer} preferredSymmetricAlgorithm Default encryption cipher {@link module:enums.symmetric} + */ + preferredSymmetricAlgorithm: enums.symmetric.aes256, + /** + * @memberof module:config + * @property {Integer} compression Default compression algorithm {@link module:enums.compression} + */ + preferredCompressionAlgorithm: enums.compression.uncompressed, + /** + * @memberof module:config + * @property {Integer} deflateLevel Default zip/zlib compression level, between 1 and 9 + */ + deflateLevel: 6, + + /** + * Use Authenticated Encryption with Additional Data (AEAD) protection for symmetric encryption. + * Note: not all OpenPGP implementations are compatible with this option. + * **FUTURE OPENPGP.JS VERSIONS MAY BREAK COMPATIBILITY WHEN USING THIS OPTION** + * @see {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-07|RFC4880bis-07} + * @memberof module:config + * @property {Boolean} aeadProtect + */ + aeadProtect: false, + /** + * Default Authenticated Encryption with Additional Data (AEAD) encryption mode + * Only has an effect when aeadProtect is set to true. + * @memberof module:config + * @property {Integer} preferredAEADAlgorithm Default AEAD mode {@link module:enums.aead} + */ + preferredAEADAlgorithm: enums.aead.eax, + /** + * Chunk Size Byte for Authenticated Encryption with Additional Data (AEAD) mode + * Only has an effect when aeadProtect is set to true. + * Must be an integer value from 0 to 56. + * @memberof module:config + * @property {Integer} aeadChunkSizeByte + */ + aeadChunkSizeByte: 12, + /** + * Use V5 keys. + * Note: not all OpenPGP implementations are compatible with this option. + * **FUTURE OPENPGP.JS VERSIONS MAY BREAK COMPATIBILITY WHEN USING THIS OPTION** + * @memberof module:config + * @property {Boolean} v5Keys + */ + v5Keys: false, + /** + * {@link https://tools.ietf.org/html/rfc4880#section-3.7.1.3|RFC4880 3.7.1.3}: + * Iteration Count Byte for S2K (String to Key) + * @memberof module:config + * @property {Integer} s2kIterationCountByte + */ + s2kIterationCountByte: 224, + /** + * Allow decryption of messages without integrity protection. + * This is an **insecure** setting: + * - message modifications cannot be detected, thus processing the decrypted data is potentially unsafe. + * - it enables downgrade attacks against integrity-protected messages. + * @memberof module:config + * @property {Boolean} allowUnauthenticatedMessages + */ + allowUnauthenticatedMessages: false, + /** + * Allow streaming unauthenticated data before its integrity has been checked. This would allow the application to + * process large streams while limiting memory usage by releasing the decrypted chunks as soon as possible + * and deferring checking their integrity until the decrypted stream has been read in full. + * + * This setting is **insecure** if the partially decrypted message is processed further or displayed to the user. + * @memberof module:config + * @property {Boolean} allowUnauthenticatedStream + */ + allowUnauthenticatedStream: false, + /** + * @memberof module:config + * @property {Boolean} checksumRequired Do not throw error when armor is missing a checksum + */ + checksumRequired: false, + /** + * Minimum RSA key size allowed for key generation and message signing, verification and encryption. + * The default is 2047 since due to a bug, previous versions of OpenPGP.js could generate 2047-bit keys instead of 2048-bit ones. + * @memberof module:config + * @property {Number} minRSABits + */ + minRSABits: 2047, + /** + * Work-around for rare GPG decryption bug when encrypting with multiple passwords. + * **Slower and slightly less secure** + * @memberof module:config + * @property {Boolean} passwordCollisionCheck + */ + passwordCollisionCheck: false, + /** + * @memberof module:config + * @property {Boolean} revocationsExpire If true, expired revocation signatures are ignored + */ + revocationsExpire: false, + /** + * Allow decryption using RSA keys without `encrypt` flag. + * This setting is potentially insecure, but it is needed to get around an old openpgpjs bug + * where key flags were ignored when selecting a key for encryption. + * @memberof module:config + * @property {Boolean} allowInsecureDecryptionWithSigningKeys + */ + allowInsecureDecryptionWithSigningKeys: false, + /** + * Allow verification of message signatures with keys whose validity at the time of signing cannot be determined. + * Instead, a verification key will also be consider valid as long as it is valid at the current time. + * This setting is potentially insecure, but it is needed to verify messages signed with keys that were later reformatted, + * and have self-signature's creation date that does not match the primary key creation date. + * @memberof module:config + * @property {Boolean} allowInsecureDecryptionWithSigningKeys + */ + allowInsecureVerificationWithReformattedKeys: false, + + /** + * Enable constant-time decryption of RSA- and ElGamal-encrypted session keys, to hinder Bleichenbacher-like attacks (https://link.springer.com/chapter/10.1007/BFb0055716). + * This setting has measurable performance impact and it is only helpful in application scenarios where both of the following conditions apply: + * - new/incoming messages are automatically decrypted (without user interaction); + * - an attacker can determine how long it takes to decrypt each message (e.g. due to decryption errors being logged remotely). + * See also `constantTimePKCS1DecryptionSupportedSymmetricAlgorithms`. + * @memberof module:config + * @property {Boolean} constantTimePKCS1Decryption + */ + constantTimePKCS1Decryption: false, + /** + * This setting is only meaningful if `constantTimePKCS1Decryption` is enabled. + * Decryption of RSA- and ElGamal-encrypted session keys of symmetric algorithms different from the ones specified here will fail. + * However, the more algorithms are added, the slower the decryption procedure becomes. + * @memberof module:config + * @property {Set} constantTimePKCS1DecryptionSupportedSymmetricAlgorithms {@link module:enums.symmetric} + */ + constantTimePKCS1DecryptionSupportedSymmetricAlgorithms: new Set([enums.symmetric.aes128, enums.symmetric.aes192, enums.symmetric.aes256]), + + /** + * @memberof module:config + * @property {Integer} minBytesForWebCrypto The minimum amount of bytes for which to use native WebCrypto APIs when available + */ + minBytesForWebCrypto: 1000, + /** + * @memberof module:config + * @property {Boolean} ignoreUnsupportedPackets Ignore unsupported/unrecognizable packets on parsing instead of throwing an error + */ + ignoreUnsupportedPackets: true, + /** + * @memberof module:config + * @property {Boolean} ignoreMalformedPackets Ignore malformed packets on parsing instead of throwing an error + */ + ignoreMalformedPackets: false, + /** + * Parsing of packets is normally restricted to a predefined set of packets. For example a Sym. Encrypted Integrity Protected Data Packet can only + * contain a certain set of packets including LiteralDataPacket. With this setting we can allow additional packets, which is probably not advisable + * as a global config setting, but can be used for specific function calls (e.g. decrypt method of Message). + * @memberof module:config + * @property {Array} additionalAllowedPackets Allow additional packets on parsing. Defined as array of packet classes, e.g. [PublicKeyPacket] + */ + additionalAllowedPackets: [], + /** + * @memberof module:config + * @property {Boolean} showVersion Whether to include {@link module:config/config.versionString} in armored messages + */ + showVersion: false, + /** + * @memberof module:config + * @property {Boolean} showComment Whether to include {@link module:config/config.commentString} in armored messages + */ + showComment: false, + /** + * @memberof module:config + * @property {String} versionString A version string to be included in armored messages + */ + versionString: 'OpenPGP.js 5.11.1', + /** + * @memberof module:config + * @property {String} commentString A comment string to be included in armored messages + */ + commentString: 'https://openpgpjs.org', + + /** + * Max userID string length (used for parsing) + * @memberof module:config + * @property {Integer} maxUserIDLength + */ + maxUserIDLength: 1024 * 5, + /** + * Contains notatations that are considered "known". Known notations do not trigger + * validation error when the notation is marked as critical. + * @memberof module:config + * @property {Array} knownNotations + */ + knownNotations: [], + /** + * Whether to use the indutny/elliptic library for curves (other than Curve25519) that are not supported by the available native crypto API. + * When false, certain standard curves will not be supported (depending on the platform). + * Note: the indutny/elliptic curve library is not designed to be constant time. + * @memberof module:config + * @property {Boolean} useIndutnyElliptic + */ + useIndutnyElliptic: true, + /** + * Reject insecure hash algorithms + * @memberof module:config + * @property {Set} rejectHashAlgorithms {@link module:enums.hash} + */ + rejectHashAlgorithms: new Set([enums.hash.md5, enums.hash.ripemd]), + /** + * Reject insecure message hash algorithms + * @memberof module:config + * @property {Set} rejectMessageHashAlgorithms {@link module:enums.hash} + */ + rejectMessageHashAlgorithms: new Set([enums.hash.md5, enums.hash.ripemd, enums.hash.sha1]), + /** + * Reject insecure public key algorithms for key generation and message encryption, signing or verification + * @memberof module:config + * @property {Set} rejectPublicKeyAlgorithms {@link module:enums.publicKey} + */ + rejectPublicKeyAlgorithms: new Set([enums.publicKey.elgamal, enums.publicKey.dsa]), + /** + * Reject non-standard curves for key generation, message encryption, signing or verification + * @memberof module:config + * @property {Set} rejectCurves {@link module:enums.curve} + */ + rejectCurves: new Set([enums.curve.secp256k1]) + }; + + /** + * @fileoverview This object contains global configuration values. + * @see module:config/config + * @module config + */ + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * Finds out which Ascii Armoring type is used. Throws error if unknown type. + * @param {String} text - ascii armored text + * @returns {Integer} 0 = MESSAGE PART n of m. + * 1 = MESSAGE PART n + * 2 = SIGNED MESSAGE + * 3 = PGP MESSAGE + * 4 = PUBLIC KEY BLOCK + * 5 = PRIVATE KEY BLOCK + * 6 = SIGNATURE + * @private + */ + function getType(text) { + const reHeader = /^-----BEGIN PGP (MESSAGE, PART \d+\/\d+|MESSAGE, PART \d+|SIGNED MESSAGE|MESSAGE|PUBLIC KEY BLOCK|PRIVATE KEY BLOCK|SIGNATURE)-----$/m; + + const header = text.match(reHeader); + + if (!header) { + throw Error('Unknown ASCII armor type'); + } + + // BEGIN PGP MESSAGE, PART X/Y + // Used for multi-part messages, where the armor is split amongst Y + // parts, and this is the Xth part out of Y. + if (/MESSAGE, PART \d+\/\d+/.test(header[1])) { + return enums.armor.multipartSection; + } else + // BEGIN PGP MESSAGE, PART X + // Used for multi-part messages, where this is the Xth part of an + // unspecified number of parts. Requires the MESSAGE-ID Armor + // Header to be used. + if (/MESSAGE, PART \d+/.test(header[1])) { + return enums.armor.multipartLast; + } else + // BEGIN PGP SIGNED MESSAGE + if (/SIGNED MESSAGE/.test(header[1])) { + return enums.armor.signed; + } else + // BEGIN PGP MESSAGE + // Used for signed, encrypted, or compressed files. + if (/MESSAGE/.test(header[1])) { + return enums.armor.message; + } else + // BEGIN PGP PUBLIC KEY BLOCK + // Used for armoring public keys. + if (/PUBLIC KEY BLOCK/.test(header[1])) { + return enums.armor.publicKey; + } else + // BEGIN PGP PRIVATE KEY BLOCK + // Used for armoring private keys. + if (/PRIVATE KEY BLOCK/.test(header[1])) { + return enums.armor.privateKey; + } else + // BEGIN PGP SIGNATURE + // Used for detached signatures, OpenPGP/MIME signatures, and + // cleartext signatures. Note that PGP 2.x uses BEGIN PGP MESSAGE + // for detached signatures. + if (/SIGNATURE/.test(header[1])) { + return enums.armor.signature; + } + } + + /** + * Add additional information to the armor version of an OpenPGP binary + * packet block. + * @author Alex + * @version 2011-12-16 + * @param {String} [customComment] - Additional comment to add to the armored string + * @returns {String} The header information. + * @private + */ + function addheader(customComment, config) { + let result = ''; + if (config.showVersion) { + result += 'Version: ' + config.versionString + '\n'; + } + if (config.showComment) { + result += 'Comment: ' + config.commentString + '\n'; + } + if (customComment) { + result += 'Comment: ' + customComment + '\n'; + } + result += '\n'; + return result; + } + + + /** + * Calculates a checksum over the given data and returns it base64 encoded + * @param {String | ReadableStream} data - Data to create a CRC-24 checksum for + * @returns {String | ReadableStream} Base64 encoded checksum. + * @private + */ + function getCheckSum(data) { + const crc = createcrc24(data); + return encode$1(crc); + } + + // https://create.stephan-brumme.com/crc32/#slicing-by-8-overview + + const crc_table = [ + new Array(0xFF), + new Array(0xFF), + new Array(0xFF), + new Array(0xFF) + ]; + + for (let i = 0; i <= 0xFF; i++) { + let crc = i << 16; + for (let j = 0; j < 8; j++) { + crc = (crc << 1) ^ ((crc & 0x800000) !== 0 ? 0x864CFB : 0); + } + crc_table[0][i] = + ((crc & 0xFF0000) >> 16) | + (crc & 0x00FF00) | + ((crc & 0x0000FF) << 16); + } + for (let i = 0; i <= 0xFF; i++) { + crc_table[1][i] = (crc_table[0][i] >> 8) ^ crc_table[0][crc_table[0][i] & 0xFF]; + } + for (let i = 0; i <= 0xFF; i++) { + crc_table[2][i] = (crc_table[1][i] >> 8) ^ crc_table[0][crc_table[1][i] & 0xFF]; + } + for (let i = 0; i <= 0xFF; i++) { + crc_table[3][i] = (crc_table[2][i] >> 8) ^ crc_table[0][crc_table[2][i] & 0xFF]; + } + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView#Endianness + const isLittleEndian = (function() { + const buffer = new ArrayBuffer(2); + new DataView(buffer).setInt16(0, 0xFF, true /* littleEndian */); + // Int16Array uses the platform's endianness. + return new Int16Array(buffer)[0] === 0xFF; + }()); + + /** + * Internal function to calculate a CRC-24 checksum over a given string (data) + * @param {String | ReadableStream} input - Data to create a CRC-24 checksum for + * @returns {Uint8Array | ReadableStream} The CRC-24 checksum. + * @private + */ + function createcrc24(input) { + let crc = 0xCE04B7; + return transform(input, value => { + const len32 = isLittleEndian ? Math.floor(value.length / 4) : 0; + const arr32 = new Uint32Array(value.buffer, value.byteOffset, len32); + for (let i = 0; i < len32; i++) { + crc ^= arr32[i]; + crc = + crc_table[0][(crc >> 24) & 0xFF] ^ + crc_table[1][(crc >> 16) & 0xFF] ^ + crc_table[2][(crc >> 8) & 0xFF] ^ + crc_table[3][(crc >> 0) & 0xFF]; + } + for (let i = len32 * 4; i < value.length; i++) { + crc = (crc >> 8) ^ crc_table[0][(crc & 0xFF) ^ value[i]]; + } + }, () => new Uint8Array([crc, crc >> 8, crc >> 16])); + } + + /** + * Verify armored headers. crypto-refresh-06, section 6.2: + * "An OpenPGP implementation may consider improperly formatted Armor + * Headers to be corruption of the ASCII Armor, but SHOULD make an + * effort to recover." + * @private + * @param {Array} headers - Armor headers + */ + function verifyHeaders$1(headers) { + for (let i = 0; i < headers.length; i++) { + if (!/^([^\s:]|[^\s:][^:]*[^\s:]): .+$/.test(headers[i])) { + throw Error('Improperly formatted armor header: ' + headers[i]); + } + if (!/^(Version|Comment|MessageID|Hash|Charset): .+$/.test(headers[i])) { + console.error(Error('Unknown header: ' + headers[i])); + } + } + } + + /** + * Splits a message into two parts, the body and the checksum. This is an internal function + * @param {String} text - OpenPGP armored message part + * @returns {Object} An object with attribute "body" containing the body. + * and an attribute "checksum" containing the checksum. + * @private + */ + function splitChecksum(text) { + let body = text; + let checksum = ''; + + const lastEquals = text.lastIndexOf('='); + + if (lastEquals >= 0 && lastEquals !== text.length - 1) { // '=' as the last char means no checksum + body = text.slice(0, lastEquals); + checksum = text.slice(lastEquals + 1).substr(0, 4); + } + + return { body: body, checksum: checksum }; + } + + /** + * Dearmor an OpenPGP armored message; verify the checksum and return + * the encoded bytes + * @param {String} input - OpenPGP armored message + * @returns {Promise} An object with attribute "text" containing the message text, + * an attribute "data" containing a stream of bytes and "type" for the ASCII armor type + * @async + * @static + */ + function unarmor(input, config$1 = config) { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + try { + const reSplit = /^-----[^-]+-----$/m; + const reEmptyLine = /^[ \f\r\t\u00a0\u2000-\u200a\u202f\u205f\u3000]*$/; + + let type; + const headers = []; + let lastHeaders = headers; + let headersDone; + let text = []; + let textDone; + let checksum; + let data = decode$2(transformPair(input, async (readable, writable) => { + const reader = getReader(readable); + try { + while (true) { + let line = await reader.readLine(); + if (line === undefined) { + throw Error('Misformed armored text'); + } + // remove trailing whitespace at end of lines + line = util.removeTrailingSpaces(line.replace(/[\r\n]/g, '')); + if (!type) { + if (reSplit.test(line)) { + type = getType(line); + } + } else if (!headersDone) { + if (reSplit.test(line)) { + reject(Error('Mandatory blank line missing between armor headers and armor data')); + } + if (!reEmptyLine.test(line)) { + lastHeaders.push(line); + } else { + verifyHeaders$1(lastHeaders); + headersDone = true; + if (textDone || type !== 2) { + resolve({ text, data, headers, type }); + break; + } + } + } else if (!textDone && type === 2) { + if (!reSplit.test(line)) { + // Reverse dash-escaping for msg + text.push(line.replace(/^- /, '')); + } else { + text = text.join('\r\n'); + textDone = true; + verifyHeaders$1(lastHeaders); + lastHeaders = []; + headersDone = false; + } + } + } + } catch (e) { + reject(e); + return; + } + const writer = getWriter(writable); + try { + while (true) { + await writer.ready; + const { done, value } = await reader.read(); + if (done) { + throw Error('Misformed armored text'); + } + const line = value + ''; + if (line.indexOf('=') === -1 && line.indexOf('-') === -1) { + await writer.write(line); + } else { + let remainder = await reader.readToEnd(); + if (!remainder.length) remainder = ''; + remainder = line + remainder; + remainder = util.removeTrailingSpaces(remainder.replace(/\r/g, '')); + const parts = remainder.split(reSplit); + if (parts.length === 1) { + throw Error('Misformed armored text'); + } + const split = splitChecksum(parts[0].slice(0, -1)); + checksum = split.checksum; + await writer.write(split.body); + break; + } + } + await writer.ready; + await writer.close(); + } catch (e) { + await writer.abort(e); + } + })); + data = transformPair(data, async (readable, writable) => { + const checksumVerified = readToEnd(getCheckSum(passiveClone(readable))); + checksumVerified.catch(() => {}); + await pipe(readable, writable, { + preventClose: true + }); + const writer = getWriter(writable); + try { + const checksumVerifiedString = (await checksumVerified).replace('\n', ''); + if (checksum !== checksumVerifiedString && (checksum || config$1.checksumRequired)) { + throw Error('Ascii armor integrity check failed'); + } + await writer.ready; + await writer.close(); + } catch (e) { + await writer.abort(e); + } + }); + } catch (e) { + reject(e); + } + }).then(async result => { + if (isArrayStream(result.data)) { + result.data = await readToEnd(result.data); + } + return result; + }); + } + + + /** + * Armor an OpenPGP binary packet block + * @param {module:enums.armor} messageType - Type of the message + * @param {Uint8Array | ReadableStream} body - The message body to armor + * @param {Integer} [partIndex] + * @param {Integer} [partTotal] + * @param {String} [customComment] - Additional comment to add to the armored string + * @returns {String | ReadableStream} Armored text. + * @static + */ + function armor(messageType, body, partIndex, partTotal, customComment, config$1 = config) { + let text; + let hash; + if (messageType === enums.armor.signed) { + text = body.text; + hash = body.hash; + body = body.data; + } + const bodyClone = passiveClone(body); + const result = []; + switch (messageType) { + case enums.armor.multipartSection: + result.push('-----BEGIN PGP MESSAGE, PART ' + partIndex + '/' + partTotal + '-----\n'); + result.push(addheader(customComment, config$1)); + result.push(encode$1(body)); + result.push('=', getCheckSum(bodyClone)); + result.push('-----END PGP MESSAGE, PART ' + partIndex + '/' + partTotal + '-----\n'); + break; + case enums.armor.multipartLast: + result.push('-----BEGIN PGP MESSAGE, PART ' + partIndex + '-----\n'); + result.push(addheader(customComment, config$1)); + result.push(encode$1(body)); + result.push('=', getCheckSum(bodyClone)); + result.push('-----END PGP MESSAGE, PART ' + partIndex + '-----\n'); + break; + case enums.armor.signed: + result.push('-----BEGIN PGP SIGNED MESSAGE-----\n'); + result.push('Hash: ' + hash + '\n\n'); + result.push(text.replace(/^-/mg, '- -')); + result.push('\n-----BEGIN PGP SIGNATURE-----\n'); + result.push(addheader(customComment, config$1)); + result.push(encode$1(body)); + result.push('=', getCheckSum(bodyClone)); + result.push('-----END PGP SIGNATURE-----\n'); + break; + case enums.armor.message: + result.push('-----BEGIN PGP MESSAGE-----\n'); + result.push(addheader(customComment, config$1)); + result.push(encode$1(body)); + result.push('=', getCheckSum(bodyClone)); + result.push('-----END PGP MESSAGE-----\n'); + break; + case enums.armor.publicKey: + result.push('-----BEGIN PGP PUBLIC KEY BLOCK-----\n'); + result.push(addheader(customComment, config$1)); + result.push(encode$1(body)); + result.push('=', getCheckSum(bodyClone)); + result.push('-----END PGP PUBLIC KEY BLOCK-----\n'); + break; + case enums.armor.privateKey: + result.push('-----BEGIN PGP PRIVATE KEY BLOCK-----\n'); + result.push(addheader(customComment, config$1)); + result.push(encode$1(body)); + result.push('=', getCheckSum(bodyClone)); + result.push('-----END PGP PRIVATE KEY BLOCK-----\n'); + break; + case enums.armor.signature: + result.push('-----BEGIN PGP SIGNATURE-----\n'); + result.push(addheader(customComment, config$1)); + result.push(encode$1(body)); + result.push('=', getCheckSum(bodyClone)); + result.push('-----END PGP SIGNATURE-----\n'); + break; + } + + return util.concat(result); + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * Implementation of type key id + * + * {@link https://tools.ietf.org/html/rfc4880#section-3.3|RFC4880 3.3}: + * A Key ID is an eight-octet scalar that identifies a key. + * Implementations SHOULD NOT assume that Key IDs are unique. The + * section "Enhanced Key Formats" below describes how Key IDs are + * formed. + */ + class KeyID { + constructor() { + this.bytes = ''; + } + + /** + * Parsing method for a key id + * @param {Uint8Array} bytes - Input to read the key id from + */ + read(bytes) { + this.bytes = util.uint8ArrayToString(bytes.subarray(0, 8)); + return this.bytes.length; + } + + /** + * Serializes the Key ID + * @returns {Uint8Array} Key ID as a Uint8Array. + */ + write() { + return util.stringToUint8Array(this.bytes); + } + + /** + * Returns the Key ID represented as a hexadecimal string + * @returns {String} Key ID as a hexadecimal string. + */ + toHex() { + return util.uint8ArrayToHex(util.stringToUint8Array(this.bytes)); + } + + /** + * Checks equality of Key ID's + * @param {KeyID} keyID + * @param {Boolean} matchWildcard - Indicates whether to check if either keyID is a wildcard + */ + equals(keyID, matchWildcard = false) { + return (matchWildcard && (keyID.isWildcard() || this.isWildcard())) || this.bytes === keyID.bytes; + } + + /** + * Checks to see if the Key ID is unset + * @returns {Boolean} True if the Key ID is null. + */ + isNull() { + return this.bytes === ''; + } + + /** + * Checks to see if the Key ID is a "wildcard" Key ID (all zeros) + * @returns {Boolean} True if this is a wildcard Key ID. + */ + isWildcard() { + return /^0+$/.test(this.toHex()); + } + + static mapToHex(keyID) { + return keyID.toHex(); + } + + static fromID(hex) { + const keyID = new KeyID(); + keyID.read(util.hexToUint8Array(hex)); + return keyID; + } + + static wildcard() { + const keyID = new KeyID(); + keyID.read(new Uint8Array(8)); + return keyID; + } + } + + /** + * @file {@link http://asmjs.org Asm.js} implementation of the {@link https://en.wikipedia.org/wiki/Advanced_Encryption_Standard Advanced Encryption Standard}. + * @author Artem S Vybornov + * @license MIT + */ + var AES_asm = function () { + + /** + * Galois Field stuff init flag + */ + var ginit_done = false; + + /** + * Galois Field exponentiation and logarithm tables for 3 (the generator) + */ + var gexp3, glog3; + + /** + * Init Galois Field tables + */ + function ginit() { + gexp3 = [], + glog3 = []; + + var a = 1, c, d; + for (c = 0; c < 255; c++) { + gexp3[c] = a; + + // Multiply by three + d = a & 0x80, a <<= 1, a &= 255; + if (d === 0x80) a ^= 0x1b; + a ^= gexp3[c]; + + // Set the log table value + glog3[gexp3[c]] = c; + } + gexp3[255] = gexp3[0]; + glog3[0] = 0; + + ginit_done = true; + } + + /** + * Galois Field multiplication + * @param {number} a + * @param {number} b + * @return {number} + */ + function gmul(a, b) { + var c = gexp3[(glog3[a] + glog3[b]) % 255]; + if (a === 0 || b === 0) c = 0; + return c; + } + + /** + * Galois Field reciprocal + * @param {number} a + * @return {number} + */ + function ginv(a) { + var i = gexp3[255 - glog3[a]]; + if (a === 0) i = 0; + return i; + } + + /** + * AES stuff init flag + */ + var aes_init_done = false; + + /** + * Encryption, Decryption, S-Box and KeyTransform tables + * + * @type {number[]} + */ + var aes_sbox; + + /** + * @type {number[]} + */ + var aes_sinv; + + /** + * @type {number[][]} + */ + var aes_enc; + + /** + * @type {number[][]} + */ + var aes_dec; + + /** + * Init AES tables + */ + function aes_init() { + if (!ginit_done) ginit(); + + // Calculates AES S-Box value + function _s(a) { + var c, s, x; + s = x = ginv(a); + for (c = 0; c < 4; c++) { + s = ((s << 1) | (s >>> 7)) & 255; + x ^= s; + } + x ^= 99; + return x; + } + + // Tables + aes_sbox = [], + aes_sinv = [], + aes_enc = [[], [], [], []], + aes_dec = [[], [], [], []]; + + for (var i = 0; i < 256; i++) { + var s = _s(i); + + // S-Box and its inverse + aes_sbox[i] = s; + aes_sinv[s] = i; + + // Ecryption and Decryption tables + aes_enc[0][i] = (gmul(2, s) << 24) | (s << 16) | (s << 8) | gmul(3, s); + aes_dec[0][s] = (gmul(14, i) << 24) | (gmul(9, i) << 16) | (gmul(13, i) << 8) | gmul(11, i); + // Rotate tables + for (var t = 1; t < 4; t++) { + aes_enc[t][i] = (aes_enc[t - 1][i] >>> 8) | (aes_enc[t - 1][i] << 24); + aes_dec[t][s] = (aes_dec[t - 1][s] >>> 8) | (aes_dec[t - 1][s] << 24); + } + } + + aes_init_done = true; + } + + /** + * Asm.js module constructor. + * + *

    + * Heap buffer layout by offset: + *

    +     * 0x0000   encryption key schedule
    +     * 0x0400   decryption key schedule
    +     * 0x0800   sbox
    +     * 0x0c00   inv sbox
    +     * 0x1000   encryption tables
    +     * 0x2000   decryption tables
    +     * 0x3000   reserved (future GCM multiplication lookup table)
    +     * 0x4000   data
    +     * 
    + * Don't touch anything before 0x400. + *

    + * + * @alias AES_asm + * @class + * @param foreign - ignored + * @param buffer - heap buffer to link with + */ + var wrapper = function (foreign, buffer) { + // Init AES stuff for the first time + if (!aes_init_done) aes_init(); + + // Fill up AES tables + var heap = new Uint32Array(buffer); + heap.set(aes_sbox, 0x0800 >> 2); + heap.set(aes_sinv, 0x0c00 >> 2); + for (var i = 0; i < 4; i++) { + heap.set(aes_enc[i], (0x1000 + 0x400 * i) >> 2); + heap.set(aes_dec[i], (0x2000 + 0x400 * i) >> 2); + } + + /** + * Calculate AES key schedules. + * @instance + * @memberof AES_asm + * @param {number} ks - key size, 4/6/8 (for 128/192/256-bit key correspondingly) + * @param {number} k0 - key vector components + * @param {number} k1 - key vector components + * @param {number} k2 - key vector components + * @param {number} k3 - key vector components + * @param {number} k4 - key vector components + * @param {number} k5 - key vector components + * @param {number} k6 - key vector components + * @param {number} k7 - key vector components + */ + function set_key(ks, k0, k1, k2, k3, k4, k5, k6, k7) { + var ekeys = heap.subarray(0x000, 60), + dkeys = heap.subarray(0x100, 0x100 + 60); + + // Encryption key schedule + ekeys.set([k0, k1, k2, k3, k4, k5, k6, k7]); + for (var i = ks, rcon = 1; i < 4 * ks + 28; i++) { + var k = ekeys[i - 1]; + if ((i % ks === 0) || (ks === 8 && i % ks === 4)) { + k = aes_sbox[k >>> 24] << 24 ^ aes_sbox[k >>> 16 & 255] << 16 ^ aes_sbox[k >>> 8 & 255] << 8 ^ aes_sbox[k & 255]; + } + if (i % ks === 0) { + k = (k << 8) ^ (k >>> 24) ^ (rcon << 24); + rcon = (rcon << 1) ^ ((rcon & 0x80) ? 0x1b : 0); + } + ekeys[i] = ekeys[i - ks] ^ k; + } + + // Decryption key schedule + for (var j = 0; j < i; j += 4) { + for (var jj = 0; jj < 4; jj++) { + var k = ekeys[i - (4 + j) + (4 - jj) % 4]; + if (j < 4 || j >= i - 4) { + dkeys[j + jj] = k; + } else { + dkeys[j + jj] = aes_dec[0][aes_sbox[k >>> 24]] + ^ aes_dec[1][aes_sbox[k >>> 16 & 255]] + ^ aes_dec[2][aes_sbox[k >>> 8 & 255]] + ^ aes_dec[3][aes_sbox[k & 255]]; + } + } + } + + // Set rounds number + asm.set_rounds(ks + 5); + } + + // create library object with necessary properties + var stdlib = {Uint8Array: Uint8Array, Uint32Array: Uint32Array}; + + var asm = function (stdlib, foreign, buffer) { + "use asm"; + + var S0 = 0, S1 = 0, S2 = 0, S3 = 0, + I0 = 0, I1 = 0, I2 = 0, I3 = 0, + N0 = 0, N1 = 0, N2 = 0, N3 = 0, + M0 = 0, M1 = 0, M2 = 0, M3 = 0, + H0 = 0, H1 = 0, H2 = 0, H3 = 0, + R = 0; + + var HEAP = new stdlib.Uint32Array(buffer), + DATA = new stdlib.Uint8Array(buffer); + + /** + * AES core + * @param {number} k - precomputed key schedule offset + * @param {number} s - precomputed sbox table offset + * @param {number} t - precomputed round table offset + * @param {number} r - number of inner rounds to perform + * @param {number} x0 - 128-bit input block vector + * @param {number} x1 - 128-bit input block vector + * @param {number} x2 - 128-bit input block vector + * @param {number} x3 - 128-bit input block vector + */ + function _core(k, s, t, r, x0, x1, x2, x3) { + k = k | 0; + s = s | 0; + t = t | 0; + r = r | 0; + x0 = x0 | 0; + x1 = x1 | 0; + x2 = x2 | 0; + x3 = x3 | 0; + + var t1 = 0, t2 = 0, t3 = 0, + y0 = 0, y1 = 0, y2 = 0, y3 = 0, + i = 0; + + t1 = t | 0x400, t2 = t | 0x800, t3 = t | 0xc00; + + // round 0 + x0 = x0 ^ HEAP[(k | 0) >> 2], + x1 = x1 ^ HEAP[(k | 4) >> 2], + x2 = x2 ^ HEAP[(k | 8) >> 2], + x3 = x3 ^ HEAP[(k | 12) >> 2]; + + // round 1..r + for (i = 16; (i | 0) <= (r << 4); i = (i + 16) | 0) { + y0 = HEAP[(t | x0 >> 22 & 1020) >> 2] ^ HEAP[(t1 | x1 >> 14 & 1020) >> 2] ^ HEAP[(t2 | x2 >> 6 & 1020) >> 2] ^ HEAP[(t3 | x3 << 2 & 1020) >> 2] ^ HEAP[(k | i | 0) >> 2], + y1 = HEAP[(t | x1 >> 22 & 1020) >> 2] ^ HEAP[(t1 | x2 >> 14 & 1020) >> 2] ^ HEAP[(t2 | x3 >> 6 & 1020) >> 2] ^ HEAP[(t3 | x0 << 2 & 1020) >> 2] ^ HEAP[(k | i | 4) >> 2], + y2 = HEAP[(t | x2 >> 22 & 1020) >> 2] ^ HEAP[(t1 | x3 >> 14 & 1020) >> 2] ^ HEAP[(t2 | x0 >> 6 & 1020) >> 2] ^ HEAP[(t3 | x1 << 2 & 1020) >> 2] ^ HEAP[(k | i | 8) >> 2], + y3 = HEAP[(t | x3 >> 22 & 1020) >> 2] ^ HEAP[(t1 | x0 >> 14 & 1020) >> 2] ^ HEAP[(t2 | x1 >> 6 & 1020) >> 2] ^ HEAP[(t3 | x2 << 2 & 1020) >> 2] ^ HEAP[(k | i | 12) >> 2]; + x0 = y0, x1 = y1, x2 = y2, x3 = y3; + } + + // final round + S0 = HEAP[(s | x0 >> 22 & 1020) >> 2] << 24 ^ HEAP[(s | x1 >> 14 & 1020) >> 2] << 16 ^ HEAP[(s | x2 >> 6 & 1020) >> 2] << 8 ^ HEAP[(s | x3 << 2 & 1020) >> 2] ^ HEAP[(k | i | 0) >> 2], + S1 = HEAP[(s | x1 >> 22 & 1020) >> 2] << 24 ^ HEAP[(s | x2 >> 14 & 1020) >> 2] << 16 ^ HEAP[(s | x3 >> 6 & 1020) >> 2] << 8 ^ HEAP[(s | x0 << 2 & 1020) >> 2] ^ HEAP[(k | i | 4) >> 2], + S2 = HEAP[(s | x2 >> 22 & 1020) >> 2] << 24 ^ HEAP[(s | x3 >> 14 & 1020) >> 2] << 16 ^ HEAP[(s | x0 >> 6 & 1020) >> 2] << 8 ^ HEAP[(s | x1 << 2 & 1020) >> 2] ^ HEAP[(k | i | 8) >> 2], + S3 = HEAP[(s | x3 >> 22 & 1020) >> 2] << 24 ^ HEAP[(s | x0 >> 14 & 1020) >> 2] << 16 ^ HEAP[(s | x1 >> 6 & 1020) >> 2] << 8 ^ HEAP[(s | x2 << 2 & 1020) >> 2] ^ HEAP[(k | i | 12) >> 2]; + } + + /** + * ECB mode encryption + * @param {number} x0 - 128-bit input block vector + * @param {number} x1 - 128-bit input block vector + * @param {number} x2 - 128-bit input block vector + * @param {number} x3 - 128-bit input block vector + */ + function _ecb_enc(x0, x1, x2, x3) { + x0 = x0 | 0; + x1 = x1 | 0; + x2 = x2 | 0; + x3 = x3 | 0; + + _core( + 0x0000, 0x0800, 0x1000, + R, + x0, + x1, + x2, + x3 + ); + } + + /** + * ECB mode decryption + * @param {number} x0 - 128-bit input block vector + * @param {number} x1 - 128-bit input block vector + * @param {number} x2 - 128-bit input block vector + * @param {number} x3 - 128-bit input block vector + */ + function _ecb_dec(x0, x1, x2, x3) { + x0 = x0 | 0; + x1 = x1 | 0; + x2 = x2 | 0; + x3 = x3 | 0; + + var t = 0; + + _core( + 0x0400, 0x0c00, 0x2000, + R, + x0, + x3, + x2, + x1 + ); + + t = S1, S1 = S3, S3 = t; + } + + + /** + * CBC mode encryption + * @param {number} x0 - 128-bit input block vector + * @param {number} x1 - 128-bit input block vector + * @param {number} x2 - 128-bit input block vector + * @param {number} x3 - 128-bit input block vector + */ + function _cbc_enc(x0, x1, x2, x3) { + x0 = x0 | 0; + x1 = x1 | 0; + x2 = x2 | 0; + x3 = x3 | 0; + + _core( + 0x0000, 0x0800, 0x1000, + R, + I0 ^ x0, + I1 ^ x1, + I2 ^ x2, + I3 ^ x3 + ); + + I0 = S0, + I1 = S1, + I2 = S2, + I3 = S3; + } + + /** + * CBC mode decryption + * @param {number} x0 - 128-bit input block vector + * @param {number} x1 - 128-bit input block vector + * @param {number} x2 - 128-bit input block vector + * @param {number} x3 - 128-bit input block vector + */ + function _cbc_dec(x0, x1, x2, x3) { + x0 = x0 | 0; + x1 = x1 | 0; + x2 = x2 | 0; + x3 = x3 | 0; + + var t = 0; + + _core( + 0x0400, 0x0c00, 0x2000, + R, + x0, + x3, + x2, + x1 + ); + + t = S1, S1 = S3, S3 = t; + + S0 = S0 ^ I0, + S1 = S1 ^ I1, + S2 = S2 ^ I2, + S3 = S3 ^ I3; + + I0 = x0, + I1 = x1, + I2 = x2, + I3 = x3; + } + + /** + * CFB mode encryption + * @param {number} x0 - 128-bit input block vector + * @param {number} x1 - 128-bit input block vector + * @param {number} x2 - 128-bit input block vector + * @param {number} x3 - 128-bit input block vector + */ + function _cfb_enc(x0, x1, x2, x3) { + x0 = x0 | 0; + x1 = x1 | 0; + x2 = x2 | 0; + x3 = x3 | 0; + + _core( + 0x0000, 0x0800, 0x1000, + R, + I0, + I1, + I2, + I3 + ); + + I0 = S0 = S0 ^ x0, + I1 = S1 = S1 ^ x1, + I2 = S2 = S2 ^ x2, + I3 = S3 = S3 ^ x3; + } + + + /** + * CFB mode decryption + * @param {number} x0 - 128-bit input block vector + * @param {number} x1 - 128-bit input block vector + * @param {number} x2 - 128-bit input block vector + * @param {number} x3 - 128-bit input block vector + */ + function _cfb_dec(x0, x1, x2, x3) { + x0 = x0 | 0; + x1 = x1 | 0; + x2 = x2 | 0; + x3 = x3 | 0; + + _core( + 0x0000, 0x0800, 0x1000, + R, + I0, + I1, + I2, + I3 + ); + + S0 = S0 ^ x0, + S1 = S1 ^ x1, + S2 = S2 ^ x2, + S3 = S3 ^ x3; + + I0 = x0, + I1 = x1, + I2 = x2, + I3 = x3; + } + + /** + * OFB mode encryption / decryption + * @param {number} x0 - 128-bit input block vector + * @param {number} x1 - 128-bit input block vector + * @param {number} x2 - 128-bit input block vector + * @param {number} x3 - 128-bit input block vector + */ + function _ofb(x0, x1, x2, x3) { + x0 = x0 | 0; + x1 = x1 | 0; + x2 = x2 | 0; + x3 = x3 | 0; + + _core( + 0x0000, 0x0800, 0x1000, + R, + I0, + I1, + I2, + I3 + ); + + I0 = S0, + I1 = S1, + I2 = S2, + I3 = S3; + + S0 = S0 ^ x0, + S1 = S1 ^ x1, + S2 = S2 ^ x2, + S3 = S3 ^ x3; + } + + /** + * CTR mode encryption / decryption + * @param {number} x0 - 128-bit input block vector + * @param {number} x1 - 128-bit input block vector + * @param {number} x2 - 128-bit input block vector + * @param {number} x3 - 128-bit input block vector + */ + function _ctr(x0, x1, x2, x3) { + x0 = x0 | 0; + x1 = x1 | 0; + x2 = x2 | 0; + x3 = x3 | 0; + + _core( + 0x0000, 0x0800, 0x1000, + R, + N0, + N1, + N2, + N3 + ); + + N3 = (~M3 & N3) | M3 & (N3 + 1); + N2 = (~M2 & N2) | M2 & (N2 + ((N3 | 0) == 0)); + N1 = (~M1 & N1) | M1 & (N1 + ((N2 | 0) == 0)); + N0 = (~M0 & N0) | M0 & (N0 + ((N1 | 0) == 0)); + + S0 = S0 ^ x0; + S1 = S1 ^ x1; + S2 = S2 ^ x2; + S3 = S3 ^ x3; + } + + /** + * GCM mode MAC calculation + * @param {number} x0 - 128-bit input block vector + * @param {number} x1 - 128-bit input block vector + * @param {number} x2 - 128-bit input block vector + * @param {number} x3 - 128-bit input block vector + */ + function _gcm_mac(x0, x1, x2, x3) { + x0 = x0 | 0; + x1 = x1 | 0; + x2 = x2 | 0; + x3 = x3 | 0; + + var y0 = 0, y1 = 0, y2 = 0, y3 = 0, + z0 = 0, z1 = 0, z2 = 0, z3 = 0, + i = 0, c = 0; + + x0 = x0 ^ I0, + x1 = x1 ^ I1, + x2 = x2 ^ I2, + x3 = x3 ^ I3; + + y0 = H0 | 0, + y1 = H1 | 0, + y2 = H2 | 0, + y3 = H3 | 0; + + for (; (i | 0) < 128; i = (i + 1) | 0) { + if (y0 >>> 31) { + z0 = z0 ^ x0, + z1 = z1 ^ x1, + z2 = z2 ^ x2, + z3 = z3 ^ x3; + } + + y0 = (y0 << 1) | (y1 >>> 31), + y1 = (y1 << 1) | (y2 >>> 31), + y2 = (y2 << 1) | (y3 >>> 31), + y3 = (y3 << 1); + + c = x3 & 1; + + x3 = (x3 >>> 1) | (x2 << 31), + x2 = (x2 >>> 1) | (x1 << 31), + x1 = (x1 >>> 1) | (x0 << 31), + x0 = (x0 >>> 1); + + if (c) x0 = x0 ^ 0xe1000000; + } + + I0 = z0, + I1 = z1, + I2 = z2, + I3 = z3; + } + + /** + * Set the internal rounds number. + * @instance + * @memberof AES_asm + * @param {number} r - number if inner AES rounds + */ + function set_rounds(r) { + r = r | 0; + R = r; + } + + /** + * Populate the internal state of the module. + * @instance + * @memberof AES_asm + * @param {number} s0 - state vector + * @param {number} s1 - state vector + * @param {number} s2 - state vector + * @param {number} s3 - state vector + */ + function set_state(s0, s1, s2, s3) { + s0 = s0 | 0; + s1 = s1 | 0; + s2 = s2 | 0; + s3 = s3 | 0; + + S0 = s0, + S1 = s1, + S2 = s2, + S3 = s3; + } + + /** + * Populate the internal iv of the module. + * @instance + * @memberof AES_asm + * @param {number} i0 - iv vector + * @param {number} i1 - iv vector + * @param {number} i2 - iv vector + * @param {number} i3 - iv vector + */ + function set_iv(i0, i1, i2, i3) { + i0 = i0 | 0; + i1 = i1 | 0; + i2 = i2 | 0; + i3 = i3 | 0; + + I0 = i0, + I1 = i1, + I2 = i2, + I3 = i3; + } + + /** + * Set nonce for CTR-family modes. + * @instance + * @memberof AES_asm + * @param {number} n0 - nonce vector + * @param {number} n1 - nonce vector + * @param {number} n2 - nonce vector + * @param {number} n3 - nonce vector + */ + function set_nonce(n0, n1, n2, n3) { + n0 = n0 | 0; + n1 = n1 | 0; + n2 = n2 | 0; + n3 = n3 | 0; + + N0 = n0, + N1 = n1, + N2 = n2, + N3 = n3; + } + + /** + * Set counter mask for CTR-family modes. + * @instance + * @memberof AES_asm + * @param {number} m0 - counter mask vector + * @param {number} m1 - counter mask vector + * @param {number} m2 - counter mask vector + * @param {number} m3 - counter mask vector + */ + function set_mask(m0, m1, m2, m3) { + m0 = m0 | 0; + m1 = m1 | 0; + m2 = m2 | 0; + m3 = m3 | 0; + + M0 = m0, + M1 = m1, + M2 = m2, + M3 = m3; + } + + /** + * Set counter for CTR-family modes. + * @instance + * @memberof AES_asm + * @param {number} c0 - counter vector + * @param {number} c1 - counter vector + * @param {number} c2 - counter vector + * @param {number} c3 - counter vector + */ + function set_counter(c0, c1, c2, c3) { + c0 = c0 | 0; + c1 = c1 | 0; + c2 = c2 | 0; + c3 = c3 | 0; + + N3 = (~M3 & N3) | M3 & c3, + N2 = (~M2 & N2) | M2 & c2, + N1 = (~M1 & N1) | M1 & c1, + N0 = (~M0 & N0) | M0 & c0; + } + + /** + * Store the internal state vector into the heap. + * @instance + * @memberof AES_asm + * @param {number} pos - offset where to put the data + * @return {number} The number of bytes have been written into the heap, always 16. + */ + function get_state(pos) { + pos = pos | 0; + + if (pos & 15) return -1; + + DATA[pos | 0] = S0 >>> 24, + DATA[pos | 1] = S0 >>> 16 & 255, + DATA[pos | 2] = S0 >>> 8 & 255, + DATA[pos | 3] = S0 & 255, + DATA[pos | 4] = S1 >>> 24, + DATA[pos | 5] = S1 >>> 16 & 255, + DATA[pos | 6] = S1 >>> 8 & 255, + DATA[pos | 7] = S1 & 255, + DATA[pos | 8] = S2 >>> 24, + DATA[pos | 9] = S2 >>> 16 & 255, + DATA[pos | 10] = S2 >>> 8 & 255, + DATA[pos | 11] = S2 & 255, + DATA[pos | 12] = S3 >>> 24, + DATA[pos | 13] = S3 >>> 16 & 255, + DATA[pos | 14] = S3 >>> 8 & 255, + DATA[pos | 15] = S3 & 255; + + return 16; + } + + /** + * Store the internal iv vector into the heap. + * @instance + * @memberof AES_asm + * @param {number} pos - offset where to put the data + * @return {number} The number of bytes have been written into the heap, always 16. + */ + function get_iv(pos) { + pos = pos | 0; + + if (pos & 15) return -1; + + DATA[pos | 0] = I0 >>> 24, + DATA[pos | 1] = I0 >>> 16 & 255, + DATA[pos | 2] = I0 >>> 8 & 255, + DATA[pos | 3] = I0 & 255, + DATA[pos | 4] = I1 >>> 24, + DATA[pos | 5] = I1 >>> 16 & 255, + DATA[pos | 6] = I1 >>> 8 & 255, + DATA[pos | 7] = I1 & 255, + DATA[pos | 8] = I2 >>> 24, + DATA[pos | 9] = I2 >>> 16 & 255, + DATA[pos | 10] = I2 >>> 8 & 255, + DATA[pos | 11] = I2 & 255, + DATA[pos | 12] = I3 >>> 24, + DATA[pos | 13] = I3 >>> 16 & 255, + DATA[pos | 14] = I3 >>> 8 & 255, + DATA[pos | 15] = I3 & 255; + + return 16; + } + + /** + * GCM initialization. + * @instance + * @memberof AES_asm + */ + function gcm_init() { + _ecb_enc(0, 0, 0, 0); + H0 = S0, + H1 = S1, + H2 = S2, + H3 = S3; + } + + /** + * Perform ciphering operation on the supplied data. + * @instance + * @memberof AES_asm + * @param {number} mode - block cipher mode (see {@link AES_asm} mode constants) + * @param {number} pos - offset of the data being processed + * @param {number} len - length of the data being processed + * @return {number} Actual amount of data have been processed. + */ + function cipher(mode, pos, len) { + mode = mode | 0; + pos = pos | 0; + len = len | 0; + + var ret = 0; + + if (pos & 15) return -1; + + while ((len | 0) >= 16) { + _cipher_modes[mode & 7]( + DATA[pos | 0] << 24 | DATA[pos | 1] << 16 | DATA[pos | 2] << 8 | DATA[pos | 3], + DATA[pos | 4] << 24 | DATA[pos | 5] << 16 | DATA[pos | 6] << 8 | DATA[pos | 7], + DATA[pos | 8] << 24 | DATA[pos | 9] << 16 | DATA[pos | 10] << 8 | DATA[pos | 11], + DATA[pos | 12] << 24 | DATA[pos | 13] << 16 | DATA[pos | 14] << 8 | DATA[pos | 15] + ); + + DATA[pos | 0] = S0 >>> 24, + DATA[pos | 1] = S0 >>> 16 & 255, + DATA[pos | 2] = S0 >>> 8 & 255, + DATA[pos | 3] = S0 & 255, + DATA[pos | 4] = S1 >>> 24, + DATA[pos | 5] = S1 >>> 16 & 255, + DATA[pos | 6] = S1 >>> 8 & 255, + DATA[pos | 7] = S1 & 255, + DATA[pos | 8] = S2 >>> 24, + DATA[pos | 9] = S2 >>> 16 & 255, + DATA[pos | 10] = S2 >>> 8 & 255, + DATA[pos | 11] = S2 & 255, + DATA[pos | 12] = S3 >>> 24, + DATA[pos | 13] = S3 >>> 16 & 255, + DATA[pos | 14] = S3 >>> 8 & 255, + DATA[pos | 15] = S3 & 255; + + ret = (ret + 16) | 0, + pos = (pos + 16) | 0, + len = (len - 16) | 0; + } + + return ret | 0; + } + + /** + * Calculates MAC of the supplied data. + * @instance + * @memberof AES_asm + * @param {number} mode - block cipher mode (see {@link AES_asm} mode constants) + * @param {number} pos - offset of the data being processed + * @param {number} len - length of the data being processed + * @return {number} Actual amount of data have been processed. + */ + function mac(mode, pos, len) { + mode = mode | 0; + pos = pos | 0; + len = len | 0; + + var ret = 0; + + if (pos & 15) return -1; + + while ((len | 0) >= 16) { + _mac_modes[mode & 1]( + DATA[pos | 0] << 24 | DATA[pos | 1] << 16 | DATA[pos | 2] << 8 | DATA[pos | 3], + DATA[pos | 4] << 24 | DATA[pos | 5] << 16 | DATA[pos | 6] << 8 | DATA[pos | 7], + DATA[pos | 8] << 24 | DATA[pos | 9] << 16 | DATA[pos | 10] << 8 | DATA[pos | 11], + DATA[pos | 12] << 24 | DATA[pos | 13] << 16 | DATA[pos | 14] << 8 | DATA[pos | 15] + ); + + ret = (ret + 16) | 0, + pos = (pos + 16) | 0, + len = (len - 16) | 0; + } + + return ret | 0; + } + + /** + * AES cipher modes table (virual methods) + */ + var _cipher_modes = [_ecb_enc, _ecb_dec, _cbc_enc, _cbc_dec, _cfb_enc, _cfb_dec, _ofb, _ctr]; + + /** + * AES MAC modes table (virual methods) + */ + var _mac_modes = [_cbc_enc, _gcm_mac]; + + /** + * Asm.js module exports + */ + return { + set_rounds: set_rounds, + set_state: set_state, + set_iv: set_iv, + set_nonce: set_nonce, + set_mask: set_mask, + set_counter: set_counter, + get_state: get_state, + get_iv: get_iv, + gcm_init: gcm_init, + cipher: cipher, + mac: mac, + }; + }(stdlib, foreign, buffer); + + asm.set_key = set_key; + + return asm; + }; + + /** + * AES enciphering mode constants + * @enum {number} + * @const + */ + wrapper.ENC = { + ECB: 0, + CBC: 2, + CFB: 4, + OFB: 6, + CTR: 7, + }, + + /** + * AES deciphering mode constants + * @enum {number} + * @const + */ + wrapper.DEC = { + ECB: 1, + CBC: 3, + CFB: 5, + OFB: 6, + CTR: 7, + }, + + /** + * AES MAC mode constants + * @enum {number} + * @const + */ + wrapper.MAC = { + CBC: 0, + GCM: 1, + }; + + /** + * Heap data offset + * @type {number} + * @const + */ + wrapper.HEAP_DATA = 0x4000; + + return wrapper; + }(); + + function is_bytes(a) { + return a instanceof Uint8Array; + } + function _heap_init(heap, heapSize) { + const size = heap ? heap.byteLength : heapSize || 65536; + if (size & 0xfff || size <= 0) + throw Error('heap size must be a positive integer and a multiple of 4096'); + heap = heap || new Uint8Array(new ArrayBuffer(size)); + return heap; + } + function _heap_write(heap, hpos, data, dpos, dlen) { + const hlen = heap.length - hpos; + const wlen = hlen < dlen ? hlen : dlen; + heap.set(data.subarray(dpos, dpos + wlen), hpos); + return wlen; + } + function joinBytes(...arg) { + const totalLenght = arg.reduce((sum, curr) => sum + curr.length, 0); + const ret = new Uint8Array(totalLenght); + let cursor = 0; + for (let i = 0; i < arg.length; i++) { + ret.set(arg[i], cursor); + cursor += arg[i].length; + } + return ret; + } + + class IllegalStateError extends Error { + constructor(...args) { + super(...args); + } + } + class IllegalArgumentError extends Error { + constructor(...args) { + super(...args); + } + } + class SecurityError extends Error { + constructor(...args) { + super(...args); + } + } + + const heap_pool$2 = []; + const asm_pool$2 = []; + class AES { + constructor(key, iv, padding = true, mode, heap, asm) { + this.pos = 0; + this.len = 0; + this.mode = mode; + // The AES object state + this.pos = 0; + this.len = 0; + this.key = key; + this.iv = iv; + this.padding = padding; + // The AES "worker" + this.acquire_asm(heap, asm); + } + acquire_asm(heap, asm) { + if (this.heap === undefined || this.asm === undefined) { + this.heap = heap || heap_pool$2.pop() || _heap_init().subarray(AES_asm.HEAP_DATA); + this.asm = asm || asm_pool$2.pop() || new AES_asm(null, this.heap.buffer); + this.reset(this.key, this.iv); + } + return { heap: this.heap, asm: this.asm }; + } + release_asm() { + if (this.heap !== undefined && this.asm !== undefined) { + heap_pool$2.push(this.heap); + asm_pool$2.push(this.asm); + } + this.heap = undefined; + this.asm = undefined; + } + reset(key, iv) { + const { asm } = this.acquire_asm(); + // Key + const keylen = key.length; + if (keylen !== 16 && keylen !== 24 && keylen !== 32) + throw new IllegalArgumentError('illegal key size'); + const keyview = new DataView(key.buffer, key.byteOffset, key.byteLength); + asm.set_key(keylen >> 2, keyview.getUint32(0), keyview.getUint32(4), keyview.getUint32(8), keyview.getUint32(12), keylen > 16 ? keyview.getUint32(16) : 0, keylen > 16 ? keyview.getUint32(20) : 0, keylen > 24 ? keyview.getUint32(24) : 0, keylen > 24 ? keyview.getUint32(28) : 0); + // IV + if (iv !== undefined) { + if (iv.length !== 16) + throw new IllegalArgumentError('illegal iv size'); + let ivview = new DataView(iv.buffer, iv.byteOffset, iv.byteLength); + asm.set_iv(ivview.getUint32(0), ivview.getUint32(4), ivview.getUint32(8), ivview.getUint32(12)); + } + else { + asm.set_iv(0, 0, 0, 0); + } + } + AES_Encrypt_process(data) { + if (!is_bytes(data)) + throw new TypeError("data isn't of expected type"); + let { heap, asm } = this.acquire_asm(); + let amode = AES_asm.ENC[this.mode]; + let hpos = AES_asm.HEAP_DATA; + let pos = this.pos; + let len = this.len; + let dpos = 0; + let dlen = data.length || 0; + let rpos = 0; + let rlen = (len + dlen) & -16; + let wlen = 0; + let result = new Uint8Array(rlen); + while (dlen > 0) { + wlen = _heap_write(heap, pos + len, data, dpos, dlen); + len += wlen; + dpos += wlen; + dlen -= wlen; + wlen = asm.cipher(amode, hpos + pos, len); + if (wlen) + result.set(heap.subarray(pos, pos + wlen), rpos); + rpos += wlen; + if (wlen < len) { + pos += wlen; + len -= wlen; + } + else { + pos = 0; + len = 0; + } + } + this.pos = pos; + this.len = len; + return result; + } + AES_Encrypt_finish() { + let { heap, asm } = this.acquire_asm(); + let amode = AES_asm.ENC[this.mode]; + let hpos = AES_asm.HEAP_DATA; + let pos = this.pos; + let len = this.len; + let plen = 16 - (len % 16); + let rlen = len; + if (this.hasOwnProperty('padding')) { + if (this.padding) { + for (let p = 0; p < plen; ++p) { + heap[pos + len + p] = plen; + } + len += plen; + rlen = len; + } + else if (len % 16) { + throw new IllegalArgumentError('data length must be a multiple of the block size'); + } + } + else { + len += plen; + } + const result = new Uint8Array(rlen); + if (len) + asm.cipher(amode, hpos + pos, len); + if (rlen) + result.set(heap.subarray(pos, pos + rlen)); + this.pos = 0; + this.len = 0; + this.release_asm(); + return result; + } + AES_Decrypt_process(data) { + if (!is_bytes(data)) + throw new TypeError("data isn't of expected type"); + let { heap, asm } = this.acquire_asm(); + let amode = AES_asm.DEC[this.mode]; + let hpos = AES_asm.HEAP_DATA; + let pos = this.pos; + let len = this.len; + let dpos = 0; + let dlen = data.length || 0; + let rpos = 0; + let rlen = (len + dlen) & -16; + let plen = 0; + let wlen = 0; + if (this.padding) { + plen = len + dlen - rlen || 16; + rlen -= plen; + } + const result = new Uint8Array(rlen); + while (dlen > 0) { + wlen = _heap_write(heap, pos + len, data, dpos, dlen); + len += wlen; + dpos += wlen; + dlen -= wlen; + wlen = asm.cipher(amode, hpos + pos, len - (!dlen ? plen : 0)); + if (wlen) + result.set(heap.subarray(pos, pos + wlen), rpos); + rpos += wlen; + if (wlen < len) { + pos += wlen; + len -= wlen; + } + else { + pos = 0; + len = 0; + } + } + this.pos = pos; + this.len = len; + return result; + } + AES_Decrypt_finish() { + let { heap, asm } = this.acquire_asm(); + let amode = AES_asm.DEC[this.mode]; + let hpos = AES_asm.HEAP_DATA; + let pos = this.pos; + let len = this.len; + let rlen = len; + if (len > 0) { + if (len % 16) { + if (this.hasOwnProperty('padding')) { + throw new IllegalArgumentError('data length must be a multiple of the block size'); + } + else { + len += 16 - (len % 16); + } + } + asm.cipher(amode, hpos + pos, len); + if (this.hasOwnProperty('padding') && this.padding) { + let pad = heap[pos + rlen - 1]; + if (pad < 1 || pad > 16 || pad > rlen) + throw new SecurityError('bad padding'); + let pcheck = 0; + for (let i = pad; i > 1; i--) + pcheck |= pad ^ heap[pos + rlen - i]; + if (pcheck) + throw new SecurityError('bad padding'); + rlen -= pad; + } + } + const result = new Uint8Array(rlen); + if (rlen > 0) { + result.set(heap.subarray(pos, pos + rlen)); + } + this.pos = 0; + this.len = 0; + this.release_asm(); + return result; + } + } + + class AES_ECB { + static encrypt(data, key, padding = false) { + return new AES_ECB(key, padding).encrypt(data); + } + static decrypt(data, key, padding = false) { + return new AES_ECB(key, padding).decrypt(data); + } + constructor(key, padding = false, aes) { + this.aes = aes ? aes : new AES(key, undefined, padding, 'ECB'); + } + encrypt(data) { + const r1 = this.aes.AES_Encrypt_process(data); + const r2 = this.aes.AES_Encrypt_finish(); + return joinBytes(r1, r2); + } + decrypt(data) { + const r1 = this.aes.AES_Decrypt_process(data); + const r2 = this.aes.AES_Decrypt_finish(); + return joinBytes(r1, r2); + } + } + + /** + * Javascript AES implementation. + * This is used as fallback if the native Crypto APIs are not available. + */ + function aes(length) { + const C = function(key) { + const aesECB = new AES_ECB(key); + + this.encrypt = function(block) { + return aesECB.encrypt(block); + }; + + this.decrypt = function(block) { + return aesECB.decrypt(block); + }; + }; + + C.blockSize = C.prototype.blockSize = 16; + C.keySize = C.prototype.keySize = length / 8; + + return C; + } + + //Paul Tero, July 2001 + //http://www.tero.co.uk/des/ + // + //Optimised for performance with large blocks by Michael Hayworth, November 2001 + //http://www.netdealing.com + // + // Modified by Recurity Labs GmbH + + //THIS SOFTWARE IS PROVIDED "AS IS" AND + //ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + //IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + //ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + //FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + //DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + //OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + //HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + //LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + //OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + //SUCH DAMAGE. + + //des + //this takes the key, the message, and whether to encrypt or decrypt + + function des$1(keys, message, encrypt, mode, iv, padding) { + //declaring this locally speeds things up a bit + const spfunction1 = [ + 0x1010400, 0, 0x10000, 0x1010404, 0x1010004, 0x10404, 0x4, 0x10000, 0x400, 0x1010400, + 0x1010404, 0x400, 0x1000404, 0x1010004, 0x1000000, 0x4, 0x404, 0x1000400, 0x1000400, 0x10400, 0x10400, 0x1010000, + 0x1010000, 0x1000404, 0x10004, 0x1000004, 0x1000004, 0x10004, 0, 0x404, 0x10404, 0x1000000, 0x10000, 0x1010404, 0x4, + 0x1010000, 0x1010400, 0x1000000, 0x1000000, 0x400, 0x1010004, 0x10000, 0x10400, 0x1000004, 0x400, 0x4, 0x1000404, + 0x10404, 0x1010404, 0x10004, 0x1010000, 0x1000404, 0x1000004, 0x404, 0x10404, 0x1010400, 0x404, 0x1000400, + 0x1000400, 0, 0x10004, 0x10400, 0, 0x1010004 + ]; + const spfunction2 = [ + -0x7fef7fe0, -0x7fff8000, 0x8000, 0x108020, 0x100000, 0x20, -0x7fefffe0, -0x7fff7fe0, + -0x7fffffe0, -0x7fef7fe0, -0x7fef8000, -0x80000000, -0x7fff8000, 0x100000, 0x20, -0x7fefffe0, 0x108000, 0x100020, + -0x7fff7fe0, 0, -0x80000000, 0x8000, 0x108020, -0x7ff00000, 0x100020, -0x7fffffe0, 0, 0x108000, 0x8020, -0x7fef8000, + -0x7ff00000, 0x8020, 0, 0x108020, -0x7fefffe0, 0x100000, -0x7fff7fe0, -0x7ff00000, -0x7fef8000, 0x8000, -0x7ff00000, + -0x7fff8000, 0x20, -0x7fef7fe0, 0x108020, 0x20, 0x8000, -0x80000000, 0x8020, -0x7fef8000, 0x100000, -0x7fffffe0, + 0x100020, -0x7fff7fe0, -0x7fffffe0, 0x100020, 0x108000, 0, -0x7fff8000, 0x8020, -0x80000000, -0x7fefffe0, + -0x7fef7fe0, 0x108000 + ]; + const spfunction3 = [ + 0x208, 0x8020200, 0, 0x8020008, 0x8000200, 0, 0x20208, 0x8000200, 0x20008, 0x8000008, + 0x8000008, 0x20000, 0x8020208, 0x20008, 0x8020000, 0x208, 0x8000000, 0x8, 0x8020200, 0x200, 0x20200, 0x8020000, + 0x8020008, 0x20208, 0x8000208, 0x20200, 0x20000, 0x8000208, 0x8, 0x8020208, 0x200, 0x8000000, 0x8020200, 0x8000000, + 0x20008, 0x208, 0x20000, 0x8020200, 0x8000200, 0, 0x200, 0x20008, 0x8020208, 0x8000200, 0x8000008, 0x200, 0, + 0x8020008, 0x8000208, 0x20000, 0x8000000, 0x8020208, 0x8, 0x20208, 0x20200, 0x8000008, 0x8020000, 0x8000208, 0x208, + 0x8020000, 0x20208, 0x8, 0x8020008, 0x20200 + ]; + const spfunction4 = [ + 0x802001, 0x2081, 0x2081, 0x80, 0x802080, 0x800081, 0x800001, 0x2001, 0, 0x802000, + 0x802000, 0x802081, 0x81, 0, 0x800080, 0x800001, 0x1, 0x2000, 0x800000, 0x802001, 0x80, 0x800000, 0x2001, 0x2080, + 0x800081, 0x1, 0x2080, 0x800080, 0x2000, 0x802080, 0x802081, 0x81, 0x800080, 0x800001, 0x802000, 0x802081, 0x81, 0, + 0, 0x802000, 0x2080, 0x800080, 0x800081, 0x1, 0x802001, 0x2081, 0x2081, 0x80, 0x802081, 0x81, 0x1, 0x2000, 0x800001, + 0x2001, 0x802080, 0x800081, 0x2001, 0x2080, 0x800000, 0x802001, 0x80, 0x800000, 0x2000, 0x802080 + ]; + const spfunction5 = [ + 0x100, 0x2080100, 0x2080000, 0x42000100, 0x80000, 0x100, 0x40000000, 0x2080000, + 0x40080100, 0x80000, 0x2000100, 0x40080100, 0x42000100, 0x42080000, 0x80100, 0x40000000, 0x2000000, 0x40080000, + 0x40080000, 0, 0x40000100, 0x42080100, 0x42080100, 0x2000100, 0x42080000, 0x40000100, 0, 0x42000000, 0x2080100, + 0x2000000, 0x42000000, 0x80100, 0x80000, 0x42000100, 0x100, 0x2000000, 0x40000000, 0x2080000, 0x42000100, + 0x40080100, 0x2000100, 0x40000000, 0x42080000, 0x2080100, 0x40080100, 0x100, 0x2000000, 0x42080000, 0x42080100, + 0x80100, 0x42000000, 0x42080100, 0x2080000, 0, 0x40080000, 0x42000000, 0x80100, 0x2000100, 0x40000100, 0x80000, 0, + 0x40080000, 0x2080100, 0x40000100 + ]; + const spfunction6 = [ + 0x20000010, 0x20400000, 0x4000, 0x20404010, 0x20400000, 0x10, 0x20404010, 0x400000, + 0x20004000, 0x404010, 0x400000, 0x20000010, 0x400010, 0x20004000, 0x20000000, 0x4010, 0, 0x400010, 0x20004010, + 0x4000, 0x404000, 0x20004010, 0x10, 0x20400010, 0x20400010, 0, 0x404010, 0x20404000, 0x4010, 0x404000, 0x20404000, + 0x20000000, 0x20004000, 0x10, 0x20400010, 0x404000, 0x20404010, 0x400000, 0x4010, 0x20000010, 0x400000, 0x20004000, + 0x20000000, 0x4010, 0x20000010, 0x20404010, 0x404000, 0x20400000, 0x404010, 0x20404000, 0, 0x20400010, 0x10, 0x4000, + 0x20400000, 0x404010, 0x4000, 0x400010, 0x20004010, 0, 0x20404000, 0x20000000, 0x400010, 0x20004010 + ]; + const spfunction7 = [ + 0x200000, 0x4200002, 0x4000802, 0, 0x800, 0x4000802, 0x200802, 0x4200800, 0x4200802, + 0x200000, 0, 0x4000002, 0x2, 0x4000000, 0x4200002, 0x802, 0x4000800, 0x200802, 0x200002, 0x4000800, 0x4000002, + 0x4200000, 0x4200800, 0x200002, 0x4200000, 0x800, 0x802, 0x4200802, 0x200800, 0x2, 0x4000000, 0x200800, 0x4000000, + 0x200800, 0x200000, 0x4000802, 0x4000802, 0x4200002, 0x4200002, 0x2, 0x200002, 0x4000000, 0x4000800, 0x200000, + 0x4200800, 0x802, 0x200802, 0x4200800, 0x802, 0x4000002, 0x4200802, 0x4200000, 0x200800, 0, 0x2, 0x4200802, 0, + 0x200802, 0x4200000, 0x800, 0x4000002, 0x4000800, 0x800, 0x200002 + ]; + const spfunction8 = [ + 0x10001040, 0x1000, 0x40000, 0x10041040, 0x10000000, 0x10001040, 0x40, 0x10000000, + 0x40040, 0x10040000, 0x10041040, 0x41000, 0x10041000, 0x41040, 0x1000, 0x40, 0x10040000, 0x10000040, 0x10001000, + 0x1040, 0x41000, 0x40040, 0x10040040, 0x10041000, 0x1040, 0, 0, 0x10040040, 0x10000040, 0x10001000, 0x41040, + 0x40000, 0x41040, 0x40000, 0x10041000, 0x1000, 0x40, 0x10040040, 0x1000, 0x41040, 0x10001000, 0x40, 0x10000040, + 0x10040000, 0x10040040, 0x10000000, 0x40000, 0x10001040, 0, 0x10041040, 0x40040, 0x10000040, 0x10040000, 0x10001000, + 0x10001040, 0, 0x10041040, 0x41000, 0x41000, 0x1040, 0x1040, 0x40040, 0x10000000, 0x10041000 + ]; + + //create the 16 or 48 subkeys we will need + let m = 0; + let i; + let j; + let temp; + let right1; + let right2; + let left; + let right; + let looping; + let cbcleft; + let cbcleft2; + let cbcright; + let cbcright2; + let endloop; + let loopinc; + let len = message.length; + + //set up the loops for single and triple des + const iterations = keys.length === 32 ? 3 : 9; //single or triple des + if (iterations === 3) { + looping = encrypt ? [0, 32, 2] : [30, -2, -2]; + } else { + looping = encrypt ? [0, 32, 2, 62, 30, -2, 64, 96, 2] : [94, 62, -2, 32, 64, 2, 30, -2, -2]; + } + + //pad the message depending on the padding parameter + //only add padding if encrypting - note that you need to use the same padding option for both encrypt and decrypt + if (encrypt) { + message = desAddPadding(message, padding); + len = message.length; + } + + //store the result here + let result = new Uint8Array(len); + let k = 0; + + if (mode === 1) { //CBC mode + cbcleft = (iv[m++] << 24) | (iv[m++] << 16) | (iv[m++] << 8) | iv[m++]; + cbcright = (iv[m++] << 24) | (iv[m++] << 16) | (iv[m++] << 8) | iv[m++]; + m = 0; + } + + //loop through each 64 bit chunk of the message + while (m < len) { + left = (message[m++] << 24) | (message[m++] << 16) | (message[m++] << 8) | message[m++]; + right = (message[m++] << 24) | (message[m++] << 16) | (message[m++] << 8) | message[m++]; + + //for Cipher Block Chaining mode, xor the message with the previous result + if (mode === 1) { + if (encrypt) { + left ^= cbcleft; + right ^= cbcright; + } else { + cbcleft2 = cbcleft; + cbcright2 = cbcright; + cbcleft = left; + cbcright = right; + } + } + + //first each 64 but chunk of the message must be permuted according to IP + temp = ((left >>> 4) ^ right) & 0x0f0f0f0f; + right ^= temp; + left ^= (temp << 4); + temp = ((left >>> 16) ^ right) & 0x0000ffff; + right ^= temp; + left ^= (temp << 16); + temp = ((right >>> 2) ^ left) & 0x33333333; + left ^= temp; + right ^= (temp << 2); + temp = ((right >>> 8) ^ left) & 0x00ff00ff; + left ^= temp; + right ^= (temp << 8); + temp = ((left >>> 1) ^ right) & 0x55555555; + right ^= temp; + left ^= (temp << 1); + + left = ((left << 1) | (left >>> 31)); + right = ((right << 1) | (right >>> 31)); + + //do this either 1 or 3 times for each chunk of the message + for (j = 0; j < iterations; j += 3) { + endloop = looping[j + 1]; + loopinc = looping[j + 2]; + //now go through and perform the encryption or decryption + for (i = looping[j]; i !== endloop; i += loopinc) { //for efficiency + right1 = right ^ keys[i]; + right2 = ((right >>> 4) | (right << 28)) ^ keys[i + 1]; + //the result is attained by passing these bytes through the S selection functions + temp = left; + left = right; + right = temp ^ (spfunction2[(right1 >>> 24) & 0x3f] | spfunction4[(right1 >>> 16) & 0x3f] | spfunction6[(right1 >>> + 8) & 0x3f] | spfunction8[right1 & 0x3f] | spfunction1[(right2 >>> 24) & 0x3f] | spfunction3[(right2 >>> 16) & + 0x3f] | spfunction5[(right2 >>> 8) & 0x3f] | spfunction7[right2 & 0x3f]); + } + temp = left; + left = right; + right = temp; //unreverse left and right + } //for either 1 or 3 iterations + + //move then each one bit to the right + left = ((left >>> 1) | (left << 31)); + right = ((right >>> 1) | (right << 31)); + + //now perform IP-1, which is IP in the opposite direction + temp = ((left >>> 1) ^ right) & 0x55555555; + right ^= temp; + left ^= (temp << 1); + temp = ((right >>> 8) ^ left) & 0x00ff00ff; + left ^= temp; + right ^= (temp << 8); + temp = ((right >>> 2) ^ left) & 0x33333333; + left ^= temp; + right ^= (temp << 2); + temp = ((left >>> 16) ^ right) & 0x0000ffff; + right ^= temp; + left ^= (temp << 16); + temp = ((left >>> 4) ^ right) & 0x0f0f0f0f; + right ^= temp; + left ^= (temp << 4); + + //for Cipher Block Chaining mode, xor the message with the previous result + if (mode === 1) { + if (encrypt) { + cbcleft = left; + cbcright = right; + } else { + left ^= cbcleft2; + right ^= cbcright2; + } + } + + result[k++] = (left >>> 24); + result[k++] = ((left >>> 16) & 0xff); + result[k++] = ((left >>> 8) & 0xff); + result[k++] = (left & 0xff); + result[k++] = (right >>> 24); + result[k++] = ((right >>> 16) & 0xff); + result[k++] = ((right >>> 8) & 0xff); + result[k++] = (right & 0xff); + } //for every 8 characters, or 64 bits in the message + + //only remove padding if decrypting - note that you need to use the same padding option for both encrypt and decrypt + if (!encrypt) { + result = desRemovePadding(result, padding); + } + + return result; + } //end of des + + + //desCreateKeys + //this takes as input a 64 bit key (even though only 56 bits are used) + //as an array of 2 integers, and returns 16 48 bit keys + + function desCreateKeys(key) { + //declaring this locally speeds things up a bit + const pc2bytes0 = [ + 0, 0x4, 0x20000000, 0x20000004, 0x10000, 0x10004, 0x20010000, 0x20010004, 0x200, 0x204, + 0x20000200, 0x20000204, 0x10200, 0x10204, 0x20010200, 0x20010204 + ]; + const pc2bytes1 = [ + 0, 0x1, 0x100000, 0x100001, 0x4000000, 0x4000001, 0x4100000, 0x4100001, 0x100, 0x101, 0x100100, + 0x100101, 0x4000100, 0x4000101, 0x4100100, 0x4100101 + ]; + const pc2bytes2 = [ + 0, 0x8, 0x800, 0x808, 0x1000000, 0x1000008, 0x1000800, 0x1000808, 0, 0x8, 0x800, 0x808, + 0x1000000, 0x1000008, 0x1000800, 0x1000808 + ]; + const pc2bytes3 = [ + 0, 0x200000, 0x8000000, 0x8200000, 0x2000, 0x202000, 0x8002000, 0x8202000, 0x20000, 0x220000, + 0x8020000, 0x8220000, 0x22000, 0x222000, 0x8022000, 0x8222000 + ]; + const pc2bytes4 = [ + 0, 0x40000, 0x10, 0x40010, 0, 0x40000, 0x10, 0x40010, 0x1000, 0x41000, 0x1010, 0x41010, 0x1000, + 0x41000, 0x1010, 0x41010 + ]; + const pc2bytes5 = [ + 0, 0x400, 0x20, 0x420, 0, 0x400, 0x20, 0x420, 0x2000000, 0x2000400, 0x2000020, 0x2000420, + 0x2000000, 0x2000400, 0x2000020, 0x2000420 + ]; + const pc2bytes6 = [ + 0, 0x10000000, 0x80000, 0x10080000, 0x2, 0x10000002, 0x80002, 0x10080002, 0, 0x10000000, + 0x80000, 0x10080000, 0x2, 0x10000002, 0x80002, 0x10080002 + ]; + const pc2bytes7 = [ + 0, 0x10000, 0x800, 0x10800, 0x20000000, 0x20010000, 0x20000800, 0x20010800, 0x20000, 0x30000, + 0x20800, 0x30800, 0x20020000, 0x20030000, 0x20020800, 0x20030800 + ]; + const pc2bytes8 = [ + 0, 0x40000, 0, 0x40000, 0x2, 0x40002, 0x2, 0x40002, 0x2000000, 0x2040000, 0x2000000, 0x2040000, + 0x2000002, 0x2040002, 0x2000002, 0x2040002 + ]; + const pc2bytes9 = [ + 0, 0x10000000, 0x8, 0x10000008, 0, 0x10000000, 0x8, 0x10000008, 0x400, 0x10000400, 0x408, + 0x10000408, 0x400, 0x10000400, 0x408, 0x10000408 + ]; + const pc2bytes10 = [ + 0, 0x20, 0, 0x20, 0x100000, 0x100020, 0x100000, 0x100020, 0x2000, 0x2020, 0x2000, 0x2020, + 0x102000, 0x102020, 0x102000, 0x102020 + ]; + const pc2bytes11 = [ + 0, 0x1000000, 0x200, 0x1000200, 0x200000, 0x1200000, 0x200200, 0x1200200, 0x4000000, 0x5000000, + 0x4000200, 0x5000200, 0x4200000, 0x5200000, 0x4200200, 0x5200200 + ]; + const pc2bytes12 = [ + 0, 0x1000, 0x8000000, 0x8001000, 0x80000, 0x81000, 0x8080000, 0x8081000, 0x10, 0x1010, + 0x8000010, 0x8001010, 0x80010, 0x81010, 0x8080010, 0x8081010 + ]; + const pc2bytes13 = [0, 0x4, 0x100, 0x104, 0, 0x4, 0x100, 0x104, 0x1, 0x5, 0x101, 0x105, 0x1, 0x5, 0x101, 0x105]; + + //how many iterations (1 for des, 3 for triple des) + const iterations = key.length > 8 ? 3 : 1; //changed by Paul 16/6/2007 to use Triple DES for 9+ byte keys + //stores the return keys + const keys = new Array(32 * iterations); + //now define the left shifts which need to be done + const shifts = [0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0]; + //other variables + let lefttemp; + let righttemp; + let m = 0; + let n = 0; + let temp; + + for (let j = 0; j < iterations; j++) { //either 1 or 3 iterations + let left = (key[m++] << 24) | (key[m++] << 16) | (key[m++] << 8) | key[m++]; + let right = (key[m++] << 24) | (key[m++] << 16) | (key[m++] << 8) | key[m++]; + + temp = ((left >>> 4) ^ right) & 0x0f0f0f0f; + right ^= temp; + left ^= (temp << 4); + temp = ((right >>> -16) ^ left) & 0x0000ffff; + left ^= temp; + right ^= (temp << -16); + temp = ((left >>> 2) ^ right) & 0x33333333; + right ^= temp; + left ^= (temp << 2); + temp = ((right >>> -16) ^ left) & 0x0000ffff; + left ^= temp; + right ^= (temp << -16); + temp = ((left >>> 1) ^ right) & 0x55555555; + right ^= temp; + left ^= (temp << 1); + temp = ((right >>> 8) ^ left) & 0x00ff00ff; + left ^= temp; + right ^= (temp << 8); + temp = ((left >>> 1) ^ right) & 0x55555555; + right ^= temp; + left ^= (temp << 1); + + //the right side needs to be shifted and to get the last four bits of the left side + temp = (left << 8) | ((right >>> 20) & 0x000000f0); + //left needs to be put upside down + left = (right << 24) | ((right << 8) & 0xff0000) | ((right >>> 8) & 0xff00) | ((right >>> 24) & 0xf0); + right = temp; + + //now go through and perform these shifts on the left and right keys + for (let i = 0; i < shifts.length; i++) { + //shift the keys either one or two bits to the left + if (shifts[i]) { + left = (left << 2) | (left >>> 26); + right = (right << 2) | (right >>> 26); + } else { + left = (left << 1) | (left >>> 27); + right = (right << 1) | (right >>> 27); + } + left &= -0xf; + right &= -0xf; + + //now apply PC-2, in such a way that E is easier when encrypting or decrypting + //this conversion will look like PC-2 except only the last 6 bits of each byte are used + //rather than 48 consecutive bits and the order of lines will be according to + //how the S selection functions will be applied: S2, S4, S6, S8, S1, S3, S5, S7 + lefttemp = pc2bytes0[left >>> 28] | pc2bytes1[(left >>> 24) & 0xf] | pc2bytes2[(left >>> 20) & 0xf] | pc2bytes3[( + left >>> 16) & 0xf] | pc2bytes4[(left >>> 12) & 0xf] | pc2bytes5[(left >>> 8) & 0xf] | pc2bytes6[(left >>> 4) & + 0xf]; + righttemp = pc2bytes7[right >>> 28] | pc2bytes8[(right >>> 24) & 0xf] | pc2bytes9[(right >>> 20) & 0xf] | + pc2bytes10[(right >>> 16) & 0xf] | pc2bytes11[(right >>> 12) & 0xf] | pc2bytes12[(right >>> 8) & 0xf] | + pc2bytes13[(right >>> 4) & 0xf]; + temp = ((righttemp >>> 16) ^ lefttemp) & 0x0000ffff; + keys[n++] = lefttemp ^ temp; + keys[n++] = righttemp ^ (temp << 16); + } + } //for each iterations + //return the keys we've created + return keys; + } //end of desCreateKeys + + + function desAddPadding(message, padding) { + const padLength = 8 - (message.length % 8); + + let pad; + if (padding === 2 && (padLength < 8)) { //pad the message with spaces + pad = ' '.charCodeAt(0); + } else if (padding === 1) { //PKCS7 padding + pad = padLength; + } else if (!padding && (padLength < 8)) { //pad the message out with null bytes + pad = 0; + } else if (padLength === 8) { + return message; + } else { + throw Error('des: invalid padding'); + } + + const paddedMessage = new Uint8Array(message.length + padLength); + for (let i = 0; i < message.length; i++) { + paddedMessage[i] = message[i]; + } + for (let j = 0; j < padLength; j++) { + paddedMessage[message.length + j] = pad; + } + + return paddedMessage; + } + + function desRemovePadding(message, padding) { + let padLength = null; + let pad; + if (padding === 2) { // space padded + pad = ' '.charCodeAt(0); + } else if (padding === 1) { // PKCS7 + padLength = message[message.length - 1]; + } else if (!padding) { // null padding + pad = 0; + } else { + throw Error('des: invalid padding'); + } + + if (!padLength) { + padLength = 1; + while (message[message.length - padLength] === pad) { + padLength++; + } + padLength--; + } + + return message.subarray(0, message.length - padLength); + } + + // added by Recurity Labs + + function TripleDES(key) { + this.key = []; + + for (let i = 0; i < 3; i++) { + this.key.push(new Uint8Array(key.subarray(i * 8, (i * 8) + 8))); + } + + this.encrypt = function(block) { + return des$1( + desCreateKeys(this.key[2]), + des$1( + desCreateKeys(this.key[1]), + des$1( + desCreateKeys(this.key[0]), + block, true, 0, null, null + ), + false, 0, null, null + ), true, 0, null, null + ); + }; + } + + TripleDES.keySize = TripleDES.prototype.keySize = 24; + TripleDES.blockSize = TripleDES.prototype.blockSize = 8; + + // This is "original" DES + + function DES(key) { + this.key = key; + + this.encrypt = function(block, padding) { + const keys = desCreateKeys(this.key); + return des$1(keys, block, true, 0, null, padding); + }; + + this.decrypt = function(block, padding) { + const keys = desCreateKeys(this.key); + return des$1(keys, block, false, 0, null, padding); + }; + } + + // Use of this source code is governed by a BSD-style + // license that can be found in the LICENSE file. + + // Copyright 2010 pjacobs@xeekr.com . All rights reserved. + + // Modified by Recurity Labs GmbH + + // fixed/modified by Herbert Hanewinkel, www.haneWIN.de + // check www.haneWIN.de for the latest version + + // cast5.js is a Javascript implementation of CAST-128, as defined in RFC 2144. + // CAST-128 is a common OpenPGP cipher. + + + // CAST5 constructor + + function OpenPGPSymEncCAST5() { + this.BlockSize = 8; + this.KeySize = 16; + + this.setKey = function(key) { + this.masking = new Array(16); + this.rotate = new Array(16); + + this.reset(); + + if (key.length === this.KeySize) { + this.keySchedule(key); + } else { + throw Error('CAST-128: keys must be 16 bytes'); + } + return true; + }; + + this.reset = function() { + for (let i = 0; i < 16; i++) { + this.masking[i] = 0; + this.rotate[i] = 0; + } + }; + + this.getBlockSize = function() { + return this.BlockSize; + }; + + this.encrypt = function(src) { + const dst = new Array(src.length); + + for (let i = 0; i < src.length; i += 8) { + let l = (src[i] << 24) | (src[i + 1] << 16) | (src[i + 2] << 8) | src[i + 3]; + let r = (src[i + 4] << 24) | (src[i + 5] << 16) | (src[i + 6] << 8) | src[i + 7]; + let t; + + t = r; + r = l ^ f1(r, this.masking[0], this.rotate[0]); + l = t; + t = r; + r = l ^ f2(r, this.masking[1], this.rotate[1]); + l = t; + t = r; + r = l ^ f3(r, this.masking[2], this.rotate[2]); + l = t; + t = r; + r = l ^ f1(r, this.masking[3], this.rotate[3]); + l = t; + + t = r; + r = l ^ f2(r, this.masking[4], this.rotate[4]); + l = t; + t = r; + r = l ^ f3(r, this.masking[5], this.rotate[5]); + l = t; + t = r; + r = l ^ f1(r, this.masking[6], this.rotate[6]); + l = t; + t = r; + r = l ^ f2(r, this.masking[7], this.rotate[7]); + l = t; + + t = r; + r = l ^ f3(r, this.masking[8], this.rotate[8]); + l = t; + t = r; + r = l ^ f1(r, this.masking[9], this.rotate[9]); + l = t; + t = r; + r = l ^ f2(r, this.masking[10], this.rotate[10]); + l = t; + t = r; + r = l ^ f3(r, this.masking[11], this.rotate[11]); + l = t; + + t = r; + r = l ^ f1(r, this.masking[12], this.rotate[12]); + l = t; + t = r; + r = l ^ f2(r, this.masking[13], this.rotate[13]); + l = t; + t = r; + r = l ^ f3(r, this.masking[14], this.rotate[14]); + l = t; + t = r; + r = l ^ f1(r, this.masking[15], this.rotate[15]); + l = t; + + dst[i] = (r >>> 24) & 255; + dst[i + 1] = (r >>> 16) & 255; + dst[i + 2] = (r >>> 8) & 255; + dst[i + 3] = r & 255; + dst[i + 4] = (l >>> 24) & 255; + dst[i + 5] = (l >>> 16) & 255; + dst[i + 6] = (l >>> 8) & 255; + dst[i + 7] = l & 255; + } + + return dst; + }; + + this.decrypt = function(src) { + const dst = new Array(src.length); + + for (let i = 0; i < src.length; i += 8) { + let l = (src[i] << 24) | (src[i + 1] << 16) | (src[i + 2] << 8) | src[i + 3]; + let r = (src[i + 4] << 24) | (src[i + 5] << 16) | (src[i + 6] << 8) | src[i + 7]; + let t; + + t = r; + r = l ^ f1(r, this.masking[15], this.rotate[15]); + l = t; + t = r; + r = l ^ f3(r, this.masking[14], this.rotate[14]); + l = t; + t = r; + r = l ^ f2(r, this.masking[13], this.rotate[13]); + l = t; + t = r; + r = l ^ f1(r, this.masking[12], this.rotate[12]); + l = t; + + t = r; + r = l ^ f3(r, this.masking[11], this.rotate[11]); + l = t; + t = r; + r = l ^ f2(r, this.masking[10], this.rotate[10]); + l = t; + t = r; + r = l ^ f1(r, this.masking[9], this.rotate[9]); + l = t; + t = r; + r = l ^ f3(r, this.masking[8], this.rotate[8]); + l = t; + + t = r; + r = l ^ f2(r, this.masking[7], this.rotate[7]); + l = t; + t = r; + r = l ^ f1(r, this.masking[6], this.rotate[6]); + l = t; + t = r; + r = l ^ f3(r, this.masking[5], this.rotate[5]); + l = t; + t = r; + r = l ^ f2(r, this.masking[4], this.rotate[4]); + l = t; + + t = r; + r = l ^ f1(r, this.masking[3], this.rotate[3]); + l = t; + t = r; + r = l ^ f3(r, this.masking[2], this.rotate[2]); + l = t; + t = r; + r = l ^ f2(r, this.masking[1], this.rotate[1]); + l = t; + t = r; + r = l ^ f1(r, this.masking[0], this.rotate[0]); + l = t; + + dst[i] = (r >>> 24) & 255; + dst[i + 1] = (r >>> 16) & 255; + dst[i + 2] = (r >>> 8) & 255; + dst[i + 3] = r & 255; + dst[i + 4] = (l >>> 24) & 255; + dst[i + 5] = (l >> 16) & 255; + dst[i + 6] = (l >> 8) & 255; + dst[i + 7] = l & 255; + } + + return dst; + }; + const scheduleA = new Array(4); + + scheduleA[0] = new Array(4); + scheduleA[0][0] = [4, 0, 0xd, 0xf, 0xc, 0xe, 0x8]; + scheduleA[0][1] = [5, 2, 16 + 0, 16 + 2, 16 + 1, 16 + 3, 0xa]; + scheduleA[0][2] = [6, 3, 16 + 7, 16 + 6, 16 + 5, 16 + 4, 9]; + scheduleA[0][3] = [7, 1, 16 + 0xa, 16 + 9, 16 + 0xb, 16 + 8, 0xb]; + + scheduleA[1] = new Array(4); + scheduleA[1][0] = [0, 6, 16 + 5, 16 + 7, 16 + 4, 16 + 6, 16 + 0]; + scheduleA[1][1] = [1, 4, 0, 2, 1, 3, 16 + 2]; + scheduleA[1][2] = [2, 5, 7, 6, 5, 4, 16 + 1]; + scheduleA[1][3] = [3, 7, 0xa, 9, 0xb, 8, 16 + 3]; + + scheduleA[2] = new Array(4); + scheduleA[2][0] = [4, 0, 0xd, 0xf, 0xc, 0xe, 8]; + scheduleA[2][1] = [5, 2, 16 + 0, 16 + 2, 16 + 1, 16 + 3, 0xa]; + scheduleA[2][2] = [6, 3, 16 + 7, 16 + 6, 16 + 5, 16 + 4, 9]; + scheduleA[2][3] = [7, 1, 16 + 0xa, 16 + 9, 16 + 0xb, 16 + 8, 0xb]; + + + scheduleA[3] = new Array(4); + scheduleA[3][0] = [0, 6, 16 + 5, 16 + 7, 16 + 4, 16 + 6, 16 + 0]; + scheduleA[3][1] = [1, 4, 0, 2, 1, 3, 16 + 2]; + scheduleA[3][2] = [2, 5, 7, 6, 5, 4, 16 + 1]; + scheduleA[3][3] = [3, 7, 0xa, 9, 0xb, 8, 16 + 3]; + + const scheduleB = new Array(4); + + scheduleB[0] = new Array(4); + scheduleB[0][0] = [16 + 8, 16 + 9, 16 + 7, 16 + 6, 16 + 2]; + scheduleB[0][1] = [16 + 0xa, 16 + 0xb, 16 + 5, 16 + 4, 16 + 6]; + scheduleB[0][2] = [16 + 0xc, 16 + 0xd, 16 + 3, 16 + 2, 16 + 9]; + scheduleB[0][3] = [16 + 0xe, 16 + 0xf, 16 + 1, 16 + 0, 16 + 0xc]; + + scheduleB[1] = new Array(4); + scheduleB[1][0] = [3, 2, 0xc, 0xd, 8]; + scheduleB[1][1] = [1, 0, 0xe, 0xf, 0xd]; + scheduleB[1][2] = [7, 6, 8, 9, 3]; + scheduleB[1][3] = [5, 4, 0xa, 0xb, 7]; + + + scheduleB[2] = new Array(4); + scheduleB[2][0] = [16 + 3, 16 + 2, 16 + 0xc, 16 + 0xd, 16 + 9]; + scheduleB[2][1] = [16 + 1, 16 + 0, 16 + 0xe, 16 + 0xf, 16 + 0xc]; + scheduleB[2][2] = [16 + 7, 16 + 6, 16 + 8, 16 + 9, 16 + 2]; + scheduleB[2][3] = [16 + 5, 16 + 4, 16 + 0xa, 16 + 0xb, 16 + 6]; + + + scheduleB[3] = new Array(4); + scheduleB[3][0] = [8, 9, 7, 6, 3]; + scheduleB[3][1] = [0xa, 0xb, 5, 4, 7]; + scheduleB[3][2] = [0xc, 0xd, 3, 2, 8]; + scheduleB[3][3] = [0xe, 0xf, 1, 0, 0xd]; + + // changed 'in' to 'inn' (in javascript 'in' is a reserved word) + this.keySchedule = function(inn) { + const t = new Array(8); + const k = new Array(32); + + let j; + + for (let i = 0; i < 4; i++) { + j = i * 4; + t[i] = (inn[j] << 24) | (inn[j + 1] << 16) | (inn[j + 2] << 8) | inn[j + 3]; + } + + const x = [6, 7, 4, 5]; + let ki = 0; + let w; + + for (let half = 0; half < 2; half++) { + for (let round = 0; round < 4; round++) { + for (j = 0; j < 4; j++) { + const a = scheduleA[round][j]; + w = t[a[1]]; + + w ^= sBox[4][(t[a[2] >>> 2] >>> (24 - 8 * (a[2] & 3))) & 0xff]; + w ^= sBox[5][(t[a[3] >>> 2] >>> (24 - 8 * (a[3] & 3))) & 0xff]; + w ^= sBox[6][(t[a[4] >>> 2] >>> (24 - 8 * (a[4] & 3))) & 0xff]; + w ^= sBox[7][(t[a[5] >>> 2] >>> (24 - 8 * (a[5] & 3))) & 0xff]; + w ^= sBox[x[j]][(t[a[6] >>> 2] >>> (24 - 8 * (a[6] & 3))) & 0xff]; + t[a[0]] = w; + } + + for (j = 0; j < 4; j++) { + const b = scheduleB[round][j]; + w = sBox[4][(t[b[0] >>> 2] >>> (24 - 8 * (b[0] & 3))) & 0xff]; + + w ^= sBox[5][(t[b[1] >>> 2] >>> (24 - 8 * (b[1] & 3))) & 0xff]; + w ^= sBox[6][(t[b[2] >>> 2] >>> (24 - 8 * (b[2] & 3))) & 0xff]; + w ^= sBox[7][(t[b[3] >>> 2] >>> (24 - 8 * (b[3] & 3))) & 0xff]; + w ^= sBox[4 + j][(t[b[4] >>> 2] >>> (24 - 8 * (b[4] & 3))) & 0xff]; + k[ki] = w; + ki++; + } + } + } + + for (let i = 0; i < 16; i++) { + this.masking[i] = k[i]; + this.rotate[i] = k[16 + i] & 0x1f; + } + }; + + // These are the three 'f' functions. See RFC 2144, section 2.2. + + function f1(d, m, r) { + const t = m + d; + const I = (t << r) | (t >>> (32 - r)); + return ((sBox[0][I >>> 24] ^ sBox[1][(I >>> 16) & 255]) - sBox[2][(I >>> 8) & 255]) + sBox[3][I & 255]; + } + + function f2(d, m, r) { + const t = m ^ d; + const I = (t << r) | (t >>> (32 - r)); + return ((sBox[0][I >>> 24] - sBox[1][(I >>> 16) & 255]) + sBox[2][(I >>> 8) & 255]) ^ sBox[3][I & 255]; + } + + function f3(d, m, r) { + const t = m - d; + const I = (t << r) | (t >>> (32 - r)); + return ((sBox[0][I >>> 24] + sBox[1][(I >>> 16) & 255]) ^ sBox[2][(I >>> 8) & 255]) - sBox[3][I & 255]; + } + + const sBox = new Array(8); + sBox[0] = [ + 0x30fb40d4, 0x9fa0ff0b, 0x6beccd2f, 0x3f258c7a, 0x1e213f2f, 0x9c004dd3, 0x6003e540, 0xcf9fc949, + 0xbfd4af27, 0x88bbbdb5, 0xe2034090, 0x98d09675, 0x6e63a0e0, 0x15c361d2, 0xc2e7661d, 0x22d4ff8e, + 0x28683b6f, 0xc07fd059, 0xff2379c8, 0x775f50e2, 0x43c340d3, 0xdf2f8656, 0x887ca41a, 0xa2d2bd2d, + 0xa1c9e0d6, 0x346c4819, 0x61b76d87, 0x22540f2f, 0x2abe32e1, 0xaa54166b, 0x22568e3a, 0xa2d341d0, + 0x66db40c8, 0xa784392f, 0x004dff2f, 0x2db9d2de, 0x97943fac, 0x4a97c1d8, 0x527644b7, 0xb5f437a7, + 0xb82cbaef, 0xd751d159, 0x6ff7f0ed, 0x5a097a1f, 0x827b68d0, 0x90ecf52e, 0x22b0c054, 0xbc8e5935, + 0x4b6d2f7f, 0x50bb64a2, 0xd2664910, 0xbee5812d, 0xb7332290, 0xe93b159f, 0xb48ee411, 0x4bff345d, + 0xfd45c240, 0xad31973f, 0xc4f6d02e, 0x55fc8165, 0xd5b1caad, 0xa1ac2dae, 0xa2d4b76d, 0xc19b0c50, + 0x882240f2, 0x0c6e4f38, 0xa4e4bfd7, 0x4f5ba272, 0x564c1d2f, 0xc59c5319, 0xb949e354, 0xb04669fe, + 0xb1b6ab8a, 0xc71358dd, 0x6385c545, 0x110f935d, 0x57538ad5, 0x6a390493, 0xe63d37e0, 0x2a54f6b3, + 0x3a787d5f, 0x6276a0b5, 0x19a6fcdf, 0x7a42206a, 0x29f9d4d5, 0xf61b1891, 0xbb72275e, 0xaa508167, + 0x38901091, 0xc6b505eb, 0x84c7cb8c, 0x2ad75a0f, 0x874a1427, 0xa2d1936b, 0x2ad286af, 0xaa56d291, + 0xd7894360, 0x425c750d, 0x93b39e26, 0x187184c9, 0x6c00b32d, 0x73e2bb14, 0xa0bebc3c, 0x54623779, + 0x64459eab, 0x3f328b82, 0x7718cf82, 0x59a2cea6, 0x04ee002e, 0x89fe78e6, 0x3fab0950, 0x325ff6c2, + 0x81383f05, 0x6963c5c8, 0x76cb5ad6, 0xd49974c9, 0xca180dcf, 0x380782d5, 0xc7fa5cf6, 0x8ac31511, + 0x35e79e13, 0x47da91d0, 0xf40f9086, 0xa7e2419e, 0x31366241, 0x051ef495, 0xaa573b04, 0x4a805d8d, + 0x548300d0, 0x00322a3c, 0xbf64cddf, 0xba57a68e, 0x75c6372b, 0x50afd341, 0xa7c13275, 0x915a0bf5, + 0x6b54bfab, 0x2b0b1426, 0xab4cc9d7, 0x449ccd82, 0xf7fbf265, 0xab85c5f3, 0x1b55db94, 0xaad4e324, + 0xcfa4bd3f, 0x2deaa3e2, 0x9e204d02, 0xc8bd25ac, 0xeadf55b3, 0xd5bd9e98, 0xe31231b2, 0x2ad5ad6c, + 0x954329de, 0xadbe4528, 0xd8710f69, 0xaa51c90f, 0xaa786bf6, 0x22513f1e, 0xaa51a79b, 0x2ad344cc, + 0x7b5a41f0, 0xd37cfbad, 0x1b069505, 0x41ece491, 0xb4c332e6, 0x032268d4, 0xc9600acc, 0xce387e6d, + 0xbf6bb16c, 0x6a70fb78, 0x0d03d9c9, 0xd4df39de, 0xe01063da, 0x4736f464, 0x5ad328d8, 0xb347cc96, + 0x75bb0fc3, 0x98511bfb, 0x4ffbcc35, 0xb58bcf6a, 0xe11f0abc, 0xbfc5fe4a, 0xa70aec10, 0xac39570a, + 0x3f04442f, 0x6188b153, 0xe0397a2e, 0x5727cb79, 0x9ceb418f, 0x1cacd68d, 0x2ad37c96, 0x0175cb9d, + 0xc69dff09, 0xc75b65f0, 0xd9db40d8, 0xec0e7779, 0x4744ead4, 0xb11c3274, 0xdd24cb9e, 0x7e1c54bd, + 0xf01144f9, 0xd2240eb1, 0x9675b3fd, 0xa3ac3755, 0xd47c27af, 0x51c85f4d, 0x56907596, 0xa5bb15e6, + 0x580304f0, 0xca042cf1, 0x011a37ea, 0x8dbfaadb, 0x35ba3e4a, 0x3526ffa0, 0xc37b4d09, 0xbc306ed9, + 0x98a52666, 0x5648f725, 0xff5e569d, 0x0ced63d0, 0x7c63b2cf, 0x700b45e1, 0xd5ea50f1, 0x85a92872, + 0xaf1fbda7, 0xd4234870, 0xa7870bf3, 0x2d3b4d79, 0x42e04198, 0x0cd0ede7, 0x26470db8, 0xf881814c, + 0x474d6ad7, 0x7c0c5e5c, 0xd1231959, 0x381b7298, 0xf5d2f4db, 0xab838653, 0x6e2f1e23, 0x83719c9e, + 0xbd91e046, 0x9a56456e, 0xdc39200c, 0x20c8c571, 0x962bda1c, 0xe1e696ff, 0xb141ab08, 0x7cca89b9, + 0x1a69e783, 0x02cc4843, 0xa2f7c579, 0x429ef47d, 0x427b169c, 0x5ac9f049, 0xdd8f0f00, 0x5c8165bf + ]; + + sBox[1] = [ + 0x1f201094, 0xef0ba75b, 0x69e3cf7e, 0x393f4380, 0xfe61cf7a, 0xeec5207a, 0x55889c94, 0x72fc0651, + 0xada7ef79, 0x4e1d7235, 0xd55a63ce, 0xde0436ba, 0x99c430ef, 0x5f0c0794, 0x18dcdb7d, 0xa1d6eff3, + 0xa0b52f7b, 0x59e83605, 0xee15b094, 0xe9ffd909, 0xdc440086, 0xef944459, 0xba83ccb3, 0xe0c3cdfb, + 0xd1da4181, 0x3b092ab1, 0xf997f1c1, 0xa5e6cf7b, 0x01420ddb, 0xe4e7ef5b, 0x25a1ff41, 0xe180f806, + 0x1fc41080, 0x179bee7a, 0xd37ac6a9, 0xfe5830a4, 0x98de8b7f, 0x77e83f4e, 0x79929269, 0x24fa9f7b, + 0xe113c85b, 0xacc40083, 0xd7503525, 0xf7ea615f, 0x62143154, 0x0d554b63, 0x5d681121, 0xc866c359, + 0x3d63cf73, 0xcee234c0, 0xd4d87e87, 0x5c672b21, 0x071f6181, 0x39f7627f, 0x361e3084, 0xe4eb573b, + 0x602f64a4, 0xd63acd9c, 0x1bbc4635, 0x9e81032d, 0x2701f50c, 0x99847ab4, 0xa0e3df79, 0xba6cf38c, + 0x10843094, 0x2537a95e, 0xf46f6ffe, 0xa1ff3b1f, 0x208cfb6a, 0x8f458c74, 0xd9e0a227, 0x4ec73a34, + 0xfc884f69, 0x3e4de8df, 0xef0e0088, 0x3559648d, 0x8a45388c, 0x1d804366, 0x721d9bfd, 0xa58684bb, + 0xe8256333, 0x844e8212, 0x128d8098, 0xfed33fb4, 0xce280ae1, 0x27e19ba5, 0xd5a6c252, 0xe49754bd, + 0xc5d655dd, 0xeb667064, 0x77840b4d, 0xa1b6a801, 0x84db26a9, 0xe0b56714, 0x21f043b7, 0xe5d05860, + 0x54f03084, 0x066ff472, 0xa31aa153, 0xdadc4755, 0xb5625dbf, 0x68561be6, 0x83ca6b94, 0x2d6ed23b, + 0xeccf01db, 0xa6d3d0ba, 0xb6803d5c, 0xaf77a709, 0x33b4a34c, 0x397bc8d6, 0x5ee22b95, 0x5f0e5304, + 0x81ed6f61, 0x20e74364, 0xb45e1378, 0xde18639b, 0x881ca122, 0xb96726d1, 0x8049a7e8, 0x22b7da7b, + 0x5e552d25, 0x5272d237, 0x79d2951c, 0xc60d894c, 0x488cb402, 0x1ba4fe5b, 0xa4b09f6b, 0x1ca815cf, + 0xa20c3005, 0x8871df63, 0xb9de2fcb, 0x0cc6c9e9, 0x0beeff53, 0xe3214517, 0xb4542835, 0x9f63293c, + 0xee41e729, 0x6e1d2d7c, 0x50045286, 0x1e6685f3, 0xf33401c6, 0x30a22c95, 0x31a70850, 0x60930f13, + 0x73f98417, 0xa1269859, 0xec645c44, 0x52c877a9, 0xcdff33a6, 0xa02b1741, 0x7cbad9a2, 0x2180036f, + 0x50d99c08, 0xcb3f4861, 0xc26bd765, 0x64a3f6ab, 0x80342676, 0x25a75e7b, 0xe4e6d1fc, 0x20c710e6, + 0xcdf0b680, 0x17844d3b, 0x31eef84d, 0x7e0824e4, 0x2ccb49eb, 0x846a3bae, 0x8ff77888, 0xee5d60f6, + 0x7af75673, 0x2fdd5cdb, 0xa11631c1, 0x30f66f43, 0xb3faec54, 0x157fd7fa, 0xef8579cc, 0xd152de58, + 0xdb2ffd5e, 0x8f32ce19, 0x306af97a, 0x02f03ef8, 0x99319ad5, 0xc242fa0f, 0xa7e3ebb0, 0xc68e4906, + 0xb8da230c, 0x80823028, 0xdcdef3c8, 0xd35fb171, 0x088a1bc8, 0xbec0c560, 0x61a3c9e8, 0xbca8f54d, + 0xc72feffa, 0x22822e99, 0x82c570b4, 0xd8d94e89, 0x8b1c34bc, 0x301e16e6, 0x273be979, 0xb0ffeaa6, + 0x61d9b8c6, 0x00b24869, 0xb7ffce3f, 0x08dc283b, 0x43daf65a, 0xf7e19798, 0x7619b72f, 0x8f1c9ba4, + 0xdc8637a0, 0x16a7d3b1, 0x9fc393b7, 0xa7136eeb, 0xc6bcc63e, 0x1a513742, 0xef6828bc, 0x520365d6, + 0x2d6a77ab, 0x3527ed4b, 0x821fd216, 0x095c6e2e, 0xdb92f2fb, 0x5eea29cb, 0x145892f5, 0x91584f7f, + 0x5483697b, 0x2667a8cc, 0x85196048, 0x8c4bacea, 0x833860d4, 0x0d23e0f9, 0x6c387e8a, 0x0ae6d249, + 0xb284600c, 0xd835731d, 0xdcb1c647, 0xac4c56ea, 0x3ebd81b3, 0x230eabb0, 0x6438bc87, 0xf0b5b1fa, + 0x8f5ea2b3, 0xfc184642, 0x0a036b7a, 0x4fb089bd, 0x649da589, 0xa345415e, 0x5c038323, 0x3e5d3bb9, + 0x43d79572, 0x7e6dd07c, 0x06dfdf1e, 0x6c6cc4ef, 0x7160a539, 0x73bfbe70, 0x83877605, 0x4523ecf1 + ]; + + sBox[2] = [ + 0x8defc240, 0x25fa5d9f, 0xeb903dbf, 0xe810c907, 0x47607fff, 0x369fe44b, 0x8c1fc644, 0xaececa90, + 0xbeb1f9bf, 0xeefbcaea, 0xe8cf1950, 0x51df07ae, 0x920e8806, 0xf0ad0548, 0xe13c8d83, 0x927010d5, + 0x11107d9f, 0x07647db9, 0xb2e3e4d4, 0x3d4f285e, 0xb9afa820, 0xfade82e0, 0xa067268b, 0x8272792e, + 0x553fb2c0, 0x489ae22b, 0xd4ef9794, 0x125e3fbc, 0x21fffcee, 0x825b1bfd, 0x9255c5ed, 0x1257a240, + 0x4e1a8302, 0xbae07fff, 0x528246e7, 0x8e57140e, 0x3373f7bf, 0x8c9f8188, 0xa6fc4ee8, 0xc982b5a5, + 0xa8c01db7, 0x579fc264, 0x67094f31, 0xf2bd3f5f, 0x40fff7c1, 0x1fb78dfc, 0x8e6bd2c1, 0x437be59b, + 0x99b03dbf, 0xb5dbc64b, 0x638dc0e6, 0x55819d99, 0xa197c81c, 0x4a012d6e, 0xc5884a28, 0xccc36f71, + 0xb843c213, 0x6c0743f1, 0x8309893c, 0x0feddd5f, 0x2f7fe850, 0xd7c07f7e, 0x02507fbf, 0x5afb9a04, + 0xa747d2d0, 0x1651192e, 0xaf70bf3e, 0x58c31380, 0x5f98302e, 0x727cc3c4, 0x0a0fb402, 0x0f7fef82, + 0x8c96fdad, 0x5d2c2aae, 0x8ee99a49, 0x50da88b8, 0x8427f4a0, 0x1eac5790, 0x796fb449, 0x8252dc15, + 0xefbd7d9b, 0xa672597d, 0xada840d8, 0x45f54504, 0xfa5d7403, 0xe83ec305, 0x4f91751a, 0x925669c2, + 0x23efe941, 0xa903f12e, 0x60270df2, 0x0276e4b6, 0x94fd6574, 0x927985b2, 0x8276dbcb, 0x02778176, + 0xf8af918d, 0x4e48f79e, 0x8f616ddf, 0xe29d840e, 0x842f7d83, 0x340ce5c8, 0x96bbb682, 0x93b4b148, + 0xef303cab, 0x984faf28, 0x779faf9b, 0x92dc560d, 0x224d1e20, 0x8437aa88, 0x7d29dc96, 0x2756d3dc, + 0x8b907cee, 0xb51fd240, 0xe7c07ce3, 0xe566b4a1, 0xc3e9615e, 0x3cf8209d, 0x6094d1e3, 0xcd9ca341, + 0x5c76460e, 0x00ea983b, 0xd4d67881, 0xfd47572c, 0xf76cedd9, 0xbda8229c, 0x127dadaa, 0x438a074e, + 0x1f97c090, 0x081bdb8a, 0x93a07ebe, 0xb938ca15, 0x97b03cff, 0x3dc2c0f8, 0x8d1ab2ec, 0x64380e51, + 0x68cc7bfb, 0xd90f2788, 0x12490181, 0x5de5ffd4, 0xdd7ef86a, 0x76a2e214, 0xb9a40368, 0x925d958f, + 0x4b39fffa, 0xba39aee9, 0xa4ffd30b, 0xfaf7933b, 0x6d498623, 0x193cbcfa, 0x27627545, 0x825cf47a, + 0x61bd8ba0, 0xd11e42d1, 0xcead04f4, 0x127ea392, 0x10428db7, 0x8272a972, 0x9270c4a8, 0x127de50b, + 0x285ba1c8, 0x3c62f44f, 0x35c0eaa5, 0xe805d231, 0x428929fb, 0xb4fcdf82, 0x4fb66a53, 0x0e7dc15b, + 0x1f081fab, 0x108618ae, 0xfcfd086d, 0xf9ff2889, 0x694bcc11, 0x236a5cae, 0x12deca4d, 0x2c3f8cc5, + 0xd2d02dfe, 0xf8ef5896, 0xe4cf52da, 0x95155b67, 0x494a488c, 0xb9b6a80c, 0x5c8f82bc, 0x89d36b45, + 0x3a609437, 0xec00c9a9, 0x44715253, 0x0a874b49, 0xd773bc40, 0x7c34671c, 0x02717ef6, 0x4feb5536, + 0xa2d02fff, 0xd2bf60c4, 0xd43f03c0, 0x50b4ef6d, 0x07478cd1, 0x006e1888, 0xa2e53f55, 0xb9e6d4bc, + 0xa2048016, 0x97573833, 0xd7207d67, 0xde0f8f3d, 0x72f87b33, 0xabcc4f33, 0x7688c55d, 0x7b00a6b0, + 0x947b0001, 0x570075d2, 0xf9bb88f8, 0x8942019e, 0x4264a5ff, 0x856302e0, 0x72dbd92b, 0xee971b69, + 0x6ea22fde, 0x5f08ae2b, 0xaf7a616d, 0xe5c98767, 0xcf1febd2, 0x61efc8c2, 0xf1ac2571, 0xcc8239c2, + 0x67214cb8, 0xb1e583d1, 0xb7dc3e62, 0x7f10bdce, 0xf90a5c38, 0x0ff0443d, 0x606e6dc6, 0x60543a49, + 0x5727c148, 0x2be98a1d, 0x8ab41738, 0x20e1be24, 0xaf96da0f, 0x68458425, 0x99833be5, 0x600d457d, + 0x282f9350, 0x8334b362, 0xd91d1120, 0x2b6d8da0, 0x642b1e31, 0x9c305a00, 0x52bce688, 0x1b03588a, + 0xf7baefd5, 0x4142ed9c, 0xa4315c11, 0x83323ec5, 0xdfef4636, 0xa133c501, 0xe9d3531c, 0xee353783 + ]; + + sBox[3] = [ + 0x9db30420, 0x1fb6e9de, 0xa7be7bef, 0xd273a298, 0x4a4f7bdb, 0x64ad8c57, 0x85510443, 0xfa020ed1, + 0x7e287aff, 0xe60fb663, 0x095f35a1, 0x79ebf120, 0xfd059d43, 0x6497b7b1, 0xf3641f63, 0x241e4adf, + 0x28147f5f, 0x4fa2b8cd, 0xc9430040, 0x0cc32220, 0xfdd30b30, 0xc0a5374f, 0x1d2d00d9, 0x24147b15, + 0xee4d111a, 0x0fca5167, 0x71ff904c, 0x2d195ffe, 0x1a05645f, 0x0c13fefe, 0x081b08ca, 0x05170121, + 0x80530100, 0xe83e5efe, 0xac9af4f8, 0x7fe72701, 0xd2b8ee5f, 0x06df4261, 0xbb9e9b8a, 0x7293ea25, + 0xce84ffdf, 0xf5718801, 0x3dd64b04, 0xa26f263b, 0x7ed48400, 0x547eebe6, 0x446d4ca0, 0x6cf3d6f5, + 0x2649abdf, 0xaea0c7f5, 0x36338cc1, 0x503f7e93, 0xd3772061, 0x11b638e1, 0x72500e03, 0xf80eb2bb, + 0xabe0502e, 0xec8d77de, 0x57971e81, 0xe14f6746, 0xc9335400, 0x6920318f, 0x081dbb99, 0xffc304a5, + 0x4d351805, 0x7f3d5ce3, 0xa6c866c6, 0x5d5bcca9, 0xdaec6fea, 0x9f926f91, 0x9f46222f, 0x3991467d, + 0xa5bf6d8e, 0x1143c44f, 0x43958302, 0xd0214eeb, 0x022083b8, 0x3fb6180c, 0x18f8931e, 0x281658e6, + 0x26486e3e, 0x8bd78a70, 0x7477e4c1, 0xb506e07c, 0xf32d0a25, 0x79098b02, 0xe4eabb81, 0x28123b23, + 0x69dead38, 0x1574ca16, 0xdf871b62, 0x211c40b7, 0xa51a9ef9, 0x0014377b, 0x041e8ac8, 0x09114003, + 0xbd59e4d2, 0xe3d156d5, 0x4fe876d5, 0x2f91a340, 0x557be8de, 0x00eae4a7, 0x0ce5c2ec, 0x4db4bba6, + 0xe756bdff, 0xdd3369ac, 0xec17b035, 0x06572327, 0x99afc8b0, 0x56c8c391, 0x6b65811c, 0x5e146119, + 0x6e85cb75, 0xbe07c002, 0xc2325577, 0x893ff4ec, 0x5bbfc92d, 0xd0ec3b25, 0xb7801ab7, 0x8d6d3b24, + 0x20c763ef, 0xc366a5fc, 0x9c382880, 0x0ace3205, 0xaac9548a, 0xeca1d7c7, 0x041afa32, 0x1d16625a, + 0x6701902c, 0x9b757a54, 0x31d477f7, 0x9126b031, 0x36cc6fdb, 0xc70b8b46, 0xd9e66a48, 0x56e55a79, + 0x026a4ceb, 0x52437eff, 0x2f8f76b4, 0x0df980a5, 0x8674cde3, 0xedda04eb, 0x17a9be04, 0x2c18f4df, + 0xb7747f9d, 0xab2af7b4, 0xefc34d20, 0x2e096b7c, 0x1741a254, 0xe5b6a035, 0x213d42f6, 0x2c1c7c26, + 0x61c2f50f, 0x6552daf9, 0xd2c231f8, 0x25130f69, 0xd8167fa2, 0x0418f2c8, 0x001a96a6, 0x0d1526ab, + 0x63315c21, 0x5e0a72ec, 0x49bafefd, 0x187908d9, 0x8d0dbd86, 0x311170a7, 0x3e9b640c, 0xcc3e10d7, + 0xd5cad3b6, 0x0caec388, 0xf73001e1, 0x6c728aff, 0x71eae2a1, 0x1f9af36e, 0xcfcbd12f, 0xc1de8417, + 0xac07be6b, 0xcb44a1d8, 0x8b9b0f56, 0x013988c3, 0xb1c52fca, 0xb4be31cd, 0xd8782806, 0x12a3a4e2, + 0x6f7de532, 0x58fd7eb6, 0xd01ee900, 0x24adffc2, 0xf4990fc5, 0x9711aac5, 0x001d7b95, 0x82e5e7d2, + 0x109873f6, 0x00613096, 0xc32d9521, 0xada121ff, 0x29908415, 0x7fbb977f, 0xaf9eb3db, 0x29c9ed2a, + 0x5ce2a465, 0xa730f32c, 0xd0aa3fe8, 0x8a5cc091, 0xd49e2ce7, 0x0ce454a9, 0xd60acd86, 0x015f1919, + 0x77079103, 0xdea03af6, 0x78a8565e, 0xdee356df, 0x21f05cbe, 0x8b75e387, 0xb3c50651, 0xb8a5c3ef, + 0xd8eeb6d2, 0xe523be77, 0xc2154529, 0x2f69efdf, 0xafe67afb, 0xf470c4b2, 0xf3e0eb5b, 0xd6cc9876, + 0x39e4460c, 0x1fda8538, 0x1987832f, 0xca007367, 0xa99144f8, 0x296b299e, 0x492fc295, 0x9266beab, + 0xb5676e69, 0x9bd3ddda, 0xdf7e052f, 0xdb25701c, 0x1b5e51ee, 0xf65324e6, 0x6afce36c, 0x0316cc04, + 0x8644213e, 0xb7dc59d0, 0x7965291f, 0xccd6fd43, 0x41823979, 0x932bcdf6, 0xb657c34d, 0x4edfd282, + 0x7ae5290c, 0x3cb9536b, 0x851e20fe, 0x9833557e, 0x13ecf0b0, 0xd3ffb372, 0x3f85c5c1, 0x0aef7ed2 + ]; + + sBox[4] = [ + 0x7ec90c04, 0x2c6e74b9, 0x9b0e66df, 0xa6337911, 0xb86a7fff, 0x1dd358f5, 0x44dd9d44, 0x1731167f, + 0x08fbf1fa, 0xe7f511cc, 0xd2051b00, 0x735aba00, 0x2ab722d8, 0x386381cb, 0xacf6243a, 0x69befd7a, + 0xe6a2e77f, 0xf0c720cd, 0xc4494816, 0xccf5c180, 0x38851640, 0x15b0a848, 0xe68b18cb, 0x4caadeff, + 0x5f480a01, 0x0412b2aa, 0x259814fc, 0x41d0efe2, 0x4e40b48d, 0x248eb6fb, 0x8dba1cfe, 0x41a99b02, + 0x1a550a04, 0xba8f65cb, 0x7251f4e7, 0x95a51725, 0xc106ecd7, 0x97a5980a, 0xc539b9aa, 0x4d79fe6a, + 0xf2f3f763, 0x68af8040, 0xed0c9e56, 0x11b4958b, 0xe1eb5a88, 0x8709e6b0, 0xd7e07156, 0x4e29fea7, + 0x6366e52d, 0x02d1c000, 0xc4ac8e05, 0x9377f571, 0x0c05372a, 0x578535f2, 0x2261be02, 0xd642a0c9, + 0xdf13a280, 0x74b55bd2, 0x682199c0, 0xd421e5ec, 0x53fb3ce8, 0xc8adedb3, 0x28a87fc9, 0x3d959981, + 0x5c1ff900, 0xfe38d399, 0x0c4eff0b, 0x062407ea, 0xaa2f4fb1, 0x4fb96976, 0x90c79505, 0xb0a8a774, + 0xef55a1ff, 0xe59ca2c2, 0xa6b62d27, 0xe66a4263, 0xdf65001f, 0x0ec50966, 0xdfdd55bc, 0x29de0655, + 0x911e739a, 0x17af8975, 0x32c7911c, 0x89f89468, 0x0d01e980, 0x524755f4, 0x03b63cc9, 0x0cc844b2, + 0xbcf3f0aa, 0x87ac36e9, 0xe53a7426, 0x01b3d82b, 0x1a9e7449, 0x64ee2d7e, 0xcddbb1da, 0x01c94910, + 0xb868bf80, 0x0d26f3fd, 0x9342ede7, 0x04a5c284, 0x636737b6, 0x50f5b616, 0xf24766e3, 0x8eca36c1, + 0x136e05db, 0xfef18391, 0xfb887a37, 0xd6e7f7d4, 0xc7fb7dc9, 0x3063fcdf, 0xb6f589de, 0xec2941da, + 0x26e46695, 0xb7566419, 0xf654efc5, 0xd08d58b7, 0x48925401, 0xc1bacb7f, 0xe5ff550f, 0xb6083049, + 0x5bb5d0e8, 0x87d72e5a, 0xab6a6ee1, 0x223a66ce, 0xc62bf3cd, 0x9e0885f9, 0x68cb3e47, 0x086c010f, + 0xa21de820, 0xd18b69de, 0xf3f65777, 0xfa02c3f6, 0x407edac3, 0xcbb3d550, 0x1793084d, 0xb0d70eba, + 0x0ab378d5, 0xd951fb0c, 0xded7da56, 0x4124bbe4, 0x94ca0b56, 0x0f5755d1, 0xe0e1e56e, 0x6184b5be, + 0x580a249f, 0x94f74bc0, 0xe327888e, 0x9f7b5561, 0xc3dc0280, 0x05687715, 0x646c6bd7, 0x44904db3, + 0x66b4f0a3, 0xc0f1648a, 0x697ed5af, 0x49e92ff6, 0x309e374f, 0x2cb6356a, 0x85808573, 0x4991f840, + 0x76f0ae02, 0x083be84d, 0x28421c9a, 0x44489406, 0x736e4cb8, 0xc1092910, 0x8bc95fc6, 0x7d869cf4, + 0x134f616f, 0x2e77118d, 0xb31b2be1, 0xaa90b472, 0x3ca5d717, 0x7d161bba, 0x9cad9010, 0xaf462ba2, + 0x9fe459d2, 0x45d34559, 0xd9f2da13, 0xdbc65487, 0xf3e4f94e, 0x176d486f, 0x097c13ea, 0x631da5c7, + 0x445f7382, 0x175683f4, 0xcdc66a97, 0x70be0288, 0xb3cdcf72, 0x6e5dd2f3, 0x20936079, 0x459b80a5, + 0xbe60e2db, 0xa9c23101, 0xeba5315c, 0x224e42f2, 0x1c5c1572, 0xf6721b2c, 0x1ad2fff3, 0x8c25404e, + 0x324ed72f, 0x4067b7fd, 0x0523138e, 0x5ca3bc78, 0xdc0fd66e, 0x75922283, 0x784d6b17, 0x58ebb16e, + 0x44094f85, 0x3f481d87, 0xfcfeae7b, 0x77b5ff76, 0x8c2302bf, 0xaaf47556, 0x5f46b02a, 0x2b092801, + 0x3d38f5f7, 0x0ca81f36, 0x52af4a8a, 0x66d5e7c0, 0xdf3b0874, 0x95055110, 0x1b5ad7a8, 0xf61ed5ad, + 0x6cf6e479, 0x20758184, 0xd0cefa65, 0x88f7be58, 0x4a046826, 0x0ff6f8f3, 0xa09c7f70, 0x5346aba0, + 0x5ce96c28, 0xe176eda3, 0x6bac307f, 0x376829d2, 0x85360fa9, 0x17e3fe2a, 0x24b79767, 0xf5a96b20, + 0xd6cd2595, 0x68ff1ebf, 0x7555442c, 0xf19f06be, 0xf9e0659a, 0xeeb9491d, 0x34010718, 0xbb30cab8, + 0xe822fe15, 0x88570983, 0x750e6249, 0xda627e55, 0x5e76ffa8, 0xb1534546, 0x6d47de08, 0xefe9e7d4 + ]; + + sBox[5] = [ + 0xf6fa8f9d, 0x2cac6ce1, 0x4ca34867, 0xe2337f7c, 0x95db08e7, 0x016843b4, 0xeced5cbc, 0x325553ac, + 0xbf9f0960, 0xdfa1e2ed, 0x83f0579d, 0x63ed86b9, 0x1ab6a6b8, 0xde5ebe39, 0xf38ff732, 0x8989b138, + 0x33f14961, 0xc01937bd, 0xf506c6da, 0xe4625e7e, 0xa308ea99, 0x4e23e33c, 0x79cbd7cc, 0x48a14367, + 0xa3149619, 0xfec94bd5, 0xa114174a, 0xeaa01866, 0xa084db2d, 0x09a8486f, 0xa888614a, 0x2900af98, + 0x01665991, 0xe1992863, 0xc8f30c60, 0x2e78ef3c, 0xd0d51932, 0xcf0fec14, 0xf7ca07d2, 0xd0a82072, + 0xfd41197e, 0x9305a6b0, 0xe86be3da, 0x74bed3cd, 0x372da53c, 0x4c7f4448, 0xdab5d440, 0x6dba0ec3, + 0x083919a7, 0x9fbaeed9, 0x49dbcfb0, 0x4e670c53, 0x5c3d9c01, 0x64bdb941, 0x2c0e636a, 0xba7dd9cd, + 0xea6f7388, 0xe70bc762, 0x35f29adb, 0x5c4cdd8d, 0xf0d48d8c, 0xb88153e2, 0x08a19866, 0x1ae2eac8, + 0x284caf89, 0xaa928223, 0x9334be53, 0x3b3a21bf, 0x16434be3, 0x9aea3906, 0xefe8c36e, 0xf890cdd9, + 0x80226dae, 0xc340a4a3, 0xdf7e9c09, 0xa694a807, 0x5b7c5ecc, 0x221db3a6, 0x9a69a02f, 0x68818a54, + 0xceb2296f, 0x53c0843a, 0xfe893655, 0x25bfe68a, 0xb4628abc, 0xcf222ebf, 0x25ac6f48, 0xa9a99387, + 0x53bddb65, 0xe76ffbe7, 0xe967fd78, 0x0ba93563, 0x8e342bc1, 0xe8a11be9, 0x4980740d, 0xc8087dfc, + 0x8de4bf99, 0xa11101a0, 0x7fd37975, 0xda5a26c0, 0xe81f994f, 0x9528cd89, 0xfd339fed, 0xb87834bf, + 0x5f04456d, 0x22258698, 0xc9c4c83b, 0x2dc156be, 0x4f628daa, 0x57f55ec5, 0xe2220abe, 0xd2916ebf, + 0x4ec75b95, 0x24f2c3c0, 0x42d15d99, 0xcd0d7fa0, 0x7b6e27ff, 0xa8dc8af0, 0x7345c106, 0xf41e232f, + 0x35162386, 0xe6ea8926, 0x3333b094, 0x157ec6f2, 0x372b74af, 0x692573e4, 0xe9a9d848, 0xf3160289, + 0x3a62ef1d, 0xa787e238, 0xf3a5f676, 0x74364853, 0x20951063, 0x4576698d, 0xb6fad407, 0x592af950, + 0x36f73523, 0x4cfb6e87, 0x7da4cec0, 0x6c152daa, 0xcb0396a8, 0xc50dfe5d, 0xfcd707ab, 0x0921c42f, + 0x89dff0bb, 0x5fe2be78, 0x448f4f33, 0x754613c9, 0x2b05d08d, 0x48b9d585, 0xdc049441, 0xc8098f9b, + 0x7dede786, 0xc39a3373, 0x42410005, 0x6a091751, 0x0ef3c8a6, 0x890072d6, 0x28207682, 0xa9a9f7be, + 0xbf32679d, 0xd45b5b75, 0xb353fd00, 0xcbb0e358, 0x830f220a, 0x1f8fb214, 0xd372cf08, 0xcc3c4a13, + 0x8cf63166, 0x061c87be, 0x88c98f88, 0x6062e397, 0x47cf8e7a, 0xb6c85283, 0x3cc2acfb, 0x3fc06976, + 0x4e8f0252, 0x64d8314d, 0xda3870e3, 0x1e665459, 0xc10908f0, 0x513021a5, 0x6c5b68b7, 0x822f8aa0, + 0x3007cd3e, 0x74719eef, 0xdc872681, 0x073340d4, 0x7e432fd9, 0x0c5ec241, 0x8809286c, 0xf592d891, + 0x08a930f6, 0x957ef305, 0xb7fbffbd, 0xc266e96f, 0x6fe4ac98, 0xb173ecc0, 0xbc60b42a, 0x953498da, + 0xfba1ae12, 0x2d4bd736, 0x0f25faab, 0xa4f3fceb, 0xe2969123, 0x257f0c3d, 0x9348af49, 0x361400bc, + 0xe8816f4a, 0x3814f200, 0xa3f94043, 0x9c7a54c2, 0xbc704f57, 0xda41e7f9, 0xc25ad33a, 0x54f4a084, + 0xb17f5505, 0x59357cbe, 0xedbd15c8, 0x7f97c5ab, 0xba5ac7b5, 0xb6f6deaf, 0x3a479c3a, 0x5302da25, + 0x653d7e6a, 0x54268d49, 0x51a477ea, 0x5017d55b, 0xd7d25d88, 0x44136c76, 0x0404a8c8, 0xb8e5a121, + 0xb81a928a, 0x60ed5869, 0x97c55b96, 0xeaec991b, 0x29935913, 0x01fdb7f1, 0x088e8dfa, 0x9ab6f6f5, + 0x3b4cbf9f, 0x4a5de3ab, 0xe6051d35, 0xa0e1d855, 0xd36b4cf1, 0xf544edeb, 0xb0e93524, 0xbebb8fbd, + 0xa2d762cf, 0x49c92f54, 0x38b5f331, 0x7128a454, 0x48392905, 0xa65b1db8, 0x851c97bd, 0xd675cf2f + ]; + + sBox[6] = [ + 0x85e04019, 0x332bf567, 0x662dbfff, 0xcfc65693, 0x2a8d7f6f, 0xab9bc912, 0xde6008a1, 0x2028da1f, + 0x0227bce7, 0x4d642916, 0x18fac300, 0x50f18b82, 0x2cb2cb11, 0xb232e75c, 0x4b3695f2, 0xb28707de, + 0xa05fbcf6, 0xcd4181e9, 0xe150210c, 0xe24ef1bd, 0xb168c381, 0xfde4e789, 0x5c79b0d8, 0x1e8bfd43, + 0x4d495001, 0x38be4341, 0x913cee1d, 0x92a79c3f, 0x089766be, 0xbaeeadf4, 0x1286becf, 0xb6eacb19, + 0x2660c200, 0x7565bde4, 0x64241f7a, 0x8248dca9, 0xc3b3ad66, 0x28136086, 0x0bd8dfa8, 0x356d1cf2, + 0x107789be, 0xb3b2e9ce, 0x0502aa8f, 0x0bc0351e, 0x166bf52a, 0xeb12ff82, 0xe3486911, 0xd34d7516, + 0x4e7b3aff, 0x5f43671b, 0x9cf6e037, 0x4981ac83, 0x334266ce, 0x8c9341b7, 0xd0d854c0, 0xcb3a6c88, + 0x47bc2829, 0x4725ba37, 0xa66ad22b, 0x7ad61f1e, 0x0c5cbafa, 0x4437f107, 0xb6e79962, 0x42d2d816, + 0x0a961288, 0xe1a5c06e, 0x13749e67, 0x72fc081a, 0xb1d139f7, 0xf9583745, 0xcf19df58, 0xbec3f756, + 0xc06eba30, 0x07211b24, 0x45c28829, 0xc95e317f, 0xbc8ec511, 0x38bc46e9, 0xc6e6fa14, 0xbae8584a, + 0xad4ebc46, 0x468f508b, 0x7829435f, 0xf124183b, 0x821dba9f, 0xaff60ff4, 0xea2c4e6d, 0x16e39264, + 0x92544a8b, 0x009b4fc3, 0xaba68ced, 0x9ac96f78, 0x06a5b79a, 0xb2856e6e, 0x1aec3ca9, 0xbe838688, + 0x0e0804e9, 0x55f1be56, 0xe7e5363b, 0xb3a1f25d, 0xf7debb85, 0x61fe033c, 0x16746233, 0x3c034c28, + 0xda6d0c74, 0x79aac56c, 0x3ce4e1ad, 0x51f0c802, 0x98f8f35a, 0x1626a49f, 0xeed82b29, 0x1d382fe3, + 0x0c4fb99a, 0xbb325778, 0x3ec6d97b, 0x6e77a6a9, 0xcb658b5c, 0xd45230c7, 0x2bd1408b, 0x60c03eb7, + 0xb9068d78, 0xa33754f4, 0xf430c87d, 0xc8a71302, 0xb96d8c32, 0xebd4e7be, 0xbe8b9d2d, 0x7979fb06, + 0xe7225308, 0x8b75cf77, 0x11ef8da4, 0xe083c858, 0x8d6b786f, 0x5a6317a6, 0xfa5cf7a0, 0x5dda0033, + 0xf28ebfb0, 0xf5b9c310, 0xa0eac280, 0x08b9767a, 0xa3d9d2b0, 0x79d34217, 0x021a718d, 0x9ac6336a, + 0x2711fd60, 0x438050e3, 0x069908a8, 0x3d7fedc4, 0x826d2bef, 0x4eeb8476, 0x488dcf25, 0x36c9d566, + 0x28e74e41, 0xc2610aca, 0x3d49a9cf, 0xbae3b9df, 0xb65f8de6, 0x92aeaf64, 0x3ac7d5e6, 0x9ea80509, + 0xf22b017d, 0xa4173f70, 0xdd1e16c3, 0x15e0d7f9, 0x50b1b887, 0x2b9f4fd5, 0x625aba82, 0x6a017962, + 0x2ec01b9c, 0x15488aa9, 0xd716e740, 0x40055a2c, 0x93d29a22, 0xe32dbf9a, 0x058745b9, 0x3453dc1e, + 0xd699296e, 0x496cff6f, 0x1c9f4986, 0xdfe2ed07, 0xb87242d1, 0x19de7eae, 0x053e561a, 0x15ad6f8c, + 0x66626c1c, 0x7154c24c, 0xea082b2a, 0x93eb2939, 0x17dcb0f0, 0x58d4f2ae, 0x9ea294fb, 0x52cf564c, + 0x9883fe66, 0x2ec40581, 0x763953c3, 0x01d6692e, 0xd3a0c108, 0xa1e7160e, 0xe4f2dfa6, 0x693ed285, + 0x74904698, 0x4c2b0edd, 0x4f757656, 0x5d393378, 0xa132234f, 0x3d321c5d, 0xc3f5e194, 0x4b269301, + 0xc79f022f, 0x3c997e7e, 0x5e4f9504, 0x3ffafbbd, 0x76f7ad0e, 0x296693f4, 0x3d1fce6f, 0xc61e45be, + 0xd3b5ab34, 0xf72bf9b7, 0x1b0434c0, 0x4e72b567, 0x5592a33d, 0xb5229301, 0xcfd2a87f, 0x60aeb767, + 0x1814386b, 0x30bcc33d, 0x38a0c07d, 0xfd1606f2, 0xc363519b, 0x589dd390, 0x5479f8e6, 0x1cb8d647, + 0x97fd61a9, 0xea7759f4, 0x2d57539d, 0x569a58cf, 0xe84e63ad, 0x462e1b78, 0x6580f87e, 0xf3817914, + 0x91da55f4, 0x40a230f3, 0xd1988f35, 0xb6e318d2, 0x3ffa50bc, 0x3d40f021, 0xc3c0bdae, 0x4958c24c, + 0x518f36b2, 0x84b1d370, 0x0fedce83, 0x878ddada, 0xf2a279c7, 0x94e01be8, 0x90716f4b, 0x954b8aa3 + ]; + + sBox[7] = [ + 0xe216300d, 0xbbddfffc, 0xa7ebdabd, 0x35648095, 0x7789f8b7, 0xe6c1121b, 0x0e241600, 0x052ce8b5, + 0x11a9cfb0, 0xe5952f11, 0xece7990a, 0x9386d174, 0x2a42931c, 0x76e38111, 0xb12def3a, 0x37ddddfc, + 0xde9adeb1, 0x0a0cc32c, 0xbe197029, 0x84a00940, 0xbb243a0f, 0xb4d137cf, 0xb44e79f0, 0x049eedfd, + 0x0b15a15d, 0x480d3168, 0x8bbbde5a, 0x669ded42, 0xc7ece831, 0x3f8f95e7, 0x72df191b, 0x7580330d, + 0x94074251, 0x5c7dcdfa, 0xabbe6d63, 0xaa402164, 0xb301d40a, 0x02e7d1ca, 0x53571dae, 0x7a3182a2, + 0x12a8ddec, 0xfdaa335d, 0x176f43e8, 0x71fb46d4, 0x38129022, 0xce949ad4, 0xb84769ad, 0x965bd862, + 0x82f3d055, 0x66fb9767, 0x15b80b4e, 0x1d5b47a0, 0x4cfde06f, 0xc28ec4b8, 0x57e8726e, 0x647a78fc, + 0x99865d44, 0x608bd593, 0x6c200e03, 0x39dc5ff6, 0x5d0b00a3, 0xae63aff2, 0x7e8bd632, 0x70108c0c, + 0xbbd35049, 0x2998df04, 0x980cf42a, 0x9b6df491, 0x9e7edd53, 0x06918548, 0x58cb7e07, 0x3b74ef2e, + 0x522fffb1, 0xd24708cc, 0x1c7e27cd, 0xa4eb215b, 0x3cf1d2e2, 0x19b47a38, 0x424f7618, 0x35856039, + 0x9d17dee7, 0x27eb35e6, 0xc9aff67b, 0x36baf5b8, 0x09c467cd, 0xc18910b1, 0xe11dbf7b, 0x06cd1af8, + 0x7170c608, 0x2d5e3354, 0xd4de495a, 0x64c6d006, 0xbcc0c62c, 0x3dd00db3, 0x708f8f34, 0x77d51b42, + 0x264f620f, 0x24b8d2bf, 0x15c1b79e, 0x46a52564, 0xf8d7e54e, 0x3e378160, 0x7895cda5, 0x859c15a5, + 0xe6459788, 0xc37bc75f, 0xdb07ba0c, 0x0676a3ab, 0x7f229b1e, 0x31842e7b, 0x24259fd7, 0xf8bef472, + 0x835ffcb8, 0x6df4c1f2, 0x96f5b195, 0xfd0af0fc, 0xb0fe134c, 0xe2506d3d, 0x4f9b12ea, 0xf215f225, + 0xa223736f, 0x9fb4c428, 0x25d04979, 0x34c713f8, 0xc4618187, 0xea7a6e98, 0x7cd16efc, 0x1436876c, + 0xf1544107, 0xbedeee14, 0x56e9af27, 0xa04aa441, 0x3cf7c899, 0x92ecbae6, 0xdd67016d, 0x151682eb, + 0xa842eedf, 0xfdba60b4, 0xf1907b75, 0x20e3030f, 0x24d8c29e, 0xe139673b, 0xefa63fb8, 0x71873054, + 0xb6f2cf3b, 0x9f326442, 0xcb15a4cc, 0xb01a4504, 0xf1e47d8d, 0x844a1be5, 0xbae7dfdc, 0x42cbda70, + 0xcd7dae0a, 0x57e85b7a, 0xd53f5af6, 0x20cf4d8c, 0xcea4d428, 0x79d130a4, 0x3486ebfb, 0x33d3cddc, + 0x77853b53, 0x37effcb5, 0xc5068778, 0xe580b3e6, 0x4e68b8f4, 0xc5c8b37e, 0x0d809ea2, 0x398feb7c, + 0x132a4f94, 0x43b7950e, 0x2fee7d1c, 0x223613bd, 0xdd06caa2, 0x37df932b, 0xc4248289, 0xacf3ebc3, + 0x5715f6b7, 0xef3478dd, 0xf267616f, 0xc148cbe4, 0x9052815e, 0x5e410fab, 0xb48a2465, 0x2eda7fa4, + 0xe87b40e4, 0xe98ea084, 0x5889e9e1, 0xefd390fc, 0xdd07d35b, 0xdb485694, 0x38d7e5b2, 0x57720101, + 0x730edebc, 0x5b643113, 0x94917e4f, 0x503c2fba, 0x646f1282, 0x7523d24a, 0xe0779695, 0xf9c17a8f, + 0x7a5b2121, 0xd187b896, 0x29263a4d, 0xba510cdf, 0x81f47c9f, 0xad1163ed, 0xea7b5965, 0x1a00726e, + 0x11403092, 0x00da6d77, 0x4a0cdd61, 0xad1f4603, 0x605bdfb0, 0x9eedc364, 0x22ebe6a8, 0xcee7d28a, + 0xa0e736a0, 0x5564a6b9, 0x10853209, 0xc7eb8f37, 0x2de705ca, 0x8951570f, 0xdf09822b, 0xbd691a6c, + 0xaa12e4f2, 0x87451c0f, 0xe0f6a27a, 0x3ada4819, 0x4cf1764f, 0x0d771c2b, 0x67cdb156, 0x350d8384, + 0x5938fa0f, 0x42399ef3, 0x36997b07, 0x0e84093d, 0x4aa93e61, 0x8360d87b, 0x1fa98b0c, 0x1149382c, + 0xe97625a5, 0x0614d1b7, 0x0e25244b, 0x0c768347, 0x589e8d82, 0x0d2059d1, 0xa466bb1e, 0xf8da0a82, + 0x04f19130, 0xba6e4ec0, 0x99265164, 0x1ee7230d, 0x50b2ad80, 0xeaee6801, 0x8db2a283, 0xea8bf59e + ]; + } + + function CAST5(key) { + this.cast5 = new OpenPGPSymEncCAST5(); + this.cast5.setKey(key); + + this.encrypt = function(block) { + return this.cast5.encrypt(block); + }; + } + + CAST5.blockSize = CAST5.prototype.blockSize = 8; + CAST5.keySize = CAST5.prototype.keySize = 16; + + /* eslint-disable no-mixed-operators, no-fallthrough */ + + + /* Modified by Recurity Labs GmbH + * + * Cipher.js + * A block-cipher algorithm implementation on JavaScript + * See Cipher.readme.txt for further information. + * + * Copyright(c) 2009 Atsushi Oka [ http://oka.nu/ ] + * This script file is distributed under the LGPL + * + * ACKNOWLEDGMENT + * + * The main subroutines are written by Michiel van Everdingen. + * + * Michiel van Everdingen + * http://home.versatel.nl/MAvanEverdingen/index.html + * + * All rights for these routines are reserved to Michiel van Everdingen. + * + */ + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //Math + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + const MAXINT = 0xFFFFFFFF; + + function rotw(w, n) { + return (w << n | w >>> (32 - n)) & MAXINT; + } + + function getW(a, i) { + return a[i] | a[i + 1] << 8 | a[i + 2] << 16 | a[i + 3] << 24; + } + + function setW(a, i, w) { + a.splice(i, 4, w & 0xFF, (w >>> 8) & 0xFF, (w >>> 16) & 0xFF, (w >>> 24) & 0xFF); + } + + function getB(x, n) { + return (x >>> (n * 8)) & 0xFF; + } + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Twofish + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + function createTwofish() { + // + let keyBytes = null; + let dataBytes = null; + let dataOffset = -1; + // var dataLength = -1; + // var idx2 = -1; + // + + let tfsKey = []; + let tfsM = [ + [], + [], + [], + [] + ]; + + function tfsInit(key) { + keyBytes = key; + let i; + let a; + let b; + let c; + let d; + const meKey = []; + const moKey = []; + const inKey = []; + let kLen; + const sKey = []; + let f01; + let f5b; + let fef; + + const q0 = [ + [8, 1, 7, 13, 6, 15, 3, 2, 0, 11, 5, 9, 14, 12, 10, 4], + [2, 8, 11, 13, 15, 7, 6, 14, 3, 1, 9, 4, 0, 10, 12, 5] + ]; + const q1 = [ + [14, 12, 11, 8, 1, 2, 3, 5, 15, 4, 10, 6, 7, 0, 9, 13], + [1, 14, 2, 11, 4, 12, 3, 7, 6, 13, 10, 5, 15, 9, 0, 8] + ]; + const q2 = [ + [11, 10, 5, 14, 6, 13, 9, 0, 12, 8, 15, 3, 2, 4, 7, 1], + [4, 12, 7, 5, 1, 6, 9, 10, 0, 14, 13, 8, 2, 11, 3, 15] + ]; + const q3 = [ + [13, 7, 15, 4, 1, 2, 6, 14, 9, 11, 3, 0, 8, 5, 12, 10], + [11, 9, 5, 1, 12, 3, 13, 14, 6, 4, 7, 15, 2, 0, 8, 10] + ]; + const ror4 = [0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15]; + const ashx = [0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 5, 14, 7]; + const q = [ + [], + [] + ]; + const m = [ + [], + [], + [], + [] + ]; + + function ffm5b(x) { + return x ^ (x >> 2) ^ [0, 90, 180, 238][x & 3]; + } + + function ffmEf(x) { + return x ^ (x >> 1) ^ (x >> 2) ^ [0, 238, 180, 90][x & 3]; + } + + function mdsRem(p, q) { + let i; + let t; + let u; + for (i = 0; i < 8; i++) { + t = q >>> 24; + q = ((q << 8) & MAXINT) | p >>> 24; + p = (p << 8) & MAXINT; + u = t << 1; + if (t & 128) { + u ^= 333; + } + q ^= t ^ (u << 16); + u ^= t >>> 1; + if (t & 1) { + u ^= 166; + } + q ^= u << 24 | u << 8; + } + return q; + } + + function qp(n, x) { + const a = x >> 4; + const b = x & 15; + const c = q0[n][a ^ b]; + const d = q1[n][ror4[b] ^ ashx[a]]; + return q3[n][ror4[d] ^ ashx[c]] << 4 | q2[n][c ^ d]; + } + + function hFun(x, key) { + let a = getB(x, 0); + let b = getB(x, 1); + let c = getB(x, 2); + let d = getB(x, 3); + switch (kLen) { + case 4: + a = q[1][a] ^ getB(key[3], 0); + b = q[0][b] ^ getB(key[3], 1); + c = q[0][c] ^ getB(key[3], 2); + d = q[1][d] ^ getB(key[3], 3); + case 3: + a = q[1][a] ^ getB(key[2], 0); + b = q[1][b] ^ getB(key[2], 1); + c = q[0][c] ^ getB(key[2], 2); + d = q[0][d] ^ getB(key[2], 3); + case 2: + a = q[0][q[0][a] ^ getB(key[1], 0)] ^ getB(key[0], 0); + b = q[0][q[1][b] ^ getB(key[1], 1)] ^ getB(key[0], 1); + c = q[1][q[0][c] ^ getB(key[1], 2)] ^ getB(key[0], 2); + d = q[1][q[1][d] ^ getB(key[1], 3)] ^ getB(key[0], 3); + } + return m[0][a] ^ m[1][b] ^ m[2][c] ^ m[3][d]; + } + + keyBytes = keyBytes.slice(0, 32); + i = keyBytes.length; + while (i !== 16 && i !== 24 && i !== 32) { + keyBytes[i++] = 0; + } + + for (i = 0; i < keyBytes.length; i += 4) { + inKey[i >> 2] = getW(keyBytes, i); + } + for (i = 0; i < 256; i++) { + q[0][i] = qp(0, i); + q[1][i] = qp(1, i); + } + for (i = 0; i < 256; i++) { + f01 = q[1][i]; + f5b = ffm5b(f01); + fef = ffmEf(f01); + m[0][i] = f01 + (f5b << 8) + (fef << 16) + (fef << 24); + m[2][i] = f5b + (fef << 8) + (f01 << 16) + (fef << 24); + f01 = q[0][i]; + f5b = ffm5b(f01); + fef = ffmEf(f01); + m[1][i] = fef + (fef << 8) + (f5b << 16) + (f01 << 24); + m[3][i] = f5b + (f01 << 8) + (fef << 16) + (f5b << 24); + } + + kLen = inKey.length / 2; + for (i = 0; i < kLen; i++) { + a = inKey[i + i]; + meKey[i] = a; + b = inKey[i + i + 1]; + moKey[i] = b; + sKey[kLen - i - 1] = mdsRem(a, b); + } + for (i = 0; i < 40; i += 2) { + a = 0x1010101 * i; + b = a + 0x1010101; + a = hFun(a, meKey); + b = rotw(hFun(b, moKey), 8); + tfsKey[i] = (a + b) & MAXINT; + tfsKey[i + 1] = rotw(a + 2 * b, 9); + } + for (i = 0; i < 256; i++) { + a = b = c = d = i; + switch (kLen) { + case 4: + a = q[1][a] ^ getB(sKey[3], 0); + b = q[0][b] ^ getB(sKey[3], 1); + c = q[0][c] ^ getB(sKey[3], 2); + d = q[1][d] ^ getB(sKey[3], 3); + case 3: + a = q[1][a] ^ getB(sKey[2], 0); + b = q[1][b] ^ getB(sKey[2], 1); + c = q[0][c] ^ getB(sKey[2], 2); + d = q[0][d] ^ getB(sKey[2], 3); + case 2: + tfsM[0][i] = m[0][q[0][q[0][a] ^ getB(sKey[1], 0)] ^ getB(sKey[0], 0)]; + tfsM[1][i] = m[1][q[0][q[1][b] ^ getB(sKey[1], 1)] ^ getB(sKey[0], 1)]; + tfsM[2][i] = m[2][q[1][q[0][c] ^ getB(sKey[1], 2)] ^ getB(sKey[0], 2)]; + tfsM[3][i] = m[3][q[1][q[1][d] ^ getB(sKey[1], 3)] ^ getB(sKey[0], 3)]; + } + } + } + + function tfsG0(x) { + return tfsM[0][getB(x, 0)] ^ tfsM[1][getB(x, 1)] ^ tfsM[2][getB(x, 2)] ^ tfsM[3][getB(x, 3)]; + } + + function tfsG1(x) { + return tfsM[0][getB(x, 3)] ^ tfsM[1][getB(x, 0)] ^ tfsM[2][getB(x, 1)] ^ tfsM[3][getB(x, 2)]; + } + + function tfsFrnd(r, blk) { + let a = tfsG0(blk[0]); + let b = tfsG1(blk[1]); + blk[2] = rotw(blk[2] ^ (a + b + tfsKey[4 * r + 8]) & MAXINT, 31); + blk[3] = rotw(blk[3], 1) ^ (a + 2 * b + tfsKey[4 * r + 9]) & MAXINT; + a = tfsG0(blk[2]); + b = tfsG1(blk[3]); + blk[0] = rotw(blk[0] ^ (a + b + tfsKey[4 * r + 10]) & MAXINT, 31); + blk[1] = rotw(blk[1], 1) ^ (a + 2 * b + tfsKey[4 * r + 11]) & MAXINT; + } + + function tfsIrnd(i, blk) { + let a = tfsG0(blk[0]); + let b = tfsG1(blk[1]); + blk[2] = rotw(blk[2], 1) ^ (a + b + tfsKey[4 * i + 10]) & MAXINT; + blk[3] = rotw(blk[3] ^ (a + 2 * b + tfsKey[4 * i + 11]) & MAXINT, 31); + a = tfsG0(blk[2]); + b = tfsG1(blk[3]); + blk[0] = rotw(blk[0], 1) ^ (a + b + tfsKey[4 * i + 8]) & MAXINT; + blk[1] = rotw(blk[1] ^ (a + 2 * b + tfsKey[4 * i + 9]) & MAXINT, 31); + } + + function tfsClose() { + tfsKey = []; + tfsM = [ + [], + [], + [], + [] + ]; + } + + function tfsEncrypt(data, offset) { + dataBytes = data; + dataOffset = offset; + const blk = [getW(dataBytes, dataOffset) ^ tfsKey[0], + getW(dataBytes, dataOffset + 4) ^ tfsKey[1], + getW(dataBytes, dataOffset + 8) ^ tfsKey[2], + getW(dataBytes, dataOffset + 12) ^ tfsKey[3]]; + for (let j = 0; j < 8; j++) { + tfsFrnd(j, blk); + } + setW(dataBytes, dataOffset, blk[2] ^ tfsKey[4]); + setW(dataBytes, dataOffset + 4, blk[3] ^ tfsKey[5]); + setW(dataBytes, dataOffset + 8, blk[0] ^ tfsKey[6]); + setW(dataBytes, dataOffset + 12, blk[1] ^ tfsKey[7]); + dataOffset += 16; + return dataBytes; + } + + function tfsDecrypt(data, offset) { + dataBytes = data; + dataOffset = offset; + const blk = [getW(dataBytes, dataOffset) ^ tfsKey[4], + getW(dataBytes, dataOffset + 4) ^ tfsKey[5], + getW(dataBytes, dataOffset + 8) ^ tfsKey[6], + getW(dataBytes, dataOffset + 12) ^ tfsKey[7]]; + for (let j = 7; j >= 0; j--) { + tfsIrnd(j, blk); + } + setW(dataBytes, dataOffset, blk[2] ^ tfsKey[0]); + setW(dataBytes, dataOffset + 4, blk[3] ^ tfsKey[1]); + setW(dataBytes, dataOffset + 8, blk[0] ^ tfsKey[2]); + setW(dataBytes, dataOffset + 12, blk[1] ^ tfsKey[3]); + dataOffset += 16; + } + + // added by Recurity Labs + + function tfsFinal() { + return dataBytes; + } + + return { + name: 'twofish', + blocksize: 128 / 8, + open: tfsInit, + close: tfsClose, + encrypt: tfsEncrypt, + decrypt: tfsDecrypt, + // added by Recurity Labs + finalize: tfsFinal + }; + } + + // added by Recurity Labs + + function TF(key) { + this.tf = createTwofish(); + this.tf.open(Array.from(key), 0); + + this.encrypt = function(block) { + return this.tf.encrypt(Array.from(block), 0); + }; + } + + TF.keySize = TF.prototype.keySize = 32; + TF.blockSize = TF.prototype.blockSize = 16; + + /* Modified by Recurity Labs GmbH + * + * Originally written by nklein software (nklein.com) + */ + + /* + * Javascript implementation based on Bruce Schneier's reference implementation. + * + * + * The constructor doesn't do much of anything. It's just here + * so we can start defining properties and methods and such. + */ + function Blowfish() {} + + /* + * Declare the block size so that protocols know what size + * Initialization Vector (IV) they will need. + */ + Blowfish.prototype.BLOCKSIZE = 8; + + /* + * These are the default SBOXES. + */ + Blowfish.prototype.SBOXES = [ + [ + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, + 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, + 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, + 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, + 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, + 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, + 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, + 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, + 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, + 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, + 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, + 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, + 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, + 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, + 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, + 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, + 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, + 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, + 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, + 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, + 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, + 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a + ], + [ + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, + 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, + 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, + 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, + 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, + 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, + 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, + 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, + 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, + 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, + 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, + 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, + 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, + 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, + 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, + 0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, + 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, + 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, + 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, + 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, + 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, + 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7 + ], + [ + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, + 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, + 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, + 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, + 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, + 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, + 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, + 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, + 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, + 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, + 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, + 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, + 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, + 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, + 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, + 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, + 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, + 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, + 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, + 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, + 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, + 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0 + ], + [ + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, + 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, + 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, + 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, + 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, + 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, + 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, + 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, + 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, + 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, + 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, + 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, + 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, + 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, + 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, + 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, + 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, + 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, + 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, + 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, + 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, + 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + ] + ]; + + //* + //* This is the default PARRAY + //* + Blowfish.prototype.PARRAY = [ + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, + 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b + ]; + + //* + //* This is the number of rounds the cipher will go + //* + Blowfish.prototype.NN = 16; + + //* + //* This function is needed to get rid of problems + //* with the high-bit getting set. If we don't do + //* this, then sometimes ( aa & 0x00FFFFFFFF ) is not + //* equal to ( bb & 0x00FFFFFFFF ) even when they + //* agree bit-for-bit for the first 32 bits. + //* + Blowfish.prototype._clean = function(xx) { + if (xx < 0) { + const yy = xx & 0x7FFFFFFF; + xx = yy + 0x80000000; + } + return xx; + }; + + //* + //* This is the mixing function that uses the sboxes + //* + Blowfish.prototype._F = function(xx) { + let yy; + + const dd = xx & 0x00FF; + xx >>>= 8; + const cc = xx & 0x00FF; + xx >>>= 8; + const bb = xx & 0x00FF; + xx >>>= 8; + const aa = xx & 0x00FF; + + yy = this.sboxes[0][aa] + this.sboxes[1][bb]; + yy ^= this.sboxes[2][cc]; + yy += this.sboxes[3][dd]; + + return yy; + }; + + //* + //* This method takes an array with two values, left and right + //* and does NN rounds of Blowfish on them. + //* + Blowfish.prototype._encryptBlock = function(vals) { + let dataL = vals[0]; + let dataR = vals[1]; + + let ii; + + for (ii = 0; ii < this.NN; ++ii) { + dataL ^= this.parray[ii]; + dataR = this._F(dataL) ^ dataR; + + const tmp = dataL; + dataL = dataR; + dataR = tmp; + } + + dataL ^= this.parray[this.NN + 0]; + dataR ^= this.parray[this.NN + 1]; + + vals[0] = this._clean(dataR); + vals[1] = this._clean(dataL); + }; + + //* + //* This method takes a vector of numbers and turns them + //* into long words so that they can be processed by the + //* real algorithm. + //* + //* Maybe I should make the real algorithm above take a vector + //* instead. That will involve more looping, but it won't require + //* the F() method to deconstruct the vector. + //* + Blowfish.prototype.encryptBlock = function(vector) { + let ii; + const vals = [0, 0]; + const off = this.BLOCKSIZE / 2; + for (ii = 0; ii < this.BLOCKSIZE / 2; ++ii) { + vals[0] = (vals[0] << 8) | (vector[ii + 0] & 0x00FF); + vals[1] = (vals[1] << 8) | (vector[ii + off] & 0x00FF); + } + + this._encryptBlock(vals); + + const ret = []; + for (ii = 0; ii < this.BLOCKSIZE / 2; ++ii) { + ret[ii + 0] = ((vals[0] >>> (24 - 8 * (ii))) & 0x00FF); + ret[ii + off] = ((vals[1] >>> (24 - 8 * (ii))) & 0x00FF); + // vals[ 0 ] = ( vals[ 0 ] >>> 8 ); + // vals[ 1 ] = ( vals[ 1 ] >>> 8 ); + } + + return ret; + }; + + //* + //* This method takes an array with two values, left and right + //* and undoes NN rounds of Blowfish on them. + //* + Blowfish.prototype._decryptBlock = function(vals) { + let dataL = vals[0]; + let dataR = vals[1]; + + let ii; + + for (ii = this.NN + 1; ii > 1; --ii) { + dataL ^= this.parray[ii]; + dataR = this._F(dataL) ^ dataR; + + const tmp = dataL; + dataL = dataR; + dataR = tmp; + } + + dataL ^= this.parray[1]; + dataR ^= this.parray[0]; + + vals[0] = this._clean(dataR); + vals[1] = this._clean(dataL); + }; + + //* + //* This method takes a key array and initializes the + //* sboxes and parray for this encryption. + //* + Blowfish.prototype.init = function(key) { + let ii; + let jj = 0; + + this.parray = []; + for (ii = 0; ii < this.NN + 2; ++ii) { + let data = 0x00000000; + for (let kk = 0; kk < 4; ++kk) { + data = (data << 8) | (key[jj] & 0x00FF); + if (++jj >= key.length) { + jj = 0; + } + } + this.parray[ii] = this.PARRAY[ii] ^ data; + } + + this.sboxes = []; + for (ii = 0; ii < 4; ++ii) { + this.sboxes[ii] = []; + for (jj = 0; jj < 256; ++jj) { + this.sboxes[ii][jj] = this.SBOXES[ii][jj]; + } + } + + const vals = [0x00000000, 0x00000000]; + + for (ii = 0; ii < this.NN + 2; ii += 2) { + this._encryptBlock(vals); + this.parray[ii + 0] = vals[0]; + this.parray[ii + 1] = vals[1]; + } + + for (ii = 0; ii < 4; ++ii) { + for (jj = 0; jj < 256; jj += 2) { + this._encryptBlock(vals); + this.sboxes[ii][jj + 0] = vals[0]; + this.sboxes[ii][jj + 1] = vals[1]; + } + } + }; + + // added by Recurity Labs + function BF(key) { + this.bf = new Blowfish(); + this.bf.init(key); + + this.encrypt = function(block) { + return this.bf.encryptBlock(block); + }; + } + + BF.keySize = BF.prototype.keySize = 16; + BF.blockSize = BF.prototype.blockSize = 8; + + /** + * @fileoverview Symmetric cryptography functions + * @module crypto/cipher + * @private + */ + + /** + * AES-128 encryption and decryption (ID 7) + * @function + * @param {String} key - 128-bit key + * @see {@link https://github.com/asmcrypto/asmcrypto.js|asmCrypto} + * @see {@link https://csrc.nist.gov/publications/fips/fips197/fips-197.pdf|NIST FIPS-197} + * @returns {Object} + */ + const aes128 = aes(128); + /** + * AES-128 Block Cipher (ID 8) + * @function + * @param {String} key - 192-bit key + * @see {@link https://github.com/asmcrypto/asmcrypto.js|asmCrypto} + * @see {@link https://csrc.nist.gov/publications/fips/fips197/fips-197.pdf|NIST FIPS-197} + * @returns {Object} + */ + const aes192 = aes(192); + /** + * AES-128 Block Cipher (ID 9) + * @function + * @param {String} key - 256-bit key + * @see {@link https://github.com/asmcrypto/asmcrypto.js|asmCrypto} + * @see {@link https://csrc.nist.gov/publications/fips/fips197/fips-197.pdf|NIST FIPS-197} + * @returns {Object} + */ + const aes256 = aes(256); + // Not in OpenPGP specifications + const des = DES; + /** + * Triple DES Block Cipher (ID 2) + * @function + * @param {String} key - 192-bit key + * @see {@link https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-67r2.pdf|NIST SP 800-67} + * @returns {Object} + */ + const tripledes = TripleDES; + /** + * CAST-128 Block Cipher (ID 3) + * @function + * @param {String} key - 128-bit key + * @see {@link https://tools.ietf.org/html/rfc2144|The CAST-128 Encryption Algorithm} + * @returns {Object} + */ + const cast5 = CAST5; + /** + * Twofish Block Cipher (ID 10) + * @function + * @param {String} key - 256-bit key + * @see {@link https://tools.ietf.org/html/rfc4880#ref-TWOFISH|TWOFISH} + * @returns {Object} + */ + const twofish = TF; + /** + * Blowfish Block Cipher (ID 4) + * @function + * @param {String} key - 128-bit key + * @see {@link https://tools.ietf.org/html/rfc4880#ref-BLOWFISH|BLOWFISH} + * @returns {Object} + */ + const blowfish = BF; + /** + * Not implemented + * @function + * @throws {Error} + */ + const idea = function() { + throw Error('IDEA symmetric-key algorithm not implemented'); + }; + + var cipher = /*#__PURE__*/Object.freeze({ + __proto__: null, + aes128: aes128, + aes192: aes192, + aes256: aes256, + des: des, + tripledes: tripledes, + cast5: cast5, + twofish: twofish, + blowfish: blowfish, + idea: idea + }); + + var sha1_asm = function ( stdlib, foreign, buffer ) { + "use asm"; + + // SHA256 state + var H0 = 0, H1 = 0, H2 = 0, H3 = 0, H4 = 0, + TOTAL0 = 0, TOTAL1 = 0; + + // HMAC state + var I0 = 0, I1 = 0, I2 = 0, I3 = 0, I4 = 0, + O0 = 0, O1 = 0, O2 = 0, O3 = 0, O4 = 0; + + // I/O buffer + var HEAP = new stdlib.Uint8Array(buffer); + + function _core ( w0, w1, w2, w3, w4, w5, w6, w7, w8, w9, w10, w11, w12, w13, w14, w15 ) { + w0 = w0|0; + w1 = w1|0; + w2 = w2|0; + w3 = w3|0; + w4 = w4|0; + w5 = w5|0; + w6 = w6|0; + w7 = w7|0; + w8 = w8|0; + w9 = w9|0; + w10 = w10|0; + w11 = w11|0; + w12 = w12|0; + w13 = w13|0; + w14 = w14|0; + w15 = w15|0; + + var a = 0, b = 0, c = 0, d = 0, e = 0, n = 0, t = 0, + w16 = 0, w17 = 0, w18 = 0, w19 = 0, + w20 = 0, w21 = 0, w22 = 0, w23 = 0, w24 = 0, w25 = 0, w26 = 0, w27 = 0, w28 = 0, w29 = 0, + w30 = 0, w31 = 0, w32 = 0, w33 = 0, w34 = 0, w35 = 0, w36 = 0, w37 = 0, w38 = 0, w39 = 0, + w40 = 0, w41 = 0, w42 = 0, w43 = 0, w44 = 0, w45 = 0, w46 = 0, w47 = 0, w48 = 0, w49 = 0, + w50 = 0, w51 = 0, w52 = 0, w53 = 0, w54 = 0, w55 = 0, w56 = 0, w57 = 0, w58 = 0, w59 = 0, + w60 = 0, w61 = 0, w62 = 0, w63 = 0, w64 = 0, w65 = 0, w66 = 0, w67 = 0, w68 = 0, w69 = 0, + w70 = 0, w71 = 0, w72 = 0, w73 = 0, w74 = 0, w75 = 0, w76 = 0, w77 = 0, w78 = 0, w79 = 0; + + a = H0; + b = H1; + c = H2; + d = H3; + e = H4; + + // 0 + t = ( w0 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 1 + t = ( w1 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 2 + t = ( w2 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 3 + t = ( w3 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 4 + t = ( w4 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 5 + t = ( w5 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 6 + t = ( w6 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 7 + t = ( w7 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 8 + t = ( w8 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 9 + t = ( w9 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 10 + t = ( w10 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 11 + t = ( w11 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 12 + t = ( w12 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 13 + t = ( w13 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 14 + t = ( w14 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 15 + t = ( w15 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 16 + n = w13 ^ w8 ^ w2 ^ w0; + w16 = (n << 1) | (n >>> 31); + t = (w16 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 17 + n = w14 ^ w9 ^ w3 ^ w1; + w17 = (n << 1) | (n >>> 31); + t = (w17 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 18 + n = w15 ^ w10 ^ w4 ^ w2; + w18 = (n << 1) | (n >>> 31); + t = (w18 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 19 + n = w16 ^ w11 ^ w5 ^ w3; + w19 = (n << 1) | (n >>> 31); + t = (w19 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (~b & d)) + 0x5a827999 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 20 + n = w17 ^ w12 ^ w6 ^ w4; + w20 = (n << 1) | (n >>> 31); + t = (w20 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 21 + n = w18 ^ w13 ^ w7 ^ w5; + w21 = (n << 1) | (n >>> 31); + t = (w21 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 22 + n = w19 ^ w14 ^ w8 ^ w6; + w22 = (n << 1) | (n >>> 31); + t = (w22 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 23 + n = w20 ^ w15 ^ w9 ^ w7; + w23 = (n << 1) | (n >>> 31); + t = (w23 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 24 + n = w21 ^ w16 ^ w10 ^ w8; + w24 = (n << 1) | (n >>> 31); + t = (w24 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 25 + n = w22 ^ w17 ^ w11 ^ w9; + w25 = (n << 1) | (n >>> 31); + t = (w25 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 26 + n = w23 ^ w18 ^ w12 ^ w10; + w26 = (n << 1) | (n >>> 31); + t = (w26 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 27 + n = w24 ^ w19 ^ w13 ^ w11; + w27 = (n << 1) | (n >>> 31); + t = (w27 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 28 + n = w25 ^ w20 ^ w14 ^ w12; + w28 = (n << 1) | (n >>> 31); + t = (w28 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 29 + n = w26 ^ w21 ^ w15 ^ w13; + w29 = (n << 1) | (n >>> 31); + t = (w29 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 30 + n = w27 ^ w22 ^ w16 ^ w14; + w30 = (n << 1) | (n >>> 31); + t = (w30 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 31 + n = w28 ^ w23 ^ w17 ^ w15; + w31 = (n << 1) | (n >>> 31); + t = (w31 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 32 + n = w29 ^ w24 ^ w18 ^ w16; + w32 = (n << 1) | (n >>> 31); + t = (w32 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 33 + n = w30 ^ w25 ^ w19 ^ w17; + w33 = (n << 1) | (n >>> 31); + t = (w33 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 34 + n = w31 ^ w26 ^ w20 ^ w18; + w34 = (n << 1) | (n >>> 31); + t = (w34 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 35 + n = w32 ^ w27 ^ w21 ^ w19; + w35 = (n << 1) | (n >>> 31); + t = (w35 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 36 + n = w33 ^ w28 ^ w22 ^ w20; + w36 = (n << 1) | (n >>> 31); + t = (w36 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 37 + n = w34 ^ w29 ^ w23 ^ w21; + w37 = (n << 1) | (n >>> 31); + t = (w37 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 38 + n = w35 ^ w30 ^ w24 ^ w22; + w38 = (n << 1) | (n >>> 31); + t = (w38 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 39 + n = w36 ^ w31 ^ w25 ^ w23; + w39 = (n << 1) | (n >>> 31); + t = (w39 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) + 0x6ed9eba1 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 40 + n = w37 ^ w32 ^ w26 ^ w24; + w40 = (n << 1) | (n >>> 31); + t = (w40 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 41 + n = w38 ^ w33 ^ w27 ^ w25; + w41 = (n << 1) | (n >>> 31); + t = (w41 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 42 + n = w39 ^ w34 ^ w28 ^ w26; + w42 = (n << 1) | (n >>> 31); + t = (w42 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 43 + n = w40 ^ w35 ^ w29 ^ w27; + w43 = (n << 1) | (n >>> 31); + t = (w43 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 44 + n = w41 ^ w36 ^ w30 ^ w28; + w44 = (n << 1) | (n >>> 31); + t = (w44 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 45 + n = w42 ^ w37 ^ w31 ^ w29; + w45 = (n << 1) | (n >>> 31); + t = (w45 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 46 + n = w43 ^ w38 ^ w32 ^ w30; + w46 = (n << 1) | (n >>> 31); + t = (w46 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 47 + n = w44 ^ w39 ^ w33 ^ w31; + w47 = (n << 1) | (n >>> 31); + t = (w47 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 48 + n = w45 ^ w40 ^ w34 ^ w32; + w48 = (n << 1) | (n >>> 31); + t = (w48 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 49 + n = w46 ^ w41 ^ w35 ^ w33; + w49 = (n << 1) | (n >>> 31); + t = (w49 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 50 + n = w47 ^ w42 ^ w36 ^ w34; + w50 = (n << 1) | (n >>> 31); + t = (w50 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 51 + n = w48 ^ w43 ^ w37 ^ w35; + w51 = (n << 1) | (n >>> 31); + t = (w51 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 52 + n = w49 ^ w44 ^ w38 ^ w36; + w52 = (n << 1) | (n >>> 31); + t = (w52 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 53 + n = w50 ^ w45 ^ w39 ^ w37; + w53 = (n << 1) | (n >>> 31); + t = (w53 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 54 + n = w51 ^ w46 ^ w40 ^ w38; + w54 = (n << 1) | (n >>> 31); + t = (w54 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 55 + n = w52 ^ w47 ^ w41 ^ w39; + w55 = (n << 1) | (n >>> 31); + t = (w55 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 56 + n = w53 ^ w48 ^ w42 ^ w40; + w56 = (n << 1) | (n >>> 31); + t = (w56 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 57 + n = w54 ^ w49 ^ w43 ^ w41; + w57 = (n << 1) | (n >>> 31); + t = (w57 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 58 + n = w55 ^ w50 ^ w44 ^ w42; + w58 = (n << 1) | (n >>> 31); + t = (w58 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 59 + n = w56 ^ w51 ^ w45 ^ w43; + w59 = (n << 1) | (n >>> 31); + t = (w59 + ((a << 5) | (a >>> 27)) + e + ((b & c) | (b & d) | (c & d)) - 0x70e44324 )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 60 + n = w57 ^ w52 ^ w46 ^ w44; + w60 = (n << 1) | (n >>> 31); + t = (w60 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 61 + n = w58 ^ w53 ^ w47 ^ w45; + w61 = (n << 1) | (n >>> 31); + t = (w61 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 62 + n = w59 ^ w54 ^ w48 ^ w46; + w62 = (n << 1) | (n >>> 31); + t = (w62 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 63 + n = w60 ^ w55 ^ w49 ^ w47; + w63 = (n << 1) | (n >>> 31); + t = (w63 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 64 + n = w61 ^ w56 ^ w50 ^ w48; + w64 = (n << 1) | (n >>> 31); + t = (w64 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 65 + n = w62 ^ w57 ^ w51 ^ w49; + w65 = (n << 1) | (n >>> 31); + t = (w65 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 66 + n = w63 ^ w58 ^ w52 ^ w50; + w66 = (n << 1) | (n >>> 31); + t = (w66 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 67 + n = w64 ^ w59 ^ w53 ^ w51; + w67 = (n << 1) | (n >>> 31); + t = (w67 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 68 + n = w65 ^ w60 ^ w54 ^ w52; + w68 = (n << 1) | (n >>> 31); + t = (w68 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 69 + n = w66 ^ w61 ^ w55 ^ w53; + w69 = (n << 1) | (n >>> 31); + t = (w69 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 70 + n = w67 ^ w62 ^ w56 ^ w54; + w70 = (n << 1) | (n >>> 31); + t = (w70 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 71 + n = w68 ^ w63 ^ w57 ^ w55; + w71 = (n << 1) | (n >>> 31); + t = (w71 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 72 + n = w69 ^ w64 ^ w58 ^ w56; + w72 = (n << 1) | (n >>> 31); + t = (w72 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 73 + n = w70 ^ w65 ^ w59 ^ w57; + w73 = (n << 1) | (n >>> 31); + t = (w73 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 74 + n = w71 ^ w66 ^ w60 ^ w58; + w74 = (n << 1) | (n >>> 31); + t = (w74 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 75 + n = w72 ^ w67 ^ w61 ^ w59; + w75 = (n << 1) | (n >>> 31); + t = (w75 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 76 + n = w73 ^ w68 ^ w62 ^ w60; + w76 = (n << 1) | (n >>> 31); + t = (w76 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 77 + n = w74 ^ w69 ^ w63 ^ w61; + w77 = (n << 1) | (n >>> 31); + t = (w77 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 78 + n = w75 ^ w70 ^ w64 ^ w62; + w78 = (n << 1) | (n >>> 31); + t = (w78 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + // 79 + n = w76 ^ w71 ^ w65 ^ w63; + w79 = (n << 1) | (n >>> 31); + t = (w79 + ((a << 5) | (a >>> 27)) + e + (b ^ c ^ d) - 0x359d3e2a )|0; + e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; + + H0 = ( H0 + a )|0; + H1 = ( H1 + b )|0; + H2 = ( H2 + c )|0; + H3 = ( H3 + d )|0; + H4 = ( H4 + e )|0; + + } + + function _core_heap ( offset ) { + offset = offset|0; + + _core( + HEAP[offset|0]<<24 | HEAP[offset|1]<<16 | HEAP[offset|2]<<8 | HEAP[offset|3], + HEAP[offset|4]<<24 | HEAP[offset|5]<<16 | HEAP[offset|6]<<8 | HEAP[offset|7], + HEAP[offset|8]<<24 | HEAP[offset|9]<<16 | HEAP[offset|10]<<8 | HEAP[offset|11], + HEAP[offset|12]<<24 | HEAP[offset|13]<<16 | HEAP[offset|14]<<8 | HEAP[offset|15], + HEAP[offset|16]<<24 | HEAP[offset|17]<<16 | HEAP[offset|18]<<8 | HEAP[offset|19], + HEAP[offset|20]<<24 | HEAP[offset|21]<<16 | HEAP[offset|22]<<8 | HEAP[offset|23], + HEAP[offset|24]<<24 | HEAP[offset|25]<<16 | HEAP[offset|26]<<8 | HEAP[offset|27], + HEAP[offset|28]<<24 | HEAP[offset|29]<<16 | HEAP[offset|30]<<8 | HEAP[offset|31], + HEAP[offset|32]<<24 | HEAP[offset|33]<<16 | HEAP[offset|34]<<8 | HEAP[offset|35], + HEAP[offset|36]<<24 | HEAP[offset|37]<<16 | HEAP[offset|38]<<8 | HEAP[offset|39], + HEAP[offset|40]<<24 | HEAP[offset|41]<<16 | HEAP[offset|42]<<8 | HEAP[offset|43], + HEAP[offset|44]<<24 | HEAP[offset|45]<<16 | HEAP[offset|46]<<8 | HEAP[offset|47], + HEAP[offset|48]<<24 | HEAP[offset|49]<<16 | HEAP[offset|50]<<8 | HEAP[offset|51], + HEAP[offset|52]<<24 | HEAP[offset|53]<<16 | HEAP[offset|54]<<8 | HEAP[offset|55], + HEAP[offset|56]<<24 | HEAP[offset|57]<<16 | HEAP[offset|58]<<8 | HEAP[offset|59], + HEAP[offset|60]<<24 | HEAP[offset|61]<<16 | HEAP[offset|62]<<8 | HEAP[offset|63] + ); + } + + // offset — multiple of 32 + function _state_to_heap ( output ) { + output = output|0; + + HEAP[output|0] = H0>>>24; + HEAP[output|1] = H0>>>16&255; + HEAP[output|2] = H0>>>8&255; + HEAP[output|3] = H0&255; + HEAP[output|4] = H1>>>24; + HEAP[output|5] = H1>>>16&255; + HEAP[output|6] = H1>>>8&255; + HEAP[output|7] = H1&255; + HEAP[output|8] = H2>>>24; + HEAP[output|9] = H2>>>16&255; + HEAP[output|10] = H2>>>8&255; + HEAP[output|11] = H2&255; + HEAP[output|12] = H3>>>24; + HEAP[output|13] = H3>>>16&255; + HEAP[output|14] = H3>>>8&255; + HEAP[output|15] = H3&255; + HEAP[output|16] = H4>>>24; + HEAP[output|17] = H4>>>16&255; + HEAP[output|18] = H4>>>8&255; + HEAP[output|19] = H4&255; + } + + function reset () { + H0 = 0x67452301; + H1 = 0xefcdab89; + H2 = 0x98badcfe; + H3 = 0x10325476; + H4 = 0xc3d2e1f0; + TOTAL0 = TOTAL1 = 0; + } + + function init ( h0, h1, h2, h3, h4, total0, total1 ) { + h0 = h0|0; + h1 = h1|0; + h2 = h2|0; + h3 = h3|0; + h4 = h4|0; + total0 = total0|0; + total1 = total1|0; + + H0 = h0; + H1 = h1; + H2 = h2; + H3 = h3; + H4 = h4; + TOTAL0 = total0; + TOTAL1 = total1; + } + + // offset — multiple of 64 + function process ( offset, length ) { + offset = offset|0; + length = length|0; + + var hashed = 0; + + if ( offset & 63 ) + return -1; + + while ( (length|0) >= 64 ) { + _core_heap(offset); + + offset = ( offset + 64 )|0; + length = ( length - 64 )|0; + + hashed = ( hashed + 64 )|0; + } + + TOTAL0 = ( TOTAL0 + hashed )|0; + if ( TOTAL0>>>0 < hashed>>>0 ) TOTAL1 = ( TOTAL1 + 1 )|0; + + return hashed|0; + } + + // offset — multiple of 64 + // output — multiple of 32 + function finish ( offset, length, output ) { + offset = offset|0; + length = length|0; + output = output|0; + + var hashed = 0, + i = 0; + + if ( offset & 63 ) + return -1; + + if ( ~output ) + if ( output & 31 ) + return -1; + + if ( (length|0) >= 64 ) { + hashed = process( offset, length )|0; + if ( (hashed|0) == -1 ) + return -1; + + offset = ( offset + hashed )|0; + length = ( length - hashed )|0; + } + + hashed = ( hashed + length )|0; + TOTAL0 = ( TOTAL0 + length )|0; + if ( TOTAL0>>>0 < length>>>0 ) TOTAL1 = (TOTAL1 + 1)|0; + + HEAP[offset|length] = 0x80; + + if ( (length|0) >= 56 ) { + for ( i = (length+1)|0; (i|0) < 64; i = (i+1)|0 ) + HEAP[offset|i] = 0x00; + _core_heap(offset); + + length = 0; + + HEAP[offset|0] = 0; + } + + for ( i = (length+1)|0; (i|0) < 59; i = (i+1)|0 ) + HEAP[offset|i] = 0; + + HEAP[offset|56] = TOTAL1>>>21&255; + HEAP[offset|57] = TOTAL1>>>13&255; + HEAP[offset|58] = TOTAL1>>>5&255; + HEAP[offset|59] = TOTAL1<<3&255 | TOTAL0>>>29; + HEAP[offset|60] = TOTAL0>>>21&255; + HEAP[offset|61] = TOTAL0>>>13&255; + HEAP[offset|62] = TOTAL0>>>5&255; + HEAP[offset|63] = TOTAL0<<3&255; + _core_heap(offset); + + if ( ~output ) + _state_to_heap(output); + + return hashed|0; + } + + function hmac_reset () { + H0 = I0; + H1 = I1; + H2 = I2; + H3 = I3; + H4 = I4; + TOTAL0 = 64; + TOTAL1 = 0; + } + + function _hmac_opad () { + H0 = O0; + H1 = O1; + H2 = O2; + H3 = O3; + H4 = O4; + TOTAL0 = 64; + TOTAL1 = 0; + } + + function hmac_init ( p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15 ) { + p0 = p0|0; + p1 = p1|0; + p2 = p2|0; + p3 = p3|0; + p4 = p4|0; + p5 = p5|0; + p6 = p6|0; + p7 = p7|0; + p8 = p8|0; + p9 = p9|0; + p10 = p10|0; + p11 = p11|0; + p12 = p12|0; + p13 = p13|0; + p14 = p14|0; + p15 = p15|0; + + // opad + reset(); + _core( + p0 ^ 0x5c5c5c5c, + p1 ^ 0x5c5c5c5c, + p2 ^ 0x5c5c5c5c, + p3 ^ 0x5c5c5c5c, + p4 ^ 0x5c5c5c5c, + p5 ^ 0x5c5c5c5c, + p6 ^ 0x5c5c5c5c, + p7 ^ 0x5c5c5c5c, + p8 ^ 0x5c5c5c5c, + p9 ^ 0x5c5c5c5c, + p10 ^ 0x5c5c5c5c, + p11 ^ 0x5c5c5c5c, + p12 ^ 0x5c5c5c5c, + p13 ^ 0x5c5c5c5c, + p14 ^ 0x5c5c5c5c, + p15 ^ 0x5c5c5c5c + ); + O0 = H0; + O1 = H1; + O2 = H2; + O3 = H3; + O4 = H4; + + // ipad + reset(); + _core( + p0 ^ 0x36363636, + p1 ^ 0x36363636, + p2 ^ 0x36363636, + p3 ^ 0x36363636, + p4 ^ 0x36363636, + p5 ^ 0x36363636, + p6 ^ 0x36363636, + p7 ^ 0x36363636, + p8 ^ 0x36363636, + p9 ^ 0x36363636, + p10 ^ 0x36363636, + p11 ^ 0x36363636, + p12 ^ 0x36363636, + p13 ^ 0x36363636, + p14 ^ 0x36363636, + p15 ^ 0x36363636 + ); + I0 = H0; + I1 = H1; + I2 = H2; + I3 = H3; + I4 = H4; + + TOTAL0 = 64; + TOTAL1 = 0; + } + + // offset — multiple of 64 + // output — multiple of 32 + function hmac_finish ( offset, length, output ) { + offset = offset|0; + length = length|0; + output = output|0; + + var t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0, hashed = 0; + + if ( offset & 63 ) + return -1; + + if ( ~output ) + if ( output & 31 ) + return -1; + + hashed = finish( offset, length, -1 )|0; + t0 = H0, t1 = H1, t2 = H2, t3 = H3, t4 = H4; + + _hmac_opad(); + _core( t0, t1, t2, t3, t4, 0x80000000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 672 ); + + if ( ~output ) + _state_to_heap(output); + + return hashed|0; + } + + // salt is assumed to be already processed + // offset — multiple of 64 + // output — multiple of 32 + function pbkdf2_generate_block ( offset, length, block, count, output ) { + offset = offset|0; + length = length|0; + block = block|0; + count = count|0; + output = output|0; + + var h0 = 0, h1 = 0, h2 = 0, h3 = 0, h4 = 0, + t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0; + + if ( offset & 63 ) + return -1; + + if ( ~output ) + if ( output & 31 ) + return -1; + + // pad block number into heap + // FIXME probable OOB write + HEAP[(offset+length)|0] = block>>>24; + HEAP[(offset+length+1)|0] = block>>>16&255; + HEAP[(offset+length+2)|0] = block>>>8&255; + HEAP[(offset+length+3)|0] = block&255; + + // finish first iteration + hmac_finish( offset, (length+4)|0, -1 )|0; + h0 = t0 = H0, h1 = t1 = H1, h2 = t2 = H2, h3 = t3 = H3, h4 = t4 = H4; + count = (count-1)|0; + + // perform the rest iterations + while ( (count|0) > 0 ) { + hmac_reset(); + _core( t0, t1, t2, t3, t4, 0x80000000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 672 ); + t0 = H0, t1 = H1, t2 = H2, t3 = H3, t4 = H4; + + _hmac_opad(); + _core( t0, t1, t2, t3, t4, 0x80000000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 672 ); + t0 = H0, t1 = H1, t2 = H2, t3 = H3, t4 = H4; + + h0 = h0 ^ H0; + h1 = h1 ^ H1; + h2 = h2 ^ H2; + h3 = h3 ^ H3; + h4 = h4 ^ H4; + + count = (count-1)|0; + } + + H0 = h0; + H1 = h1; + H2 = h2; + H3 = h3; + H4 = h4; + + if ( ~output ) + _state_to_heap(output); + + return 0; + } + + return { + // SHA1 + reset: reset, + init: init, + process: process, + finish: finish, + + // HMAC-SHA1 + hmac_reset: hmac_reset, + hmac_init: hmac_init, + hmac_finish: hmac_finish, + + // PBKDF2-HMAC-SHA1 + pbkdf2_generate_block: pbkdf2_generate_block + } + }; + + class Hash { + constructor() { + this.pos = 0; + this.len = 0; + } + reset() { + const { asm } = this.acquire_asm(); + this.result = null; + this.pos = 0; + this.len = 0; + asm.reset(); + return this; + } + process(data) { + if (this.result !== null) + throw new IllegalStateError('state must be reset before processing new data'); + const { asm, heap } = this.acquire_asm(); + let hpos = this.pos; + let hlen = this.len; + let dpos = 0; + let dlen = data.length; + let wlen = 0; + while (dlen > 0) { + wlen = _heap_write(heap, hpos + hlen, data, dpos, dlen); + hlen += wlen; + dpos += wlen; + dlen -= wlen; + wlen = asm.process(hpos, hlen); + hpos += wlen; + hlen -= wlen; + if (!hlen) + hpos = 0; + } + this.pos = hpos; + this.len = hlen; + return this; + } + finish() { + if (this.result !== null) + throw new IllegalStateError('state must be reset before processing new data'); + const { asm, heap } = this.acquire_asm(); + asm.finish(this.pos, this.len, 0); + this.result = new Uint8Array(this.HASH_SIZE); + this.result.set(heap.subarray(0, this.HASH_SIZE)); + this.pos = 0; + this.len = 0; + this.release_asm(); + return this; + } + } + + const _sha1_block_size = 64; + const _sha1_hash_size = 20; + const heap_pool$1 = []; + const asm_pool$1 = []; + class Sha1 extends Hash { + constructor() { + super(); + this.NAME = 'sha1'; + this.BLOCK_SIZE = _sha1_block_size; + this.HASH_SIZE = _sha1_hash_size; + this.acquire_asm(); + } + acquire_asm() { + if (this.heap === undefined || this.asm === undefined) { + this.heap = heap_pool$1.pop() || _heap_init(); + this.asm = asm_pool$1.pop() || sha1_asm({ Uint8Array: Uint8Array }, null, this.heap.buffer); + this.reset(); + } + return { heap: this.heap, asm: this.asm }; + } + release_asm() { + if (this.heap !== undefined && this.asm !== undefined) { + heap_pool$1.push(this.heap); + asm_pool$1.push(this.asm); + } + this.heap = undefined; + this.asm = undefined; + } + static bytes(data) { + return new Sha1().process(data).finish().result; + } + } + Sha1.NAME = 'sha1'; + Sha1.heap_pool = []; + Sha1.asm_pool = []; + Sha1.asm_function = sha1_asm; + + var sha256_asm = function ( stdlib, foreign, buffer ) { + "use asm"; + + // SHA256 state + var H0 = 0, H1 = 0, H2 = 0, H3 = 0, H4 = 0, H5 = 0, H6 = 0, H7 = 0, + TOTAL0 = 0, TOTAL1 = 0; + + // HMAC state + var I0 = 0, I1 = 0, I2 = 0, I3 = 0, I4 = 0, I5 = 0, I6 = 0, I7 = 0, + O0 = 0, O1 = 0, O2 = 0, O3 = 0, O4 = 0, O5 = 0, O6 = 0, O7 = 0; + + // I/O buffer + var HEAP = new stdlib.Uint8Array(buffer); + + function _core ( w0, w1, w2, w3, w4, w5, w6, w7, w8, w9, w10, w11, w12, w13, w14, w15 ) { + w0 = w0|0; + w1 = w1|0; + w2 = w2|0; + w3 = w3|0; + w4 = w4|0; + w5 = w5|0; + w6 = w6|0; + w7 = w7|0; + w8 = w8|0; + w9 = w9|0; + w10 = w10|0; + w11 = w11|0; + w12 = w12|0; + w13 = w13|0; + w14 = w14|0; + w15 = w15|0; + + var a = 0, b = 0, c = 0, d = 0, e = 0, f = 0, g = 0, h = 0; + + a = H0; + b = H1; + c = H2; + d = H3; + e = H4; + f = H5; + g = H6; + h = H7; + + // 0 + h = ( w0 + h + ( e>>>6 ^ e>>>11 ^ e>>>25 ^ e<<26 ^ e<<21 ^ e<<7 ) + ( g ^ e & (f^g) ) + 0x428a2f98 )|0; + d = ( d + h )|0; + h = ( h + ( (a & b) ^ ( c & (a ^ b) ) ) + ( a>>>2 ^ a>>>13 ^ a>>>22 ^ a<<30 ^ a<<19 ^ a<<10 ) )|0; + + // 1 + g = ( w1 + g + ( d>>>6 ^ d>>>11 ^ d>>>25 ^ d<<26 ^ d<<21 ^ d<<7 ) + ( f ^ d & (e^f) ) + 0x71374491 )|0; + c = ( c + g )|0; + g = ( g + ( (h & a) ^ ( b & (h ^ a) ) ) + ( h>>>2 ^ h>>>13 ^ h>>>22 ^ h<<30 ^ h<<19 ^ h<<10 ) )|0; + + // 2 + f = ( w2 + f + ( c>>>6 ^ c>>>11 ^ c>>>25 ^ c<<26 ^ c<<21 ^ c<<7 ) + ( e ^ c & (d^e) ) + 0xb5c0fbcf )|0; + b = ( b + f )|0; + f = ( f + ( (g & h) ^ ( a & (g ^ h) ) ) + ( g>>>2 ^ g>>>13 ^ g>>>22 ^ g<<30 ^ g<<19 ^ g<<10 ) )|0; + + // 3 + e = ( w3 + e + ( b>>>6 ^ b>>>11 ^ b>>>25 ^ b<<26 ^ b<<21 ^ b<<7 ) + ( d ^ b & (c^d) ) + 0xe9b5dba5 )|0; + a = ( a + e )|0; + e = ( e + ( (f & g) ^ ( h & (f ^ g) ) ) + ( f>>>2 ^ f>>>13 ^ f>>>22 ^ f<<30 ^ f<<19 ^ f<<10 ) )|0; + + // 4 + d = ( w4 + d + ( a>>>6 ^ a>>>11 ^ a>>>25 ^ a<<26 ^ a<<21 ^ a<<7 ) + ( c ^ a & (b^c) ) + 0x3956c25b )|0; + h = ( h + d )|0; + d = ( d + ( (e & f) ^ ( g & (e ^ f) ) ) + ( e>>>2 ^ e>>>13 ^ e>>>22 ^ e<<30 ^ e<<19 ^ e<<10 ) )|0; + + // 5 + c = ( w5 + c + ( h>>>6 ^ h>>>11 ^ h>>>25 ^ h<<26 ^ h<<21 ^ h<<7 ) + ( b ^ h & (a^b) ) + 0x59f111f1 )|0; + g = ( g + c )|0; + c = ( c + ( (d & e) ^ ( f & (d ^ e) ) ) + ( d>>>2 ^ d>>>13 ^ d>>>22 ^ d<<30 ^ d<<19 ^ d<<10 ) )|0; + + // 6 + b = ( w6 + b + ( g>>>6 ^ g>>>11 ^ g>>>25 ^ g<<26 ^ g<<21 ^ g<<7 ) + ( a ^ g & (h^a) ) + 0x923f82a4 )|0; + f = ( f + b )|0; + b = ( b + ( (c & d) ^ ( e & (c ^ d) ) ) + ( c>>>2 ^ c>>>13 ^ c>>>22 ^ c<<30 ^ c<<19 ^ c<<10 ) )|0; + + // 7 + a = ( w7 + a + ( f>>>6 ^ f>>>11 ^ f>>>25 ^ f<<26 ^ f<<21 ^ f<<7 ) + ( h ^ f & (g^h) ) + 0xab1c5ed5 )|0; + e = ( e + a )|0; + a = ( a + ( (b & c) ^ ( d & (b ^ c) ) ) + ( b>>>2 ^ b>>>13 ^ b>>>22 ^ b<<30 ^ b<<19 ^ b<<10 ) )|0; + + // 8 + h = ( w8 + h + ( e>>>6 ^ e>>>11 ^ e>>>25 ^ e<<26 ^ e<<21 ^ e<<7 ) + ( g ^ e & (f^g) ) + 0xd807aa98 )|0; + d = ( d + h )|0; + h = ( h + ( (a & b) ^ ( c & (a ^ b) ) ) + ( a>>>2 ^ a>>>13 ^ a>>>22 ^ a<<30 ^ a<<19 ^ a<<10 ) )|0; + + // 9 + g = ( w9 + g + ( d>>>6 ^ d>>>11 ^ d>>>25 ^ d<<26 ^ d<<21 ^ d<<7 ) + ( f ^ d & (e^f) ) + 0x12835b01 )|0; + c = ( c + g )|0; + g = ( g + ( (h & a) ^ ( b & (h ^ a) ) ) + ( h>>>2 ^ h>>>13 ^ h>>>22 ^ h<<30 ^ h<<19 ^ h<<10 ) )|0; + + // 10 + f = ( w10 + f + ( c>>>6 ^ c>>>11 ^ c>>>25 ^ c<<26 ^ c<<21 ^ c<<7 ) + ( e ^ c & (d^e) ) + 0x243185be )|0; + b = ( b + f )|0; + f = ( f + ( (g & h) ^ ( a & (g ^ h) ) ) + ( g>>>2 ^ g>>>13 ^ g>>>22 ^ g<<30 ^ g<<19 ^ g<<10 ) )|0; + + // 11 + e = ( w11 + e + ( b>>>6 ^ b>>>11 ^ b>>>25 ^ b<<26 ^ b<<21 ^ b<<7 ) + ( d ^ b & (c^d) ) + 0x550c7dc3 )|0; + a = ( a + e )|0; + e = ( e + ( (f & g) ^ ( h & (f ^ g) ) ) + ( f>>>2 ^ f>>>13 ^ f>>>22 ^ f<<30 ^ f<<19 ^ f<<10 ) )|0; + + // 12 + d = ( w12 + d + ( a>>>6 ^ a>>>11 ^ a>>>25 ^ a<<26 ^ a<<21 ^ a<<7 ) + ( c ^ a & (b^c) ) + 0x72be5d74 )|0; + h = ( h + d )|0; + d = ( d + ( (e & f) ^ ( g & (e ^ f) ) ) + ( e>>>2 ^ e>>>13 ^ e>>>22 ^ e<<30 ^ e<<19 ^ e<<10 ) )|0; + + // 13 + c = ( w13 + c + ( h>>>6 ^ h>>>11 ^ h>>>25 ^ h<<26 ^ h<<21 ^ h<<7 ) + ( b ^ h & (a^b) ) + 0x80deb1fe )|0; + g = ( g + c )|0; + c = ( c + ( (d & e) ^ ( f & (d ^ e) ) ) + ( d>>>2 ^ d>>>13 ^ d>>>22 ^ d<<30 ^ d<<19 ^ d<<10 ) )|0; + + // 14 + b = ( w14 + b + ( g>>>6 ^ g>>>11 ^ g>>>25 ^ g<<26 ^ g<<21 ^ g<<7 ) + ( a ^ g & (h^a) ) + 0x9bdc06a7 )|0; + f = ( f + b )|0; + b = ( b + ( (c & d) ^ ( e & (c ^ d) ) ) + ( c>>>2 ^ c>>>13 ^ c>>>22 ^ c<<30 ^ c<<19 ^ c<<10 ) )|0; + + // 15 + a = ( w15 + a + ( f>>>6 ^ f>>>11 ^ f>>>25 ^ f<<26 ^ f<<21 ^ f<<7 ) + ( h ^ f & (g^h) ) + 0xc19bf174 )|0; + e = ( e + a )|0; + a = ( a + ( (b & c) ^ ( d & (b ^ c) ) ) + ( b>>>2 ^ b>>>13 ^ b>>>22 ^ b<<30 ^ b<<19 ^ b<<10 ) )|0; + + // 16 + w0 = ( ( w1>>>7 ^ w1>>>18 ^ w1>>>3 ^ w1<<25 ^ w1<<14 ) + ( w14>>>17 ^ w14>>>19 ^ w14>>>10 ^ w14<<15 ^ w14<<13 ) + w0 + w9 )|0; + h = ( w0 + h + ( e>>>6 ^ e>>>11 ^ e>>>25 ^ e<<26 ^ e<<21 ^ e<<7 ) + ( g ^ e & (f^g) ) + 0xe49b69c1 )|0; + d = ( d + h )|0; + h = ( h + ( (a & b) ^ ( c & (a ^ b) ) ) + ( a>>>2 ^ a>>>13 ^ a>>>22 ^ a<<30 ^ a<<19 ^ a<<10 ) )|0; + + // 17 + w1 = ( ( w2>>>7 ^ w2>>>18 ^ w2>>>3 ^ w2<<25 ^ w2<<14 ) + ( w15>>>17 ^ w15>>>19 ^ w15>>>10 ^ w15<<15 ^ w15<<13 ) + w1 + w10 )|0; + g = ( w1 + g + ( d>>>6 ^ d>>>11 ^ d>>>25 ^ d<<26 ^ d<<21 ^ d<<7 ) + ( f ^ d & (e^f) ) + 0xefbe4786 )|0; + c = ( c + g )|0; + g = ( g + ( (h & a) ^ ( b & (h ^ a) ) ) + ( h>>>2 ^ h>>>13 ^ h>>>22 ^ h<<30 ^ h<<19 ^ h<<10 ) )|0; + + // 18 + w2 = ( ( w3>>>7 ^ w3>>>18 ^ w3>>>3 ^ w3<<25 ^ w3<<14 ) + ( w0>>>17 ^ w0>>>19 ^ w0>>>10 ^ w0<<15 ^ w0<<13 ) + w2 + w11 )|0; + f = ( w2 + f + ( c>>>6 ^ c>>>11 ^ c>>>25 ^ c<<26 ^ c<<21 ^ c<<7 ) + ( e ^ c & (d^e) ) + 0x0fc19dc6 )|0; + b = ( b + f )|0; + f = ( f + ( (g & h) ^ ( a & (g ^ h) ) ) + ( g>>>2 ^ g>>>13 ^ g>>>22 ^ g<<30 ^ g<<19 ^ g<<10 ) )|0; + + // 19 + w3 = ( ( w4>>>7 ^ w4>>>18 ^ w4>>>3 ^ w4<<25 ^ w4<<14 ) + ( w1>>>17 ^ w1>>>19 ^ w1>>>10 ^ w1<<15 ^ w1<<13 ) + w3 + w12 )|0; + e = ( w3 + e + ( b>>>6 ^ b>>>11 ^ b>>>25 ^ b<<26 ^ b<<21 ^ b<<7 ) + ( d ^ b & (c^d) ) + 0x240ca1cc )|0; + a = ( a + e )|0; + e = ( e + ( (f & g) ^ ( h & (f ^ g) ) ) + ( f>>>2 ^ f>>>13 ^ f>>>22 ^ f<<30 ^ f<<19 ^ f<<10 ) )|0; + + // 20 + w4 = ( ( w5>>>7 ^ w5>>>18 ^ w5>>>3 ^ w5<<25 ^ w5<<14 ) + ( w2>>>17 ^ w2>>>19 ^ w2>>>10 ^ w2<<15 ^ w2<<13 ) + w4 + w13 )|0; + d = ( w4 + d + ( a>>>6 ^ a>>>11 ^ a>>>25 ^ a<<26 ^ a<<21 ^ a<<7 ) + ( c ^ a & (b^c) ) + 0x2de92c6f )|0; + h = ( h + d )|0; + d = ( d + ( (e & f) ^ ( g & (e ^ f) ) ) + ( e>>>2 ^ e>>>13 ^ e>>>22 ^ e<<30 ^ e<<19 ^ e<<10 ) )|0; + + // 21 + w5 = ( ( w6>>>7 ^ w6>>>18 ^ w6>>>3 ^ w6<<25 ^ w6<<14 ) + ( w3>>>17 ^ w3>>>19 ^ w3>>>10 ^ w3<<15 ^ w3<<13 ) + w5 + w14 )|0; + c = ( w5 + c + ( h>>>6 ^ h>>>11 ^ h>>>25 ^ h<<26 ^ h<<21 ^ h<<7 ) + ( b ^ h & (a^b) ) + 0x4a7484aa )|0; + g = ( g + c )|0; + c = ( c + ( (d & e) ^ ( f & (d ^ e) ) ) + ( d>>>2 ^ d>>>13 ^ d>>>22 ^ d<<30 ^ d<<19 ^ d<<10 ) )|0; + + // 22 + w6 = ( ( w7>>>7 ^ w7>>>18 ^ w7>>>3 ^ w7<<25 ^ w7<<14 ) + ( w4>>>17 ^ w4>>>19 ^ w4>>>10 ^ w4<<15 ^ w4<<13 ) + w6 + w15 )|0; + b = ( w6 + b + ( g>>>6 ^ g>>>11 ^ g>>>25 ^ g<<26 ^ g<<21 ^ g<<7 ) + ( a ^ g & (h^a) ) + 0x5cb0a9dc )|0; + f = ( f + b )|0; + b = ( b + ( (c & d) ^ ( e & (c ^ d) ) ) + ( c>>>2 ^ c>>>13 ^ c>>>22 ^ c<<30 ^ c<<19 ^ c<<10 ) )|0; + + // 23 + w7 = ( ( w8>>>7 ^ w8>>>18 ^ w8>>>3 ^ w8<<25 ^ w8<<14 ) + ( w5>>>17 ^ w5>>>19 ^ w5>>>10 ^ w5<<15 ^ w5<<13 ) + w7 + w0 )|0; + a = ( w7 + a + ( f>>>6 ^ f>>>11 ^ f>>>25 ^ f<<26 ^ f<<21 ^ f<<7 ) + ( h ^ f & (g^h) ) + 0x76f988da )|0; + e = ( e + a )|0; + a = ( a + ( (b & c) ^ ( d & (b ^ c) ) ) + ( b>>>2 ^ b>>>13 ^ b>>>22 ^ b<<30 ^ b<<19 ^ b<<10 ) )|0; + + // 24 + w8 = ( ( w9>>>7 ^ w9>>>18 ^ w9>>>3 ^ w9<<25 ^ w9<<14 ) + ( w6>>>17 ^ w6>>>19 ^ w6>>>10 ^ w6<<15 ^ w6<<13 ) + w8 + w1 )|0; + h = ( w8 + h + ( e>>>6 ^ e>>>11 ^ e>>>25 ^ e<<26 ^ e<<21 ^ e<<7 ) + ( g ^ e & (f^g) ) + 0x983e5152 )|0; + d = ( d + h )|0; + h = ( h + ( (a & b) ^ ( c & (a ^ b) ) ) + ( a>>>2 ^ a>>>13 ^ a>>>22 ^ a<<30 ^ a<<19 ^ a<<10 ) )|0; + + // 25 + w9 = ( ( w10>>>7 ^ w10>>>18 ^ w10>>>3 ^ w10<<25 ^ w10<<14 ) + ( w7>>>17 ^ w7>>>19 ^ w7>>>10 ^ w7<<15 ^ w7<<13 ) + w9 + w2 )|0; + g = ( w9 + g + ( d>>>6 ^ d>>>11 ^ d>>>25 ^ d<<26 ^ d<<21 ^ d<<7 ) + ( f ^ d & (e^f) ) + 0xa831c66d )|0; + c = ( c + g )|0; + g = ( g + ( (h & a) ^ ( b & (h ^ a) ) ) + ( h>>>2 ^ h>>>13 ^ h>>>22 ^ h<<30 ^ h<<19 ^ h<<10 ) )|0; + + // 26 + w10 = ( ( w11>>>7 ^ w11>>>18 ^ w11>>>3 ^ w11<<25 ^ w11<<14 ) + ( w8>>>17 ^ w8>>>19 ^ w8>>>10 ^ w8<<15 ^ w8<<13 ) + w10 + w3 )|0; + f = ( w10 + f + ( c>>>6 ^ c>>>11 ^ c>>>25 ^ c<<26 ^ c<<21 ^ c<<7 ) + ( e ^ c & (d^e) ) + 0xb00327c8 )|0; + b = ( b + f )|0; + f = ( f + ( (g & h) ^ ( a & (g ^ h) ) ) + ( g>>>2 ^ g>>>13 ^ g>>>22 ^ g<<30 ^ g<<19 ^ g<<10 ) )|0; + + // 27 + w11 = ( ( w12>>>7 ^ w12>>>18 ^ w12>>>3 ^ w12<<25 ^ w12<<14 ) + ( w9>>>17 ^ w9>>>19 ^ w9>>>10 ^ w9<<15 ^ w9<<13 ) + w11 + w4 )|0; + e = ( w11 + e + ( b>>>6 ^ b>>>11 ^ b>>>25 ^ b<<26 ^ b<<21 ^ b<<7 ) + ( d ^ b & (c^d) ) + 0xbf597fc7 )|0; + a = ( a + e )|0; + e = ( e + ( (f & g) ^ ( h & (f ^ g) ) ) + ( f>>>2 ^ f>>>13 ^ f>>>22 ^ f<<30 ^ f<<19 ^ f<<10 ) )|0; + + // 28 + w12 = ( ( w13>>>7 ^ w13>>>18 ^ w13>>>3 ^ w13<<25 ^ w13<<14 ) + ( w10>>>17 ^ w10>>>19 ^ w10>>>10 ^ w10<<15 ^ w10<<13 ) + w12 + w5 )|0; + d = ( w12 + d + ( a>>>6 ^ a>>>11 ^ a>>>25 ^ a<<26 ^ a<<21 ^ a<<7 ) + ( c ^ a & (b^c) ) + 0xc6e00bf3 )|0; + h = ( h + d )|0; + d = ( d + ( (e & f) ^ ( g & (e ^ f) ) ) + ( e>>>2 ^ e>>>13 ^ e>>>22 ^ e<<30 ^ e<<19 ^ e<<10 ) )|0; + + // 29 + w13 = ( ( w14>>>7 ^ w14>>>18 ^ w14>>>3 ^ w14<<25 ^ w14<<14 ) + ( w11>>>17 ^ w11>>>19 ^ w11>>>10 ^ w11<<15 ^ w11<<13 ) + w13 + w6 )|0; + c = ( w13 + c + ( h>>>6 ^ h>>>11 ^ h>>>25 ^ h<<26 ^ h<<21 ^ h<<7 ) + ( b ^ h & (a^b) ) + 0xd5a79147 )|0; + g = ( g + c )|0; + c = ( c + ( (d & e) ^ ( f & (d ^ e) ) ) + ( d>>>2 ^ d>>>13 ^ d>>>22 ^ d<<30 ^ d<<19 ^ d<<10 ) )|0; + + // 30 + w14 = ( ( w15>>>7 ^ w15>>>18 ^ w15>>>3 ^ w15<<25 ^ w15<<14 ) + ( w12>>>17 ^ w12>>>19 ^ w12>>>10 ^ w12<<15 ^ w12<<13 ) + w14 + w7 )|0; + b = ( w14 + b + ( g>>>6 ^ g>>>11 ^ g>>>25 ^ g<<26 ^ g<<21 ^ g<<7 ) + ( a ^ g & (h^a) ) + 0x06ca6351 )|0; + f = ( f + b )|0; + b = ( b + ( (c & d) ^ ( e & (c ^ d) ) ) + ( c>>>2 ^ c>>>13 ^ c>>>22 ^ c<<30 ^ c<<19 ^ c<<10 ) )|0; + + // 31 + w15 = ( ( w0>>>7 ^ w0>>>18 ^ w0>>>3 ^ w0<<25 ^ w0<<14 ) + ( w13>>>17 ^ w13>>>19 ^ w13>>>10 ^ w13<<15 ^ w13<<13 ) + w15 + w8 )|0; + a = ( w15 + a + ( f>>>6 ^ f>>>11 ^ f>>>25 ^ f<<26 ^ f<<21 ^ f<<7 ) + ( h ^ f & (g^h) ) + 0x14292967 )|0; + e = ( e + a )|0; + a = ( a + ( (b & c) ^ ( d & (b ^ c) ) ) + ( b>>>2 ^ b>>>13 ^ b>>>22 ^ b<<30 ^ b<<19 ^ b<<10 ) )|0; + + // 32 + w0 = ( ( w1>>>7 ^ w1>>>18 ^ w1>>>3 ^ w1<<25 ^ w1<<14 ) + ( w14>>>17 ^ w14>>>19 ^ w14>>>10 ^ w14<<15 ^ w14<<13 ) + w0 + w9 )|0; + h = ( w0 + h + ( e>>>6 ^ e>>>11 ^ e>>>25 ^ e<<26 ^ e<<21 ^ e<<7 ) + ( g ^ e & (f^g) ) + 0x27b70a85 )|0; + d = ( d + h )|0; + h = ( h + ( (a & b) ^ ( c & (a ^ b) ) ) + ( a>>>2 ^ a>>>13 ^ a>>>22 ^ a<<30 ^ a<<19 ^ a<<10 ) )|0; + + // 33 + w1 = ( ( w2>>>7 ^ w2>>>18 ^ w2>>>3 ^ w2<<25 ^ w2<<14 ) + ( w15>>>17 ^ w15>>>19 ^ w15>>>10 ^ w15<<15 ^ w15<<13 ) + w1 + w10 )|0; + g = ( w1 + g + ( d>>>6 ^ d>>>11 ^ d>>>25 ^ d<<26 ^ d<<21 ^ d<<7 ) + ( f ^ d & (e^f) ) + 0x2e1b2138 )|0; + c = ( c + g )|0; + g = ( g + ( (h & a) ^ ( b & (h ^ a) ) ) + ( h>>>2 ^ h>>>13 ^ h>>>22 ^ h<<30 ^ h<<19 ^ h<<10 ) )|0; + + // 34 + w2 = ( ( w3>>>7 ^ w3>>>18 ^ w3>>>3 ^ w3<<25 ^ w3<<14 ) + ( w0>>>17 ^ w0>>>19 ^ w0>>>10 ^ w0<<15 ^ w0<<13 ) + w2 + w11 )|0; + f = ( w2 + f + ( c>>>6 ^ c>>>11 ^ c>>>25 ^ c<<26 ^ c<<21 ^ c<<7 ) + ( e ^ c & (d^e) ) + 0x4d2c6dfc )|0; + b = ( b + f )|0; + f = ( f + ( (g & h) ^ ( a & (g ^ h) ) ) + ( g>>>2 ^ g>>>13 ^ g>>>22 ^ g<<30 ^ g<<19 ^ g<<10 ) )|0; + + // 35 + w3 = ( ( w4>>>7 ^ w4>>>18 ^ w4>>>3 ^ w4<<25 ^ w4<<14 ) + ( w1>>>17 ^ w1>>>19 ^ w1>>>10 ^ w1<<15 ^ w1<<13 ) + w3 + w12 )|0; + e = ( w3 + e + ( b>>>6 ^ b>>>11 ^ b>>>25 ^ b<<26 ^ b<<21 ^ b<<7 ) + ( d ^ b & (c^d) ) + 0x53380d13 )|0; + a = ( a + e )|0; + e = ( e + ( (f & g) ^ ( h & (f ^ g) ) ) + ( f>>>2 ^ f>>>13 ^ f>>>22 ^ f<<30 ^ f<<19 ^ f<<10 ) )|0; + + // 36 + w4 = ( ( w5>>>7 ^ w5>>>18 ^ w5>>>3 ^ w5<<25 ^ w5<<14 ) + ( w2>>>17 ^ w2>>>19 ^ w2>>>10 ^ w2<<15 ^ w2<<13 ) + w4 + w13 )|0; + d = ( w4 + d + ( a>>>6 ^ a>>>11 ^ a>>>25 ^ a<<26 ^ a<<21 ^ a<<7 ) + ( c ^ a & (b^c) ) + 0x650a7354 )|0; + h = ( h + d )|0; + d = ( d + ( (e & f) ^ ( g & (e ^ f) ) ) + ( e>>>2 ^ e>>>13 ^ e>>>22 ^ e<<30 ^ e<<19 ^ e<<10 ) )|0; + + // 37 + w5 = ( ( w6>>>7 ^ w6>>>18 ^ w6>>>3 ^ w6<<25 ^ w6<<14 ) + ( w3>>>17 ^ w3>>>19 ^ w3>>>10 ^ w3<<15 ^ w3<<13 ) + w5 + w14 )|0; + c = ( w5 + c + ( h>>>6 ^ h>>>11 ^ h>>>25 ^ h<<26 ^ h<<21 ^ h<<7 ) + ( b ^ h & (a^b) ) + 0x766a0abb )|0; + g = ( g + c )|0; + c = ( c + ( (d & e) ^ ( f & (d ^ e) ) ) + ( d>>>2 ^ d>>>13 ^ d>>>22 ^ d<<30 ^ d<<19 ^ d<<10 ) )|0; + + // 38 + w6 = ( ( w7>>>7 ^ w7>>>18 ^ w7>>>3 ^ w7<<25 ^ w7<<14 ) + ( w4>>>17 ^ w4>>>19 ^ w4>>>10 ^ w4<<15 ^ w4<<13 ) + w6 + w15 )|0; + b = ( w6 + b + ( g>>>6 ^ g>>>11 ^ g>>>25 ^ g<<26 ^ g<<21 ^ g<<7 ) + ( a ^ g & (h^a) ) + 0x81c2c92e )|0; + f = ( f + b )|0; + b = ( b + ( (c & d) ^ ( e & (c ^ d) ) ) + ( c>>>2 ^ c>>>13 ^ c>>>22 ^ c<<30 ^ c<<19 ^ c<<10 ) )|0; + + // 39 + w7 = ( ( w8>>>7 ^ w8>>>18 ^ w8>>>3 ^ w8<<25 ^ w8<<14 ) + ( w5>>>17 ^ w5>>>19 ^ w5>>>10 ^ w5<<15 ^ w5<<13 ) + w7 + w0 )|0; + a = ( w7 + a + ( f>>>6 ^ f>>>11 ^ f>>>25 ^ f<<26 ^ f<<21 ^ f<<7 ) + ( h ^ f & (g^h) ) + 0x92722c85 )|0; + e = ( e + a )|0; + a = ( a + ( (b & c) ^ ( d & (b ^ c) ) ) + ( b>>>2 ^ b>>>13 ^ b>>>22 ^ b<<30 ^ b<<19 ^ b<<10 ) )|0; + + // 40 + w8 = ( ( w9>>>7 ^ w9>>>18 ^ w9>>>3 ^ w9<<25 ^ w9<<14 ) + ( w6>>>17 ^ w6>>>19 ^ w6>>>10 ^ w6<<15 ^ w6<<13 ) + w8 + w1 )|0; + h = ( w8 + h + ( e>>>6 ^ e>>>11 ^ e>>>25 ^ e<<26 ^ e<<21 ^ e<<7 ) + ( g ^ e & (f^g) ) + 0xa2bfe8a1 )|0; + d = ( d + h )|0; + h = ( h + ( (a & b) ^ ( c & (a ^ b) ) ) + ( a>>>2 ^ a>>>13 ^ a>>>22 ^ a<<30 ^ a<<19 ^ a<<10 ) )|0; + + // 41 + w9 = ( ( w10>>>7 ^ w10>>>18 ^ w10>>>3 ^ w10<<25 ^ w10<<14 ) + ( w7>>>17 ^ w7>>>19 ^ w7>>>10 ^ w7<<15 ^ w7<<13 ) + w9 + w2 )|0; + g = ( w9 + g + ( d>>>6 ^ d>>>11 ^ d>>>25 ^ d<<26 ^ d<<21 ^ d<<7 ) + ( f ^ d & (e^f) ) + 0xa81a664b )|0; + c = ( c + g )|0; + g = ( g + ( (h & a) ^ ( b & (h ^ a) ) ) + ( h>>>2 ^ h>>>13 ^ h>>>22 ^ h<<30 ^ h<<19 ^ h<<10 ) )|0; + + // 42 + w10 = ( ( w11>>>7 ^ w11>>>18 ^ w11>>>3 ^ w11<<25 ^ w11<<14 ) + ( w8>>>17 ^ w8>>>19 ^ w8>>>10 ^ w8<<15 ^ w8<<13 ) + w10 + w3 )|0; + f = ( w10 + f + ( c>>>6 ^ c>>>11 ^ c>>>25 ^ c<<26 ^ c<<21 ^ c<<7 ) + ( e ^ c & (d^e) ) + 0xc24b8b70 )|0; + b = ( b + f )|0; + f = ( f + ( (g & h) ^ ( a & (g ^ h) ) ) + ( g>>>2 ^ g>>>13 ^ g>>>22 ^ g<<30 ^ g<<19 ^ g<<10 ) )|0; + + // 43 + w11 = ( ( w12>>>7 ^ w12>>>18 ^ w12>>>3 ^ w12<<25 ^ w12<<14 ) + ( w9>>>17 ^ w9>>>19 ^ w9>>>10 ^ w9<<15 ^ w9<<13 ) + w11 + w4 )|0; + e = ( w11 + e + ( b>>>6 ^ b>>>11 ^ b>>>25 ^ b<<26 ^ b<<21 ^ b<<7 ) + ( d ^ b & (c^d) ) + 0xc76c51a3 )|0; + a = ( a + e )|0; + e = ( e + ( (f & g) ^ ( h & (f ^ g) ) ) + ( f>>>2 ^ f>>>13 ^ f>>>22 ^ f<<30 ^ f<<19 ^ f<<10 ) )|0; + + // 44 + w12 = ( ( w13>>>7 ^ w13>>>18 ^ w13>>>3 ^ w13<<25 ^ w13<<14 ) + ( w10>>>17 ^ w10>>>19 ^ w10>>>10 ^ w10<<15 ^ w10<<13 ) + w12 + w5 )|0; + d = ( w12 + d + ( a>>>6 ^ a>>>11 ^ a>>>25 ^ a<<26 ^ a<<21 ^ a<<7 ) + ( c ^ a & (b^c) ) + 0xd192e819 )|0; + h = ( h + d )|0; + d = ( d + ( (e & f) ^ ( g & (e ^ f) ) ) + ( e>>>2 ^ e>>>13 ^ e>>>22 ^ e<<30 ^ e<<19 ^ e<<10 ) )|0; + + // 45 + w13 = ( ( w14>>>7 ^ w14>>>18 ^ w14>>>3 ^ w14<<25 ^ w14<<14 ) + ( w11>>>17 ^ w11>>>19 ^ w11>>>10 ^ w11<<15 ^ w11<<13 ) + w13 + w6 )|0; + c = ( w13 + c + ( h>>>6 ^ h>>>11 ^ h>>>25 ^ h<<26 ^ h<<21 ^ h<<7 ) + ( b ^ h & (a^b) ) + 0xd6990624 )|0; + g = ( g + c )|0; + c = ( c + ( (d & e) ^ ( f & (d ^ e) ) ) + ( d>>>2 ^ d>>>13 ^ d>>>22 ^ d<<30 ^ d<<19 ^ d<<10 ) )|0; + + // 46 + w14 = ( ( w15>>>7 ^ w15>>>18 ^ w15>>>3 ^ w15<<25 ^ w15<<14 ) + ( w12>>>17 ^ w12>>>19 ^ w12>>>10 ^ w12<<15 ^ w12<<13 ) + w14 + w7 )|0; + b = ( w14 + b + ( g>>>6 ^ g>>>11 ^ g>>>25 ^ g<<26 ^ g<<21 ^ g<<7 ) + ( a ^ g & (h^a) ) + 0xf40e3585 )|0; + f = ( f + b )|0; + b = ( b + ( (c & d) ^ ( e & (c ^ d) ) ) + ( c>>>2 ^ c>>>13 ^ c>>>22 ^ c<<30 ^ c<<19 ^ c<<10 ) )|0; + + // 47 + w15 = ( ( w0>>>7 ^ w0>>>18 ^ w0>>>3 ^ w0<<25 ^ w0<<14 ) + ( w13>>>17 ^ w13>>>19 ^ w13>>>10 ^ w13<<15 ^ w13<<13 ) + w15 + w8 )|0; + a = ( w15 + a + ( f>>>6 ^ f>>>11 ^ f>>>25 ^ f<<26 ^ f<<21 ^ f<<7 ) + ( h ^ f & (g^h) ) + 0x106aa070 )|0; + e = ( e + a )|0; + a = ( a + ( (b & c) ^ ( d & (b ^ c) ) ) + ( b>>>2 ^ b>>>13 ^ b>>>22 ^ b<<30 ^ b<<19 ^ b<<10 ) )|0; + + // 48 + w0 = ( ( w1>>>7 ^ w1>>>18 ^ w1>>>3 ^ w1<<25 ^ w1<<14 ) + ( w14>>>17 ^ w14>>>19 ^ w14>>>10 ^ w14<<15 ^ w14<<13 ) + w0 + w9 )|0; + h = ( w0 + h + ( e>>>6 ^ e>>>11 ^ e>>>25 ^ e<<26 ^ e<<21 ^ e<<7 ) + ( g ^ e & (f^g) ) + 0x19a4c116 )|0; + d = ( d + h )|0; + h = ( h + ( (a & b) ^ ( c & (a ^ b) ) ) + ( a>>>2 ^ a>>>13 ^ a>>>22 ^ a<<30 ^ a<<19 ^ a<<10 ) )|0; + + // 49 + w1 = ( ( w2>>>7 ^ w2>>>18 ^ w2>>>3 ^ w2<<25 ^ w2<<14 ) + ( w15>>>17 ^ w15>>>19 ^ w15>>>10 ^ w15<<15 ^ w15<<13 ) + w1 + w10 )|0; + g = ( w1 + g + ( d>>>6 ^ d>>>11 ^ d>>>25 ^ d<<26 ^ d<<21 ^ d<<7 ) + ( f ^ d & (e^f) ) + 0x1e376c08 )|0; + c = ( c + g )|0; + g = ( g + ( (h & a) ^ ( b & (h ^ a) ) ) + ( h>>>2 ^ h>>>13 ^ h>>>22 ^ h<<30 ^ h<<19 ^ h<<10 ) )|0; + + // 50 + w2 = ( ( w3>>>7 ^ w3>>>18 ^ w3>>>3 ^ w3<<25 ^ w3<<14 ) + ( w0>>>17 ^ w0>>>19 ^ w0>>>10 ^ w0<<15 ^ w0<<13 ) + w2 + w11 )|0; + f = ( w2 + f + ( c>>>6 ^ c>>>11 ^ c>>>25 ^ c<<26 ^ c<<21 ^ c<<7 ) + ( e ^ c & (d^e) ) + 0x2748774c )|0; + b = ( b + f )|0; + f = ( f + ( (g & h) ^ ( a & (g ^ h) ) ) + ( g>>>2 ^ g>>>13 ^ g>>>22 ^ g<<30 ^ g<<19 ^ g<<10 ) )|0; + + // 51 + w3 = ( ( w4>>>7 ^ w4>>>18 ^ w4>>>3 ^ w4<<25 ^ w4<<14 ) + ( w1>>>17 ^ w1>>>19 ^ w1>>>10 ^ w1<<15 ^ w1<<13 ) + w3 + w12 )|0; + e = ( w3 + e + ( b>>>6 ^ b>>>11 ^ b>>>25 ^ b<<26 ^ b<<21 ^ b<<7 ) + ( d ^ b & (c^d) ) + 0x34b0bcb5 )|0; + a = ( a + e )|0; + e = ( e + ( (f & g) ^ ( h & (f ^ g) ) ) + ( f>>>2 ^ f>>>13 ^ f>>>22 ^ f<<30 ^ f<<19 ^ f<<10 ) )|0; + + // 52 + w4 = ( ( w5>>>7 ^ w5>>>18 ^ w5>>>3 ^ w5<<25 ^ w5<<14 ) + ( w2>>>17 ^ w2>>>19 ^ w2>>>10 ^ w2<<15 ^ w2<<13 ) + w4 + w13 )|0; + d = ( w4 + d + ( a>>>6 ^ a>>>11 ^ a>>>25 ^ a<<26 ^ a<<21 ^ a<<7 ) + ( c ^ a & (b^c) ) + 0x391c0cb3 )|0; + h = ( h + d )|0; + d = ( d + ( (e & f) ^ ( g & (e ^ f) ) ) + ( e>>>2 ^ e>>>13 ^ e>>>22 ^ e<<30 ^ e<<19 ^ e<<10 ) )|0; + + // 53 + w5 = ( ( w6>>>7 ^ w6>>>18 ^ w6>>>3 ^ w6<<25 ^ w6<<14 ) + ( w3>>>17 ^ w3>>>19 ^ w3>>>10 ^ w3<<15 ^ w3<<13 ) + w5 + w14 )|0; + c = ( w5 + c + ( h>>>6 ^ h>>>11 ^ h>>>25 ^ h<<26 ^ h<<21 ^ h<<7 ) + ( b ^ h & (a^b) ) + 0x4ed8aa4a )|0; + g = ( g + c )|0; + c = ( c + ( (d & e) ^ ( f & (d ^ e) ) ) + ( d>>>2 ^ d>>>13 ^ d>>>22 ^ d<<30 ^ d<<19 ^ d<<10 ) )|0; + + // 54 + w6 = ( ( w7>>>7 ^ w7>>>18 ^ w7>>>3 ^ w7<<25 ^ w7<<14 ) + ( w4>>>17 ^ w4>>>19 ^ w4>>>10 ^ w4<<15 ^ w4<<13 ) + w6 + w15 )|0; + b = ( w6 + b + ( g>>>6 ^ g>>>11 ^ g>>>25 ^ g<<26 ^ g<<21 ^ g<<7 ) + ( a ^ g & (h^a) ) + 0x5b9cca4f )|0; + f = ( f + b )|0; + b = ( b + ( (c & d) ^ ( e & (c ^ d) ) ) + ( c>>>2 ^ c>>>13 ^ c>>>22 ^ c<<30 ^ c<<19 ^ c<<10 ) )|0; + + // 55 + w7 = ( ( w8>>>7 ^ w8>>>18 ^ w8>>>3 ^ w8<<25 ^ w8<<14 ) + ( w5>>>17 ^ w5>>>19 ^ w5>>>10 ^ w5<<15 ^ w5<<13 ) + w7 + w0 )|0; + a = ( w7 + a + ( f>>>6 ^ f>>>11 ^ f>>>25 ^ f<<26 ^ f<<21 ^ f<<7 ) + ( h ^ f & (g^h) ) + 0x682e6ff3 )|0; + e = ( e + a )|0; + a = ( a + ( (b & c) ^ ( d & (b ^ c) ) ) + ( b>>>2 ^ b>>>13 ^ b>>>22 ^ b<<30 ^ b<<19 ^ b<<10 ) )|0; + + // 56 + w8 = ( ( w9>>>7 ^ w9>>>18 ^ w9>>>3 ^ w9<<25 ^ w9<<14 ) + ( w6>>>17 ^ w6>>>19 ^ w6>>>10 ^ w6<<15 ^ w6<<13 ) + w8 + w1 )|0; + h = ( w8 + h + ( e>>>6 ^ e>>>11 ^ e>>>25 ^ e<<26 ^ e<<21 ^ e<<7 ) + ( g ^ e & (f^g) ) + 0x748f82ee )|0; + d = ( d + h )|0; + h = ( h + ( (a & b) ^ ( c & (a ^ b) ) ) + ( a>>>2 ^ a>>>13 ^ a>>>22 ^ a<<30 ^ a<<19 ^ a<<10 ) )|0; + + // 57 + w9 = ( ( w10>>>7 ^ w10>>>18 ^ w10>>>3 ^ w10<<25 ^ w10<<14 ) + ( w7>>>17 ^ w7>>>19 ^ w7>>>10 ^ w7<<15 ^ w7<<13 ) + w9 + w2 )|0; + g = ( w9 + g + ( d>>>6 ^ d>>>11 ^ d>>>25 ^ d<<26 ^ d<<21 ^ d<<7 ) + ( f ^ d & (e^f) ) + 0x78a5636f )|0; + c = ( c + g )|0; + g = ( g + ( (h & a) ^ ( b & (h ^ a) ) ) + ( h>>>2 ^ h>>>13 ^ h>>>22 ^ h<<30 ^ h<<19 ^ h<<10 ) )|0; + + // 58 + w10 = ( ( w11>>>7 ^ w11>>>18 ^ w11>>>3 ^ w11<<25 ^ w11<<14 ) + ( w8>>>17 ^ w8>>>19 ^ w8>>>10 ^ w8<<15 ^ w8<<13 ) + w10 + w3 )|0; + f = ( w10 + f + ( c>>>6 ^ c>>>11 ^ c>>>25 ^ c<<26 ^ c<<21 ^ c<<7 ) + ( e ^ c & (d^e) ) + 0x84c87814 )|0; + b = ( b + f )|0; + f = ( f + ( (g & h) ^ ( a & (g ^ h) ) ) + ( g>>>2 ^ g>>>13 ^ g>>>22 ^ g<<30 ^ g<<19 ^ g<<10 ) )|0; + + // 59 + w11 = ( ( w12>>>7 ^ w12>>>18 ^ w12>>>3 ^ w12<<25 ^ w12<<14 ) + ( w9>>>17 ^ w9>>>19 ^ w9>>>10 ^ w9<<15 ^ w9<<13 ) + w11 + w4 )|0; + e = ( w11 + e + ( b>>>6 ^ b>>>11 ^ b>>>25 ^ b<<26 ^ b<<21 ^ b<<7 ) + ( d ^ b & (c^d) ) + 0x8cc70208 )|0; + a = ( a + e )|0; + e = ( e + ( (f & g) ^ ( h & (f ^ g) ) ) + ( f>>>2 ^ f>>>13 ^ f>>>22 ^ f<<30 ^ f<<19 ^ f<<10 ) )|0; + + // 60 + w12 = ( ( w13>>>7 ^ w13>>>18 ^ w13>>>3 ^ w13<<25 ^ w13<<14 ) + ( w10>>>17 ^ w10>>>19 ^ w10>>>10 ^ w10<<15 ^ w10<<13 ) + w12 + w5 )|0; + d = ( w12 + d + ( a>>>6 ^ a>>>11 ^ a>>>25 ^ a<<26 ^ a<<21 ^ a<<7 ) + ( c ^ a & (b^c) ) + 0x90befffa )|0; + h = ( h + d )|0; + d = ( d + ( (e & f) ^ ( g & (e ^ f) ) ) + ( e>>>2 ^ e>>>13 ^ e>>>22 ^ e<<30 ^ e<<19 ^ e<<10 ) )|0; + + // 61 + w13 = ( ( w14>>>7 ^ w14>>>18 ^ w14>>>3 ^ w14<<25 ^ w14<<14 ) + ( w11>>>17 ^ w11>>>19 ^ w11>>>10 ^ w11<<15 ^ w11<<13 ) + w13 + w6 )|0; + c = ( w13 + c + ( h>>>6 ^ h>>>11 ^ h>>>25 ^ h<<26 ^ h<<21 ^ h<<7 ) + ( b ^ h & (a^b) ) + 0xa4506ceb )|0; + g = ( g + c )|0; + c = ( c + ( (d & e) ^ ( f & (d ^ e) ) ) + ( d>>>2 ^ d>>>13 ^ d>>>22 ^ d<<30 ^ d<<19 ^ d<<10 ) )|0; + + // 62 + w14 = ( ( w15>>>7 ^ w15>>>18 ^ w15>>>3 ^ w15<<25 ^ w15<<14 ) + ( w12>>>17 ^ w12>>>19 ^ w12>>>10 ^ w12<<15 ^ w12<<13 ) + w14 + w7 )|0; + b = ( w14 + b + ( g>>>6 ^ g>>>11 ^ g>>>25 ^ g<<26 ^ g<<21 ^ g<<7 ) + ( a ^ g & (h^a) ) + 0xbef9a3f7 )|0; + f = ( f + b )|0; + b = ( b + ( (c & d) ^ ( e & (c ^ d) ) ) + ( c>>>2 ^ c>>>13 ^ c>>>22 ^ c<<30 ^ c<<19 ^ c<<10 ) )|0; + + // 63 + w15 = ( ( w0>>>7 ^ w0>>>18 ^ w0>>>3 ^ w0<<25 ^ w0<<14 ) + ( w13>>>17 ^ w13>>>19 ^ w13>>>10 ^ w13<<15 ^ w13<<13 ) + w15 + w8 )|0; + a = ( w15 + a + ( f>>>6 ^ f>>>11 ^ f>>>25 ^ f<<26 ^ f<<21 ^ f<<7 ) + ( h ^ f & (g^h) ) + 0xc67178f2 )|0; + e = ( e + a )|0; + a = ( a + ( (b & c) ^ ( d & (b ^ c) ) ) + ( b>>>2 ^ b>>>13 ^ b>>>22 ^ b<<30 ^ b<<19 ^ b<<10 ) )|0; + + H0 = ( H0 + a )|0; + H1 = ( H1 + b )|0; + H2 = ( H2 + c )|0; + H3 = ( H3 + d )|0; + H4 = ( H4 + e )|0; + H5 = ( H5 + f )|0; + H6 = ( H6 + g )|0; + H7 = ( H7 + h )|0; + } + + function _core_heap ( offset ) { + offset = offset|0; + + _core( + HEAP[offset|0]<<24 | HEAP[offset|1]<<16 | HEAP[offset|2]<<8 | HEAP[offset|3], + HEAP[offset|4]<<24 | HEAP[offset|5]<<16 | HEAP[offset|6]<<8 | HEAP[offset|7], + HEAP[offset|8]<<24 | HEAP[offset|9]<<16 | HEAP[offset|10]<<8 | HEAP[offset|11], + HEAP[offset|12]<<24 | HEAP[offset|13]<<16 | HEAP[offset|14]<<8 | HEAP[offset|15], + HEAP[offset|16]<<24 | HEAP[offset|17]<<16 | HEAP[offset|18]<<8 | HEAP[offset|19], + HEAP[offset|20]<<24 | HEAP[offset|21]<<16 | HEAP[offset|22]<<8 | HEAP[offset|23], + HEAP[offset|24]<<24 | HEAP[offset|25]<<16 | HEAP[offset|26]<<8 | HEAP[offset|27], + HEAP[offset|28]<<24 | HEAP[offset|29]<<16 | HEAP[offset|30]<<8 | HEAP[offset|31], + HEAP[offset|32]<<24 | HEAP[offset|33]<<16 | HEAP[offset|34]<<8 | HEAP[offset|35], + HEAP[offset|36]<<24 | HEAP[offset|37]<<16 | HEAP[offset|38]<<8 | HEAP[offset|39], + HEAP[offset|40]<<24 | HEAP[offset|41]<<16 | HEAP[offset|42]<<8 | HEAP[offset|43], + HEAP[offset|44]<<24 | HEAP[offset|45]<<16 | HEAP[offset|46]<<8 | HEAP[offset|47], + HEAP[offset|48]<<24 | HEAP[offset|49]<<16 | HEAP[offset|50]<<8 | HEAP[offset|51], + HEAP[offset|52]<<24 | HEAP[offset|53]<<16 | HEAP[offset|54]<<8 | HEAP[offset|55], + HEAP[offset|56]<<24 | HEAP[offset|57]<<16 | HEAP[offset|58]<<8 | HEAP[offset|59], + HEAP[offset|60]<<24 | HEAP[offset|61]<<16 | HEAP[offset|62]<<8 | HEAP[offset|63] + ); + } + + // offset — multiple of 32 + function _state_to_heap ( output ) { + output = output|0; + + HEAP[output|0] = H0>>>24; + HEAP[output|1] = H0>>>16&255; + HEAP[output|2] = H0>>>8&255; + HEAP[output|3] = H0&255; + HEAP[output|4] = H1>>>24; + HEAP[output|5] = H1>>>16&255; + HEAP[output|6] = H1>>>8&255; + HEAP[output|7] = H1&255; + HEAP[output|8] = H2>>>24; + HEAP[output|9] = H2>>>16&255; + HEAP[output|10] = H2>>>8&255; + HEAP[output|11] = H2&255; + HEAP[output|12] = H3>>>24; + HEAP[output|13] = H3>>>16&255; + HEAP[output|14] = H3>>>8&255; + HEAP[output|15] = H3&255; + HEAP[output|16] = H4>>>24; + HEAP[output|17] = H4>>>16&255; + HEAP[output|18] = H4>>>8&255; + HEAP[output|19] = H4&255; + HEAP[output|20] = H5>>>24; + HEAP[output|21] = H5>>>16&255; + HEAP[output|22] = H5>>>8&255; + HEAP[output|23] = H5&255; + HEAP[output|24] = H6>>>24; + HEAP[output|25] = H6>>>16&255; + HEAP[output|26] = H6>>>8&255; + HEAP[output|27] = H6&255; + HEAP[output|28] = H7>>>24; + HEAP[output|29] = H7>>>16&255; + HEAP[output|30] = H7>>>8&255; + HEAP[output|31] = H7&255; + } + + function reset () { + H0 = 0x6a09e667; + H1 = 0xbb67ae85; + H2 = 0x3c6ef372; + H3 = 0xa54ff53a; + H4 = 0x510e527f; + H5 = 0x9b05688c; + H6 = 0x1f83d9ab; + H7 = 0x5be0cd19; + TOTAL0 = TOTAL1 = 0; + } + + function init ( h0, h1, h2, h3, h4, h5, h6, h7, total0, total1 ) { + h0 = h0|0; + h1 = h1|0; + h2 = h2|0; + h3 = h3|0; + h4 = h4|0; + h5 = h5|0; + h6 = h6|0; + h7 = h7|0; + total0 = total0|0; + total1 = total1|0; + + H0 = h0; + H1 = h1; + H2 = h2; + H3 = h3; + H4 = h4; + H5 = h5; + H6 = h6; + H7 = h7; + TOTAL0 = total0; + TOTAL1 = total1; + } + + // offset — multiple of 64 + function process ( offset, length ) { + offset = offset|0; + length = length|0; + + var hashed = 0; + + if ( offset & 63 ) + return -1; + + while ( (length|0) >= 64 ) { + _core_heap(offset); + + offset = ( offset + 64 )|0; + length = ( length - 64 )|0; + + hashed = ( hashed + 64 )|0; + } + + TOTAL0 = ( TOTAL0 + hashed )|0; + if ( TOTAL0>>>0 < hashed>>>0 ) TOTAL1 = ( TOTAL1 + 1 )|0; + + return hashed|0; + } + + // offset — multiple of 64 + // output — multiple of 32 + function finish ( offset, length, output ) { + offset = offset|0; + length = length|0; + output = output|0; + + var hashed = 0, + i = 0; + + if ( offset & 63 ) + return -1; + + if ( ~output ) + if ( output & 31 ) + return -1; + + if ( (length|0) >= 64 ) { + hashed = process( offset, length )|0; + if ( (hashed|0) == -1 ) + return -1; + + offset = ( offset + hashed )|0; + length = ( length - hashed )|0; + } + + hashed = ( hashed + length )|0; + TOTAL0 = ( TOTAL0 + length )|0; + if ( TOTAL0>>>0 < length>>>0 ) TOTAL1 = ( TOTAL1 + 1 )|0; + + HEAP[offset|length] = 0x80; + + if ( (length|0) >= 56 ) { + for ( i = (length+1)|0; (i|0) < 64; i = (i+1)|0 ) + HEAP[offset|i] = 0x00; + + _core_heap(offset); + + length = 0; + + HEAP[offset|0] = 0; + } + + for ( i = (length+1)|0; (i|0) < 59; i = (i+1)|0 ) + HEAP[offset|i] = 0; + + HEAP[offset|56] = TOTAL1>>>21&255; + HEAP[offset|57] = TOTAL1>>>13&255; + HEAP[offset|58] = TOTAL1>>>5&255; + HEAP[offset|59] = TOTAL1<<3&255 | TOTAL0>>>29; + HEAP[offset|60] = TOTAL0>>>21&255; + HEAP[offset|61] = TOTAL0>>>13&255; + HEAP[offset|62] = TOTAL0>>>5&255; + HEAP[offset|63] = TOTAL0<<3&255; + _core_heap(offset); + + if ( ~output ) + _state_to_heap(output); + + return hashed|0; + } + + function hmac_reset () { + H0 = I0; + H1 = I1; + H2 = I2; + H3 = I3; + H4 = I4; + H5 = I5; + H6 = I6; + H7 = I7; + TOTAL0 = 64; + TOTAL1 = 0; + } + + function _hmac_opad () { + H0 = O0; + H1 = O1; + H2 = O2; + H3 = O3; + H4 = O4; + H5 = O5; + H6 = O6; + H7 = O7; + TOTAL0 = 64; + TOTAL1 = 0; + } + + function hmac_init ( p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15 ) { + p0 = p0|0; + p1 = p1|0; + p2 = p2|0; + p3 = p3|0; + p4 = p4|0; + p5 = p5|0; + p6 = p6|0; + p7 = p7|0; + p8 = p8|0; + p9 = p9|0; + p10 = p10|0; + p11 = p11|0; + p12 = p12|0; + p13 = p13|0; + p14 = p14|0; + p15 = p15|0; + + // opad + reset(); + _core( + p0 ^ 0x5c5c5c5c, + p1 ^ 0x5c5c5c5c, + p2 ^ 0x5c5c5c5c, + p3 ^ 0x5c5c5c5c, + p4 ^ 0x5c5c5c5c, + p5 ^ 0x5c5c5c5c, + p6 ^ 0x5c5c5c5c, + p7 ^ 0x5c5c5c5c, + p8 ^ 0x5c5c5c5c, + p9 ^ 0x5c5c5c5c, + p10 ^ 0x5c5c5c5c, + p11 ^ 0x5c5c5c5c, + p12 ^ 0x5c5c5c5c, + p13 ^ 0x5c5c5c5c, + p14 ^ 0x5c5c5c5c, + p15 ^ 0x5c5c5c5c + ); + O0 = H0; + O1 = H1; + O2 = H2; + O3 = H3; + O4 = H4; + O5 = H5; + O6 = H6; + O7 = H7; + + // ipad + reset(); + _core( + p0 ^ 0x36363636, + p1 ^ 0x36363636, + p2 ^ 0x36363636, + p3 ^ 0x36363636, + p4 ^ 0x36363636, + p5 ^ 0x36363636, + p6 ^ 0x36363636, + p7 ^ 0x36363636, + p8 ^ 0x36363636, + p9 ^ 0x36363636, + p10 ^ 0x36363636, + p11 ^ 0x36363636, + p12 ^ 0x36363636, + p13 ^ 0x36363636, + p14 ^ 0x36363636, + p15 ^ 0x36363636 + ); + I0 = H0; + I1 = H1; + I2 = H2; + I3 = H3; + I4 = H4; + I5 = H5; + I6 = H6; + I7 = H7; + + TOTAL0 = 64; + TOTAL1 = 0; + } + + // offset — multiple of 64 + // output — multiple of 32 + function hmac_finish ( offset, length, output ) { + offset = offset|0; + length = length|0; + output = output|0; + + var t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, t7 = 0, + hashed = 0; + + if ( offset & 63 ) + return -1; + + if ( ~output ) + if ( output & 31 ) + return -1; + + hashed = finish( offset, length, -1 )|0; + t0 = H0, t1 = H1, t2 = H2, t3 = H3, t4 = H4, t5 = H5, t6 = H6, t7 = H7; + + _hmac_opad(); + _core( t0, t1, t2, t3, t4, t5, t6, t7, 0x80000000, 0, 0, 0, 0, 0, 0, 768 ); + + if ( ~output ) + _state_to_heap(output); + + return hashed|0; + } + + // salt is assumed to be already processed + // offset — multiple of 64 + // output — multiple of 32 + function pbkdf2_generate_block ( offset, length, block, count, output ) { + offset = offset|0; + length = length|0; + block = block|0; + count = count|0; + output = output|0; + + var h0 = 0, h1 = 0, h2 = 0, h3 = 0, h4 = 0, h5 = 0, h6 = 0, h7 = 0, + t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, t7 = 0; + + if ( offset & 63 ) + return -1; + + if ( ~output ) + if ( output & 31 ) + return -1; + + // pad block number into heap + // FIXME probable OOB write + HEAP[(offset+length)|0] = block>>>24; + HEAP[(offset+length+1)|0] = block>>>16&255; + HEAP[(offset+length+2)|0] = block>>>8&255; + HEAP[(offset+length+3)|0] = block&255; + + // finish first iteration + hmac_finish( offset, (length+4)|0, -1 )|0; + h0 = t0 = H0, h1 = t1 = H1, h2 = t2 = H2, h3 = t3 = H3, h4 = t4 = H4, h5 = t5 = H5, h6 = t6 = H6, h7 = t7 = H7; + count = (count-1)|0; + + // perform the rest iterations + while ( (count|0) > 0 ) { + hmac_reset(); + _core( t0, t1, t2, t3, t4, t5, t6, t7, 0x80000000, 0, 0, 0, 0, 0, 0, 768 ); + t0 = H0, t1 = H1, t2 = H2, t3 = H3, t4 = H4, t5 = H5, t6 = H6, t7 = H7; + + _hmac_opad(); + _core( t0, t1, t2, t3, t4, t5, t6, t7, 0x80000000, 0, 0, 0, 0, 0, 0, 768 ); + t0 = H0, t1 = H1, t2 = H2, t3 = H3, t4 = H4, t5 = H5, t6 = H6, t7 = H7; + + h0 = h0 ^ H0; + h1 = h1 ^ H1; + h2 = h2 ^ H2; + h3 = h3 ^ H3; + h4 = h4 ^ H4; + h5 = h5 ^ H5; + h6 = h6 ^ H6; + h7 = h7 ^ H7; + + count = (count-1)|0; + } + + H0 = h0; + H1 = h1; + H2 = h2; + H3 = h3; + H4 = h4; + H5 = h5; + H6 = h6; + H7 = h7; + + if ( ~output ) + _state_to_heap(output); + + return 0; + } + + return { + // SHA256 + reset: reset, + init: init, + process: process, + finish: finish, + + // HMAC-SHA256 + hmac_reset: hmac_reset, + hmac_init: hmac_init, + hmac_finish: hmac_finish, + + // PBKDF2-HMAC-SHA256 + pbkdf2_generate_block: pbkdf2_generate_block + } + }; + + const _sha256_block_size = 64; + const _sha256_hash_size = 32; + const heap_pool = []; + const asm_pool = []; + class Sha256 extends Hash { + constructor() { + super(); + this.NAME = 'sha256'; + this.BLOCK_SIZE = _sha256_block_size; + this.HASH_SIZE = _sha256_hash_size; + this.acquire_asm(); + } + acquire_asm() { + if (this.heap === undefined || this.asm === undefined) { + this.heap = heap_pool.pop() || _heap_init(); + this.asm = asm_pool.pop() || sha256_asm({ Uint8Array: Uint8Array }, null, this.heap.buffer); + this.reset(); + } + return { heap: this.heap, asm: this.asm }; + } + release_asm() { + if (this.heap !== undefined && this.asm !== undefined) { + heap_pool.push(this.heap); + asm_pool.push(this.asm); + } + this.heap = undefined; + this.asm = undefined; + } + static bytes(data) { + return new Sha256().process(data).finish().result; + } + } + Sha256.NAME = 'sha256'; + + var minimalisticAssert = assert$a; + + function assert$a(val, msg) { + if (!val) + throw Error(msg || 'Assertion failed'); + } + + assert$a.equal = function assertEqual(l, r, msg) { + if (l != r) + throw Error(msg || ('Assertion failed: ' + l + ' != ' + r)); + }; + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + function commonjsRequire () { + throw Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs'); + } + + var inherits_browser = createCommonjsModule(function (module) { + if (typeof Object.create === 'function') { + // implementation from standard node.js 'util' module + module.exports = function inherits(ctor, superCtor) { + if (superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + } + }; + } else { + // old school shim for old browsers + module.exports = function inherits(ctor, superCtor) { + if (superCtor) { + ctor.super_ = superCtor; + var TempCtor = function () {}; + TempCtor.prototype = superCtor.prototype; + ctor.prototype = new TempCtor(); + ctor.prototype.constructor = ctor; + } + }; + } + }); + + var inherits_1 = inherits_browser; + + function isSurrogatePair(msg, i) { + if ((msg.charCodeAt(i) & 0xFC00) !== 0xD800) { + return false; + } + if (i < 0 || i + 1 >= msg.length) { + return false; + } + return (msg.charCodeAt(i + 1) & 0xFC00) === 0xDC00; + } + + function toArray$1(msg, enc) { + if (Array.isArray(msg)) + return msg.slice(); + if (!msg) + return []; + var res = []; + if (typeof msg === 'string') { + if (!enc) { + // Inspired by stringToUtf8ByteArray() in closure-library by Google + // https://github.com/google/closure-library/blob/8598d87242af59aac233270742c8984e2b2bdbe0/closure/goog/crypt/crypt.js#L117-L143 + // Apache License 2.0 + // https://github.com/google/closure-library/blob/master/LICENSE + var p = 0; + for (var i = 0; i < msg.length; i++) { + var c = msg.charCodeAt(i); + if (c < 128) { + res[p++] = c; + } else if (c < 2048) { + res[p++] = (c >> 6) | 192; + res[p++] = (c & 63) | 128; + } else if (isSurrogatePair(msg, i)) { + c = 0x10000 + ((c & 0x03FF) << 10) + (msg.charCodeAt(++i) & 0x03FF); + res[p++] = (c >> 18) | 240; + res[p++] = ((c >> 12) & 63) | 128; + res[p++] = ((c >> 6) & 63) | 128; + res[p++] = (c & 63) | 128; + } else { + res[p++] = (c >> 12) | 224; + res[p++] = ((c >> 6) & 63) | 128; + res[p++] = (c & 63) | 128; + } + } + } else if (enc === 'hex') { + msg = msg.replace(/[^a-z0-9]+/ig, ''); + if (msg.length % 2 !== 0) + msg = '0' + msg; + for (i = 0; i < msg.length; i += 2) + res.push(parseInt(msg[i] + msg[i + 1], 16)); + } + } else { + for (i = 0; i < msg.length; i++) + res[i] = msg[i] | 0; + } + return res; + } + var toArray_1 = toArray$1; + + function toHex(msg) { + var res = ''; + for (var i = 0; i < msg.length; i++) + res += zero2(msg[i].toString(16)); + return res; + } + var toHex_1 = toHex; + + function htonl(w) { + var res = (w >>> 24) | + ((w >>> 8) & 0xff00) | + ((w << 8) & 0xff0000) | + ((w & 0xff) << 24); + return res >>> 0; + } + var htonl_1 = htonl; + + function toHex32(msg, endian) { + var res = ''; + for (var i = 0; i < msg.length; i++) { + var w = msg[i]; + if (endian === 'little') + w = htonl(w); + res += zero8(w.toString(16)); + } + return res; + } + var toHex32_1 = toHex32; + + function zero2(word) { + if (word.length === 1) + return '0' + word; + else + return word; + } + var zero2_1 = zero2; + + function zero8(word) { + if (word.length === 7) + return '0' + word; + else if (word.length === 6) + return '00' + word; + else if (word.length === 5) + return '000' + word; + else if (word.length === 4) + return '0000' + word; + else if (word.length === 3) + return '00000' + word; + else if (word.length === 2) + return '000000' + word; + else if (word.length === 1) + return '0000000' + word; + else + return word; + } + var zero8_1 = zero8; + + function join32(msg, start, end, endian) { + var len = end - start; + minimalisticAssert(len % 4 === 0); + var res = new Array(len / 4); + for (var i = 0, k = start; i < res.length; i++, k += 4) { + var w; + if (endian === 'big') + w = (msg[k] << 24) | (msg[k + 1] << 16) | (msg[k + 2] << 8) | msg[k + 3]; + else + w = (msg[k + 3] << 24) | (msg[k + 2] << 16) | (msg[k + 1] << 8) | msg[k]; + res[i] = w >>> 0; + } + return res; + } + var join32_1 = join32; + + function split32(msg, endian) { + var res = new Array(msg.length * 4); + for (var i = 0, k = 0; i < msg.length; i++, k += 4) { + var m = msg[i]; + if (endian === 'big') { + res[k] = m >>> 24; + res[k + 1] = (m >>> 16) & 0xff; + res[k + 2] = (m >>> 8) & 0xff; + res[k + 3] = m & 0xff; + } else { + res[k + 3] = m >>> 24; + res[k + 2] = (m >>> 16) & 0xff; + res[k + 1] = (m >>> 8) & 0xff; + res[k] = m & 0xff; + } + } + return res; + } + var split32_1 = split32; + + function rotr32$1(w, b) { + return (w >>> b) | (w << (32 - b)); + } + var rotr32_1 = rotr32$1; + + function rotl32$2(w, b) { + return (w << b) | (w >>> (32 - b)); + } + var rotl32_1 = rotl32$2; + + function sum32$3(a, b) { + return (a + b) >>> 0; + } + var sum32_1 = sum32$3; + + function sum32_3$1(a, b, c) { + return (a + b + c) >>> 0; + } + var sum32_3_1 = sum32_3$1; + + function sum32_4$2(a, b, c, d) { + return (a + b + c + d) >>> 0; + } + var sum32_4_1 = sum32_4$2; + + function sum32_5$2(a, b, c, d, e) { + return (a + b + c + d + e) >>> 0; + } + var sum32_5_1 = sum32_5$2; + + function sum64$1(buf, pos, ah, al) { + var bh = buf[pos]; + var bl = buf[pos + 1]; + + var lo = (al + bl) >>> 0; + var hi = (lo < al ? 1 : 0) + ah + bh; + buf[pos] = hi >>> 0; + buf[pos + 1] = lo; + } + var sum64_1 = sum64$1; + + function sum64_hi$1(ah, al, bh, bl) { + var lo = (al + bl) >>> 0; + var hi = (lo < al ? 1 : 0) + ah + bh; + return hi >>> 0; + } + var sum64_hi_1 = sum64_hi$1; + + function sum64_lo$1(ah, al, bh, bl) { + var lo = al + bl; + return lo >>> 0; + } + var sum64_lo_1 = sum64_lo$1; + + function sum64_4_hi$1(ah, al, bh, bl, ch, cl, dh, dl) { + var carry = 0; + var lo = al; + lo = (lo + bl) >>> 0; + carry += lo < al ? 1 : 0; + lo = (lo + cl) >>> 0; + carry += lo < cl ? 1 : 0; + lo = (lo + dl) >>> 0; + carry += lo < dl ? 1 : 0; + + var hi = ah + bh + ch + dh + carry; + return hi >>> 0; + } + var sum64_4_hi_1 = sum64_4_hi$1; + + function sum64_4_lo$1(ah, al, bh, bl, ch, cl, dh, dl) { + var lo = al + bl + cl + dl; + return lo >>> 0; + } + var sum64_4_lo_1 = sum64_4_lo$1; + + function sum64_5_hi$1(ah, al, bh, bl, ch, cl, dh, dl, eh, el) { + var carry = 0; + var lo = al; + lo = (lo + bl) >>> 0; + carry += lo < al ? 1 : 0; + lo = (lo + cl) >>> 0; + carry += lo < cl ? 1 : 0; + lo = (lo + dl) >>> 0; + carry += lo < dl ? 1 : 0; + lo = (lo + el) >>> 0; + carry += lo < el ? 1 : 0; + + var hi = ah + bh + ch + dh + eh + carry; + return hi >>> 0; + } + var sum64_5_hi_1 = sum64_5_hi$1; + + function sum64_5_lo$1(ah, al, bh, bl, ch, cl, dh, dl, eh, el) { + var lo = al + bl + cl + dl + el; + + return lo >>> 0; + } + var sum64_5_lo_1 = sum64_5_lo$1; + + function rotr64_hi$1(ah, al, num) { + var r = (al << (32 - num)) | (ah >>> num); + return r >>> 0; + } + var rotr64_hi_1 = rotr64_hi$1; + + function rotr64_lo$1(ah, al, num) { + var r = (ah << (32 - num)) | (al >>> num); + return r >>> 0; + } + var rotr64_lo_1 = rotr64_lo$1; + + function shr64_hi$1(ah, al, num) { + return ah >>> num; + } + var shr64_hi_1 = shr64_hi$1; + + function shr64_lo$1(ah, al, num) { + var r = (ah << (32 - num)) | (al >>> num); + return r >>> 0; + } + var shr64_lo_1 = shr64_lo$1; + + var utils = { + inherits: inherits_1, + toArray: toArray_1, + toHex: toHex_1, + htonl: htonl_1, + toHex32: toHex32_1, + zero2: zero2_1, + zero8: zero8_1, + join32: join32_1, + split32: split32_1, + rotr32: rotr32_1, + rotl32: rotl32_1, + sum32: sum32_1, + sum32_3: sum32_3_1, + sum32_4: sum32_4_1, + sum32_5: sum32_5_1, + sum64: sum64_1, + sum64_hi: sum64_hi_1, + sum64_lo: sum64_lo_1, + sum64_4_hi: sum64_4_hi_1, + sum64_4_lo: sum64_4_lo_1, + sum64_5_hi: sum64_5_hi_1, + sum64_5_lo: sum64_5_lo_1, + rotr64_hi: rotr64_hi_1, + rotr64_lo: rotr64_lo_1, + shr64_hi: shr64_hi_1, + shr64_lo: shr64_lo_1 + }; + + function BlockHash$4() { + this.pending = null; + this.pendingTotal = 0; + this.blockSize = this.constructor.blockSize; + this.outSize = this.constructor.outSize; + this.hmacStrength = this.constructor.hmacStrength; + this.padLength = this.constructor.padLength / 8; + this.endian = 'big'; + + this._delta8 = this.blockSize / 8; + this._delta32 = this.blockSize / 32; + } + var BlockHash_1 = BlockHash$4; + + BlockHash$4.prototype.update = function update(msg, enc) { + // Convert message to array, pad it, and join into 32bit blocks + msg = utils.toArray(msg, enc); + if (!this.pending) + this.pending = msg; + else + this.pending = this.pending.concat(msg); + this.pendingTotal += msg.length; + + // Enough data, try updating + if (this.pending.length >= this._delta8) { + msg = this.pending; + + // Process pending data in blocks + var r = msg.length % this._delta8; + this.pending = msg.slice(msg.length - r, msg.length); + if (this.pending.length === 0) + this.pending = null; + + msg = utils.join32(msg, 0, msg.length - r, this.endian); + for (var i = 0; i < msg.length; i += this._delta32) + this._update(msg, i, i + this._delta32); + } + + return this; + }; + + BlockHash$4.prototype.digest = function digest(enc) { + this.update(this._pad()); + minimalisticAssert(this.pending === null); + + return this._digest(enc); + }; + + BlockHash$4.prototype._pad = function pad() { + var len = this.pendingTotal; + var bytes = this._delta8; + var k = bytes - ((len + this.padLength) % bytes); + var res = new Array(k + this.padLength); + res[0] = 0x80; + for (var i = 1; i < k; i++) + res[i] = 0; + + // Append length + len <<= 3; + if (this.endian === 'big') { + for (var t = 8; t < this.padLength; t++) + res[i++] = 0; + + res[i++] = 0; + res[i++] = 0; + res[i++] = 0; + res[i++] = 0; + res[i++] = (len >>> 24) & 0xff; + res[i++] = (len >>> 16) & 0xff; + res[i++] = (len >>> 8) & 0xff; + res[i++] = len & 0xff; + } else { + res[i++] = len & 0xff; + res[i++] = (len >>> 8) & 0xff; + res[i++] = (len >>> 16) & 0xff; + res[i++] = (len >>> 24) & 0xff; + res[i++] = 0; + res[i++] = 0; + res[i++] = 0; + res[i++] = 0; + + for (t = 8; t < this.padLength; t++) + res[i++] = 0; + } + + return res; + }; + + var common$1 = { + BlockHash: BlockHash_1 + }; + + var rotr32 = utils.rotr32; + + function ft_1$1(s, x, y, z) { + if (s === 0) + return ch32$1(x, y, z); + if (s === 1 || s === 3) + return p32(x, y, z); + if (s === 2) + return maj32$1(x, y, z); + } + var ft_1_1 = ft_1$1; + + function ch32$1(x, y, z) { + return (x & y) ^ ((~x) & z); + } + var ch32_1 = ch32$1; + + function maj32$1(x, y, z) { + return (x & y) ^ (x & z) ^ (y & z); + } + var maj32_1 = maj32$1; + + function p32(x, y, z) { + return x ^ y ^ z; + } + var p32_1 = p32; + + function s0_256$1(x) { + return rotr32(x, 2) ^ rotr32(x, 13) ^ rotr32(x, 22); + } + var s0_256_1 = s0_256$1; + + function s1_256$1(x) { + return rotr32(x, 6) ^ rotr32(x, 11) ^ rotr32(x, 25); + } + var s1_256_1 = s1_256$1; + + function g0_256$1(x) { + return rotr32(x, 7) ^ rotr32(x, 18) ^ (x >>> 3); + } + var g0_256_1 = g0_256$1; + + function g1_256$1(x) { + return rotr32(x, 17) ^ rotr32(x, 19) ^ (x >>> 10); + } + var g1_256_1 = g1_256$1; + + var common = { + ft_1: ft_1_1, + ch32: ch32_1, + maj32: maj32_1, + p32: p32_1, + s0_256: s0_256_1, + s1_256: s1_256_1, + g0_256: g0_256_1, + g1_256: g1_256_1 + }; + + var sum32$2 = utils.sum32; + var sum32_4$1 = utils.sum32_4; + var sum32_5$1 = utils.sum32_5; + var ch32 = common.ch32; + var maj32 = common.maj32; + var s0_256 = common.s0_256; + var s1_256 = common.s1_256; + var g0_256 = common.g0_256; + var g1_256 = common.g1_256; + + var BlockHash$3 = common$1.BlockHash; + + var sha256_K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]; + + function SHA256() { + if (!(this instanceof SHA256)) + return new SHA256(); + + BlockHash$3.call(this); + this.h = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 + ]; + this.k = sha256_K; + this.W = new Array(64); + } + utils.inherits(SHA256, BlockHash$3); + var _256 = SHA256; + + SHA256.blockSize = 512; + SHA256.outSize = 256; + SHA256.hmacStrength = 192; + SHA256.padLength = 64; + + SHA256.prototype._update = function _update(msg, start) { + var W = this.W; + + for (var i = 0; i < 16; i++) + W[i] = msg[start + i]; + for (; i < W.length; i++) + W[i] = sum32_4$1(g1_256(W[i - 2]), W[i - 7], g0_256(W[i - 15]), W[i - 16]); + + var a = this.h[0]; + var b = this.h[1]; + var c = this.h[2]; + var d = this.h[3]; + var e = this.h[4]; + var f = this.h[5]; + var g = this.h[6]; + var h = this.h[7]; + + minimalisticAssert(this.k.length === W.length); + for (i = 0; i < W.length; i++) { + var T1 = sum32_5$1(h, s1_256(e), ch32(e, f, g), this.k[i], W[i]); + var T2 = sum32$2(s0_256(a), maj32(a, b, c)); + h = g; + g = f; + f = e; + e = sum32$2(d, T1); + d = c; + c = b; + b = a; + a = sum32$2(T1, T2); + } + + this.h[0] = sum32$2(this.h[0], a); + this.h[1] = sum32$2(this.h[1], b); + this.h[2] = sum32$2(this.h[2], c); + this.h[3] = sum32$2(this.h[3], d); + this.h[4] = sum32$2(this.h[4], e); + this.h[5] = sum32$2(this.h[5], f); + this.h[6] = sum32$2(this.h[6], g); + this.h[7] = sum32$2(this.h[7], h); + }; + + SHA256.prototype._digest = function digest(enc) { + if (enc === 'hex') + return utils.toHex32(this.h, 'big'); + else + return utils.split32(this.h, 'big'); + }; + + function SHA224() { + if (!(this instanceof SHA224)) + return new SHA224(); + + _256.call(this); + this.h = [ + 0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, + 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4 ]; + } + utils.inherits(SHA224, _256); + var _224 = SHA224; + + SHA224.blockSize = 512; + SHA224.outSize = 224; + SHA224.hmacStrength = 192; + SHA224.padLength = 64; + + SHA224.prototype._digest = function digest(enc) { + // Just truncate output + if (enc === 'hex') + return utils.toHex32(this.h.slice(0, 7), 'big'); + else + return utils.split32(this.h.slice(0, 7), 'big'); + }; + + var rotr64_hi = utils.rotr64_hi; + var rotr64_lo = utils.rotr64_lo; + var shr64_hi = utils.shr64_hi; + var shr64_lo = utils.shr64_lo; + var sum64 = utils.sum64; + var sum64_hi = utils.sum64_hi; + var sum64_lo = utils.sum64_lo; + var sum64_4_hi = utils.sum64_4_hi; + var sum64_4_lo = utils.sum64_4_lo; + var sum64_5_hi = utils.sum64_5_hi; + var sum64_5_lo = utils.sum64_5_lo; + + var BlockHash$2 = common$1.BlockHash; + + var sha512_K = [ + 0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd, + 0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc, + 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019, + 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118, + 0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe, + 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2, + 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1, + 0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694, + 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3, + 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65, + 0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483, + 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5, + 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210, + 0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4, + 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725, + 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70, + 0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926, + 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df, + 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8, + 0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b, + 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001, + 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30, + 0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910, + 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8, + 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53, + 0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8, + 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb, + 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3, + 0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60, + 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec, + 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9, + 0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b, + 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207, + 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178, + 0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6, + 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b, + 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493, + 0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c, + 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a, + 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817 + ]; + + function SHA512() { + if (!(this instanceof SHA512)) + return new SHA512(); + + BlockHash$2.call(this); + this.h = [ + 0x6a09e667, 0xf3bcc908, + 0xbb67ae85, 0x84caa73b, + 0x3c6ef372, 0xfe94f82b, + 0xa54ff53a, 0x5f1d36f1, + 0x510e527f, 0xade682d1, + 0x9b05688c, 0x2b3e6c1f, + 0x1f83d9ab, 0xfb41bd6b, + 0x5be0cd19, 0x137e2179 ]; + this.k = sha512_K; + this.W = new Array(160); + } + utils.inherits(SHA512, BlockHash$2); + var _512 = SHA512; + + SHA512.blockSize = 1024; + SHA512.outSize = 512; + SHA512.hmacStrength = 192; + SHA512.padLength = 128; + + SHA512.prototype._prepareBlock = function _prepareBlock(msg, start) { + var W = this.W; + + // 32 x 32bit words + for (var i = 0; i < 32; i++) + W[i] = msg[start + i]; + for (; i < W.length; i += 2) { + var c0_hi = g1_512_hi(W[i - 4], W[i - 3]); // i - 2 + var c0_lo = g1_512_lo(W[i - 4], W[i - 3]); + var c1_hi = W[i - 14]; // i - 7 + var c1_lo = W[i - 13]; + var c2_hi = g0_512_hi(W[i - 30], W[i - 29]); // i - 15 + var c2_lo = g0_512_lo(W[i - 30], W[i - 29]); + var c3_hi = W[i - 32]; // i - 16 + var c3_lo = W[i - 31]; + + W[i] = sum64_4_hi( + c0_hi, c0_lo, + c1_hi, c1_lo, + c2_hi, c2_lo, + c3_hi, c3_lo); + W[i + 1] = sum64_4_lo( + c0_hi, c0_lo, + c1_hi, c1_lo, + c2_hi, c2_lo, + c3_hi, c3_lo); + } + }; + + SHA512.prototype._update = function _update(msg, start) { + this._prepareBlock(msg, start); + + var W = this.W; + + var ah = this.h[0]; + var al = this.h[1]; + var bh = this.h[2]; + var bl = this.h[3]; + var ch = this.h[4]; + var cl = this.h[5]; + var dh = this.h[6]; + var dl = this.h[7]; + var eh = this.h[8]; + var el = this.h[9]; + var fh = this.h[10]; + var fl = this.h[11]; + var gh = this.h[12]; + var gl = this.h[13]; + var hh = this.h[14]; + var hl = this.h[15]; + + minimalisticAssert(this.k.length === W.length); + for (var i = 0; i < W.length; i += 2) { + var c0_hi = hh; + var c0_lo = hl; + var c1_hi = s1_512_hi(eh, el); + var c1_lo = s1_512_lo(eh, el); + var c2_hi = ch64_hi(eh, el, fh, fl, gh); + var c2_lo = ch64_lo(eh, el, fh, fl, gh, gl); + var c3_hi = this.k[i]; + var c3_lo = this.k[i + 1]; + var c4_hi = W[i]; + var c4_lo = W[i + 1]; + + var T1_hi = sum64_5_hi( + c0_hi, c0_lo, + c1_hi, c1_lo, + c2_hi, c2_lo, + c3_hi, c3_lo, + c4_hi, c4_lo); + var T1_lo = sum64_5_lo( + c0_hi, c0_lo, + c1_hi, c1_lo, + c2_hi, c2_lo, + c3_hi, c3_lo, + c4_hi, c4_lo); + + c0_hi = s0_512_hi(ah, al); + c0_lo = s0_512_lo(ah, al); + c1_hi = maj64_hi(ah, al, bh, bl, ch); + c1_lo = maj64_lo(ah, al, bh, bl, ch, cl); + + var T2_hi = sum64_hi(c0_hi, c0_lo, c1_hi, c1_lo); + var T2_lo = sum64_lo(c0_hi, c0_lo, c1_hi, c1_lo); + + hh = gh; + hl = gl; + + gh = fh; + gl = fl; + + fh = eh; + fl = el; + + eh = sum64_hi(dh, dl, T1_hi, T1_lo); + el = sum64_lo(dl, dl, T1_hi, T1_lo); + + dh = ch; + dl = cl; + + ch = bh; + cl = bl; + + bh = ah; + bl = al; + + ah = sum64_hi(T1_hi, T1_lo, T2_hi, T2_lo); + al = sum64_lo(T1_hi, T1_lo, T2_hi, T2_lo); + } + + sum64(this.h, 0, ah, al); + sum64(this.h, 2, bh, bl); + sum64(this.h, 4, ch, cl); + sum64(this.h, 6, dh, dl); + sum64(this.h, 8, eh, el); + sum64(this.h, 10, fh, fl); + sum64(this.h, 12, gh, gl); + sum64(this.h, 14, hh, hl); + }; + + SHA512.prototype._digest = function digest(enc) { + if (enc === 'hex') + return utils.toHex32(this.h, 'big'); + else + return utils.split32(this.h, 'big'); + }; + + function ch64_hi(xh, xl, yh, yl, zh) { + var r = (xh & yh) ^ ((~xh) & zh); + if (r < 0) + r += 0x100000000; + return r; + } + + function ch64_lo(xh, xl, yh, yl, zh, zl) { + var r = (xl & yl) ^ ((~xl) & zl); + if (r < 0) + r += 0x100000000; + return r; + } + + function maj64_hi(xh, xl, yh, yl, zh) { + var r = (xh & yh) ^ (xh & zh) ^ (yh & zh); + if (r < 0) + r += 0x100000000; + return r; + } + + function maj64_lo(xh, xl, yh, yl, zh, zl) { + var r = (xl & yl) ^ (xl & zl) ^ (yl & zl); + if (r < 0) + r += 0x100000000; + return r; + } + + function s0_512_hi(xh, xl) { + var c0_hi = rotr64_hi(xh, xl, 28); + var c1_hi = rotr64_hi(xl, xh, 2); // 34 + var c2_hi = rotr64_hi(xl, xh, 7); // 39 + + var r = c0_hi ^ c1_hi ^ c2_hi; + if (r < 0) + r += 0x100000000; + return r; + } + + function s0_512_lo(xh, xl) { + var c0_lo = rotr64_lo(xh, xl, 28); + var c1_lo = rotr64_lo(xl, xh, 2); // 34 + var c2_lo = rotr64_lo(xl, xh, 7); // 39 + + var r = c0_lo ^ c1_lo ^ c2_lo; + if (r < 0) + r += 0x100000000; + return r; + } + + function s1_512_hi(xh, xl) { + var c0_hi = rotr64_hi(xh, xl, 14); + var c1_hi = rotr64_hi(xh, xl, 18); + var c2_hi = rotr64_hi(xl, xh, 9); // 41 + + var r = c0_hi ^ c1_hi ^ c2_hi; + if (r < 0) + r += 0x100000000; + return r; + } + + function s1_512_lo(xh, xl) { + var c0_lo = rotr64_lo(xh, xl, 14); + var c1_lo = rotr64_lo(xh, xl, 18); + var c2_lo = rotr64_lo(xl, xh, 9); // 41 + + var r = c0_lo ^ c1_lo ^ c2_lo; + if (r < 0) + r += 0x100000000; + return r; + } + + function g0_512_hi(xh, xl) { + var c0_hi = rotr64_hi(xh, xl, 1); + var c1_hi = rotr64_hi(xh, xl, 8); + var c2_hi = shr64_hi(xh, xl, 7); + + var r = c0_hi ^ c1_hi ^ c2_hi; + if (r < 0) + r += 0x100000000; + return r; + } + + function g0_512_lo(xh, xl) { + var c0_lo = rotr64_lo(xh, xl, 1); + var c1_lo = rotr64_lo(xh, xl, 8); + var c2_lo = shr64_lo(xh, xl, 7); + + var r = c0_lo ^ c1_lo ^ c2_lo; + if (r < 0) + r += 0x100000000; + return r; + } + + function g1_512_hi(xh, xl) { + var c0_hi = rotr64_hi(xh, xl, 19); + var c1_hi = rotr64_hi(xl, xh, 29); // 61 + var c2_hi = shr64_hi(xh, xl, 6); + + var r = c0_hi ^ c1_hi ^ c2_hi; + if (r < 0) + r += 0x100000000; + return r; + } + + function g1_512_lo(xh, xl) { + var c0_lo = rotr64_lo(xh, xl, 19); + var c1_lo = rotr64_lo(xl, xh, 29); // 61 + var c2_lo = shr64_lo(xh, xl, 6); + + var r = c0_lo ^ c1_lo ^ c2_lo; + if (r < 0) + r += 0x100000000; + return r; + } + + function SHA384() { + if (!(this instanceof SHA384)) + return new SHA384(); + + _512.call(this); + this.h = [ + 0xcbbb9d5d, 0xc1059ed8, + 0x629a292a, 0x367cd507, + 0x9159015a, 0x3070dd17, + 0x152fecd8, 0xf70e5939, + 0x67332667, 0xffc00b31, + 0x8eb44a87, 0x68581511, + 0xdb0c2e0d, 0x64f98fa7, + 0x47b5481d, 0xbefa4fa4 ]; + } + utils.inherits(SHA384, _512); + var _384 = SHA384; + + SHA384.blockSize = 1024; + SHA384.outSize = 384; + SHA384.hmacStrength = 192; + SHA384.padLength = 128; + + SHA384.prototype._digest = function digest(enc) { + if (enc === 'hex') + return utils.toHex32(this.h.slice(0, 12), 'big'); + else + return utils.split32(this.h.slice(0, 12), 'big'); + }; + + var rotl32$1 = utils.rotl32; + var sum32$1 = utils.sum32; + var sum32_3 = utils.sum32_3; + var sum32_4 = utils.sum32_4; + var BlockHash$1 = common$1.BlockHash; + + function RIPEMD160() { + if (!(this instanceof RIPEMD160)) + return new RIPEMD160(); + + BlockHash$1.call(this); + + this.h = [ 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 ]; + this.endian = 'little'; + } + utils.inherits(RIPEMD160, BlockHash$1); + var ripemd160 = RIPEMD160; + + RIPEMD160.blockSize = 512; + RIPEMD160.outSize = 160; + RIPEMD160.hmacStrength = 192; + RIPEMD160.padLength = 64; + + RIPEMD160.prototype._update = function update(msg, start) { + var A = this.h[0]; + var B = this.h[1]; + var C = this.h[2]; + var D = this.h[3]; + var E = this.h[4]; + var Ah = A; + var Bh = B; + var Ch = C; + var Dh = D; + var Eh = E; + for (var j = 0; j < 80; j++) { + var T = sum32$1( + rotl32$1( + sum32_4(A, f(j, B, C, D), msg[r$1[j] + start], K(j)), + s[j]), + E); + A = E; + E = D; + D = rotl32$1(C, 10); + C = B; + B = T; + T = sum32$1( + rotl32$1( + sum32_4(Ah, f(79 - j, Bh, Ch, Dh), msg[rh[j] + start], Kh(j)), + sh[j]), + Eh); + Ah = Eh; + Eh = Dh; + Dh = rotl32$1(Ch, 10); + Ch = Bh; + Bh = T; + } + T = sum32_3(this.h[1], C, Dh); + this.h[1] = sum32_3(this.h[2], D, Eh); + this.h[2] = sum32_3(this.h[3], E, Ah); + this.h[3] = sum32_3(this.h[4], A, Bh); + this.h[4] = sum32_3(this.h[0], B, Ch); + this.h[0] = T; + }; + + RIPEMD160.prototype._digest = function digest(enc) { + if (enc === 'hex') + return utils.toHex32(this.h, 'little'); + else + return utils.split32(this.h, 'little'); + }; + + function f(j, x, y, z) { + if (j <= 15) + return x ^ y ^ z; + else if (j <= 31) + return (x & y) | ((~x) & z); + else if (j <= 47) + return (x | (~y)) ^ z; + else if (j <= 63) + return (x & z) | (y & (~z)); + else + return x ^ (y | (~z)); + } + + function K(j) { + if (j <= 15) + return 0x00000000; + else if (j <= 31) + return 0x5a827999; + else if (j <= 47) + return 0x6ed9eba1; + else if (j <= 63) + return 0x8f1bbcdc; + else + return 0xa953fd4e; + } + + function Kh(j) { + if (j <= 15) + return 0x50a28be6; + else if (j <= 31) + return 0x5c4dd124; + else if (j <= 47) + return 0x6d703ef3; + else if (j <= 63) + return 0x7a6d76e9; + else + return 0x00000000; + } + + var r$1 = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, + 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, + 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, + 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13 + ]; + + var rh = [ + 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, + 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, + 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, + 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, + 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11 + ]; + + var s = [ + 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, + 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, + 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, + 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, + 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 + ]; + + var sh = [ + 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, + 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, + 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, + 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, + 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 + ]; + + var ripemd = { + ripemd160: ripemd160 + }; + + /** + * A fast MD5 JavaScript implementation + * Copyright (c) 2012 Joseph Myers + * http://www.myersdaily.org/joseph/javascript/md5-text.html + * + * Permission to use, copy, modify, and distribute this software + * and its documentation for any purposes and without + * fee is hereby granted provided that this copyright notice + * appears in all copies. + * + * Of course, this soft is provided "as is" without express or implied + * warranty of any kind. + */ + + // MD5 Digest + async function md5(entree) { + const digest = md51(util.uint8ArrayToString(entree)); + return util.hexToUint8Array(hex(digest)); + } + + function md5cycle(x, k) { + let a = x[0]; + let b = x[1]; + let c = x[2]; + let d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); + } + + function cmn(q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); + } + + function ff(a, b, c, d, x, s, t) { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); + } + + function gg(a, b, c, d, x, s, t) { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); + } + + function hh(a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); + } + + function ii(a, b, c, d, x, s, t) { + return cmn(c ^ (b | (~d)), a, b, x, s, t); + } + + function md51(s) { + const n = s.length; + const state = [1732584193, -271733879, -1732584194, 271733878]; + let i; + for (i = 64; i <= s.length; i += 64) { + md5cycle(state, md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + const tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < s.length; i++) { + tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + } + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i++) { + tail[i] = 0; + } + } + tail[14] = n * 8; + md5cycle(state, tail); + return state; + } + + /* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ + function md5blk(s) { /* I figured global was faster. */ + const md5blks = []; + let i; /* Andy King said do it this way. */ + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << + 24); + } + return md5blks; + } + + const hex_chr = '0123456789abcdef'.split(''); + + function rhex(n) { + let s = ''; + let j = 0; + for (; j < 4; j++) { + s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; + } + return s; + } + + function hex(x) { + for (let i = 0; i < x.length; i++) { + x[i] = rhex(x[i]); + } + return x.join(''); + } + + /* this function is much faster, + so if possible we use it. Some IEs + are the only ones I know of that + need the idiotic second function, + generated by an if clause. */ + + function add32(a, b) { + return (a + b) & 0xFFFFFFFF; + } + + /** + * @fileoverview Provides an interface to hashing functions available in Node.js or external libraries. + * @see {@link https://github.com/asmcrypto/asmcrypto.js|asmCrypto} + * @see {@link https://github.com/indutny/hash.js|hash.js} + * @module crypto/hash + * @private + */ + + const webCrypto$9 = util.getWebCrypto(); + + function hashjsHash(hash, webCryptoHash) { + return async function(data, config$1 = config) { + if (isArrayStream(data)) { + data = await readToEnd(data); + } + if (!util.isStream(data) && webCrypto$9 && webCryptoHash && data.length >= config$1.minBytesForWebCrypto) { + return new Uint8Array(await webCrypto$9.digest(webCryptoHash, data)); + } + const hashInstance = hash(); + return transform(data, value => { + hashInstance.update(value); + }, () => new Uint8Array(hashInstance.digest())); + }; + } + + function asmcryptoHash(hash, webCryptoHash) { + return async function(data, config$1 = config) { + if (isArrayStream(data)) { + data = await readToEnd(data); + } + if (util.isStream(data)) { + const hashInstance = new hash(); + return transform(data, value => { + hashInstance.process(value); + }, () => hashInstance.finish().result); + } else if (webCrypto$9 && webCryptoHash && data.length >= config$1.minBytesForWebCrypto) { + return new Uint8Array(await webCrypto$9.digest(webCryptoHash, data)); + } else { + return hash.bytes(data); + } + }; + } + + let hashFunctions = { + md5: md5, + sha1: asmcryptoHash(Sha1, 'SHA-1'), + sha224: hashjsHash(_224), + sha256: asmcryptoHash(Sha256, 'SHA-256'), + sha384: hashjsHash(_384, 'SHA-384'), + sha512: hashjsHash(_512, 'SHA-512'), // asmcrypto sha512 is huge. + ripemd: hashjsHash(ripemd160) + }; + + var hash = { + + /** @see module:md5 */ + md5: hashFunctions.md5, + /** @see asmCrypto */ + sha1: hashFunctions.sha1, + /** @see hash.js */ + sha224: hashFunctions.sha224, + /** @see asmCrypto */ + sha256: hashFunctions.sha256, + /** @see hash.js */ + sha384: hashFunctions.sha384, + /** @see asmCrypto */ + sha512: hashFunctions.sha512, + /** @see hash.js */ + ripemd: hashFunctions.ripemd, + + /** + * Create a hash on the specified data using the specified algorithm + * @param {module:enums.hash} algo - Hash algorithm type (see {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC 4880 9.4}) + * @param {Uint8Array} data - Data to be hashed + * @returns {Promise} Hash value. + */ + digest(algo, data) { + switch (algo) { + case enums.hash.md5: + return this.md5(data); + case enums.hash.sha1: + return this.sha1(data); + case enums.hash.ripemd: + return this.ripemd(data); + case enums.hash.sha256: + return this.sha256(data); + case enums.hash.sha384: + return this.sha384(data); + case enums.hash.sha512: + return this.sha512(data); + case enums.hash.sha224: + return this.sha224(data); + default: + throw Error('Invalid hash function.'); + } + }, + + /** + * Returns the hash size in bytes of the specified hash algorithm type + * @param {module:enums.hash} algo - Hash algorithm type (See {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC 4880 9.4}) + * @returns {Integer} Size in bytes of the resulting hash. + */ + getHashByteLength(algo) { + switch (algo) { + case enums.hash.md5: + return 16; + case enums.hash.sha1: + case enums.hash.ripemd: + return 20; + case enums.hash.sha256: + return 32; + case enums.hash.sha384: + return 48; + case enums.hash.sha512: + return 64; + case enums.hash.sha224: + return 28; + default: + throw Error('Invalid hash algorithm.'); + } + } + }; + + class AES_CFB { + static encrypt(data, key, iv) { + return new AES_CFB(key, iv).encrypt(data); + } + static decrypt(data, key, iv) { + return new AES_CFB(key, iv).decrypt(data); + } + constructor(key, iv, aes) { + this.aes = aes ? aes : new AES(key, iv, true, 'CFB'); + delete this.aes.padding; + } + encrypt(data) { + const r1 = this.aes.AES_Encrypt_process(data); + const r2 = this.aes.AES_Encrypt_finish(); + return joinBytes(r1, r2); + } + decrypt(data) { + const r1 = this.aes.AES_Decrypt_process(data); + const r2 = this.aes.AES_Decrypt_finish(); + return joinBytes(r1, r2); + } + } + + /** + * Get implementation of the given cipher + * @param {enums.symmetric} algo + * @returns {Object} + * @throws {Error} on invalid algo + */ + function getCipher(algo) { + const algoName = enums.read(enums.symmetric, algo); + return cipher[algoName]; + } + + // Modified by ProtonTech AG + + const webCrypto$8 = util.getWebCrypto(); + + /** + * CFB encryption + * @param {enums.symmetric} algo - block cipher algorithm + * @param {Uint8Array} key + * @param {MaybeStream} plaintext + * @param {Uint8Array} iv + * @param {Object} config - full configuration, defaults to openpgp.config + * @returns MaybeStream + */ + async function encrypt$5(algo, key, plaintext, iv, config) { + enums.read(enums.symmetric, algo); + if (util.isAES(algo)) { + return aesEncrypt(algo, key, plaintext, iv, config); + } + + const Cipher = getCipher(algo); + const cipherfn = new Cipher(key); + const block_size = cipherfn.blockSize; + + const blockc = iv.slice(); + let pt = new Uint8Array(); + const process = chunk => { + if (chunk) { + pt = util.concatUint8Array([pt, chunk]); + } + const ciphertext = new Uint8Array(pt.length); + let i; + let j = 0; + while (chunk ? pt.length >= block_size : pt.length) { + const encblock = cipherfn.encrypt(blockc); + for (i = 0; i < block_size; i++) { + blockc[i] = pt[i] ^ encblock[i]; + ciphertext[j++] = blockc[i]; + } + pt = pt.subarray(block_size); + } + return ciphertext.subarray(0, j); + }; + return transform(plaintext, process, process); + } + + /** + * CFB decryption + * @param {enums.symmetric} algo - block cipher algorithm + * @param {Uint8Array} key + * @param {MaybeStream} ciphertext + * @param {Uint8Array} iv + * @returns MaybeStream + */ + async function decrypt$5(algo, key, ciphertext, iv) { + enums.read(enums.symmetric, algo); + if (util.isAES(algo)) { + return aesDecrypt(algo, key, ciphertext, iv); + } + + const Cipher = getCipher(algo); + const cipherfn = new Cipher(key); + const block_size = cipherfn.blockSize; + + let blockp = iv; + let ct = new Uint8Array(); + const process = chunk => { + if (chunk) { + ct = util.concatUint8Array([ct, chunk]); + } + const plaintext = new Uint8Array(ct.length); + let i; + let j = 0; + while (chunk ? ct.length >= block_size : ct.length) { + const decblock = cipherfn.encrypt(blockp); + blockp = ct.subarray(0, block_size); + for (i = 0; i < block_size; i++) { + plaintext[j++] = blockp[i] ^ decblock[i]; + } + ct = ct.subarray(block_size); + } + return plaintext.subarray(0, j); + }; + return transform(ciphertext, process, process); + } + + function aesEncrypt(algo, key, pt, iv, config) { + if ( + util.getWebCrypto() && + key.length !== 24 && // Chrome doesn't support 192 bit keys, see https://www.chromium.org/blink/webcrypto#TOC-AES-support + !util.isStream(pt) && + pt.length >= 3000 * config.minBytesForWebCrypto // Default to a 3MB minimum. Chrome is pretty slow for small messages, see: https://bugs.chromium.org/p/chromium/issues/detail?id=701188#c2 + ) { // Web Crypto + return webEncrypt(algo, key, pt, iv); + } + // asm.js fallback + const cfb = new AES_CFB(key, iv); + return transform(pt, value => cfb.aes.AES_Encrypt_process(value), () => cfb.aes.AES_Encrypt_finish()); + } + + function aesDecrypt(algo, key, ct, iv) { + if (util.isStream(ct)) { + const cfb = new AES_CFB(key, iv); + return transform(ct, value => cfb.aes.AES_Decrypt_process(value), () => cfb.aes.AES_Decrypt_finish()); + } + return AES_CFB.decrypt(ct, key, iv); + } + + function xorMut$1(a, b) { + for (let i = 0; i < a.length; i++) { + a[i] = a[i] ^ b[i]; + } + } + + async function webEncrypt(algo, key, pt, iv) { + const ALGO = 'AES-CBC'; + const _key = await webCrypto$8.importKey('raw', key, { name: ALGO }, false, ['encrypt']); + const { blockSize } = getCipher(algo); + const cbc_pt = util.concatUint8Array([new Uint8Array(blockSize), pt]); + const ct = new Uint8Array(await webCrypto$8.encrypt({ name: ALGO, iv }, _key, cbc_pt)).subarray(0, pt.length); + xorMut$1(ct, pt); + return ct; + } + + var cfb = /*#__PURE__*/Object.freeze({ + __proto__: null, + encrypt: encrypt$5, + decrypt: decrypt$5 + }); + + class AES_CTR { + static encrypt(data, key, nonce) { + return new AES_CTR(key, nonce).encrypt(data); + } + static decrypt(data, key, nonce) { + return new AES_CTR(key, nonce).encrypt(data); + } + constructor(key, nonce, aes) { + this.aes = aes ? aes : new AES(key, undefined, false, 'CTR'); + delete this.aes.padding; + this.AES_CTR_set_options(nonce); + } + encrypt(data) { + const r1 = this.aes.AES_Encrypt_process(data); + const r2 = this.aes.AES_Encrypt_finish(); + return joinBytes(r1, r2); + } + decrypt(data) { + const r1 = this.aes.AES_Encrypt_process(data); + const r2 = this.aes.AES_Encrypt_finish(); + return joinBytes(r1, r2); + } + AES_CTR_set_options(nonce, counter, size) { + let { asm } = this.aes.acquire_asm(); + if (size !== undefined) { + if (size < 8 || size > 48) + throw new IllegalArgumentError('illegal counter size'); + let mask = Math.pow(2, size) - 1; + asm.set_mask(0, 0, (mask / 0x100000000) | 0, mask | 0); + } + else { + size = 48; + asm.set_mask(0, 0, 0xffff, 0xffffffff); + } + if (nonce !== undefined) { + let len = nonce.length; + if (!len || len > 16) + throw new IllegalArgumentError('illegal nonce size'); + let view = new DataView(new ArrayBuffer(16)); + new Uint8Array(view.buffer).set(nonce); + asm.set_nonce(view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)); + } + else { + throw Error('nonce is required'); + } + if (counter !== undefined) { + if (counter < 0 || counter >= Math.pow(2, size)) + throw new IllegalArgumentError('illegal counter value'); + asm.set_counter(0, 0, (counter / 0x100000000) | 0, counter | 0); + } + } + } + + class AES_CBC { + static encrypt(data, key, padding = true, iv) { + return new AES_CBC(key, iv, padding).encrypt(data); + } + static decrypt(data, key, padding = true, iv) { + return new AES_CBC(key, iv, padding).decrypt(data); + } + constructor(key, iv, padding = true, aes) { + this.aes = aes ? aes : new AES(key, iv, padding, 'CBC'); + } + encrypt(data) { + const r1 = this.aes.AES_Encrypt_process(data); + const r2 = this.aes.AES_Encrypt_finish(); + return joinBytes(r1, r2); + } + decrypt(data) { + const r1 = this.aes.AES_Decrypt_process(data); + const r2 = this.aes.AES_Decrypt_finish(); + return joinBytes(r1, r2); + } + } + + /** + * @fileoverview This module implements AES-CMAC on top of + * native AES-CBC using either the WebCrypto API or Node.js' crypto API. + * @module crypto/cmac + * @private + */ + + const webCrypto$7 = util.getWebCrypto(); + + + /** + * This implementation of CMAC is based on the description of OMAC in + * http://web.cs.ucdavis.edu/~rogaway/papers/eax.pdf. As per that + * document: + * + * We have made a small modification to the OMAC algorithm as it was + * originally presented, changing one of its two constants. + * Specifically, the constant 4 at line 85 was the constant 1/2 (the + * multiplicative inverse of 2) in the original definition of OMAC [14]. + * The OMAC authors indicate that they will promulgate this modification + * [15], which slightly simplifies implementations. + */ + + const blockLength$3 = 16; + + + /** + * xor `padding` into the end of `data`. This function implements "the + * operation xor→ [which] xors the shorter string into the end of longer + * one". Since data is always as least as long as padding, we can + * simplify the implementation. + * @param {Uint8Array} data + * @param {Uint8Array} padding + */ + function rightXORMut(data, padding) { + const offset = data.length - blockLength$3; + for (let i = 0; i < blockLength$3; i++) { + data[i + offset] ^= padding[i]; + } + return data; + } + + function pad(data, padding, padding2) { + // if |M| in {n, 2n, 3n, ...} + if (data.length && data.length % blockLength$3 === 0) { + // then return M xor→ B, + return rightXORMut(data, padding); + } + // else return (M || 10^(n−1−(|M| mod n))) xor→ P + const padded = new Uint8Array(data.length + (blockLength$3 - (data.length % blockLength$3))); + padded.set(data); + padded[data.length] = 0b10000000; + return rightXORMut(padded, padding2); + } + + const zeroBlock$1 = new Uint8Array(blockLength$3); + + async function CMAC(key) { + const cbc = await CBC(key); + + // L ← E_K(0^n); B ← 2L; P ← 4L + const padding = util.double(await cbc(zeroBlock$1)); + const padding2 = util.double(padding); + + return async function(data) { + // return CBC_K(pad(M; B, P)) + return (await cbc(pad(data, padding, padding2))).subarray(-blockLength$3); + }; + } + + async function CBC(key) { + if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto/#aes-support + key = await webCrypto$7.importKey('raw', key, { name: 'AES-CBC', length: key.length * 8 }, false, ['encrypt']); + return async function(pt) { + const ct = await webCrypto$7.encrypt({ name: 'AES-CBC', iv: zeroBlock$1, length: blockLength$3 * 8 }, key, pt); + return new Uint8Array(ct).subarray(0, ct.byteLength - blockLength$3); + }; + } + // asm.js fallback + return async function(pt) { + return AES_CBC.encrypt(pt, key, false, zeroBlock$1); + }; + } + + // OpenPGP.js - An OpenPGP implementation in javascript + + const webCrypto$6 = util.getWebCrypto(); + + + const blockLength$2 = 16; + const ivLength$2 = blockLength$2; + const tagLength$2 = blockLength$2; + + const zero$2 = new Uint8Array(blockLength$2); + const one$1 = new Uint8Array(blockLength$2); one$1[blockLength$2 - 1] = 1; + const two = new Uint8Array(blockLength$2); two[blockLength$2 - 1] = 2; + + async function OMAC(key) { + const cmac = await CMAC(key); + return function(t, message) { + return cmac(util.concatUint8Array([t, message])); + }; + } + + async function CTR(key) { + if ( + util.getWebCrypto() && + key.length !== 24 // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + ) { + key = await webCrypto$6.importKey('raw', key, { name: 'AES-CTR', length: key.length * 8 }, false, ['encrypt']); + return async function(pt, iv) { + const ct = await webCrypto$6.encrypt({ name: 'AES-CTR', counter: iv, length: blockLength$2 * 8 }, key, pt); + return new Uint8Array(ct); + }; + } + // asm.js fallback + return async function(pt, iv) { + return AES_CTR.encrypt(pt, key, iv); + }; + } + + + /** + * Class to en/decrypt using EAX mode. + * @param {enums.symmetric} cipher - The symmetric cipher algorithm to use + * @param {Uint8Array} key - The encryption key + */ + async function EAX(cipher, key) { + if (cipher !== enums.symmetric.aes128 && + cipher !== enums.symmetric.aes192 && + cipher !== enums.symmetric.aes256) { + throw Error('EAX mode supports only AES cipher'); + } + + const [ + omac, + ctr + ] = await Promise.all([ + OMAC(key), + CTR(key) + ]); + + return { + /** + * Encrypt plaintext input. + * @param {Uint8Array} plaintext - The cleartext input to be encrypted + * @param {Uint8Array} nonce - The nonce (16 bytes) + * @param {Uint8Array} adata - Associated data to sign + * @returns {Promise} The ciphertext output. + */ + encrypt: async function(plaintext, nonce, adata) { + const [ + omacNonce, + omacAdata + ] = await Promise.all([ + omac(zero$2, nonce), + omac(one$1, adata) + ]); + const ciphered = await ctr(plaintext, omacNonce); + const omacCiphered = await omac(two, ciphered); + const tag = omacCiphered; // Assumes that omac(*).length === tagLength. + for (let i = 0; i < tagLength$2; i++) { + tag[i] ^= omacAdata[i] ^ omacNonce[i]; + } + return util.concatUint8Array([ciphered, tag]); + }, + + /** + * Decrypt ciphertext input. + * @param {Uint8Array} ciphertext - The ciphertext input to be decrypted + * @param {Uint8Array} nonce - The nonce (16 bytes) + * @param {Uint8Array} adata - Associated data to verify + * @returns {Promise} The plaintext output. + */ + decrypt: async function(ciphertext, nonce, adata) { + if (ciphertext.length < tagLength$2) throw Error('Invalid EAX ciphertext'); + const ciphered = ciphertext.subarray(0, -tagLength$2); + const ctTag = ciphertext.subarray(-tagLength$2); + const [ + omacNonce, + omacAdata, + omacCiphered + ] = await Promise.all([ + omac(zero$2, nonce), + omac(one$1, adata), + omac(two, ciphered) + ]); + const tag = omacCiphered; // Assumes that omac(*).length === tagLength. + for (let i = 0; i < tagLength$2; i++) { + tag[i] ^= omacAdata[i] ^ omacNonce[i]; + } + if (!util.equalsUint8Array(ctTag, tag)) throw Error('Authentication tag mismatch'); + const plaintext = await ctr(ciphered, omacNonce); + return plaintext; + } + }; + } + + + /** + * Get EAX nonce as defined by {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.1|RFC4880bis-04, section 5.16.1}. + * @param {Uint8Array} iv - The initialization vector (16 bytes) + * @param {Uint8Array} chunkIndex - The chunk index (8 bytes) + */ + EAX.getNonce = function(iv, chunkIndex) { + const nonce = iv.slice(); + for (let i = 0; i < chunkIndex.length; i++) { + nonce[8 + i] ^= chunkIndex[i]; + } + return nonce; + }; + + EAX.blockLength = blockLength$2; + EAX.ivLength = ivLength$2; + EAX.tagLength = tagLength$2; + + // OpenPGP.js - An OpenPGP implementation in javascript + + const blockLength$1 = 16; + const ivLength$1 = 15; + + // https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.2: + // While OCB [RFC7253] allows the authentication tag length to be of any + // number up to 128 bits long, this document requires a fixed + // authentication tag length of 128 bits (16 octets) for simplicity. + const tagLength$1 = 16; + + + function ntz(n) { + let ntz = 0; + for (let i = 1; (n & i) === 0; i <<= 1) { + ntz++; + } + return ntz; + } + + function xorMut(S, T) { + for (let i = 0; i < S.length; i++) { + S[i] ^= T[i]; + } + return S; + } + + function xor(S, T) { + return xorMut(S.slice(), T); + } + + const zeroBlock = new Uint8Array(blockLength$1); + const one = new Uint8Array([1]); + + /** + * Class to en/decrypt using OCB mode. + * @param {enums.symmetric} cipher - The symmetric cipher algorithm to use + * @param {Uint8Array} key - The encryption key + */ + async function OCB(cipher$1, key) { + + let maxNtz = 0; + let encipher; + let decipher; + let mask; + + constructKeyVariables(cipher$1, key); + + function constructKeyVariables(cipher$1, key) { + const cipherName = enums.read(enums.symmetric, cipher$1); + const aes = new cipher[cipherName](key); + encipher = aes.encrypt.bind(aes); + decipher = aes.decrypt.bind(aes); + + const mask_x = encipher(zeroBlock); + const mask_$ = util.double(mask_x); + mask = []; + mask[0] = util.double(mask_$); + + + mask.x = mask_x; + mask.$ = mask_$; + } + + function extendKeyVariables(text, adata) { + const newMaxNtz = util.nbits(Math.max(text.length, adata.length) / blockLength$1 | 0) - 1; + for (let i = maxNtz + 1; i <= newMaxNtz; i++) { + mask[i] = util.double(mask[i - 1]); + } + maxNtz = newMaxNtz; + } + + function hash(adata) { + if (!adata.length) { + // Fast path + return zeroBlock; + } + + // + // Consider A as a sequence of 128-bit blocks + // + const m = adata.length / blockLength$1 | 0; + + const offset = new Uint8Array(blockLength$1); + const sum = new Uint8Array(blockLength$1); + for (let i = 0; i < m; i++) { + xorMut(offset, mask[ntz(i + 1)]); + xorMut(sum, encipher(xor(offset, adata))); + adata = adata.subarray(blockLength$1); + } + + // + // Process any final partial block; compute final hash value + // + if (adata.length) { + xorMut(offset, mask.x); + + const cipherInput = new Uint8Array(blockLength$1); + cipherInput.set(adata, 0); + cipherInput[adata.length] = 0b10000000; + xorMut(cipherInput, offset); + + xorMut(sum, encipher(cipherInput)); + } + + return sum; + } + + /** + * Encrypt/decrypt data. + * @param {encipher|decipher} fn - Encryption/decryption block cipher function + * @param {Uint8Array} text - The cleartext or ciphertext (without tag) input + * @param {Uint8Array} nonce - The nonce (15 bytes) + * @param {Uint8Array} adata - Associated data to sign + * @returns {Promise} The ciphertext or plaintext output, with tag appended in both cases. + */ + function crypt(fn, text, nonce, adata) { + // + // Consider P as a sequence of 128-bit blocks + // + const m = text.length / blockLength$1 | 0; + + // + // Key-dependent variables + // + extendKeyVariables(text, adata); + + // + // Nonce-dependent and per-encryption variables + // + // Nonce = num2str(TAGLEN mod 128,7) || zeros(120-bitlen(N)) || 1 || N + // Note: We assume here that tagLength mod 16 == 0. + const paddedNonce = util.concatUint8Array([zeroBlock.subarray(0, ivLength$1 - nonce.length), one, nonce]); + // bottom = str2num(Nonce[123..128]) + const bottom = paddedNonce[blockLength$1 - 1] & 0b111111; + // Ktop = ENCIPHER(K, Nonce[1..122] || zeros(6)) + paddedNonce[blockLength$1 - 1] &= 0b11000000; + const kTop = encipher(paddedNonce); + // Stretch = Ktop || (Ktop[1..64] xor Ktop[9..72]) + const stretched = util.concatUint8Array([kTop, xor(kTop.subarray(0, 8), kTop.subarray(1, 9))]); + // Offset_0 = Stretch[1+bottom..128+bottom] + const offset = util.shiftRight(stretched.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); + // Checksum_0 = zeros(128) + const checksum = new Uint8Array(blockLength$1); + + const ct = new Uint8Array(text.length + tagLength$1); + + // + // Process any whole blocks + // + let i; + let pos = 0; + for (i = 0; i < m; i++) { + // Offset_i = Offset_{i-1} xor L_{ntz(i)} + xorMut(offset, mask[ntz(i + 1)]); + // C_i = Offset_i xor ENCIPHER(K, P_i xor Offset_i) + // P_i = Offset_i xor DECIPHER(K, C_i xor Offset_i) + ct.set(xorMut(fn(xor(offset, text)), offset), pos); + // Checksum_i = Checksum_{i-1} xor P_i + xorMut(checksum, fn === encipher ? text : ct.subarray(pos)); + + text = text.subarray(blockLength$1); + pos += blockLength$1; + } + + // + // Process any final partial block and compute raw tag + // + if (text.length) { + // Offset_* = Offset_m xor L_* + xorMut(offset, mask.x); + // Pad = ENCIPHER(K, Offset_*) + const padding = encipher(offset); + // C_* = P_* xor Pad[1..bitlen(P_*)] + ct.set(xor(text, padding), pos); + + // Checksum_* = Checksum_m xor (P_* || 1 || new Uint8Array(127-bitlen(P_*))) + const xorInput = new Uint8Array(blockLength$1); + xorInput.set(fn === encipher ? text : ct.subarray(pos, -tagLength$1), 0); + xorInput[text.length] = 0b10000000; + xorMut(checksum, xorInput); + pos += text.length; + } + // Tag = ENCIPHER(K, Checksum_* xor Offset_* xor L_$) xor HASH(K,A) + const tag = xorMut(encipher(xorMut(xorMut(checksum, offset), mask.$)), hash(adata)); + + // + // Assemble ciphertext + // + // C = C_1 || C_2 || ... || C_m || C_* || Tag[1..TAGLEN] + ct.set(tag, pos); + return ct; + } + + + return { + /** + * Encrypt plaintext input. + * @param {Uint8Array} plaintext - The cleartext input to be encrypted + * @param {Uint8Array} nonce - The nonce (15 bytes) + * @param {Uint8Array} adata - Associated data to sign + * @returns {Promise} The ciphertext output. + */ + encrypt: async function(plaintext, nonce, adata) { + return crypt(encipher, plaintext, nonce, adata); + }, + + /** + * Decrypt ciphertext input. + * @param {Uint8Array} ciphertext - The ciphertext input to be decrypted + * @param {Uint8Array} nonce - The nonce (15 bytes) + * @param {Uint8Array} adata - Associated data to sign + * @returns {Promise} The ciphertext output. + */ + decrypt: async function(ciphertext, nonce, adata) { + if (ciphertext.length < tagLength$1) throw Error('Invalid OCB ciphertext'); + + const tag = ciphertext.subarray(-tagLength$1); + ciphertext = ciphertext.subarray(0, -tagLength$1); + + const crypted = crypt(decipher, ciphertext, nonce, adata); + // if (Tag[1..TAGLEN] == T) + if (util.equalsUint8Array(tag, crypted.subarray(-tagLength$1))) { + return crypted.subarray(0, -tagLength$1); + } + throw Error('Authentication tag mismatch'); + } + }; + } + + + /** + * Get OCB nonce as defined by {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.2|RFC4880bis-04, section 5.16.2}. + * @param {Uint8Array} iv - The initialization vector (15 bytes) + * @param {Uint8Array} chunkIndex - The chunk index (8 bytes) + */ + OCB.getNonce = function(iv, chunkIndex) { + const nonce = iv.slice(); + for (let i = 0; i < chunkIndex.length; i++) { + nonce[7 + i] ^= chunkIndex[i]; + } + return nonce; + }; + + OCB.blockLength = blockLength$1; + OCB.ivLength = ivLength$1; + OCB.tagLength = tagLength$1; + + const _AES_GCM_data_maxLength = 68719476704; // 2^36 - 2^5 + class AES_GCM { + constructor(key, nonce, adata, tagSize = 16, aes) { + this.tagSize = tagSize; + this.gamma0 = 0; + this.counter = 1; + this.aes = aes ? aes : new AES(key, undefined, false, 'CTR'); + let { asm, heap } = this.aes.acquire_asm(); + // Init GCM + asm.gcm_init(); + // Tag size + if (this.tagSize < 4 || this.tagSize > 16) + throw new IllegalArgumentError('illegal tagSize value'); + // Nonce + const noncelen = nonce.length || 0; + const noncebuf = new Uint8Array(16); + if (noncelen !== 12) { + this._gcm_mac_process(nonce); + heap[0] = 0; + heap[1] = 0; + heap[2] = 0; + heap[3] = 0; + heap[4] = 0; + heap[5] = 0; + heap[6] = 0; + heap[7] = 0; + heap[8] = 0; + heap[9] = 0; + heap[10] = 0; + heap[11] = noncelen >>> 29; + heap[12] = (noncelen >>> 21) & 255; + heap[13] = (noncelen >>> 13) & 255; + heap[14] = (noncelen >>> 5) & 255; + heap[15] = (noncelen << 3) & 255; + asm.mac(AES_asm.MAC.GCM, AES_asm.HEAP_DATA, 16); + asm.get_iv(AES_asm.HEAP_DATA); + asm.set_iv(0, 0, 0, 0); + noncebuf.set(heap.subarray(0, 16)); + } + else { + noncebuf.set(nonce); + noncebuf[15] = 1; + } + const nonceview = new DataView(noncebuf.buffer); + this.gamma0 = nonceview.getUint32(12); + asm.set_nonce(nonceview.getUint32(0), nonceview.getUint32(4), nonceview.getUint32(8), 0); + asm.set_mask(0, 0, 0, 0xffffffff); + // Associated data + if (adata !== undefined) { + if (adata.length > _AES_GCM_data_maxLength) + throw new IllegalArgumentError('illegal adata length'); + if (adata.length) { + this.adata = adata; + this._gcm_mac_process(adata); + } + else { + this.adata = undefined; + } + } + else { + this.adata = undefined; + } + // Counter + if (this.counter < 1 || this.counter > 0xffffffff) + throw new RangeError('counter must be a positive 32-bit integer'); + asm.set_counter(0, 0, 0, (this.gamma0 + this.counter) | 0); + } + static encrypt(cleartext, key, nonce, adata, tagsize) { + return new AES_GCM(key, nonce, adata, tagsize).encrypt(cleartext); + } + static decrypt(ciphertext, key, nonce, adata, tagsize) { + return new AES_GCM(key, nonce, adata, tagsize).decrypt(ciphertext); + } + encrypt(data) { + return this.AES_GCM_encrypt(data); + } + decrypt(data) { + return this.AES_GCM_decrypt(data); + } + AES_GCM_Encrypt_process(data) { + let dpos = 0; + let dlen = data.length || 0; + let { asm, heap } = this.aes.acquire_asm(); + let counter = this.counter; + let pos = this.aes.pos; + let len = this.aes.len; + let rpos = 0; + let rlen = (len + dlen) & -16; + let wlen = 0; + if (((counter - 1) << 4) + len + dlen > _AES_GCM_data_maxLength) + throw new RangeError('counter overflow'); + const result = new Uint8Array(rlen); + while (dlen > 0) { + wlen = _heap_write(heap, pos + len, data, dpos, dlen); + len += wlen; + dpos += wlen; + dlen -= wlen; + wlen = asm.cipher(AES_asm.ENC.CTR, AES_asm.HEAP_DATA + pos, len); + wlen = asm.mac(AES_asm.MAC.GCM, AES_asm.HEAP_DATA + pos, wlen); + if (wlen) + result.set(heap.subarray(pos, pos + wlen), rpos); + counter += wlen >>> 4; + rpos += wlen; + if (wlen < len) { + pos += wlen; + len -= wlen; + } + else { + pos = 0; + len = 0; + } + } + this.counter = counter; + this.aes.pos = pos; + this.aes.len = len; + return result; + } + AES_GCM_Encrypt_finish() { + let { asm, heap } = this.aes.acquire_asm(); + let counter = this.counter; + let tagSize = this.tagSize; + let adata = this.adata; + let pos = this.aes.pos; + let len = this.aes.len; + const result = new Uint8Array(len + tagSize); + asm.cipher(AES_asm.ENC.CTR, AES_asm.HEAP_DATA + pos, (len + 15) & -16); + if (len) + result.set(heap.subarray(pos, pos + len)); + let i = len; + for (; i & 15; i++) + heap[pos + i] = 0; + asm.mac(AES_asm.MAC.GCM, AES_asm.HEAP_DATA + pos, i); + const alen = adata !== undefined ? adata.length : 0; + const clen = ((counter - 1) << 4) + len; + heap[0] = 0; + heap[1] = 0; + heap[2] = 0; + heap[3] = alen >>> 29; + heap[4] = alen >>> 21; + heap[5] = (alen >>> 13) & 255; + heap[6] = (alen >>> 5) & 255; + heap[7] = (alen << 3) & 255; + heap[8] = heap[9] = heap[10] = 0; + heap[11] = clen >>> 29; + heap[12] = (clen >>> 21) & 255; + heap[13] = (clen >>> 13) & 255; + heap[14] = (clen >>> 5) & 255; + heap[15] = (clen << 3) & 255; + asm.mac(AES_asm.MAC.GCM, AES_asm.HEAP_DATA, 16); + asm.get_iv(AES_asm.HEAP_DATA); + asm.set_counter(0, 0, 0, this.gamma0); + asm.cipher(AES_asm.ENC.CTR, AES_asm.HEAP_DATA, 16); + result.set(heap.subarray(0, tagSize), len); + this.counter = 1; + this.aes.pos = 0; + this.aes.len = 0; + return result; + } + AES_GCM_Decrypt_process(data) { + let dpos = 0; + let dlen = data.length || 0; + let { asm, heap } = this.aes.acquire_asm(); + let counter = this.counter; + let tagSize = this.tagSize; + let pos = this.aes.pos; + let len = this.aes.len; + let rpos = 0; + let rlen = len + dlen > tagSize ? (len + dlen - tagSize) & -16 : 0; + let tlen = len + dlen - rlen; + let wlen = 0; + if (((counter - 1) << 4) + len + dlen > _AES_GCM_data_maxLength) + throw new RangeError('counter overflow'); + const result = new Uint8Array(rlen); + while (dlen > tlen) { + wlen = _heap_write(heap, pos + len, data, dpos, dlen - tlen); + len += wlen; + dpos += wlen; + dlen -= wlen; + wlen = asm.mac(AES_asm.MAC.GCM, AES_asm.HEAP_DATA + pos, wlen); + wlen = asm.cipher(AES_asm.DEC.CTR, AES_asm.HEAP_DATA + pos, wlen); + if (wlen) + result.set(heap.subarray(pos, pos + wlen), rpos); + counter += wlen >>> 4; + rpos += wlen; + pos = 0; + len = 0; + } + if (dlen > 0) { + len += _heap_write(heap, 0, data, dpos, dlen); + } + this.counter = counter; + this.aes.pos = pos; + this.aes.len = len; + return result; + } + AES_GCM_Decrypt_finish() { + let { asm, heap } = this.aes.acquire_asm(); + let tagSize = this.tagSize; + let adata = this.adata; + let counter = this.counter; + let pos = this.aes.pos; + let len = this.aes.len; + let rlen = len - tagSize; + if (len < tagSize) + throw new IllegalStateError('authentication tag not found'); + const result = new Uint8Array(rlen); + const atag = new Uint8Array(heap.subarray(pos + rlen, pos + len)); + let i = rlen; + for (; i & 15; i++) + heap[pos + i] = 0; + asm.mac(AES_asm.MAC.GCM, AES_asm.HEAP_DATA + pos, i); + asm.cipher(AES_asm.DEC.CTR, AES_asm.HEAP_DATA + pos, i); + if (rlen) + result.set(heap.subarray(pos, pos + rlen)); + const alen = adata !== undefined ? adata.length : 0; + const clen = ((counter - 1) << 4) + len - tagSize; + heap[0] = 0; + heap[1] = 0; + heap[2] = 0; + heap[3] = alen >>> 29; + heap[4] = alen >>> 21; + heap[5] = (alen >>> 13) & 255; + heap[6] = (alen >>> 5) & 255; + heap[7] = (alen << 3) & 255; + heap[8] = heap[9] = heap[10] = 0; + heap[11] = clen >>> 29; + heap[12] = (clen >>> 21) & 255; + heap[13] = (clen >>> 13) & 255; + heap[14] = (clen >>> 5) & 255; + heap[15] = (clen << 3) & 255; + asm.mac(AES_asm.MAC.GCM, AES_asm.HEAP_DATA, 16); + asm.get_iv(AES_asm.HEAP_DATA); + asm.set_counter(0, 0, 0, this.gamma0); + asm.cipher(AES_asm.ENC.CTR, AES_asm.HEAP_DATA, 16); + let acheck = 0; + for (let i = 0; i < tagSize; ++i) + acheck |= atag[i] ^ heap[i]; + if (acheck) + throw new SecurityError('data integrity check failed'); + this.counter = 1; + this.aes.pos = 0; + this.aes.len = 0; + return result; + } + AES_GCM_decrypt(data) { + const result1 = this.AES_GCM_Decrypt_process(data); + const result2 = this.AES_GCM_Decrypt_finish(); + const result = new Uint8Array(result1.length + result2.length); + if (result1.length) + result.set(result1); + if (result2.length) + result.set(result2, result1.length); + return result; + } + AES_GCM_encrypt(data) { + const result1 = this.AES_GCM_Encrypt_process(data); + const result2 = this.AES_GCM_Encrypt_finish(); + const result = new Uint8Array(result1.length + result2.length); + if (result1.length) + result.set(result1); + if (result2.length) + result.set(result2, result1.length); + return result; + } + _gcm_mac_process(data) { + let { asm, heap } = this.aes.acquire_asm(); + let dpos = 0; + let dlen = data.length || 0; + let wlen = 0; + while (dlen > 0) { + wlen = _heap_write(heap, 0, data, dpos, dlen); + dpos += wlen; + dlen -= wlen; + while (wlen & 15) + heap[wlen++] = 0; + asm.mac(AES_asm.MAC.GCM, AES_asm.HEAP_DATA, wlen); + } + } + } + + // OpenPGP.js - An OpenPGP implementation in javascript + + const webCrypto$5 = util.getWebCrypto(); + + const blockLength = 16; + const ivLength = 12; // size of the IV in bytes + const tagLength = 16; // size of the tag in bytes + const ALGO = 'AES-GCM'; + + /** + * Class to en/decrypt using GCM mode. + * @param {enums.symmetric} cipher - The symmetric cipher algorithm to use + * @param {Uint8Array} key - The encryption key + */ + async function GCM(cipher, key) { + if (cipher !== enums.symmetric.aes128 && + cipher !== enums.symmetric.aes192 && + cipher !== enums.symmetric.aes256) { + throw Error('GCM mode supports only AES cipher'); + } + + if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + const _key = await webCrypto$5.importKey('raw', key, { name: ALGO }, false, ['encrypt', 'decrypt']); + + return { + encrypt: async function(pt, iv, adata = new Uint8Array()) { + if (!pt.length) { // iOS does not support GCM-en/decrypting empty messages + return AES_GCM.encrypt(pt, key, iv, adata); + } + const ct = await webCrypto$5.encrypt({ name: ALGO, iv, additionalData: adata, tagLength: tagLength * 8 }, _key, pt); + return new Uint8Array(ct); + }, + + decrypt: async function(ct, iv, adata = new Uint8Array()) { + if (ct.length === tagLength) { // iOS does not support GCM-en/decrypting empty messages + return AES_GCM.decrypt(ct, key, iv, adata); + } + const pt = await webCrypto$5.decrypt({ name: ALGO, iv, additionalData: adata, tagLength: tagLength * 8 }, _key, ct); + return new Uint8Array(pt); + } + }; + } + + return { + encrypt: async function(pt, iv, adata) { + return AES_GCM.encrypt(pt, key, iv, adata); + }, + + decrypt: async function(ct, iv, adata) { + return AES_GCM.decrypt(ct, key, iv, adata); + } + }; + } + + + /** + * Get GCM nonce. Note: this operation is not defined by the standard. + * A future version of the standard may define GCM mode differently, + * hopefully under a different ID (we use Private/Experimental algorithm + * ID 100) so that we can maintain backwards compatibility. + * @param {Uint8Array} iv - The initialization vector (12 bytes) + * @param {Uint8Array} chunkIndex - The chunk index (8 bytes) + */ + GCM.getNonce = function(iv, chunkIndex) { + const nonce = iv.slice(); + for (let i = 0; i < chunkIndex.length; i++) { + nonce[4 + i] ^= chunkIndex[i]; + } + return nonce; + }; + + GCM.blockLength = blockLength; + GCM.ivLength = ivLength; + GCM.tagLength = tagLength; + + /** + * @fileoverview Cipher modes + * @module crypto/mode + * @private + */ + + var mode = { + /** @see module:crypto/mode/cfb */ + cfb: cfb, + /** @see module:crypto/mode/gcm */ + gcm: GCM, + experimentalGCM: GCM, + /** @see module:crypto/mode/eax */ + eax: EAX, + /** @see module:crypto/mode/ocb */ + ocb: OCB + }; + + var naclFastLight = createCommonjsModule(function (module) { + /*jshint bitwise: false*/ + + (function(nacl) { + + // Ported in 2014 by Dmitry Chestnykh and Devi Mandiri. + // Public domain. + // + // Implementation derived from TweetNaCl version 20140427. + // See for details: http://tweetnacl.cr.yp.to/ + + var gf = function(init) { + var i, r = new Float64Array(16); + if (init) for (i = 0; i < init.length; i++) r[i] = init[i]; + return r; + }; + + // Pluggable, initialized in high-level API below. + var randombytes = function(/* x, n */) { throw Error('no PRNG'); }; + + var _9 = new Uint8Array(32); _9[0] = 9; + + var gf0 = gf(), + gf1 = gf([1]), + _121665 = gf([0xdb41, 1]), + D = gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]), + D2 = gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]), + X = gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]), + Y = gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]), + I = gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]); + + function vn(x, xi, y, yi, n) { + var i,d = 0; + for (i = 0; i < n; i++) d |= x[xi+i]^y[yi+i]; + return (1 & ((d - 1) >>> 8)) - 1; + } + + function crypto_verify_32(x, xi, y, yi) { + return vn(x,xi,y,yi,32); + } + + function set25519(r, a) { + var i; + for (i = 0; i < 16; i++) r[i] = a[i]|0; + } + + function car25519(o) { + var i, v, c = 1; + for (i = 0; i < 16; i++) { + v = o[i] + c + 65535; + c = Math.floor(v / 65536); + o[i] = v - c * 65536; + } + o[0] += c-1 + 37 * (c-1); + } + + function sel25519(p, q, b) { + var t, c = ~(b-1); + for (var i = 0; i < 16; i++) { + t = c & (p[i] ^ q[i]); + p[i] ^= t; + q[i] ^= t; + } + } + + function pack25519(o, n) { + var i, j, b; + var m = gf(), t = gf(); + for (i = 0; i < 16; i++) t[i] = n[i]; + car25519(t); + car25519(t); + car25519(t); + for (j = 0; j < 2; j++) { + m[0] = t[0] - 0xffed; + for (i = 1; i < 15; i++) { + m[i] = t[i] - 0xffff - ((m[i-1]>>16) & 1); + m[i-1] &= 0xffff; + } + m[15] = t[15] - 0x7fff - ((m[14]>>16) & 1); + b = (m[15]>>16) & 1; + m[14] &= 0xffff; + sel25519(t, m, 1-b); + } + for (i = 0; i < 16; i++) { + o[2*i] = t[i] & 0xff; + o[2*i+1] = t[i]>>8; + } + } + + function neq25519(a, b) { + var c = new Uint8Array(32), d = new Uint8Array(32); + pack25519(c, a); + pack25519(d, b); + return crypto_verify_32(c, 0, d, 0); + } + + function par25519(a) { + var d = new Uint8Array(32); + pack25519(d, a); + return d[0] & 1; + } + + function unpack25519(o, n) { + var i; + for (i = 0; i < 16; i++) o[i] = n[2*i] + (n[2*i+1] << 8); + o[15] &= 0x7fff; + } + + function A(o, a, b) { + for (var i = 0; i < 16; i++) o[i] = a[i] + b[i]; + } + + function Z(o, a, b) { + for (var i = 0; i < 16; i++) o[i] = a[i] - b[i]; + } + + function M(o, a, b) { + var v, c, + t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, t7 = 0, + t8 = 0, t9 = 0, t10 = 0, t11 = 0, t12 = 0, t13 = 0, t14 = 0, t15 = 0, + t16 = 0, t17 = 0, t18 = 0, t19 = 0, t20 = 0, t21 = 0, t22 = 0, t23 = 0, + t24 = 0, t25 = 0, t26 = 0, t27 = 0, t28 = 0, t29 = 0, t30 = 0, + b0 = b[0], + b1 = b[1], + b2 = b[2], + b3 = b[3], + b4 = b[4], + b5 = b[5], + b6 = b[6], + b7 = b[7], + b8 = b[8], + b9 = b[9], + b10 = b[10], + b11 = b[11], + b12 = b[12], + b13 = b[13], + b14 = b[14], + b15 = b[15]; + + v = a[0]; + t0 += v * b0; + t1 += v * b1; + t2 += v * b2; + t3 += v * b3; + t4 += v * b4; + t5 += v * b5; + t6 += v * b6; + t7 += v * b7; + t8 += v * b8; + t9 += v * b9; + t10 += v * b10; + t11 += v * b11; + t12 += v * b12; + t13 += v * b13; + t14 += v * b14; + t15 += v * b15; + v = a[1]; + t1 += v * b0; + t2 += v * b1; + t3 += v * b2; + t4 += v * b3; + t5 += v * b4; + t6 += v * b5; + t7 += v * b6; + t8 += v * b7; + t9 += v * b8; + t10 += v * b9; + t11 += v * b10; + t12 += v * b11; + t13 += v * b12; + t14 += v * b13; + t15 += v * b14; + t16 += v * b15; + v = a[2]; + t2 += v * b0; + t3 += v * b1; + t4 += v * b2; + t5 += v * b3; + t6 += v * b4; + t7 += v * b5; + t8 += v * b6; + t9 += v * b7; + t10 += v * b8; + t11 += v * b9; + t12 += v * b10; + t13 += v * b11; + t14 += v * b12; + t15 += v * b13; + t16 += v * b14; + t17 += v * b15; + v = a[3]; + t3 += v * b0; + t4 += v * b1; + t5 += v * b2; + t6 += v * b3; + t7 += v * b4; + t8 += v * b5; + t9 += v * b6; + t10 += v * b7; + t11 += v * b8; + t12 += v * b9; + t13 += v * b10; + t14 += v * b11; + t15 += v * b12; + t16 += v * b13; + t17 += v * b14; + t18 += v * b15; + v = a[4]; + t4 += v * b0; + t5 += v * b1; + t6 += v * b2; + t7 += v * b3; + t8 += v * b4; + t9 += v * b5; + t10 += v * b6; + t11 += v * b7; + t12 += v * b8; + t13 += v * b9; + t14 += v * b10; + t15 += v * b11; + t16 += v * b12; + t17 += v * b13; + t18 += v * b14; + t19 += v * b15; + v = a[5]; + t5 += v * b0; + t6 += v * b1; + t7 += v * b2; + t8 += v * b3; + t9 += v * b4; + t10 += v * b5; + t11 += v * b6; + t12 += v * b7; + t13 += v * b8; + t14 += v * b9; + t15 += v * b10; + t16 += v * b11; + t17 += v * b12; + t18 += v * b13; + t19 += v * b14; + t20 += v * b15; + v = a[6]; + t6 += v * b0; + t7 += v * b1; + t8 += v * b2; + t9 += v * b3; + t10 += v * b4; + t11 += v * b5; + t12 += v * b6; + t13 += v * b7; + t14 += v * b8; + t15 += v * b9; + t16 += v * b10; + t17 += v * b11; + t18 += v * b12; + t19 += v * b13; + t20 += v * b14; + t21 += v * b15; + v = a[7]; + t7 += v * b0; + t8 += v * b1; + t9 += v * b2; + t10 += v * b3; + t11 += v * b4; + t12 += v * b5; + t13 += v * b6; + t14 += v * b7; + t15 += v * b8; + t16 += v * b9; + t17 += v * b10; + t18 += v * b11; + t19 += v * b12; + t20 += v * b13; + t21 += v * b14; + t22 += v * b15; + v = a[8]; + t8 += v * b0; + t9 += v * b1; + t10 += v * b2; + t11 += v * b3; + t12 += v * b4; + t13 += v * b5; + t14 += v * b6; + t15 += v * b7; + t16 += v * b8; + t17 += v * b9; + t18 += v * b10; + t19 += v * b11; + t20 += v * b12; + t21 += v * b13; + t22 += v * b14; + t23 += v * b15; + v = a[9]; + t9 += v * b0; + t10 += v * b1; + t11 += v * b2; + t12 += v * b3; + t13 += v * b4; + t14 += v * b5; + t15 += v * b6; + t16 += v * b7; + t17 += v * b8; + t18 += v * b9; + t19 += v * b10; + t20 += v * b11; + t21 += v * b12; + t22 += v * b13; + t23 += v * b14; + t24 += v * b15; + v = a[10]; + t10 += v * b0; + t11 += v * b1; + t12 += v * b2; + t13 += v * b3; + t14 += v * b4; + t15 += v * b5; + t16 += v * b6; + t17 += v * b7; + t18 += v * b8; + t19 += v * b9; + t20 += v * b10; + t21 += v * b11; + t22 += v * b12; + t23 += v * b13; + t24 += v * b14; + t25 += v * b15; + v = a[11]; + t11 += v * b0; + t12 += v * b1; + t13 += v * b2; + t14 += v * b3; + t15 += v * b4; + t16 += v * b5; + t17 += v * b6; + t18 += v * b7; + t19 += v * b8; + t20 += v * b9; + t21 += v * b10; + t22 += v * b11; + t23 += v * b12; + t24 += v * b13; + t25 += v * b14; + t26 += v * b15; + v = a[12]; + t12 += v * b0; + t13 += v * b1; + t14 += v * b2; + t15 += v * b3; + t16 += v * b4; + t17 += v * b5; + t18 += v * b6; + t19 += v * b7; + t20 += v * b8; + t21 += v * b9; + t22 += v * b10; + t23 += v * b11; + t24 += v * b12; + t25 += v * b13; + t26 += v * b14; + t27 += v * b15; + v = a[13]; + t13 += v * b0; + t14 += v * b1; + t15 += v * b2; + t16 += v * b3; + t17 += v * b4; + t18 += v * b5; + t19 += v * b6; + t20 += v * b7; + t21 += v * b8; + t22 += v * b9; + t23 += v * b10; + t24 += v * b11; + t25 += v * b12; + t26 += v * b13; + t27 += v * b14; + t28 += v * b15; + v = a[14]; + t14 += v * b0; + t15 += v * b1; + t16 += v * b2; + t17 += v * b3; + t18 += v * b4; + t19 += v * b5; + t20 += v * b6; + t21 += v * b7; + t22 += v * b8; + t23 += v * b9; + t24 += v * b10; + t25 += v * b11; + t26 += v * b12; + t27 += v * b13; + t28 += v * b14; + t29 += v * b15; + v = a[15]; + t15 += v * b0; + t16 += v * b1; + t17 += v * b2; + t18 += v * b3; + t19 += v * b4; + t20 += v * b5; + t21 += v * b6; + t22 += v * b7; + t23 += v * b8; + t24 += v * b9; + t25 += v * b10; + t26 += v * b11; + t27 += v * b12; + t28 += v * b13; + t29 += v * b14; + t30 += v * b15; + + t0 += 38 * t16; + t1 += 38 * t17; + t2 += 38 * t18; + t3 += 38 * t19; + t4 += 38 * t20; + t5 += 38 * t21; + t6 += 38 * t22; + t7 += 38 * t23; + t8 += 38 * t24; + t9 += 38 * t25; + t10 += 38 * t26; + t11 += 38 * t27; + t12 += 38 * t28; + t13 += 38 * t29; + t14 += 38 * t30; + // t15 left as is + + // first car + c = 1; + v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536; + v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536; + v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536; + v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536; + v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536; + v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536; + v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536; + v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536; + v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536; + v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536; + v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536; + v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536; + v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536; + v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536; + v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536; + v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536; + t0 += c-1 + 37 * (c-1); + + // second car + c = 1; + v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536; + v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536; + v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536; + v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536; + v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536; + v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536; + v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536; + v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536; + v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536; + v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536; + v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536; + v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536; + v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536; + v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536; + v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536; + v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536; + t0 += c-1 + 37 * (c-1); + + o[ 0] = t0; + o[ 1] = t1; + o[ 2] = t2; + o[ 3] = t3; + o[ 4] = t4; + o[ 5] = t5; + o[ 6] = t6; + o[ 7] = t7; + o[ 8] = t8; + o[ 9] = t9; + o[10] = t10; + o[11] = t11; + o[12] = t12; + o[13] = t13; + o[14] = t14; + o[15] = t15; + } + + function S(o, a) { + M(o, a, a); + } + + function inv25519(o, i) { + var c = gf(); + var a; + for (a = 0; a < 16; a++) c[a] = i[a]; + for (a = 253; a >= 0; a--) { + S(c, c); + if(a !== 2 && a !== 4) M(c, c, i); + } + for (a = 0; a < 16; a++) o[a] = c[a]; + } + + function pow2523(o, i) { + var c = gf(); + var a; + for (a = 0; a < 16; a++) c[a] = i[a]; + for (a = 250; a >= 0; a--) { + S(c, c); + if(a !== 1) M(c, c, i); + } + for (a = 0; a < 16; a++) o[a] = c[a]; + } + + function crypto_scalarmult(q, n, p) { + var z = new Uint8Array(32); + var x = new Float64Array(80), r, i; + var a = gf(), b = gf(), c = gf(), + d = gf(), e = gf(), f = gf(); + for (i = 0; i < 31; i++) z[i] = n[i]; + z[31]=(n[31]&127)|64; + z[0]&=248; + unpack25519(x,p); + for (i = 0; i < 16; i++) { + b[i]=x[i]; + d[i]=a[i]=c[i]=0; + } + a[0]=d[0]=1; + for (i=254; i>=0; --i) { + r=(z[i>>>3]>>>(i&7))&1; + sel25519(a,b,r); + sel25519(c,d,r); + A(e,a,c); + Z(a,a,c); + A(c,b,d); + Z(b,b,d); + S(d,e); + S(f,a); + M(a,c,a); + M(c,b,e); + A(e,a,c); + Z(a,a,c); + S(b,a); + Z(c,d,f); + M(a,c,_121665); + A(a,a,d); + M(c,c,a); + M(a,d,f); + M(d,b,x); + S(b,e); + sel25519(a,b,r); + sel25519(c,d,r); + } + for (i = 0; i < 16; i++) { + x[i+16]=a[i]; + x[i+32]=c[i]; + x[i+48]=b[i]; + x[i+64]=d[i]; + } + var x32 = x.subarray(32); + var x16 = x.subarray(16); + inv25519(x32,x32); + M(x16,x16,x32); + pack25519(q,x16); + return 0; + } + + function crypto_scalarmult_base(q, n) { + return crypto_scalarmult(q, n, _9); + } + + function crypto_box_keypair(y, x) { + randombytes(x, 32); + return crypto_scalarmult_base(y, x); + } + + function add(p, q) { + var a = gf(), b = gf(), c = gf(), + d = gf(), e = gf(), f = gf(), + g = gf(), h = gf(), t = gf(); + + Z(a, p[1], p[0]); + Z(t, q[1], q[0]); + M(a, a, t); + A(b, p[0], p[1]); + A(t, q[0], q[1]); + M(b, b, t); + M(c, p[3], q[3]); + M(c, c, D2); + M(d, p[2], q[2]); + A(d, d, d); + Z(e, b, a); + Z(f, d, c); + A(g, d, c); + A(h, b, a); + + M(p[0], e, f); + M(p[1], h, g); + M(p[2], g, f); + M(p[3], e, h); + } + + function cswap(p, q, b) { + var i; + for (i = 0; i < 4; i++) { + sel25519(p[i], q[i], b); + } + } + + function pack(r, p) { + var tx = gf(), ty = gf(), zi = gf(); + inv25519(zi, p[2]); + M(tx, p[0], zi); + M(ty, p[1], zi); + pack25519(r, ty); + r[31] ^= par25519(tx) << 7; + } + + function scalarmult(p, q, s) { + var b, i; + set25519(p[0], gf0); + set25519(p[1], gf1); + set25519(p[2], gf1); + set25519(p[3], gf0); + for (i = 255; i >= 0; --i) { + b = (s[(i/8)|0] >> (i&7)) & 1; + cswap(p, q, b); + add(q, p); + add(p, p); + cswap(p, q, b); + } + } + + function scalarbase(p, s) { + var q = [gf(), gf(), gf(), gf()]; + set25519(q[0], X); + set25519(q[1], Y); + set25519(q[2], gf1); + M(q[3], X, Y); + scalarmult(p, q, s); + } + + function crypto_sign_keypair(pk, sk, seeded) { + var d; + var p = [gf(), gf(), gf(), gf()]; + var i; + + if (!seeded) randombytes(sk, 32); + d = nacl.hash(sk.subarray(0, 32)); + d[0] &= 248; + d[31] &= 127; + d[31] |= 64; + + scalarbase(p, d); + pack(pk, p); + + for (i = 0; i < 32; i++) sk[i+32] = pk[i]; + return 0; + } + + var L = new Float64Array([0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10]); + + function modL(r, x) { + var carry, i, j, k; + for (i = 63; i >= 32; --i) { + carry = 0; + for (j = i - 32, k = i - 12; j < k; ++j) { + x[j] += carry - 16 * x[i] * L[j - (i - 32)]; + carry = Math.floor((x[j] + 128) / 256); + x[j] -= carry * 256; + } + x[j] += carry; + x[i] = 0; + } + carry = 0; + for (j = 0; j < 32; j++) { + x[j] += carry - (x[31] >> 4) * L[j]; + carry = x[j] >> 8; + x[j] &= 255; + } + for (j = 0; j < 32; j++) x[j] -= carry * L[j]; + for (i = 0; i < 32; i++) { + x[i+1] += x[i] >> 8; + r[i] = x[i] & 255; + } + } + + function reduce(r) { + var x = new Float64Array(64), i; + for (i = 0; i < 64; i++) x[i] = r[i]; + for (i = 0; i < 64; i++) r[i] = 0; + modL(r, x); + } + + // Note: difference from C - smlen returned, not passed as argument. + function crypto_sign(sm, m, n, sk) { + var d, h, r; + var i, j, x = new Float64Array(64); + var p = [gf(), gf(), gf(), gf()]; + + d = nacl.hash(sk.subarray(0, 32)); + d[0] &= 248; + d[31] &= 127; + d[31] |= 64; + + var smlen = n + 64; + for (i = 0; i < n; i++) sm[64 + i] = m[i]; + for (i = 0; i < 32; i++) sm[32 + i] = d[32 + i]; + + r = nacl.hash(sm.subarray(32, smlen)); + reduce(r); + scalarbase(p, r); + pack(sm, p); + + for (i = 32; i < 64; i++) sm[i] = sk[i]; + h = nacl.hash(sm.subarray(0, smlen)); + reduce(h); + + for (i = 0; i < 64; i++) x[i] = 0; + for (i = 0; i < 32; i++) x[i] = r[i]; + for (i = 0; i < 32; i++) { + for (j = 0; j < 32; j++) { + x[i+j] += h[i] * d[j]; + } + } + + modL(sm.subarray(32), x); + return smlen; + } + + function unpackneg(r, p) { + var t = gf(), chk = gf(), num = gf(), + den = gf(), den2 = gf(), den4 = gf(), + den6 = gf(); + + set25519(r[2], gf1); + unpack25519(r[1], p); + S(num, r[1]); + M(den, num, D); + Z(num, num, r[2]); + A(den, r[2], den); + + S(den2, den); + S(den4, den2); + M(den6, den4, den2); + M(t, den6, num); + M(t, t, den); + + pow2523(t, t); + M(t, t, num); + M(t, t, den); + M(t, t, den); + M(r[0], t, den); + + S(chk, r[0]); + M(chk, chk, den); + if (neq25519(chk, num)) M(r[0], r[0], I); + + S(chk, r[0]); + M(chk, chk, den); + if (neq25519(chk, num)) return -1; + + if (par25519(r[0]) === (p[31]>>7)) Z(r[0], gf0, r[0]); + + M(r[3], r[0], r[1]); + return 0; + } + + function crypto_sign_open(m, sm, n, pk) { + var i; + var t = new Uint8Array(32), h; + var p = [gf(), gf(), gf(), gf()], + q = [gf(), gf(), gf(), gf()]; + + if (n < 64) return -1; + + if (unpackneg(q, pk)) return -1; + + for (i = 0; i < n; i++) m[i] = sm[i]; + for (i = 0; i < 32; i++) m[i+32] = pk[i]; + h = nacl.hash(m.subarray(0, n)); + reduce(h); + scalarmult(p, q, h); + + scalarbase(q, sm.subarray(32)); + add(p, q); + pack(t, p); + + n -= 64; + if (crypto_verify_32(sm, 0, t, 0)) { + for (i = 0; i < n; i++) m[i] = 0; + return -1; + } + + for (i = 0; i < n; i++) m[i] = sm[i + 64]; + return n; + } + + var crypto_scalarmult_BYTES = 32, + crypto_scalarmult_SCALARBYTES = 32, + crypto_box_PUBLICKEYBYTES = 32, + crypto_box_SECRETKEYBYTES = 32, + crypto_sign_BYTES = 64, + crypto_sign_PUBLICKEYBYTES = 32, + crypto_sign_SECRETKEYBYTES = 64, + crypto_sign_SEEDBYTES = 32; + + function checkArrayTypes() { + for (var i = 0; i < arguments.length; i++) { + if (!(arguments[i] instanceof Uint8Array)) + throw new TypeError('unexpected type, use Uint8Array'); + } + } + + function cleanup(arr) { + for (var i = 0; i < arr.length; i++) arr[i] = 0; + } + + nacl.scalarMult = function(n, p) { + checkArrayTypes(n, p); + if (n.length !== crypto_scalarmult_SCALARBYTES) throw Error('bad n size'); + if (p.length !== crypto_scalarmult_BYTES) throw Error('bad p size'); + var q = new Uint8Array(crypto_scalarmult_BYTES); + crypto_scalarmult(q, n, p); + return q; + }; + + nacl.box = {}; + + nacl.box.keyPair = function() { + var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_box_SECRETKEYBYTES); + crypto_box_keypair(pk, sk); + return {publicKey: pk, secretKey: sk}; + }; + + nacl.box.keyPair.fromSecretKey = function(secretKey) { + checkArrayTypes(secretKey); + if (secretKey.length !== crypto_box_SECRETKEYBYTES) + throw Error('bad secret key size'); + var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); + crypto_scalarmult_base(pk, secretKey); + return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; + }; + + nacl.sign = function(msg, secretKey) { + checkArrayTypes(msg, secretKey); + if (secretKey.length !== crypto_sign_SECRETKEYBYTES) + throw Error('bad secret key size'); + var signedMsg = new Uint8Array(crypto_sign_BYTES+msg.length); + crypto_sign(signedMsg, msg, msg.length, secretKey); + return signedMsg; + }; + + nacl.sign.detached = function(msg, secretKey) { + var signedMsg = nacl.sign(msg, secretKey); + var sig = new Uint8Array(crypto_sign_BYTES); + for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; + return sig; + }; + + nacl.sign.detached.verify = function(msg, sig, publicKey) { + checkArrayTypes(msg, sig, publicKey); + if (sig.length !== crypto_sign_BYTES) + throw Error('bad signature size'); + if (publicKey.length !== crypto_sign_PUBLICKEYBYTES) + throw Error('bad public key size'); + var sm = new Uint8Array(crypto_sign_BYTES + msg.length); + var m = new Uint8Array(crypto_sign_BYTES + msg.length); + var i; + for (i = 0; i < crypto_sign_BYTES; i++) sm[i] = sig[i]; + for (i = 0; i < msg.length; i++) sm[i+crypto_sign_BYTES] = msg[i]; + return (crypto_sign_open(m, sm, sm.length, publicKey) >= 0); + }; + + nacl.sign.keyPair = function() { + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); + crypto_sign_keypair(pk, sk); + return {publicKey: pk, secretKey: sk}; + }; + + nacl.sign.keyPair.fromSecretKey = function(secretKey) { + checkArrayTypes(secretKey); + if (secretKey.length !== crypto_sign_SECRETKEYBYTES) + throw Error('bad secret key size'); + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + for (var i = 0; i < pk.length; i++) pk[i] = secretKey[32+i]; + return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; + }; + + nacl.sign.keyPair.fromSeed = function(seed) { + checkArrayTypes(seed); + if (seed.length !== crypto_sign_SEEDBYTES) + throw Error('bad seed size'); + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); + for (var i = 0; i < 32; i++) sk[i] = seed[i]; + crypto_sign_keypair(pk, sk, true); + return {publicKey: pk, secretKey: sk}; + }; + + nacl.setPRNG = function(fn) { + randombytes = fn; + }; + + (function() { + // Initialize PRNG if environment provides CSPRNG. + // If not, methods calling randombytes will throw. + var crypto = typeof self !== 'undefined' ? (self.crypto || self.msCrypto) : null; + if (crypto && crypto.getRandomValues) { + // Browsers. + var QUOTA = 65536; + nacl.setPRNG(function(x, n) { + var i, v = new Uint8Array(n); + for (i = 0; i < n; i += QUOTA) { + crypto.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA))); + } + for (i = 0; i < n; i++) x[i] = v[i]; + cleanup(v); + }); + } else if (typeof commonjsRequire !== 'undefined') { + // Node.js. + crypto = void('crypto'); + if (crypto && crypto.randomBytes) { + nacl.setPRNG(function(x, n) { + var i, v = crypto.randomBytes(n); + for (i = 0; i < n; i++) x[i] = v[i]; + cleanup(v); + }); + } + } + })(); + + })(module.exports ? module.exports : (self.nacl = self.nacl || {})); + }); + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * Retrieve secure random byte array of the specified length + * @param {Integer} length - Length in bytes to generate + * @returns {Uint8Array} Random byte array. + */ + function getRandomBytes(length) { + const buf = new Uint8Array(length); + if (typeof crypto !== 'undefined' && crypto.getRandomValues) { + crypto.getRandomValues(buf); + } else { + throw Error('No secure random number generator available.'); + } + return buf; + } + + /** + * Create a secure random BigInteger that is greater than or equal to min and less than max. + * @param {module:BigInteger} min - Lower bound, included + * @param {module:BigInteger} max - Upper bound, excluded + * @returns {Promise} Random BigInteger. + * @async + */ + async function getRandomBigInteger(min, max) { + const BigInteger = await util.getBigInteger(); + + if (max.lt(min)) { + throw Error('Illegal parameter value: max <= min'); + } + + const modulus = max.sub(min); + const bytes = modulus.byteLength(); + + // Using a while loop is necessary to avoid bias introduced by the mod operation. + // However, we request 64 extra random bits so that the bias is negligible. + // Section B.1.1 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf + const r = new BigInteger(await getRandomBytes(bytes + 8)); + return r.mod(modulus).add(min); + } + + var random = /*#__PURE__*/Object.freeze({ + __proto__: null, + getRandomBytes: getRandomBytes, + getRandomBigInteger: getRandomBigInteger + }); + + // OpenPGP.js - An OpenPGP implementation in javascript + + /** + * Generate a probably prime random number + * @param {Integer} bits - Bit length of the prime + * @param {BigInteger} e - Optional RSA exponent to check against the prime + * @param {Integer} k - Optional number of iterations of Miller-Rabin test + * @returns BigInteger + * @async + */ + async function randomProbablePrime(bits, e, k) { + const BigInteger = await util.getBigInteger(); + const one = new BigInteger(1); + const min = one.leftShift(new BigInteger(bits - 1)); + const thirty = new BigInteger(30); + /* + * We can avoid any multiples of 3 and 5 by looking at n mod 30 + * n mod 30 = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 + * the next possible prime is mod 30: + * 1 7 7 7 7 7 7 11 11 11 11 13 13 17 17 17 17 19 19 23 23 23 23 29 29 29 29 29 29 1 + */ + const adds = [1, 6, 5, 4, 3, 2, 1, 4, 3, 2, 1, 2, 1, 4, 3, 2, 1, 2, 1, 4, 3, 2, 1, 6, 5, 4, 3, 2, 1, 2]; + + const n = await getRandomBigInteger(min, min.leftShift(one)); + let i = n.mod(thirty).toNumber(); + + do { + n.iadd(new BigInteger(adds[i])); + i = (i + adds[i]) % adds.length; + // If reached the maximum, go back to the minimum. + if (n.bitLength() > bits) { + n.imod(min.leftShift(one)).iadd(min); + i = n.mod(thirty).toNumber(); + } + } while (!await isProbablePrime(n, e, k)); + return n; + } + + /** + * Probabilistic primality testing + * @param {BigInteger} n - Number to test + * @param {BigInteger} e - Optional RSA exponent to check against the prime + * @param {Integer} k - Optional number of iterations of Miller-Rabin test + * @returns {boolean} + * @async + */ + async function isProbablePrime(n, e, k) { + if (e && !n.dec().gcd(e).isOne()) { + return false; + } + if (!await divisionTest(n)) { + return false; + } + if (!await fermat(n)) { + return false; + } + if (!await millerRabin(n, k)) { + return false; + } + // TODO implement the Lucas test + // See Section C.3.3 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf + return true; + } + + /** + * Tests whether n is probably prime or not using Fermat's test with b = 2. + * Fails if b^(n-1) mod n != 1. + * @param {BigInteger} n - Number to test + * @param {BigInteger} b - Optional Fermat test base + * @returns {boolean} + */ + async function fermat(n, b) { + const BigInteger = await util.getBigInteger(); + b = b || new BigInteger(2); + return b.modExp(n.dec(), n).isOne(); + } + + async function divisionTest(n) { + const BigInteger = await util.getBigInteger(); + return smallPrimes.every(m => { + return n.mod(new BigInteger(m)) !== 0; + }); + } + + // https://github.com/gpg/libgcrypt/blob/master/cipher/primegen.c + const smallPrimes = [ + 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, + 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, + 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, + 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, + 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, + 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, + 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, + 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, + 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, + 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, + 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, + 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, + 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, + 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, + 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, + 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, + 991, 997, 1009, 1013, 1019, 1021, 1031, 1033, + 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, + 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, + 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, + 1217, 1223, 1229, 1231, 1237, 1249, 1259, 1277, + 1279, 1283, 1289, 1291, 1297, 1301, 1303, 1307, + 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399, + 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, + 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, + 1499, 1511, 1523, 1531, 1543, 1549, 1553, 1559, + 1567, 1571, 1579, 1583, 1597, 1601, 1607, 1609, + 1613, 1619, 1621, 1627, 1637, 1657, 1663, 1667, + 1669, 1693, 1697, 1699, 1709, 1721, 1723, 1733, + 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789, + 1801, 1811, 1823, 1831, 1847, 1861, 1867, 1871, + 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, + 1933, 1949, 1951, 1973, 1979, 1987, 1993, 1997, + 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, + 2063, 2069, 2081, 2083, 2087, 2089, 2099, 2111, + 2113, 2129, 2131, 2137, 2141, 2143, 2153, 2161, + 2179, 2203, 2207, 2213, 2221, 2237, 2239, 2243, + 2251, 2267, 2269, 2273, 2281, 2287, 2293, 2297, + 2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357, + 2371, 2377, 2381, 2383, 2389, 2393, 2399, 2411, + 2417, 2423, 2437, 2441, 2447, 2459, 2467, 2473, + 2477, 2503, 2521, 2531, 2539, 2543, 2549, 2551, + 2557, 2579, 2591, 2593, 2609, 2617, 2621, 2633, + 2647, 2657, 2659, 2663, 2671, 2677, 2683, 2687, + 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, + 2731, 2741, 2749, 2753, 2767, 2777, 2789, 2791, + 2797, 2801, 2803, 2819, 2833, 2837, 2843, 2851, + 2857, 2861, 2879, 2887, 2897, 2903, 2909, 2917, + 2927, 2939, 2953, 2957, 2963, 2969, 2971, 2999, + 3001, 3011, 3019, 3023, 3037, 3041, 3049, 3061, + 3067, 3079, 3083, 3089, 3109, 3119, 3121, 3137, + 3163, 3167, 3169, 3181, 3187, 3191, 3203, 3209, + 3217, 3221, 3229, 3251, 3253, 3257, 3259, 3271, + 3299, 3301, 3307, 3313, 3319, 3323, 3329, 3331, + 3343, 3347, 3359, 3361, 3371, 3373, 3389, 3391, + 3407, 3413, 3433, 3449, 3457, 3461, 3463, 3467, + 3469, 3491, 3499, 3511, 3517, 3527, 3529, 3533, + 3539, 3541, 3547, 3557, 3559, 3571, 3581, 3583, + 3593, 3607, 3613, 3617, 3623, 3631, 3637, 3643, + 3659, 3671, 3673, 3677, 3691, 3697, 3701, 3709, + 3719, 3727, 3733, 3739, 3761, 3767, 3769, 3779, + 3793, 3797, 3803, 3821, 3823, 3833, 3847, 3851, + 3853, 3863, 3877, 3881, 3889, 3907, 3911, 3917, + 3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989, + 4001, 4003, 4007, 4013, 4019, 4021, 4027, 4049, + 4051, 4057, 4073, 4079, 4091, 4093, 4099, 4111, + 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177, + 4201, 4211, 4217, 4219, 4229, 4231, 4241, 4243, + 4253, 4259, 4261, 4271, 4273, 4283, 4289, 4297, + 4327, 4337, 4339, 4349, 4357, 4363, 4373, 4391, + 4397, 4409, 4421, 4423, 4441, 4447, 4451, 4457, + 4463, 4481, 4483, 4493, 4507, 4513, 4517, 4519, + 4523, 4547, 4549, 4561, 4567, 4583, 4591, 4597, + 4603, 4621, 4637, 4639, 4643, 4649, 4651, 4657, + 4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, + 4733, 4751, 4759, 4783, 4787, 4789, 4793, 4799, + 4801, 4813, 4817, 4831, 4861, 4871, 4877, 4889, + 4903, 4909, 4919, 4931, 4933, 4937, 4943, 4951, + 4957, 4967, 4969, 4973, 4987, 4993, 4999 + ]; + + + // Miller-Rabin - Miller Rabin algorithm for primality test + // Copyright Fedor Indutny, 2014. + // + // This software is licensed under the MIT License. + // + // 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. + + // Adapted on Jan 2018 from version 4.0.1 at https://github.com/indutny/miller-rabin + + // Sample syntax for Fixed-Base Miller-Rabin: + // millerRabin(n, k, () => new BN(small_primes[Math.random() * small_primes.length | 0])) + + /** + * Tests whether n is probably prime or not using the Miller-Rabin test. + * See HAC Remark 4.28. + * @param {BigInteger} n - Number to test + * @param {Integer} k - Optional number of iterations of Miller-Rabin test + * @param {Function} rand - Optional function to generate potential witnesses + * @returns {boolean} + * @async + */ + async function millerRabin(n, k, rand) { + const BigInteger = await util.getBigInteger(); + const len = n.bitLength(); + + if (!k) { + k = Math.max(1, (len / 48) | 0); + } + + const n1 = n.dec(); // n - 1 + + // Find d and s, (n - 1) = (2 ^ s) * d; + let s = 0; + while (!n1.getBit(s)) { s++; } + const d = n.rightShift(new BigInteger(s)); + + for (; k > 0; k--) { + const a = rand ? rand() : await getRandomBigInteger(new BigInteger(2), n1); + + let x = a.modExp(d, n); + if (x.isOne() || x.equal(n1)) { + continue; + } + + let i; + for (i = 1; i < s; i++) { + x = x.mul(x).mod(n); + + if (x.isOne()) { + return false; + } + if (x.equal(n1)) { + break; + } + } + + if (i === s) { + return false; + } + } + + return true; + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * ASN1 object identifiers for hashes + * @see {@link https://tools.ietf.org/html/rfc4880#section-5.2.2} + */ + const hash_headers = []; + hash_headers[1] = [0x30, 0x20, 0x30, 0x0c, 0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05, 0x05, 0x00, 0x04, + 0x10]; + hash_headers[2] = [0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14]; + hash_headers[3] = [0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, 0x24, 0x03, 0x02, 0x01, 0x05, 0x00, 0x04, 0x14]; + hash_headers[8] = [0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, + 0x04, 0x20]; + hash_headers[9] = [0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, + 0x04, 0x30]; + hash_headers[10] = [0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, + 0x00, 0x04, 0x40]; + hash_headers[11] = [0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x05, + 0x00, 0x04, 0x1C]; + + /** + * Create padding with secure random data + * @private + * @param {Integer} length - Length of the padding in bytes + * @returns {Uint8Array} Random padding. + */ + function getPKCS1Padding(length) { + const result = new Uint8Array(length); + let count = 0; + while (count < length) { + const randomBytes = getRandomBytes(length - count); + for (let i = 0; i < randomBytes.length; i++) { + if (randomBytes[i] !== 0) { + result[count++] = randomBytes[i]; + } + } + } + return result; + } + + /** + * Create a EME-PKCS1-v1_5 padded message + * @see {@link https://tools.ietf.org/html/rfc4880#section-13.1.1|RFC 4880 13.1.1} + * @param {Uint8Array} message - Message to be encoded + * @param {Integer} keyLength - The length in octets of the key modulus + * @returns {Uint8Array} EME-PKCS1 padded message. + */ + function emeEncode(message, keyLength) { + const mLength = message.length; + // length checking + if (mLength > keyLength - 11) { + throw Error('Message too long'); + } + // Generate an octet string PS of length k - mLen - 3 consisting of + // pseudo-randomly generated nonzero octets + const PS = getPKCS1Padding(keyLength - mLength - 3); + // Concatenate PS, the message M, and other padding to form an + // encoded message EM of length k octets as EM = 0x00 || 0x02 || PS || 0x00 || M. + const encoded = new Uint8Array(keyLength); + // 0x00 byte + encoded[1] = 2; + encoded.set(PS, 2); + // 0x00 bytes + encoded.set(message, keyLength - mLength); + return encoded; + } + + /** + * Decode a EME-PKCS1-v1_5 padded message + * @see {@link https://tools.ietf.org/html/rfc4880#section-13.1.2|RFC 4880 13.1.2} + * @param {Uint8Array} encoded - Encoded message bytes + * @param {Uint8Array} randomPayload - Data to return in case of decoding error (needed for constant-time processing) + * @returns {Uint8Array} decoded data or `randomPayload` (on error, if given) + * @throws {Error} on decoding failure, unless `randomPayload` is provided + */ + function emeDecode(encoded, randomPayload) { + // encoded format: 0x00 0x02 0x00 + let offset = 2; + let separatorNotFound = 1; + for (let j = offset; j < encoded.length; j++) { + separatorNotFound &= encoded[j] !== 0; + offset += separatorNotFound; + } + + const psLen = offset - 2; + const payload = encoded.subarray(offset + 1); // discard the 0x00 separator + const isValidPadding = encoded[0] === 0 & encoded[1] === 2 & psLen >= 8 & !separatorNotFound; + + if (randomPayload) { + return util.selectUint8Array(isValidPadding, payload, randomPayload); + } + + if (isValidPadding) { + return payload; + } + + throw Error('Decryption error'); + } + + /** + * Create a EMSA-PKCS1-v1_5 padded message + * @see {@link https://tools.ietf.org/html/rfc4880#section-13.1.3|RFC 4880 13.1.3} + * @param {Integer} algo - Hash algorithm type used + * @param {Uint8Array} hashed - Message to be encoded + * @param {Integer} emLen - Intended length in octets of the encoded message + * @returns {Uint8Array} Encoded message. + */ + async function emsaEncode(algo, hashed, emLen) { + let i; + if (hashed.length !== hash.getHashByteLength(algo)) { + throw Error('Invalid hash length'); + } + // produce an ASN.1 DER value for the hash function used. + // Let T be the full hash prefix + const hashPrefix = new Uint8Array(hash_headers[algo].length); + for (i = 0; i < hash_headers[algo].length; i++) { + hashPrefix[i] = hash_headers[algo][i]; + } + // and let tLen be the length in octets prefix and hashed data + const tLen = hashPrefix.length + hashed.length; + if (emLen < tLen + 11) { + throw Error('Intended encoded message length too short'); + } + // an octet string PS consisting of emLen - tLen - 3 octets with hexadecimal value 0xFF + // The length of PS will be at least 8 octets + const PS = new Uint8Array(emLen - tLen - 3).fill(0xff); + + // Concatenate PS, the hash prefix, hashed data, and other padding to form the + // encoded message EM as EM = 0x00 || 0x01 || PS || 0x00 || prefix || hashed + const EM = new Uint8Array(emLen); + EM[1] = 0x01; + EM.set(PS, 2); + EM.set(hashPrefix, emLen - tLen); + EM.set(hashed, emLen - hashed.length); + return EM; + } + + var pkcs1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + emeEncode: emeEncode, + emeDecode: emeDecode, + emsaEncode: emsaEncode + }); + + // GPG4Browsers - An OpenPGP implementation in javascript + + const webCrypto$4 = util.getWebCrypto(); + + /** Create signature + * @param {module:enums.hash} hashAlgo - Hash algorithm + * @param {Uint8Array} data - Message + * @param {Uint8Array} n - RSA public modulus + * @param {Uint8Array} e - RSA public exponent + * @param {Uint8Array} d - RSA private exponent + * @param {Uint8Array} p - RSA private prime p + * @param {Uint8Array} q - RSA private prime q + * @param {Uint8Array} u - RSA private coefficient + * @param {Uint8Array} hashed - Hashed message + * @returns {Promise} RSA Signature. + * @async + */ + async function sign$6(hashAlgo, data, n, e, d, p, q, u, hashed) { + if (data && !util.isStream(data)) { + if (util.getWebCrypto()) { + try { + return await webSign$1(enums.read(enums.webHash, hashAlgo), data, n, e, d, p, q, u); + } catch (err) { + console.error(err); + } + } + } + return bnSign(hashAlgo, n, d, hashed); + } + + /** + * Verify signature + * @param {module:enums.hash} hashAlgo - Hash algorithm + * @param {Uint8Array} data - Message + * @param {Uint8Array} s - Signature + * @param {Uint8Array} n - RSA public modulus + * @param {Uint8Array} e - RSA public exponent + * @param {Uint8Array} hashed - Hashed message + * @returns {Boolean} + * @async + */ + async function verify$6(hashAlgo, data, s, n, e, hashed) { + if (data && !util.isStream(data)) { + if (util.getWebCrypto()) { + try { + return await webVerify$1(enums.read(enums.webHash, hashAlgo), data, s, n, e); + } catch (err) { + console.error(err); + } + } + } + return bnVerify(hashAlgo, s, n, e, hashed); + } + + /** + * Encrypt message + * @param {Uint8Array} data - Message + * @param {Uint8Array} n - RSA public modulus + * @param {Uint8Array} e - RSA public exponent + * @returns {Promise} RSA Ciphertext. + * @async + */ + async function encrypt$4(data, n, e) { + return bnEncrypt(data, n, e); + } + + /** + * Decrypt RSA message + * @param {Uint8Array} m - Message + * @param {Uint8Array} n - RSA public modulus + * @param {Uint8Array} e - RSA public exponent + * @param {Uint8Array} d - RSA private exponent + * @param {Uint8Array} p - RSA private prime p + * @param {Uint8Array} q - RSA private prime q + * @param {Uint8Array} u - RSA private coefficient + * @param {Uint8Array} randomPayload - Data to return on decryption error, instead of throwing + * (needed for constant-time processing) + * @returns {Promise} RSA Plaintext. + * @throws {Error} on decryption error, unless `randomPayload` is given + * @async + */ + async function decrypt$4(data, n, e, d, p, q, u, randomPayload) { + return bnDecrypt(data, n, e, d, p, q, u, randomPayload); + } + + /** + * Generate a new random private key B bits long with public exponent E. + * + * When possible, webCrypto or nodeCrypto is used. Otherwise, primes are generated using + * 40 rounds of the Miller-Rabin probabilistic random prime generation algorithm. + * @see module:crypto/public_key/prime + * @param {Integer} bits - RSA bit length + * @param {Integer} e - RSA public exponent + * @returns {{n, e, d, + * p, q ,u: Uint8Array}} RSA public modulus, RSA public exponent, RSA private exponent, + * RSA private prime p, RSA private prime q, u = p ** -1 mod q + * @async + */ + async function generate$4(bits, e) { + const BigInteger = await util.getBigInteger(); + + e = new BigInteger(e); + + // Native RSA keygen using Web Crypto + if (util.getWebCrypto()) { + const keyGenOpt = { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: bits, // the specified keysize in bits + publicExponent: e.toUint8Array(), // take three bytes (max 65537) for exponent + hash: { + name: 'SHA-1' // not required for actual RSA keys, but for crypto api 'sign' and 'verify' + } + }; + const keyPair = await webCrypto$4.generateKey(keyGenOpt, true, ['sign', 'verify']); + + // export the generated keys as JsonWebKey (JWK) + // https://tools.ietf.org/html/draft-ietf-jose-json-web-key-33 + const jwk = await webCrypto$4.exportKey('jwk', keyPair.privateKey); + // map JWK parameters to corresponding OpenPGP names + return { + n: b64ToUint8Array(jwk.n), + e: e.toUint8Array(), + d: b64ToUint8Array(jwk.d), + // switch p and q + p: b64ToUint8Array(jwk.q), + q: b64ToUint8Array(jwk.p), + // Since p and q are switched in places, u is the inverse of jwk.q + u: b64ToUint8Array(jwk.qi) + }; + } + + // RSA keygen fallback using 40 iterations of the Miller-Rabin test + // See https://stackoverflow.com/a/6330138 for justification + // Also see section C.3 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST + let p; + let q; + let n; + do { + q = await randomProbablePrime(bits - (bits >> 1), e, 40); + p = await randomProbablePrime(bits >> 1, e, 40); + n = p.mul(q); + } while (n.bitLength() !== bits); + + const phi = p.dec().imul(q.dec()); + + if (q.lt(p)) { + [p, q] = [q, p]; + } + + return { + n: n.toUint8Array(), + e: e.toUint8Array(), + d: e.modInv(phi).toUint8Array(), + p: p.toUint8Array(), + q: q.toUint8Array(), + // dp: d.mod(p.subn(1)), + // dq: d.mod(q.subn(1)), + u: p.modInv(q).toUint8Array() + }; + } + + /** + * Validate RSA parameters + * @param {Uint8Array} n - RSA public modulus + * @param {Uint8Array} e - RSA public exponent + * @param {Uint8Array} d - RSA private exponent + * @param {Uint8Array} p - RSA private prime p + * @param {Uint8Array} q - RSA private prime q + * @param {Uint8Array} u - RSA inverse of p w.r.t. q + * @returns {Promise} Whether params are valid. + * @async + */ + async function validateParams$8(n, e, d, p, q, u) { + const BigInteger = await util.getBigInteger(); + n = new BigInteger(n); + p = new BigInteger(p); + q = new BigInteger(q); + + // expect pq = n + if (!p.mul(q).equal(n)) { + return false; + } + + const two = new BigInteger(2); + // expect p*u = 1 mod q + u = new BigInteger(u); + if (!p.mul(u).mod(q).isOne()) { + return false; + } + + e = new BigInteger(e); + d = new BigInteger(d); + /** + * In RSA pkcs#1 the exponents (d, e) are inverses modulo lcm(p-1, q-1) + * We check that [de = 1 mod (p-1)] and [de = 1 mod (q-1)] + * By CRT on coprime factors of (p-1, q-1) it follows that [de = 1 mod lcm(p-1, q-1)] + * + * We blind the multiplication with r, and check that rde = r mod lcm(p-1, q-1) + */ + const nSizeOver3 = new BigInteger(Math.floor(n.bitLength() / 3)); + const r = await getRandomBigInteger(two, two.leftShift(nSizeOver3)); // r in [ 2, 2^{|n|/3} ) < p and q + const rde = r.mul(d).mul(e); + + const areInverses = rde.mod(p.dec()).equal(r) && rde.mod(q.dec()).equal(r); + if (!areInverses) { + return false; + } + + return true; + } + + async function bnSign(hashAlgo, n, d, hashed) { + const BigInteger = await util.getBigInteger(); + n = new BigInteger(n); + const m = new BigInteger(await emsaEncode(hashAlgo, hashed, n.byteLength())); + d = new BigInteger(d); + if (m.gte(n)) { + throw Error('Message size cannot exceed modulus size'); + } + return m.modExp(d, n).toUint8Array('be', n.byteLength()); + } + + async function webSign$1(hashName, data, n, e, d, p, q, u) { + /** OpenPGP keys require that p < q, and Safari Web Crypto requires that p > q. + * We swap them in privateToJWK, so it usually works out, but nevertheless, + * not all OpenPGP keys are compatible with this requirement. + * OpenPGP.js used to generate RSA keys the wrong way around (p > q), and still + * does if the underlying Web Crypto does so (though the tested implementations + * don't do so). + */ + const jwk = await privateToJWK$1(n, e, d, p, q, u); + const algo = { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: hashName } + }; + const key = await webCrypto$4.importKey('jwk', jwk, algo, false, ['sign']); + return new Uint8Array(await webCrypto$4.sign('RSASSA-PKCS1-v1_5', key, data)); + } + + async function bnVerify(hashAlgo, s, n, e, hashed) { + const BigInteger = await util.getBigInteger(); + n = new BigInteger(n); + s = new BigInteger(s); + e = new BigInteger(e); + if (s.gte(n)) { + throw Error('Signature size cannot exceed modulus size'); + } + const EM1 = s.modExp(e, n).toUint8Array('be', n.byteLength()); + const EM2 = await emsaEncode(hashAlgo, hashed, n.byteLength()); + return util.equalsUint8Array(EM1, EM2); + } + + async function webVerify$1(hashName, data, s, n, e) { + const jwk = publicToJWK(n, e); + const key = await webCrypto$4.importKey('jwk', jwk, { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: hashName } + }, false, ['verify']); + return webCrypto$4.verify('RSASSA-PKCS1-v1_5', key, s, data); + } + + async function bnEncrypt(data, n, e) { + const BigInteger = await util.getBigInteger(); + n = new BigInteger(n); + data = new BigInteger(emeEncode(data, n.byteLength())); + e = new BigInteger(e); + if (data.gte(n)) { + throw Error('Message size cannot exceed modulus size'); + } + return data.modExp(e, n).toUint8Array('be', n.byteLength()); + } + + async function bnDecrypt(data, n, e, d, p, q, u, randomPayload) { + const BigInteger = await util.getBigInteger(); + data = new BigInteger(data); + n = new BigInteger(n); + e = new BigInteger(e); + d = new BigInteger(d); + p = new BigInteger(p); + q = new BigInteger(q); + u = new BigInteger(u); + if (data.gte(n)) { + throw Error('Data too large.'); + } + const dq = d.mod(q.dec()); // d mod (q-1) + const dp = d.mod(p.dec()); // d mod (p-1) + + const unblinder = (await getRandomBigInteger(new BigInteger(2), n)).mod(n); + const blinder = unblinder.modInv(n).modExp(e, n); + data = data.mul(blinder).mod(n); + + + const mp = data.modExp(dp, p); // data**{d mod (q-1)} mod p + const mq = data.modExp(dq, q); // data**{d mod (p-1)} mod q + const h = u.mul(mq.sub(mp)).mod(q); // u * (mq-mp) mod q (operands already < q) + + let result = h.mul(p).add(mp); // result < n due to relations above + + result = result.mul(unblinder).mod(n); + + + return emeDecode(result.toUint8Array('be', n.byteLength()), randomPayload); + } + + /** Convert Openpgp private key params to jwk key according to + * @link https://tools.ietf.org/html/rfc7517 + * @param {String} hashAlgo + * @param {Uint8Array} n + * @param {Uint8Array} e + * @param {Uint8Array} d + * @param {Uint8Array} p + * @param {Uint8Array} q + * @param {Uint8Array} u + */ + async function privateToJWK$1(n, e, d, p, q, u) { + const BigInteger = await util.getBigInteger(); + const pNum = new BigInteger(p); + const qNum = new BigInteger(q); + const dNum = new BigInteger(d); + + let dq = dNum.mod(qNum.dec()); // d mod (q-1) + let dp = dNum.mod(pNum.dec()); // d mod (p-1) + dp = dp.toUint8Array(); + dq = dq.toUint8Array(); + return { + kty: 'RSA', + n: uint8ArrayToB64(n, true), + e: uint8ArrayToB64(e, true), + d: uint8ArrayToB64(d, true), + // switch p and q + p: uint8ArrayToB64(q, true), + q: uint8ArrayToB64(p, true), + // switch dp and dq + dp: uint8ArrayToB64(dq, true), + dq: uint8ArrayToB64(dp, true), + qi: uint8ArrayToB64(u, true), + ext: true + }; + } + + /** Convert Openpgp key public params to jwk key according to + * @link https://tools.ietf.org/html/rfc7517 + * @param {String} hashAlgo + * @param {Uint8Array} n + * @param {Uint8Array} e + */ + function publicToJWK(n, e) { + return { + kty: 'RSA', + n: uint8ArrayToB64(n, true), + e: uint8ArrayToB64(e, true), + ext: true + }; + } + + var rsa = /*#__PURE__*/Object.freeze({ + __proto__: null, + sign: sign$6, + verify: verify$6, + encrypt: encrypt$4, + decrypt: decrypt$4, + generate: generate$4, + validateParams: validateParams$8 + }); + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * ElGamal Encryption function + * Note that in OpenPGP, the message needs to be padded with PKCS#1 (same as RSA) + * @param {Uint8Array} data - To be padded and encrypted + * @param {Uint8Array} p + * @param {Uint8Array} g + * @param {Uint8Array} y + * @returns {Promise<{ c1: Uint8Array, c2: Uint8Array }>} + * @async + */ + async function encrypt$3(data, p, g, y) { + const BigInteger = await util.getBigInteger(); + p = new BigInteger(p); + g = new BigInteger(g); + y = new BigInteger(y); + + const padded = emeEncode(data, p.byteLength()); + const m = new BigInteger(padded); + + // OpenPGP uses a "special" version of ElGamal where g is generator of the full group Z/pZ* + // hence g has order p-1, and to avoid that k = 0 mod p-1, we need to pick k in [1, p-2] + const k = await getRandomBigInteger(new BigInteger(1), p.dec()); + return { + c1: g.modExp(k, p).toUint8Array(), + c2: y.modExp(k, p).imul(m).imod(p).toUint8Array() + }; + } + + /** + * ElGamal Encryption function + * @param {Uint8Array} c1 + * @param {Uint8Array} c2 + * @param {Uint8Array} p + * @param {Uint8Array} x + * @param {Uint8Array} randomPayload - Data to return on unpadding error, instead of throwing + * (needed for constant-time processing) + * @returns {Promise} Unpadded message. + * @throws {Error} on decryption error, unless `randomPayload` is given + * @async + */ + async function decrypt$3(c1, c2, p, x, randomPayload) { + const BigInteger = await util.getBigInteger(); + c1 = new BigInteger(c1); + c2 = new BigInteger(c2); + p = new BigInteger(p); + x = new BigInteger(x); + + const padded = c1.modExp(x, p).modInv(p).imul(c2).imod(p); + return emeDecode(padded.toUint8Array('be', p.byteLength()), randomPayload); + } + + /** + * Validate ElGamal parameters + * @param {Uint8Array} p - ElGamal prime + * @param {Uint8Array} g - ElGamal group generator + * @param {Uint8Array} y - ElGamal public key + * @param {Uint8Array} x - ElGamal private exponent + * @returns {Promise} Whether params are valid. + * @async + */ + async function validateParams$7(p, g, y, x) { + const BigInteger = await util.getBigInteger(); + p = new BigInteger(p); + g = new BigInteger(g); + y = new BigInteger(y); + + const one = new BigInteger(1); + // Check that 1 < g < p + if (g.lte(one) || g.gte(p)) { + return false; + } + + // Expect p-1 to be large + const pSize = new BigInteger(p.bitLength()); + const n1023 = new BigInteger(1023); + if (pSize.lt(n1023)) { + return false; + } + + /** + * g should have order p-1 + * Check that g ** (p-1) = 1 mod p + */ + if (!g.modExp(p.dec(), p).isOne()) { + return false; + } + + /** + * Since p-1 is not prime, g might have a smaller order that divides p-1 + * We want to make sure that the order is large enough to hinder a small subgroup attack + * + * We just check g**i != 1 for all i up to a threshold + */ + let res = g; + const i = new BigInteger(1); + const threshold = new BigInteger(2).leftShift(new BigInteger(17)); // we want order > threshold + while (i.lt(threshold)) { + res = res.mul(g).imod(p); + if (res.isOne()) { + return false; + } + i.iinc(); + } + + /** + * Re-derive public key y' = g ** x mod p + * Expect y == y' + * + * Blinded exponentiation computes g**{r(p-1) + x} to compare to y + */ + x = new BigInteger(x); + const two = new BigInteger(2); + const r = await getRandomBigInteger(two.leftShift(pSize.dec()), two.leftShift(pSize)); // draw r of same size as p-1 + const rqx = p.dec().imul(r).iadd(x); + if (!y.equal(g.modExp(rqx, p))) { + return false; + } + + return true; + } + + var elgamal = /*#__PURE__*/Object.freeze({ + __proto__: null, + encrypt: encrypt$3, + decrypt: decrypt$3, + validateParams: validateParams$7 + }); + + // OpenPGP.js - An OpenPGP implementation in javascript + + class OID { + constructor(oid) { + if (oid instanceof OID) { + this.oid = oid.oid; + } else if (util.isArray(oid) || + util.isUint8Array(oid)) { + oid = new Uint8Array(oid); + if (oid[0] === 0x06) { // DER encoded oid byte array + if (oid[1] !== oid.length - 2) { + throw Error('Length mismatch in DER encoded oid'); + } + oid = oid.subarray(2); + } + this.oid = oid; + } else { + this.oid = ''; + } + } + + /** + * Method to read an OID object + * @param {Uint8Array} input - Where to read the OID from + * @returns {Number} Number of read bytes. + */ + read(input) { + if (input.length >= 1) { + const length = input[0]; + if (input.length >= 1 + length) { + this.oid = input.subarray(1, 1 + length); + return 1 + this.oid.length; + } + } + throw Error('Invalid oid'); + } + + /** + * Serialize an OID object + * @returns {Uint8Array} Array with the serialized value the OID. + */ + write() { + return util.concatUint8Array([new Uint8Array([this.oid.length]), this.oid]); + } + + /** + * Serialize an OID object as a hex string + * @returns {string} String with the hex value of the OID. + */ + toHex() { + return util.uint8ArrayToHex(this.oid); + } + + /** + * If a known curve object identifier, return the canonical name of the curve + * @returns {string} String with the canonical name of the curve. + */ + getName() { + const hex = this.toHex(); + if (enums.curve[hex]) { + return enums.write(enums.curve, hex); + } else { + throw Error('Unknown curve object identifier.'); + } + } + } + + // OpenPGP.js - An OpenPGP implementation in javascript + + function keyFromPrivate(indutnyCurve, priv) { + const keyPair = indutnyCurve.keyPair({ priv: priv }); + return keyPair; + } + + function keyFromPublic(indutnyCurve, pub) { + const keyPair = indutnyCurve.keyPair({ pub: pub }); + if (keyPair.validate().result !== true) { + throw Error('Invalid elliptic public key'); + } + return keyPair; + } + + async function getIndutnyCurve(name) { + if (!config.useIndutnyElliptic) { + throw Error('This curve is only supported in the full build of OpenPGP.js'); + } + const { default: elliptic$1 } = await Promise.resolve().then(function () { return elliptic; }); + return new elliptic$1.ec(name); + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + function readSimpleLength(bytes) { + let len = 0; + let offset; + const type = bytes[0]; + + + if (type < 192) { + [len] = bytes; + offset = 1; + } else if (type < 255) { + len = ((bytes[0] - 192) << 8) + (bytes[1]) + 192; + offset = 2; + } else if (type === 255) { + len = util.readNumber(bytes.subarray(1, 1 + 4)); + offset = 5; + } + + return { + len: len, + offset: offset + }; + } + + /** + * Encodes a given integer of length to the openpgp length specifier to a + * string + * + * @param {Integer} length - The length to encode + * @returns {Uint8Array} String with openpgp length representation. + */ + function writeSimpleLength(length) { + if (length < 192) { + return new Uint8Array([length]); + } else if (length > 191 && length < 8384) { + /* + * let a = (total data packet length) - 192 let bc = two octet + * representation of a let d = b + 192 + */ + return new Uint8Array([((length - 192) >> 8) + 192, (length - 192) & 0xFF]); + } + return util.concatUint8Array([new Uint8Array([255]), util.writeNumber(length, 4)]); + } + + function writePartialLength(power) { + if (power < 0 || power > 30) { + throw Error('Partial Length power must be between 1 and 30'); + } + return new Uint8Array([224 + power]); + } + + function writeTag(tag_type) { + /* we're only generating v4 packet headers here */ + return new Uint8Array([0xC0 | tag_type]); + } + + /** + * Writes a packet header version 4 with the given tag_type and length to a + * string + * + * @param {Integer} tag_type - Tag type + * @param {Integer} length - Length of the payload + * @returns {String} String of the header. + */ + function writeHeader(tag_type, length) { + /* we're only generating v4 packet headers here */ + return util.concatUint8Array([writeTag(tag_type), writeSimpleLength(length)]); + } + + /** + * Whether the packet type supports partial lengths per RFC4880 + * @param {Integer} tag - Tag type + * @returns {Boolean} String of the header. + */ + function supportsStreaming(tag) { + return [ + enums.packet.literalData, + enums.packet.compressedData, + enums.packet.symmetricallyEncryptedData, + enums.packet.symEncryptedIntegrityProtectedData, + enums.packet.aeadEncryptedData + ].includes(tag); + } + + /** + * Generic static Packet Parser function + * + * @param {Uint8Array | ReadableStream} input - Input stream as string + * @param {Function} callback - Function to call with the parsed packet + * @returns {Boolean} Returns false if the stream was empty and parsing is done, and true otherwise. + */ + async function readPackets(input, callback) { + const reader = getReader(input); + let writer; + let callbackReturned; + try { + const peekedBytes = await reader.peekBytes(2); + // some sanity checks + if (!peekedBytes || peekedBytes.length < 2 || (peekedBytes[0] & 0x80) === 0) { + throw Error('Error during parsing. This message / key probably does not conform to a valid OpenPGP format.'); + } + const headerByte = await reader.readByte(); + let tag = -1; + let format = -1; + let packetLength; + + format = 0; // 0 = old format; 1 = new format + if ((headerByte & 0x40) !== 0) { + format = 1; + } + + let packetLengthType; + if (format) { + // new format header + tag = headerByte & 0x3F; // bit 5-0 + } else { + // old format header + tag = (headerByte & 0x3F) >> 2; // bit 5-2 + packetLengthType = headerByte & 0x03; // bit 1-0 + } + + const packetSupportsStreaming = supportsStreaming(tag); + let packet = null; + if (packetSupportsStreaming) { + if (util.isStream(input) === 'array') { + const arrayStream = new ArrayStream(); + writer = getWriter(arrayStream); + packet = arrayStream; + } else { + const transform = new TransformStream$1(); + writer = getWriter(transform.writable); + packet = transform.readable; + } + // eslint-disable-next-line callback-return + callbackReturned = callback({ tag, packet }); + } else { + packet = []; + } + + let wasPartialLength; + do { + if (!format) { + // 4.2.1. Old Format Packet Lengths + switch (packetLengthType) { + case 0: + // The packet has a one-octet length. The header is 2 octets + // long. + packetLength = await reader.readByte(); + break; + case 1: + // The packet has a two-octet length. The header is 3 octets + // long. + packetLength = (await reader.readByte() << 8) | await reader.readByte(); + break; + case 2: + // The packet has a four-octet length. The header is 5 + // octets long. + packetLength = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << + 8) | await reader.readByte(); + break; + default: + // 3 - The packet is of indeterminate length. The header is 1 + // octet long, and the implementation must determine how long + // the packet is. If the packet is in a file, this means that + // the packet extends until the end of the file. In general, + // an implementation SHOULD NOT use indeterminate-length + // packets except where the end of the data will be clear + // from the context, and even then it is better to use a + // definite length, or a new format header. The new format + // headers described below have a mechanism for precisely + // encoding data of indeterminate length. + packetLength = Infinity; + break; + } + } else { // 4.2.2. New Format Packet Lengths + // 4.2.2.1. One-Octet Lengths + const lengthByte = await reader.readByte(); + wasPartialLength = false; + if (lengthByte < 192) { + packetLength = lengthByte; + // 4.2.2.2. Two-Octet Lengths + } else if (lengthByte >= 192 && lengthByte < 224) { + packetLength = ((lengthByte - 192) << 8) + (await reader.readByte()) + 192; + // 4.2.2.4. Partial Body Lengths + } else if (lengthByte > 223 && lengthByte < 255) { + packetLength = 1 << (lengthByte & 0x1F); + wasPartialLength = true; + if (!packetSupportsStreaming) { + throw new TypeError('This packet type does not support partial lengths.'); + } + // 4.2.2.3. Five-Octet Lengths + } else { + packetLength = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << + 8) | await reader.readByte(); + } + } + if (packetLength > 0) { + let bytesRead = 0; + while (true) { + if (writer) await writer.ready; + const { done, value } = await reader.read(); + if (done) { + if (packetLength === Infinity) break; + throw Error('Unexpected end of packet'); + } + const chunk = packetLength === Infinity ? value : value.subarray(0, packetLength - bytesRead); + if (writer) await writer.write(chunk); + else packet.push(chunk); + bytesRead += value.length; + if (bytesRead >= packetLength) { + reader.unshift(value.subarray(packetLength - bytesRead + value.length)); + break; + } + } + } + } while (wasPartialLength); + + // If this was not a packet that "supports streaming", we peek to check + // whether it is the last packet in the message. We peek 2 bytes instead + // of 1 because the beginning of this function also peeks 2 bytes, and we + // want to cut a `subarray` of the correct length into `web-stream-tools`' + // `externalBuffer` as a tiny optimization here. + // + // If it *was* a streaming packet (i.e. the data packets), we peek at the + // entire remainder of the stream, in order to forward errors in the + // remainder of the stream to the packet data. (Note that this means we + // read/peek at all signature packets before closing the literal data + // packet, for example.) This forwards MDC errors to the literal data + // stream, for example, so that they don't get lost / forgotten on + // decryptedMessage.packets.stream, which we never look at. + // + // An example of what we do when stream-parsing a message containing + // [ one-pass signature packet, literal data packet, signature packet ]: + // 1. Read the one-pass signature packet + // 2. Peek 2 bytes of the literal data packet + // 3. Parse the one-pass signature packet + // + // 4. Read the literal data packet, simultaneously stream-parsing it + // 5. Peek until the end of the message + // 6. Finish parsing the literal data packet + // + // 7. Read the signature packet again (we already peeked at it in step 5) + // 8. Peek at the end of the stream again (`peekBytes` returns undefined) + // 9. Parse the signature packet + // + // Note that this means that if there's an error in the very end of the + // stream, such as an MDC error, we throw in step 5 instead of in step 8 + // (or never), which is the point of this exercise. + const nextPacket = await reader.peekBytes(packetSupportsStreaming ? Infinity : 2); + if (writer) { + await writer.ready; + await writer.close(); + } else { + packet = util.concatUint8Array(packet); + // eslint-disable-next-line callback-return + await callback({ tag, packet }); + } + return !nextPacket || !nextPacket.length; + } catch (e) { + if (writer) { + await writer.abort(e); + return true; + } else { + throw e; + } + } finally { + if (writer) { + await callbackReturned; + } + reader.releaseLock(); + } + } + + class UnsupportedError extends Error { + constructor(...params) { + super(...params); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, UnsupportedError); + } + + this.name = 'UnsupportedError'; + } + } + + class UnparseablePacket { + constructor(tag, rawContent) { + this.tag = tag; + this.rawContent = rawContent; + } + + write() { + return this.rawContent; + } + } + + // OpenPGP.js - An OpenPGP implementation in javascript + + const webCrypto$3 = util.getWebCrypto(); + + const webCurves = { + 'p256': 'P-256', + 'p384': 'P-384', + 'p521': 'P-521' + }; + + const curves = { + p256: { + oid: [0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07], + keyType: enums.publicKey.ecdsa, + hash: enums.hash.sha256, + cipher: enums.symmetric.aes128, + web: webCurves.p256, + payloadSize: 32, + sharedSize: 256 + }, + p384: { + oid: [0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22], + keyType: enums.publicKey.ecdsa, + hash: enums.hash.sha384, + cipher: enums.symmetric.aes192, + web: webCurves.p384, + payloadSize: 48, + sharedSize: 384 + }, + p521: { + oid: [0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x23], + keyType: enums.publicKey.ecdsa, + hash: enums.hash.sha512, + cipher: enums.symmetric.aes256, + web: webCurves.p521, + payloadSize: 66, + sharedSize: 528 + }, + secp256k1: { + oid: [0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x0A], + keyType: enums.publicKey.ecdsa, + hash: enums.hash.sha256, + cipher: enums.symmetric.aes128, + payloadSize: 32 + }, + ed25519: { + oid: [0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01], + keyType: enums.publicKey.eddsaLegacy, + hash: enums.hash.sha512, + payloadSize: 32 + }, + curve25519: { + oid: [0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01], + keyType: enums.publicKey.ecdh, + hash: enums.hash.sha256, + cipher: enums.symmetric.aes128, + payloadSize: 32 + }, + brainpoolP256r1: { + oid: [0x06, 0x09, 0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07], + keyType: enums.publicKey.ecdsa, + hash: enums.hash.sha256, + cipher: enums.symmetric.aes128, + payloadSize: 32 + }, + brainpoolP384r1: { + oid: [0x06, 0x09, 0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0B], + keyType: enums.publicKey.ecdsa, + hash: enums.hash.sha384, + cipher: enums.symmetric.aes192, + payloadSize: 48 + }, + brainpoolP512r1: { + oid: [0x06, 0x09, 0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0D], + keyType: enums.publicKey.ecdsa, + hash: enums.hash.sha512, + cipher: enums.symmetric.aes256, + payloadSize: 64 + } + }; + + class CurveWithOID { + constructor(oidOrName, params) { + try { + if (util.isArray(oidOrName) || + util.isUint8Array(oidOrName)) { + // by oid byte array + oidOrName = new OID(oidOrName); + } + if (oidOrName instanceof OID) { + // by curve OID + oidOrName = oidOrName.getName(); + } + // by curve name or oid string + this.name = enums.write(enums.curve, oidOrName); + } catch (err) { + throw new UnsupportedError('Unknown curve'); + } + params = params || curves[this.name]; + + this.keyType = params.keyType; + + this.oid = params.oid; + this.hash = params.hash; + this.cipher = params.cipher; + this.web = params.web && curves[this.name]; + this.payloadSize = params.payloadSize; + if (this.web && util.getWebCrypto()) { + this.type = 'web'; + } else if (this.name === 'curve25519') { + this.type = 'curve25519'; + } else if (this.name === 'ed25519') { + this.type = 'ed25519'; + } + } + + async genKeyPair() { + let keyPair; + switch (this.type) { + case 'web': + try { + return await webGenKeyPair(this.name); + } catch (err) { + console.error('Browser did not support generating ec key ' + err.message); + break; + } + case 'curve25519': { + const privateKey = getRandomBytes(32); + privateKey[0] = (privateKey[0] & 127) | 64; + privateKey[31] &= 248; + const secretKey = privateKey.slice().reverse(); + keyPair = naclFastLight.box.keyPair.fromSecretKey(secretKey); + const publicKey = util.concatUint8Array([new Uint8Array([0x40]), keyPair.publicKey]); + return { publicKey, privateKey }; + } + case 'ed25519': { + const privateKey = getRandomBytes(32); + const keyPair = naclFastLight.sign.keyPair.fromSeed(privateKey); + const publicKey = util.concatUint8Array([new Uint8Array([0x40]), keyPair.publicKey]); + return { publicKey, privateKey }; + } + } + const indutnyCurve = await getIndutnyCurve(this.name); + keyPair = await indutnyCurve.genKeyPair({ + entropy: util.uint8ArrayToString(getRandomBytes(32)) + }); + return { publicKey: new Uint8Array(keyPair.getPublic('array', false)), privateKey: keyPair.getPrivate().toArrayLike(Uint8Array) }; + } + } + + async function generate$3(curve) { + const BigInteger = await util.getBigInteger(); + + curve = new CurveWithOID(curve); + const keyPair = await curve.genKeyPair(); + const Q = new BigInteger(keyPair.publicKey).toUint8Array(); + const secret = new BigInteger(keyPair.privateKey).toUint8Array('be', curve.payloadSize); + return { + oid: curve.oid, + Q, + secret, + hash: curve.hash, + cipher: curve.cipher + }; + } + + /** + * Get preferred hash algo to use with the given curve + * @param {module:type/oid} oid - curve oid + * @returns {enums.hash} hash algorithm + */ + function getPreferredHashAlgo$2(oid) { + return curves[enums.write(enums.curve, oid.toHex())].hash; + } + + /** + * Validate ECDH and ECDSA parameters + * Not suitable for EdDSA (different secret key format) + * @param {module:enums.publicKey} algo - EC algorithm, to filter supported curves + * @param {module:type/oid} oid - EC object identifier + * @param {Uint8Array} Q - EC public point + * @param {Uint8Array} d - EC secret scalar + * @returns {Promise} Whether params are valid. + * @async + */ + async function validateStandardParams(algo, oid, Q, d) { + const supportedCurves = { + p256: true, + p384: true, + p521: true, + secp256k1: true, + curve25519: algo === enums.publicKey.ecdh, + brainpoolP256r1: true, + brainpoolP384r1: true, + brainpoolP512r1: true + }; + + // Check whether the given curve is supported + const curveName = oid.getName(); + if (!supportedCurves[curveName]) { + return false; + } + + if (curveName === 'curve25519') { + d = d.slice().reverse(); + // Re-derive public point Q' + const { publicKey } = naclFastLight.box.keyPair.fromSecretKey(d); + + Q = new Uint8Array(Q); + const dG = new Uint8Array([0x40, ...publicKey]); // Add public key prefix + if (!util.equalsUint8Array(dG, Q)) { + return false; + } + + return true; + } + + const curve = await getIndutnyCurve(curveName); + try { + // Parse Q and check that it is on the curve but not at infinity + Q = keyFromPublic(curve, Q).getPublic(); + } catch (validationErrors) { + return false; + } + + /** + * Re-derive public point Q' = dG from private key + * Expect Q == Q' + */ + const dG = keyFromPrivate(curve, d).getPublic(); + if (!dG.eq(Q)) { + return false; + } + + return true; + } + + ////////////////////////// + // // + // Helper functions // + // // + ////////////////////////// + + + async function webGenKeyPair(name) { + // Note: keys generated with ECDSA and ECDH are structurally equivalent + const webCryptoKey = await webCrypto$3.generateKey({ name: 'ECDSA', namedCurve: webCurves[name] }, true, ['sign', 'verify']); + + const privateKey = await webCrypto$3.exportKey('jwk', webCryptoKey.privateKey); + const publicKey = await webCrypto$3.exportKey('jwk', webCryptoKey.publicKey); + + return { + publicKey: jwkToRawPublic(publicKey), + privateKey: b64ToUint8Array(privateKey.d) + }; + } + + ////////////////////////// + // // + // Helper functions // + // // + ////////////////////////// + + /** + * @param {JsonWebKey} jwk - key for conversion + * + * @returns {Uint8Array} Raw public key. + */ + function jwkToRawPublic(jwk) { + const bufX = b64ToUint8Array(jwk.x); + const bufY = b64ToUint8Array(jwk.y); + const publicKey = new Uint8Array(bufX.length + bufY.length + 1); + publicKey[0] = 0x04; + publicKey.set(bufX, 1); + publicKey.set(bufY, bufX.length + 1); + return publicKey; + } + + /** + * @param {Integer} payloadSize - ec payload size + * @param {String} name - curve name + * @param {Uint8Array} publicKey - public key + * + * @returns {JsonWebKey} Public key in jwk format. + */ + function rawPublicToJWK(payloadSize, name, publicKey) { + const len = payloadSize; + const bufX = publicKey.slice(1, len + 1); + const bufY = publicKey.slice(len + 1, len * 2 + 1); + // https://www.rfc-editor.org/rfc/rfc7518.txt + const jwk = { + kty: 'EC', + crv: name, + x: uint8ArrayToB64(bufX, true), + y: uint8ArrayToB64(bufY, true), + ext: true + }; + return jwk; + } + + /** + * @param {Integer} payloadSize - ec payload size + * @param {String} name - curve name + * @param {Uint8Array} publicKey - public key + * @param {Uint8Array} privateKey - private key + * + * @returns {JsonWebKey} Private key in jwk format. + */ + function privateToJWK(payloadSize, name, publicKey, privateKey) { + const jwk = rawPublicToJWK(payloadSize, name, publicKey); + jwk.d = uint8ArrayToB64(privateKey, true); + return jwk; + } + + // OpenPGP.js - An OpenPGP implementation in javascript + + const webCrypto$2 = util.getWebCrypto(); + + /** + * Sign a message using the provided key + * @param {module:type/oid} oid - Elliptic curve object identifier + * @param {module:enums.hash} hashAlgo - Hash algorithm used to sign + * @param {Uint8Array} message - Message to sign + * @param {Uint8Array} publicKey - Public key + * @param {Uint8Array} privateKey - Private key used to sign the message + * @param {Uint8Array} hashed - The hashed message + * @returns {Promise<{ + * r: Uint8Array, + * s: Uint8Array + * }>} Signature of the message + * @async + */ + async function sign$5(oid, hashAlgo, message, publicKey, privateKey, hashed) { + const curve = new CurveWithOID(oid); + if (message && !util.isStream(message)) { + const keyPair = { publicKey, privateKey }; + switch (curve.type) { + case 'web': { + // If browser doesn't support a curve, we'll catch it + try { + // Need to await to make sure browser succeeds + return await webSign(curve, hashAlgo, message, keyPair); + } catch (err) { + // We do not fallback if the error is related to key integrity + // Unfortunaley Safari does not support p521 and throws a DataError when using it + // So we need to always fallback for that curve + if (curve.name !== 'p521' && (err.name === 'DataError' || err.name === 'OperationError')) { + throw err; + } + console.error('Browser did not support signing: ' + err.message); + } + break; + } + } + } + return ellipticSign(curve, hashed, privateKey); + } + + /** + * Verifies if a signature is valid for a message + * @param {module:type/oid} oid - Elliptic curve object identifier + * @param {module:enums.hash} hashAlgo - Hash algorithm used in the signature + * @param {{r: Uint8Array, + s: Uint8Array}} signature Signature to verify + * @param {Uint8Array} message - Message to verify + * @param {Uint8Array} publicKey - Public key used to verify the message + * @param {Uint8Array} hashed - The hashed message + * @returns {Boolean} + * @async + */ + async function verify$5(oid, hashAlgo, signature, message, publicKey, hashed) { + const curve = new CurveWithOID(oid); + if (message && !util.isStream(message)) { + switch (curve.type) { + case 'web': + try { + // Need to await to make sure browser succeeds + return await webVerify(curve, hashAlgo, signature, message, publicKey); + } catch (err) { + // We do not fallback if the error is related to key integrity + // Unfortunately Safari does not support p521 and throws a DataError when using it + // So we need to always fallback for that curve + if (curve.name !== 'p521' && (err.name === 'DataError' || err.name === 'OperationError')) { + throw err; + } + console.error('Browser did not support verifying: ' + err.message); + } + break; + } + } + const digest = (typeof hashAlgo === 'undefined') ? message : hashed; + return ellipticVerify(curve, signature, digest, publicKey); + } + + /** + * Validate ECDSA parameters + * @param {module:type/oid} oid - Elliptic curve object identifier + * @param {Uint8Array} Q - ECDSA public point + * @param {Uint8Array} d - ECDSA secret scalar + * @returns {Promise} Whether params are valid. + * @async + */ + async function validateParams$6(oid, Q, d) { + const curve = new CurveWithOID(oid); + // Reject curves x25519 and ed25519 + if (curve.keyType !== enums.publicKey.ecdsa) { + return false; + } + + // To speed up the validation, we try to use node- or webcrypto when available + // and sign + verify a random message + switch (curve.type) { + case 'web': { + const message = await getRandomBytes(8); + const hashAlgo = enums.hash.sha256; + const hashed = await hash.digest(hashAlgo, message); + try { + const signature = await sign$5(oid, hashAlgo, message, Q, d, hashed); + return await verify$5(oid, hashAlgo, signature, message, Q, hashed); + } catch (err) { + return false; + } + } + default: + return validateStandardParams(enums.publicKey.ecdsa, oid, Q, d); + } + } + + + ////////////////////////// + // // + // Helper functions // + // // + ////////////////////////// + + async function ellipticSign(curve, hashed, privateKey) { + const indutnyCurve = await getIndutnyCurve(curve.name); + const key = keyFromPrivate(indutnyCurve, privateKey); + const signature = key.sign(hashed); + return { + r: signature.r.toArrayLike(Uint8Array), + s: signature.s.toArrayLike(Uint8Array) + }; + } + + async function ellipticVerify(curve, signature, digest, publicKey) { + const indutnyCurve = await getIndutnyCurve(curve.name); + const key = keyFromPublic(indutnyCurve, publicKey); + return key.verify(digest, signature); + } + + async function webSign(curve, hashAlgo, message, keyPair) { + const len = curve.payloadSize; + const jwk = privateToJWK(curve.payloadSize, webCurves[curve.name], keyPair.publicKey, keyPair.privateKey); + const key = await webCrypto$2.importKey( + 'jwk', + jwk, + { + 'name': 'ECDSA', + 'namedCurve': webCurves[curve.name], + 'hash': { name: enums.read(enums.webHash, curve.hash) } + }, + false, + ['sign'] + ); + + const signature = new Uint8Array(await webCrypto$2.sign( + { + 'name': 'ECDSA', + 'namedCurve': webCurves[curve.name], + 'hash': { name: enums.read(enums.webHash, hashAlgo) } + }, + key, + message + )); + + return { + r: signature.slice(0, len), + s: signature.slice(len, len << 1) + }; + } + + async function webVerify(curve, hashAlgo, { r, s }, message, publicKey) { + const jwk = rawPublicToJWK(curve.payloadSize, webCurves[curve.name], publicKey); + const key = await webCrypto$2.importKey( + 'jwk', + jwk, + { + 'name': 'ECDSA', + 'namedCurve': webCurves[curve.name], + 'hash': { name: enums.read(enums.webHash, curve.hash) } + }, + false, + ['verify'] + ); + + const signature = util.concatUint8Array([r, s]).buffer; + + return webCrypto$2.verify( + { + 'name': 'ECDSA', + 'namedCurve': webCurves[curve.name], + 'hash': { name: enums.read(enums.webHash, hashAlgo) } + }, + key, + signature, + message + ); + } + + // Originally written by Owen Smith https://github.com/omsmith + // Adapted on Feb 2018 from https://github.com/Brightspace/node-jwk-to-pem/ + + var ecdsa = /*#__PURE__*/Object.freeze({ + __proto__: null, + sign: sign$5, + verify: verify$5, + validateParams: validateParams$6 + }); + + // OpenPGP.js - An OpenPGP implementation in javascript + + naclFastLight.hash = bytes => new Uint8Array(_512().update(bytes).digest()); + + /** + * Sign a message using the provided legacy EdDSA key + * @param {module:type/oid} oid - Elliptic curve object identifier + * @param {module:enums.hash} hashAlgo - Hash algorithm used to sign (must be sha256 or stronger) + * @param {Uint8Array} message - Message to sign + * @param {Uint8Array} publicKey - Public key + * @param {Uint8Array} privateKey - Private key used to sign the message + * @param {Uint8Array} hashed - The hashed message + * @returns {Promise<{ + * r: Uint8Array, + * s: Uint8Array + * }>} Signature of the message + * @async + */ + async function sign$4(oid, hashAlgo, message, publicKey, privateKey, hashed) { + if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(enums.hash.sha256)) { + // see https://tools.ietf.org/id/draft-ietf-openpgp-rfc4880bis-10.html#section-15-7.2 + throw Error('Hash algorithm too weak for EdDSA.'); + } + const secretKey = util.concatUint8Array([privateKey, publicKey.subarray(1)]); + const signature = naclFastLight.sign.detached(hashed, secretKey); + // EdDSA signature params are returned in little-endian format + return { + r: signature.subarray(0, 32), + s: signature.subarray(32) + }; + } + + /** + * Verifies if a legacy EdDSA signature is valid for a message + * @param {module:type/oid} oid - Elliptic curve object identifier + * @param {module:enums.hash} hashAlgo - Hash algorithm used in the signature + * @param {{r: Uint8Array, + s: Uint8Array}} signature Signature to verify the message + * @param {Uint8Array} m - Message to verify + * @param {Uint8Array} publicKey - Public key used to verify the message + * @param {Uint8Array} hashed - The hashed message + * @returns {Boolean} + * @async + */ + async function verify$4(oid, hashAlgo, { r, s }, m, publicKey, hashed) { + if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(enums.hash.sha256)) { + throw Error('Hash algorithm too weak for EdDSA.'); + } + const signature = util.concatUint8Array([r, s]); + return naclFastLight.sign.detached.verify(hashed, signature, publicKey.subarray(1)); + } + /** + * Validate legacy EdDSA parameters + * @param {module:type/oid} oid - Elliptic curve object identifier + * @param {Uint8Array} Q - EdDSA public point + * @param {Uint8Array} k - EdDSA secret seed + * @returns {Promise} Whether params are valid. + * @async + */ + async function validateParams$5(oid, Q, k) { + // Check whether the given curve is supported + if (oid.getName() !== 'ed25519') { + return false; + } + + /** + * Derive public point Q' = dG from private key + * and expect Q == Q' + */ + const { publicKey } = naclFastLight.sign.keyPair.fromSeed(k); + const dG = new Uint8Array([0x40, ...publicKey]); // Add public key prefix + return util.equalsUint8Array(Q, dG); + + } + + var eddsa_legacy = /*#__PURE__*/Object.freeze({ + __proto__: null, + sign: sign$4, + verify: verify$4, + validateParams: validateParams$5 + }); + + // OpenPGP.js - An OpenPGP implementation in javascript + + naclFastLight.hash = bytes => new Uint8Array(_512().update(bytes).digest()); + + /** + * Generate (non-legacy) EdDSA key + * @param {module:enums.publicKey} algo - Algorithm identifier + * @returns {Promise<{ A: Uint8Array, seed: Uint8Array }>} + */ + async function generate$2(algo) { + switch (algo) { + case enums.publicKey.ed25519: { + const seed = getRandomBytes(32); + const { publicKey: A } = naclFastLight.sign.keyPair.fromSeed(seed); + return { A, seed }; + } + default: + throw Error('Unsupported EdDSA algorithm'); + } + } + + /** + * Sign a message using the provided key + * @param {module:enums.publicKey} algo - Algorithm identifier + * @param {module:enums.hash} hashAlgo - Hash algorithm used to sign (must be sha256 or stronger) + * @param {Uint8Array} message - Message to sign + * @param {Uint8Array} publicKey - Public key + * @param {Uint8Array} privateKey - Private key used to sign the message + * @param {Uint8Array} hashed - The hashed message + * @returns {Promise<{ + * RS: Uint8Array + * }>} Signature of the message + * @async + */ + async function sign$3(algo, hashAlgo, message, publicKey, privateKey, hashed) { + if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(getPreferredHashAlgo$1(algo))) { + throw Error('Hash algorithm too weak for EdDSA.'); + } + switch (algo) { + case enums.publicKey.ed25519: { + const secretKey = util.concatUint8Array([privateKey, publicKey]); + const signature = naclFastLight.sign.detached(hashed, secretKey); + return { RS: signature }; + } + case enums.publicKey.ed448: + default: + throw Error('Unsupported EdDSA algorithm'); + } + + } + + /** + * Verifies if a signature is valid for a message + * @param {module:enums.publicKey} algo - Algorithm identifier + * @param {module:enums.hash} hashAlgo - Hash algorithm used in the signature + * @param {{ RS: Uint8Array }} signature Signature to verify the message + * @param {Uint8Array} m - Message to verify + * @param {Uint8Array} publicKey - Public key used to verify the message + * @param {Uint8Array} hashed - The hashed message + * @returns {Boolean} + * @async + */ + async function verify$3(algo, hashAlgo, { RS }, m, publicKey, hashed) { + if (hash.getHashByteLength(hashAlgo) < hash.getHashByteLength(getPreferredHashAlgo$1(algo))) { + throw Error('Hash algorithm too weak for EdDSA.'); + } + switch (algo) { + case enums.publicKey.ed25519: { + return naclFastLight.sign.detached.verify(hashed, RS, publicKey); + } + case enums.publicKey.ed448: + default: + throw Error('Unsupported EdDSA algorithm'); + } + } + /** + * Validate (non-legacy) EdDSA parameters + * @param {module:enums.publicKey} algo - Algorithm identifier + * @param {Uint8Array} A - EdDSA public point + * @param {Uint8Array} seed - EdDSA secret seed + * @param {Uint8Array} oid - (legacy only) EdDSA OID + * @returns {Promise} Whether params are valid. + * @async + */ + async function validateParams$4(algo, A, seed) { + switch (algo) { + case enums.publicKey.ed25519: { + /** + * Derive public point A' from private key + * and expect A == A' + */ + const { publicKey } = naclFastLight.sign.keyPair.fromSeed(seed); + return util.equalsUint8Array(A, publicKey); + } + + case enums.publicKey.ed448: // unsupported + default: + return false; + } + } + + function getPreferredHashAlgo$1(algo) { + switch (algo) { + case enums.publicKey.ed25519: + return enums.hash.sha256; + default: + throw Error('Unknown EdDSA algo'); + } + } + + var eddsa$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + generate: generate$2, + sign: sign$3, + verify: verify$3, + validateParams: validateParams$4, + getPreferredHashAlgo: getPreferredHashAlgo$1 + }); + + // OpenPGP.js - An OpenPGP implementation in javascript + + /** + * AES key wrap + * @function + * @param {Uint8Array} key + * @param {Uint8Array} data + * @returns {Uint8Array} + */ + function wrap(key, data) { + const aes = new cipher['aes' + (key.length * 8)](key); + const IV = new Uint32Array([0xA6A6A6A6, 0xA6A6A6A6]); + const P = unpack(data); + let A = IV; + const R = P; + const n = P.length / 2; + const t = new Uint32Array([0, 0]); + let B = new Uint32Array(4); + for (let j = 0; j <= 5; ++j) { + for (let i = 0; i < n; ++i) { + t[1] = n * j + (1 + i); + // B = A + B[0] = A[0]; + B[1] = A[1]; + // B = A || R[i] + B[2] = R[2 * i]; + B[3] = R[2 * i + 1]; + // B = AES(K, B) + B = unpack(aes.encrypt(pack(B))); + // A = MSB(64, B) ^ t + A = B.subarray(0, 2); + A[0] ^= t[0]; + A[1] ^= t[1]; + // R[i] = LSB(64, B) + R[2 * i] = B[2]; + R[2 * i + 1] = B[3]; + } + } + return pack(A, R); + } + + /** + * AES key unwrap + * @function + * @param {String} key + * @param {String} data + * @returns {Uint8Array} + * @throws {Error} + */ + function unwrap(key, data) { + const aes = new cipher['aes' + (key.length * 8)](key); + const IV = new Uint32Array([0xA6A6A6A6, 0xA6A6A6A6]); + const C = unpack(data); + let A = C.subarray(0, 2); + const R = C.subarray(2); + const n = C.length / 2 - 1; + const t = new Uint32Array([0, 0]); + let B = new Uint32Array(4); + for (let j = 5; j >= 0; --j) { + for (let i = n - 1; i >= 0; --i) { + t[1] = n * j + (i + 1); + // B = A ^ t + B[0] = A[0] ^ t[0]; + B[1] = A[1] ^ t[1]; + // B = (A ^ t) || R[i] + B[2] = R[2 * i]; + B[3] = R[2 * i + 1]; + // B = AES-1(B) + B = unpack(aes.decrypt(pack(B))); + // A = MSB(64, B) + A = B.subarray(0, 2); + // R[i] = LSB(64, B) + R[2 * i] = B[2]; + R[2 * i + 1] = B[3]; + } + } + if (A[0] === IV[0] && A[1] === IV[1]) { + return pack(R); + } + throw Error('Key Data Integrity failed'); + } + + function createArrayBuffer(data) { + if (util.isString(data)) { + const { length } = data; + const buffer = new ArrayBuffer(length); + const view = new Uint8Array(buffer); + for (let j = 0; j < length; ++j) { + view[j] = data.charCodeAt(j); + } + return buffer; + } + return new Uint8Array(data).buffer; + } + + function unpack(data) { + const { length } = data; + const buffer = createArrayBuffer(data); + const view = new DataView(buffer); + const arr = new Uint32Array(length / 4); + for (let i = 0; i < length / 4; ++i) { + arr[i] = view.getUint32(4 * i); + } + return arr; + } + + function pack() { + let length = 0; + for (let k = 0; k < arguments.length; ++k) { + length += 4 * arguments[k].length; + } + const buffer = new ArrayBuffer(length); + const view = new DataView(buffer); + let offset = 0; + for (let i = 0; i < arguments.length; ++i) { + for (let j = 0; j < arguments[i].length; ++j) { + view.setUint32(offset + 4 * j, arguments[i][j]); + } + offset += 4 * arguments[i].length; + } + return new Uint8Array(buffer); + } + + var aesKW = /*#__PURE__*/Object.freeze({ + __proto__: null, + wrap: wrap, + unwrap: unwrap + }); + + // OpenPGP.js - An OpenPGP implementation in javascript + + /** + * @fileoverview Functions to add and remove PKCS5 padding + * @see PublicKeyEncryptedSessionKeyPacket + * @module crypto/pkcs5 + * @private + */ + + /** + * Add pkcs5 padding to a message + * @param {Uint8Array} message - message to pad + * @returns {Uint8Array} Padded message. + */ + function encode(message) { + const c = 8 - (message.length % 8); + const padded = new Uint8Array(message.length + c).fill(c); + padded.set(message); + return padded; + } + + /** + * Remove pkcs5 padding from a message + * @param {Uint8Array} message - message to remove padding from + * @returns {Uint8Array} Message without padding. + */ + function decode$1(message) { + const len = message.length; + if (len > 0) { + const c = message[len - 1]; + if (c >= 1) { + const provided = message.subarray(len - c); + const computed = new Uint8Array(c).fill(c); + if (util.equalsUint8Array(provided, computed)) { + return message.subarray(0, len - c); + } + } + } + throw Error('Invalid padding'); + } + + var pkcs5 = /*#__PURE__*/Object.freeze({ + __proto__: null, + encode: encode, + decode: decode$1 + }); + + // OpenPGP.js - An OpenPGP implementation in javascript + + const webCrypto$1 = util.getWebCrypto(); + + /** + * Validate ECDH parameters + * @param {module:type/oid} oid - Elliptic curve object identifier + * @param {Uint8Array} Q - ECDH public point + * @param {Uint8Array} d - ECDH secret scalar + * @returns {Promise} Whether params are valid. + * @async + */ + async function validateParams$3(oid, Q, d) { + return validateStandardParams(enums.publicKey.ecdh, oid, Q, d); + } + + // Build Param for ECDH algorithm (RFC 6637) + function buildEcdhParam(public_algo, oid, kdfParams, fingerprint) { + return util.concatUint8Array([ + oid.write(), + new Uint8Array([public_algo]), + kdfParams.write(), + util.stringToUint8Array('Anonymous Sender '), + fingerprint.subarray(0, 20) + ]); + } + + // Key Derivation Function (RFC 6637) + async function kdf(hashAlgo, X, length, param, stripLeading = false, stripTrailing = false) { + // Note: X is little endian for Curve25519, big-endian for all others. + // This is not ideal, but the RFC's are unclear + // https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-02#appendix-B + let i; + if (stripLeading) { + // Work around old go crypto bug + for (i = 0; i < X.length && X[i] === 0; i++); + X = X.subarray(i); + } + if (stripTrailing) { + // Work around old OpenPGP.js bug + for (i = X.length - 1; i >= 0 && X[i] === 0; i--); + X = X.subarray(0, i + 1); + } + const digest = await hash.digest(hashAlgo, util.concatUint8Array([ + new Uint8Array([0, 0, 0, 1]), + X, + param + ])); + return digest.subarray(0, length); + } + + /** + * Generate ECDHE ephemeral key and secret from public key + * + * @param {CurveWithOID} curve - Elliptic curve object + * @param {Uint8Array} Q - Recipient public key + * @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>} + * @async + */ + async function genPublicEphemeralKey(curve, Q) { + switch (curve.type) { + case 'curve25519': { + const d = getRandomBytes(32); + const { secretKey, sharedKey } = await genPrivateEphemeralKey(curve, Q, null, d); + let { publicKey } = naclFastLight.box.keyPair.fromSecretKey(secretKey); + publicKey = util.concatUint8Array([new Uint8Array([0x40]), publicKey]); + return { publicKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below + } + case 'web': + if (curve.web && util.getWebCrypto()) { + try { + return await webPublicEphemeralKey(curve, Q); + } catch (err) { + console.error(err); + } + } + break; + } + return ellipticPublicEphemeralKey(curve, Q); + } + + /** + * Encrypt and wrap a session key + * + * @param {module:type/oid} oid - Elliptic curve object identifier + * @param {module:type/kdf_params} kdfParams - KDF params including cipher and algorithm to use + * @param {Uint8Array} data - Unpadded session key data + * @param {Uint8Array} Q - Recipient public key + * @param {Uint8Array} fingerprint - Recipient fingerprint + * @returns {Promise<{publicKey: Uint8Array, wrappedKey: Uint8Array}>} + * @async + */ + async function encrypt$2(oid, kdfParams, data, Q, fingerprint) { + const m = encode(data); + + const curve = new CurveWithOID(oid); + const { publicKey, sharedKey } = await genPublicEphemeralKey(curve, Q); + const param = buildEcdhParam(enums.publicKey.ecdh, oid, kdfParams, fingerprint); + const { keySize } = getCipher(kdfParams.cipher); + const Z = await kdf(kdfParams.hash, sharedKey, keySize, param); + const wrappedKey = wrap(Z, m); + return { publicKey, wrappedKey }; + } + + /** + * Generate ECDHE secret from private key and public part of ephemeral key + * + * @param {CurveWithOID} curve - Elliptic curve object + * @param {Uint8Array} V - Public part of ephemeral key + * @param {Uint8Array} Q - Recipient public key + * @param {Uint8Array} d - Recipient private key + * @returns {Promise<{secretKey: Uint8Array, sharedKey: Uint8Array}>} + * @async + */ + async function genPrivateEphemeralKey(curve, V, Q, d) { + if (d.length !== curve.payloadSize) { + const privateKey = new Uint8Array(curve.payloadSize); + privateKey.set(d, curve.payloadSize - d.length); + d = privateKey; + } + switch (curve.type) { + case 'curve25519': { + const secretKey = d.slice().reverse(); + const sharedKey = naclFastLight.scalarMult(secretKey, V.subarray(1)); + return { secretKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below + } + case 'web': + if (curve.web && util.getWebCrypto()) { + try { + return await webPrivateEphemeralKey(curve, V, Q, d); + } catch (err) { + console.error(err); + } + } + break; + } + return ellipticPrivateEphemeralKey(curve, V, d); + } + + /** + * Decrypt and unwrap the value derived from session key + * + * @param {module:type/oid} oid - Elliptic curve object identifier + * @param {module:type/kdf_params} kdfParams - KDF params including cipher and algorithm to use + * @param {Uint8Array} V - Public part of ephemeral key + * @param {Uint8Array} C - Encrypted and wrapped value derived from session key + * @param {Uint8Array} Q - Recipient public key + * @param {Uint8Array} d - Recipient private key + * @param {Uint8Array} fingerprint - Recipient fingerprint + * @returns {Promise} Value derived from session key. + * @async + */ + async function decrypt$2(oid, kdfParams, V, C, Q, d, fingerprint) { + const curve = new CurveWithOID(oid); + const { sharedKey } = await genPrivateEphemeralKey(curve, V, Q, d); + const param = buildEcdhParam(enums.publicKey.ecdh, oid, kdfParams, fingerprint); + const { keySize } = getCipher(kdfParams.cipher); + let err; + for (let i = 0; i < 3; i++) { + try { + // Work around old go crypto bug and old OpenPGP.js bug, respectively. + const Z = await kdf(kdfParams.hash, sharedKey, keySize, param, i === 1, i === 2); + return decode$1(unwrap(Z, C)); + } catch (e) { + err = e; + } + } + throw err; + } + + /** + * Generate ECDHE secret from private key and public part of ephemeral key using webCrypto + * + * @param {CurveWithOID} curve - Elliptic curve object + * @param {Uint8Array} V - Public part of ephemeral key + * @param {Uint8Array} Q - Recipient public key + * @param {Uint8Array} d - Recipient private key + * @returns {Promise<{secretKey: Uint8Array, sharedKey: Uint8Array}>} + * @async + */ + async function webPrivateEphemeralKey(curve, V, Q, d) { + const recipient = privateToJWK(curve.payloadSize, curve.web.web, Q, d); + let privateKey = webCrypto$1.importKey( + 'jwk', + recipient, + { + name: 'ECDH', + namedCurve: curve.web.web + }, + true, + ['deriveKey', 'deriveBits'] + ); + const jwk = rawPublicToJWK(curve.payloadSize, curve.web.web, V); + let sender = webCrypto$1.importKey( + 'jwk', + jwk, + { + name: 'ECDH', + namedCurve: curve.web.web + }, + true, + [] + ); + [privateKey, sender] = await Promise.all([privateKey, sender]); + let S = webCrypto$1.deriveBits( + { + name: 'ECDH', + namedCurve: curve.web.web, + public: sender + }, + privateKey, + curve.web.sharedSize + ); + let secret = webCrypto$1.exportKey( + 'jwk', + privateKey + ); + [S, secret] = await Promise.all([S, secret]); + const sharedKey = new Uint8Array(S); + const secretKey = b64ToUint8Array(secret.d); + return { secretKey, sharedKey }; + } + + /** + * Generate ECDHE ephemeral key and secret from public key using webCrypto + * + * @param {CurveWithOID} curve - Elliptic curve object + * @param {Uint8Array} Q - Recipient public key + * @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>} + * @async + */ + async function webPublicEphemeralKey(curve, Q) { + const jwk = rawPublicToJWK(curve.payloadSize, curve.web.web, Q); + let keyPair = webCrypto$1.generateKey( + { + name: 'ECDH', + namedCurve: curve.web.web + }, + true, + ['deriveKey', 'deriveBits'] + ); + let recipient = webCrypto$1.importKey( + 'jwk', + jwk, + { + name: 'ECDH', + namedCurve: curve.web.web + }, + false, + [] + ); + [keyPair, recipient] = await Promise.all([keyPair, recipient]); + let s = webCrypto$1.deriveBits( + { + name: 'ECDH', + namedCurve: curve.web.web, + public: recipient + }, + keyPair.privateKey, + curve.web.sharedSize + ); + let p = webCrypto$1.exportKey( + 'jwk', + keyPair.publicKey + ); + [s, p] = await Promise.all([s, p]); + const sharedKey = new Uint8Array(s); + const publicKey = new Uint8Array(jwkToRawPublic(p)); + return { publicKey, sharedKey }; + } + + /** + * Generate ECDHE secret from private key and public part of ephemeral key using indutny/elliptic + * + * @param {CurveWithOID} curve - Elliptic curve object + * @param {Uint8Array} V - Public part of ephemeral key + * @param {Uint8Array} d - Recipient private key + * @returns {Promise<{secretKey: Uint8Array, sharedKey: Uint8Array}>} + * @async + */ + async function ellipticPrivateEphemeralKey(curve, V, d) { + const indutnyCurve = await getIndutnyCurve(curve.name); + V = keyFromPublic(indutnyCurve, V); + d = keyFromPrivate(indutnyCurve, d); + const secretKey = new Uint8Array(d.getPrivate()); + const S = d.derive(V.getPublic()); + const len = indutnyCurve.curve.p.byteLength(); + const sharedKey = S.toArrayLike(Uint8Array, 'be', len); + return { secretKey, sharedKey }; + } + + /** + * Generate ECDHE ephemeral key and secret from public key using indutny/elliptic + * + * @param {CurveWithOID} curve - Elliptic curve object + * @param {Uint8Array} Q - Recipient public key + * @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>} + * @async + */ + async function ellipticPublicEphemeralKey(curve, Q) { + const indutnyCurve = await getIndutnyCurve(curve.name); + const v = await curve.genKeyPair(); + Q = keyFromPublic(indutnyCurve, Q); + const V = keyFromPrivate(indutnyCurve, v.privateKey); + const publicKey = v.publicKey; + const S = V.derive(Q.getPublic()); + const len = indutnyCurve.curve.p.byteLength(); + const sharedKey = S.toArrayLike(Uint8Array, 'be', len); + return { publicKey, sharedKey }; + } + + var ecdh = /*#__PURE__*/Object.freeze({ + __proto__: null, + validateParams: validateParams$3, + encrypt: encrypt$2, + decrypt: decrypt$2 + }); + + /** + * @fileoverview This module implements HKDF using either the WebCrypto API or Node.js' crypto API. + * @module crypto/hkdf + * @private + */ + + const webCrypto = util.getWebCrypto(); + + async function HKDF(hashAlgo, inputKey, salt, info, outLen) { + const hash = enums.read(enums.webHash, hashAlgo); + if (!hash) throw Error('Hash algo not supported with HKDF'); + + if (webCrypto) { + const crypto = webCrypto; + const importedKey = await crypto.importKey('raw', inputKey, 'HKDF', false, ['deriveBits']); + const bits = await crypto.deriveBits({ name: 'HKDF', hash, salt, info }, importedKey, outLen * 8); + return new Uint8Array(bits); + } + + throw Error('No HKDF implementation available'); + } + + /** + * @fileoverview Key encryption and decryption for RFC 6637 ECDH + * @module crypto/public_key/elliptic/ecdh + * @private + */ + + const HKDF_INFO = { + x25519: util.encodeUTF8('OpenPGP X25519') + }; + + /** + * Generate ECDH key for Montgomery curves + * @param {module:enums.publicKey} algo - Algorithm identifier + * @returns {Promise<{ A: Uint8Array, k: Uint8Array }>} + */ + async function generate$1(algo) { + switch (algo) { + case enums.publicKey.x25519: { + // k stays in little-endian, unlike legacy ECDH over curve25519 + const k = getRandomBytes(32); + const { publicKey: A } = naclFastLight.box.keyPair.fromSecretKey(k); + return { A, k }; + } + default: + throw Error('Unsupported ECDH algorithm'); + } + } + + /** + * Validate ECDH parameters + * @param {module:enums.publicKey} algo - Algorithm identifier + * @param {Uint8Array} A - ECDH public point + * @param {Uint8Array} k - ECDH secret scalar + * @returns {Promise} Whether params are valid. + * @async + */ + async function validateParams$2(algo, A, k) { + switch (algo) { + case enums.publicKey.x25519: { + /** + * Derive public point A' from private key + * and expect A == A' + */ + const { publicKey } = naclFastLight.box.keyPair.fromSecretKey(k); + return util.equalsUint8Array(A, publicKey); + } + + default: + return false; + } + } + + /** + * Wrap and encrypt a session key + * + * @param {module:enums.publicKey} algo - Algorithm identifier + * @param {Uint8Array} data - session key data to be encrypted + * @param {Uint8Array} recipientA - Recipient public key (K_B) + * @returns {Promise<{ + * ephemeralPublicKey: Uint8Array, + * wrappedKey: Uint8Array + * }>} ephemeral public key (K_A) and encrypted key + * @async + */ + async function encrypt$1(algo, data, recipientA) { + switch (algo) { + case enums.publicKey.x25519: { + const ephemeralSecretKey = getRandomBytes(32); + const sharedSecret = naclFastLight.scalarMult(ephemeralSecretKey, recipientA); + const { publicKey: ephemeralPublicKey } = naclFastLight.box.keyPair.fromSecretKey(ephemeralSecretKey); + const hkdfInput = util.concatUint8Array([ + ephemeralPublicKey, + recipientA, + sharedSecret + ]); + const { keySize } = getCipher(enums.symmetric.aes128); + const encryptionKey = await HKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize); + const wrappedKey = wrap(encryptionKey, data); + return { ephemeralPublicKey, wrappedKey }; + } + + default: + throw Error('Unsupported ECDH algorithm'); + } + } + + /** + * Decrypt and unwrap the session key + * + * @param {module:enums.publicKey} algo - Algorithm identifier + * @param {Uint8Array} ephemeralPublicKey - (K_A) + * @param {Uint8Array} wrappedKey, + * @param {Uint8Array} A - Recipient public key (K_b), needed for KDF + * @param {Uint8Array} k - Recipient secret key (b) + * @returns {Promise} decrypted session key data + * @async + */ + async function decrypt$1(algo, ephemeralPublicKey, wrappedKey, A, k) { + switch (algo) { + case enums.publicKey.x25519: { + const sharedSecret = naclFastLight.scalarMult(k, ephemeralPublicKey); + const hkdfInput = util.concatUint8Array([ + ephemeralPublicKey, + A, + sharedSecret + ]); + const { keySize } = getCipher(enums.symmetric.aes128); + const encryptionKey = await HKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize); + return unwrap(encryptionKey, wrappedKey); + } + default: + throw Error('Unsupported ECDH algorithm'); + } + } + + var ecdh_x = /*#__PURE__*/Object.freeze({ + __proto__: null, + generate: generate$1, + validateParams: validateParams$2, + encrypt: encrypt$1, + decrypt: decrypt$1 + }); + + // OpenPGP.js - An OpenPGP implementation in javascript + + var elliptic$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + CurveWithOID: CurveWithOID, + ecdh: ecdh, + ecdhX: ecdh_x, + ecdsa: ecdsa, + eddsaLegacy: eddsa_legacy, + eddsa: eddsa$1, + generate: generate$3, + getPreferredHashAlgo: getPreferredHashAlgo$2 + }); + + // GPG4Browsers - An OpenPGP implementation in javascript + + /* + TODO regarding the hash function, read: + https://tools.ietf.org/html/rfc4880#section-13.6 + https://tools.ietf.org/html/rfc4880#section-14 + */ + + /** + * DSA Sign function + * @param {Integer} hashAlgo + * @param {Uint8Array} hashed + * @param {Uint8Array} g + * @param {Uint8Array} p + * @param {Uint8Array} q + * @param {Uint8Array} x + * @returns {Promise<{ r: Uint8Array, s: Uint8Array }>} + * @async + */ + async function sign$2(hashAlgo, hashed, g, p, q, x) { + const BigInteger = await util.getBigInteger(); + const one = new BigInteger(1); + p = new BigInteger(p); + q = new BigInteger(q); + g = new BigInteger(g); + x = new BigInteger(x); + + let k; + let r; + let s; + let t; + g = g.mod(p); + x = x.mod(q); + // If the output size of the chosen hash is larger than the number of + // bits of q, the hash result is truncated to fit by taking the number + // of leftmost bits equal to the number of bits of q. This (possibly + // truncated) hash function result is treated as a number and used + // directly in the DSA signature algorithm. + const h = new BigInteger(hashed.subarray(0, q.byteLength())).mod(q); + // FIPS-186-4, section 4.6: + // The values of r and s shall be checked to determine if r = 0 or s = 0. + // If either r = 0 or s = 0, a new value of k shall be generated, and the + // signature shall be recalculated. It is extremely unlikely that r = 0 + // or s = 0 if signatures are generated properly. + while (true) { + // See Appendix B here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf + k = await getRandomBigInteger(one, q); // returns in [1, q-1] + r = g.modExp(k, p).imod(q); // (g**k mod p) mod q + if (r.isZero()) { + continue; + } + const xr = x.mul(r).imod(q); + t = h.add(xr).imod(q); // H(m) + x*r mod q + s = k.modInv(q).imul(t).imod(q); // k**-1 * (H(m) + x*r) mod q + if (s.isZero()) { + continue; + } + break; + } + return { + r: r.toUint8Array('be', q.byteLength()), + s: s.toUint8Array('be', q.byteLength()) + }; + } + + /** + * DSA Verify function + * @param {Integer} hashAlgo + * @param {Uint8Array} r + * @param {Uint8Array} s + * @param {Uint8Array} hashed + * @param {Uint8Array} g + * @param {Uint8Array} p + * @param {Uint8Array} q + * @param {Uint8Array} y + * @returns {boolean} + * @async + */ + async function verify$2(hashAlgo, r, s, hashed, g, p, q, y) { + const BigInteger = await util.getBigInteger(); + const zero = new BigInteger(0); + r = new BigInteger(r); + s = new BigInteger(s); + + p = new BigInteger(p); + q = new BigInteger(q); + g = new BigInteger(g); + y = new BigInteger(y); + + if (r.lte(zero) || r.gte(q) || + s.lte(zero) || s.gte(q)) { + console.log('invalid DSA Signature'); + return false; + } + const h = new BigInteger(hashed.subarray(0, q.byteLength())).imod(q); + const w = s.modInv(q); // s**-1 mod q + if (w.isZero()) { + console.log('invalid DSA Signature'); + return false; + } + + g = g.mod(p); + y = y.mod(p); + const u1 = h.mul(w).imod(q); // H(m) * w mod q + const u2 = r.mul(w).imod(q); // r * w mod q + const t1 = g.modExp(u1, p); // g**u1 mod p + const t2 = y.modExp(u2, p); // y**u2 mod p + const v = t1.mul(t2).imod(p).imod(q); // (g**u1 * y**u2 mod p) mod q + return v.equal(r); + } + + /** + * Validate DSA parameters + * @param {Uint8Array} p - DSA prime + * @param {Uint8Array} q - DSA group order + * @param {Uint8Array} g - DSA sub-group generator + * @param {Uint8Array} y - DSA public key + * @param {Uint8Array} x - DSA private key + * @returns {Promise} Whether params are valid. + * @async + */ + async function validateParams$1(p, q, g, y, x) { + const BigInteger = await util.getBigInteger(); + p = new BigInteger(p); + q = new BigInteger(q); + g = new BigInteger(g); + y = new BigInteger(y); + const one = new BigInteger(1); + // Check that 1 < g < p + if (g.lte(one) || g.gte(p)) { + return false; + } + + /** + * Check that subgroup order q divides p-1 + */ + if (!p.dec().mod(q).isZero()) { + return false; + } + + /** + * g has order q + * Check that g ** q = 1 mod p + */ + if (!g.modExp(q, p).isOne()) { + return false; + } + + /** + * Check q is large and probably prime (we mainly want to avoid small factors) + */ + const qSize = new BigInteger(q.bitLength()); + const n150 = new BigInteger(150); + if (qSize.lt(n150) || !(await isProbablePrime(q, null, 32))) { + return false; + } + + /** + * Re-derive public key y' = g ** x mod p + * Expect y == y' + * + * Blinded exponentiation computes g**{rq + x} to compare to y + */ + x = new BigInteger(x); + const two = new BigInteger(2); + const r = await getRandomBigInteger(two.leftShift(qSize.dec()), two.leftShift(qSize)); // draw r of same size as q + const rqx = q.mul(r).add(x); + if (!y.equal(g.modExp(rqx, p))) { + return false; + } + + return true; + } + + var dsa = /*#__PURE__*/Object.freeze({ + __proto__: null, + sign: sign$2, + verify: verify$2, + validateParams: validateParams$1 + }); + + /** + * @fileoverview Asymmetric cryptography functions + * @module crypto/public_key + * @private + */ + + var publicKey = { + /** @see module:crypto/public_key/rsa */ + rsa: rsa, + /** @see module:crypto/public_key/elgamal */ + elgamal: elgamal, + /** @see module:crypto/public_key/elliptic */ + elliptic: elliptic$1, + /** @see module:crypto/public_key/dsa */ + dsa: dsa, + /** @see tweetnacl */ + nacl: naclFastLight + }; + + /** + * @fileoverview Provides functions for asymmetric signing and signature verification + * @module crypto/signature + * @private + */ + + /** + * Parse signature in binary form to get the parameters. + * The returned values are only padded for EdDSA, since in the other cases their expected length + * depends on the key params, hence we delegate the padding to the signature verification function. + * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} + * See {@link https://tools.ietf.org/html/rfc4880#section-5.2.2|RFC 4880 5.2.2.} + * @param {module:enums.publicKey} algo - Public key algorithm + * @param {Uint8Array} signature - Data for which the signature was created + * @returns {Promise} True if signature is valid. + * @async + */ + function parseSignatureParams(algo, signature) { + let read = 0; + switch (algo) { + // Algorithm-Specific Fields for RSA signatures: + // - MPI of RSA signature value m**d mod n. + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaSign: { + const s = util.readMPI(signature.subarray(read)); + // The signature needs to be the same length as the public key modulo n. + // We pad s on signature verification, where we have access to n. + return { s }; + } + // Algorithm-Specific Fields for DSA or ECDSA signatures: + // - MPI of DSA or ECDSA value r. + // - MPI of DSA or ECDSA value s. + case enums.publicKey.dsa: + case enums.publicKey.ecdsa: + { + const r = util.readMPI(signature.subarray(read)); read += r.length + 2; + const s = util.readMPI(signature.subarray(read)); + return { r, s }; + } + // Algorithm-Specific Fields for legacy EdDSA signatures: + // - MPI of an EC point r. + // - EdDSA value s, in MPI, in the little endian representation + case enums.publicKey.eddsaLegacy: { + // When parsing little-endian MPI data, we always need to left-pad it, as done with big-endian values: + // https://www.ietf.org/archive/id/draft-ietf-openpgp-rfc4880bis-10.html#section-3.2-9 + let r = util.readMPI(signature.subarray(read)); read += r.length + 2; + r = util.leftPad(r, 32); + let s = util.readMPI(signature.subarray(read)); + s = util.leftPad(s, 32); + return { r, s }; + } + // Algorithm-Specific Fields for Ed25519 signatures: + // - 64 octets of the native signature + case enums.publicKey.ed25519: { + const RS = signature.subarray(read, read + 64); read += RS.length; + return { RS }; + } + default: + throw new UnsupportedError('Unknown signature algorithm.'); + } + } + + /** + * Verifies the signature provided for data using specified algorithms and public key parameters. + * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} + * and {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC 4880 9.4} + * for public key and hash algorithms. + * @param {module:enums.publicKey} algo - Public key algorithm + * @param {module:enums.hash} hashAlgo - Hash algorithm + * @param {Object} signature - Named algorithm-specific signature parameters + * @param {Object} publicParams - Algorithm-specific public key parameters + * @param {Uint8Array} data - Data for which the signature was created + * @param {Uint8Array} hashed - The hashed data + * @returns {Promise} True if signature is valid. + * @async + */ + async function verify$1(algo, hashAlgo, signature, publicParams, data, hashed) { + switch (algo) { + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaSign: { + const { n, e } = publicParams; + const s = util.leftPad(signature.s, n.length); // padding needed for webcrypto and node crypto + return publicKey.rsa.verify(hashAlgo, data, s, n, e, hashed); + } + case enums.publicKey.dsa: { + const { g, p, q, y } = publicParams; + const { r, s } = signature; // no need to pad, since we always handle them as BigIntegers + return publicKey.dsa.verify(hashAlgo, r, s, hashed, g, p, q, y); + } + case enums.publicKey.ecdsa: { + const { oid, Q } = publicParams; + const curveSize = new publicKey.elliptic.CurveWithOID(oid).payloadSize; + // padding needed for webcrypto + const r = util.leftPad(signature.r, curveSize); + const s = util.leftPad(signature.s, curveSize); + return publicKey.elliptic.ecdsa.verify(oid, hashAlgo, { r, s }, data, Q, hashed); + } + case enums.publicKey.eddsaLegacy: { + const { oid, Q } = publicParams; + // signature already padded on parsing + return publicKey.elliptic.eddsaLegacy.verify(oid, hashAlgo, signature, data, Q, hashed); + } + case enums.publicKey.ed25519: { + const { A } = publicParams; + return publicKey.elliptic.eddsa.verify(algo, hashAlgo, signature, data, A, hashed); + } + default: + throw Error('Unknown signature algorithm.'); + } + } + + /** + * Creates a signature on data using specified algorithms and private key parameters. + * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} + * and {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC 4880 9.4} + * for public key and hash algorithms. + * @param {module:enums.publicKey} algo - Public key algorithm + * @param {module:enums.hash} hashAlgo - Hash algorithm + * @param {Object} publicKeyParams - Algorithm-specific public and private key parameters + * @param {Object} privateKeyParams - Algorithm-specific public and private key parameters + * @param {Uint8Array} data - Data to be signed + * @param {Uint8Array} hashed - The hashed data + * @returns {Promise} Signature Object containing named signature parameters. + * @async + */ + async function sign$1(algo, hashAlgo, publicKeyParams, privateKeyParams, data, hashed) { + if (!publicKeyParams || !privateKeyParams) { + throw Error('Missing key parameters'); + } + switch (algo) { + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaSign: { + const { n, e } = publicKeyParams; + const { d, p, q, u } = privateKeyParams; + const s = await publicKey.rsa.sign(hashAlgo, data, n, e, d, p, q, u, hashed); + return { s }; + } + case enums.publicKey.dsa: { + const { g, p, q } = publicKeyParams; + const { x } = privateKeyParams; + return publicKey.dsa.sign(hashAlgo, hashed, g, p, q, x); + } + case enums.publicKey.elgamal: { + throw Error('Signing with Elgamal is not defined in the OpenPGP standard.'); + } + case enums.publicKey.ecdsa: { + const { oid, Q } = publicKeyParams; + const { d } = privateKeyParams; + return publicKey.elliptic.ecdsa.sign(oid, hashAlgo, data, Q, d, hashed); + } + case enums.publicKey.eddsaLegacy: { + const { oid, Q } = publicKeyParams; + const { seed } = privateKeyParams; + return publicKey.elliptic.eddsaLegacy.sign(oid, hashAlgo, data, Q, seed, hashed); + } + case enums.publicKey.ed25519: { + const { A } = publicKeyParams; + const { seed } = privateKeyParams; + return publicKey.elliptic.eddsa.sign(algo, hashAlgo, data, A, seed, hashed); + } + default: + throw Error('Unknown signature algorithm.'); + } + } + + var signature$2 = /*#__PURE__*/Object.freeze({ + __proto__: null, + parseSignatureParams: parseSignatureParams, + verify: verify$1, + sign: sign$1 + }); + + // OpenPGP.js - An OpenPGP implementation in javascript + + class ECDHSymmetricKey { + constructor(data) { + if (data) { + this.data = data; + } + } + + /** + * Read an ECDHSymmetricKey from an Uint8Array: + * - 1 octect for the length `l` + * - `l` octects of encoded session key data + * @param {Uint8Array} bytes + * @returns {Number} Number of read bytes. + */ + read(bytes) { + if (bytes.length >= 1) { + const length = bytes[0]; + if (bytes.length >= 1 + length) { + this.data = bytes.subarray(1, 1 + length); + return 1 + this.data.length; + } + } + throw Error('Invalid symmetric key'); + } + + /** + * Write an ECDHSymmetricKey as an Uint8Array + * @returns {Uint8Array} Serialised data + */ + write() { + return util.concatUint8Array([new Uint8Array([this.data.length]), this.data]); + } + } + + // OpenPGP.js - An OpenPGP implementation in javascript + + /** + * Implementation of type KDF parameters + * + * {@link https://tools.ietf.org/html/rfc6637#section-7|RFC 6637 7}: + * A key derivation function (KDF) is necessary to implement the EC + * encryption. The Concatenation Key Derivation Function (Approved + * Alternative 1) [NIST-SP800-56A] with the KDF hash function that is + * SHA2-256 [FIPS-180-3] or stronger is REQUIRED. + * @module type/kdf_params + * @private + */ + + class KDFParams { + /** + * @param {enums.hash} hash - Hash algorithm + * @param {enums.symmetric} cipher - Symmetric algorithm + */ + constructor(data) { + if (data) { + const { hash, cipher } = data; + this.hash = hash; + this.cipher = cipher; + } else { + this.hash = null; + this.cipher = null; + } + } + + /** + * Read KDFParams from an Uint8Array + * @param {Uint8Array} input - Where to read the KDFParams from + * @returns {Number} Number of read bytes. + */ + read(input) { + if (input.length < 4 || input[0] !== 3 || input[1] !== 1) { + throw new UnsupportedError('Cannot read KDFParams'); + } + this.hash = input[2]; + this.cipher = input[3]; + return 4; + } + + /** + * Write KDFParams to an Uint8Array + * @returns {Uint8Array} Array with the KDFParams value + */ + write() { + return new Uint8Array([3, 1, this.hash, this.cipher]); + } + } + + /** + * Encoded symmetric key for x25519 and x448 + * The payload format varies for v3 and v6 PKESK: + * the former includes an algorithm byte preceeding the encrypted session key. + * + * @module type/x25519x448_symkey + */ + + class ECDHXSymmetricKey { + static fromObject({ wrappedKey, algorithm }) { + const instance = new ECDHXSymmetricKey(); + instance.wrappedKey = wrappedKey; + instance.algorithm = algorithm; + return instance; + } + + /** + * - 1 octect for the length `l` + * - `l` octects of encoded session key data (with optional leading algorithm byte) + * @param {Uint8Array} bytes + * @returns {Number} Number of read bytes. + */ + read(bytes) { + let read = 0; + let followLength = bytes[read++]; + this.algorithm = followLength % 2 ? bytes[read++] : null; // session key size is always even + followLength -= followLength % 2; + this.wrappedKey = bytes.subarray(read, read + followLength); read += followLength; + } + + /** + * Write an MontgomerySymmetricKey as an Uint8Array + * @returns {Uint8Array} Serialised data + */ + write() { + return util.concatUint8Array([ + this.algorithm ? + new Uint8Array([this.wrappedKey.length + 1, this.algorithm]) : + new Uint8Array([this.wrappedKey.length]), + this.wrappedKey + ]); + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * Encrypts data using specified algorithm and public key parameters. + * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} for public key algorithms. + * @param {module:enums.publicKey} keyAlgo - Public key algorithm + * @param {module:enums.symmetric} symmetricAlgo - Cipher algorithm + * @param {Object} publicParams - Algorithm-specific public key parameters + * @param {Uint8Array} data - Session key data to be encrypted + * @param {Uint8Array} fingerprint - Recipient fingerprint + * @returns {Promise} Encrypted session key parameters. + * @async + */ + async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, data, fingerprint) { + switch (keyAlgo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: { + const { n, e } = publicParams; + const c = await publicKey.rsa.encrypt(data, n, e); + return { c }; + } + case enums.publicKey.elgamal: { + const { p, g, y } = publicParams; + return publicKey.elgamal.encrypt(data, p, g, y); + } + case enums.publicKey.ecdh: { + const { oid, Q, kdfParams } = publicParams; + const { publicKey: V, wrappedKey: C } = await publicKey.elliptic.ecdh.encrypt( + oid, kdfParams, data, Q, fingerprint); + return { V, C: new ECDHSymmetricKey(C) }; + } + case enums.publicKey.x25519: { + if (!util.isAES(symmetricAlgo)) { + // see https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/276 + throw Error('X25519 keys can only encrypt AES session keys'); + } + const { A } = publicParams; + const { ephemeralPublicKey, wrappedKey } = await publicKey.elliptic.ecdhX.encrypt( + keyAlgo, data, A); + const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey }); + return { ephemeralPublicKey, C }; + } + default: + return []; + } + } + + /** + * Decrypts data using specified algorithm and private key parameters. + * See {@link https://tools.ietf.org/html/rfc4880#section-5.5.3|RFC 4880 5.5.3} + * @param {module:enums.publicKey} algo - Public key algorithm + * @param {Object} publicKeyParams - Algorithm-specific public key parameters + * @param {Object} privateKeyParams - Algorithm-specific private key parameters + * @param {Object} sessionKeyParams - Encrypted session key parameters + * @param {Uint8Array} fingerprint - Recipient fingerprint + * @param {Uint8Array} [randomPayload] - Data to return on decryption error, instead of throwing + * (needed for constant-time processing in RSA and ElGamal) + * @returns {Promise} Decrypted data. + * @throws {Error} on sensitive decryption error, unless `randomPayload` is given + * @async + */ + async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, sessionKeyParams, fingerprint, randomPayload) { + switch (algo) { + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaEncrypt: { + const { c } = sessionKeyParams; + const { n, e } = publicKeyParams; + const { d, p, q, u } = privateKeyParams; + return publicKey.rsa.decrypt(c, n, e, d, p, q, u, randomPayload); + } + case enums.publicKey.elgamal: { + const { c1, c2 } = sessionKeyParams; + const p = publicKeyParams.p; + const x = privateKeyParams.x; + return publicKey.elgamal.decrypt(c1, c2, p, x, randomPayload); + } + case enums.publicKey.ecdh: { + const { oid, Q, kdfParams } = publicKeyParams; + const { d } = privateKeyParams; + const { V, C } = sessionKeyParams; + return publicKey.elliptic.ecdh.decrypt( + oid, kdfParams, V, C.data, Q, d, fingerprint); + } + case enums.publicKey.x25519: { + const { A } = publicKeyParams; + const { k } = privateKeyParams; + const { ephemeralPublicKey, C } = sessionKeyParams; + if (!util.isAES(C.algorithm)) { + throw Error('AES session key expected'); + } + return publicKey.elliptic.ecdhX.decrypt( + algo, ephemeralPublicKey, C.wrappedKey, A, k); + } + default: + throw Error('Unknown public key encryption algorithm.'); + } + } + + /** + * Parse public key material in binary form to get the key parameters + * @param {module:enums.publicKey} algo - The key algorithm + * @param {Uint8Array} bytes - The key material to parse + * @returns {{ read: Number, publicParams: Object }} Number of read bytes plus key parameters referenced by name. + */ + function parsePublicKeyParams(algo, bytes) { + let read = 0; + switch (algo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaSign: { + const n = util.readMPI(bytes.subarray(read)); read += n.length + 2; + const e = util.readMPI(bytes.subarray(read)); read += e.length + 2; + return { read, publicParams: { n, e } }; + } + case enums.publicKey.dsa: { + const p = util.readMPI(bytes.subarray(read)); read += p.length + 2; + const q = util.readMPI(bytes.subarray(read)); read += q.length + 2; + const g = util.readMPI(bytes.subarray(read)); read += g.length + 2; + const y = util.readMPI(bytes.subarray(read)); read += y.length + 2; + return { read, publicParams: { p, q, g, y } }; + } + case enums.publicKey.elgamal: { + const p = util.readMPI(bytes.subarray(read)); read += p.length + 2; + const g = util.readMPI(bytes.subarray(read)); read += g.length + 2; + const y = util.readMPI(bytes.subarray(read)); read += y.length + 2; + return { read, publicParams: { p, g, y } }; + } + case enums.publicKey.ecdsa: { + const oid = new OID(); read += oid.read(bytes); + checkSupportedCurve(oid); + const Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; + return { read: read, publicParams: { oid, Q } }; + } + case enums.publicKey.eddsaLegacy: { + const oid = new OID(); read += oid.read(bytes); + checkSupportedCurve(oid); + let Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; + Q = util.leftPad(Q, 33); + return { read: read, publicParams: { oid, Q } }; + } + case enums.publicKey.ecdh: { + const oid = new OID(); read += oid.read(bytes); + checkSupportedCurve(oid); + const Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; + const kdfParams = new KDFParams(); read += kdfParams.read(bytes.subarray(read)); + return { read: read, publicParams: { oid, Q, kdfParams } }; + } + case enums.publicKey.ed25519: + case enums.publicKey.x25519: { + const A = bytes.subarray(read, read + 32); read += A.length; + return { read, publicParams: { A } }; + } + default: + throw new UnsupportedError('Unknown public key encryption algorithm.'); + } + } + + /** + * Parse private key material in binary form to get the key parameters + * @param {module:enums.publicKey} algo - The key algorithm + * @param {Uint8Array} bytes - The key material to parse + * @param {Object} publicParams - (ECC only) public params, needed to format some private params + * @returns {{ read: Number, privateParams: Object }} Number of read bytes plus the key parameters referenced by name. + */ + function parsePrivateKeyParams(algo, bytes, publicParams) { + let read = 0; + switch (algo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaSign: { + const d = util.readMPI(bytes.subarray(read)); read += d.length + 2; + const p = util.readMPI(bytes.subarray(read)); read += p.length + 2; + const q = util.readMPI(bytes.subarray(read)); read += q.length + 2; + const u = util.readMPI(bytes.subarray(read)); read += u.length + 2; + return { read, privateParams: { d, p, q, u } }; + } + case enums.publicKey.dsa: + case enums.publicKey.elgamal: { + const x = util.readMPI(bytes.subarray(read)); read += x.length + 2; + return { read, privateParams: { x } }; + } + case enums.publicKey.ecdsa: + case enums.publicKey.ecdh: { + const curve = new CurveWithOID(publicParams.oid); + let d = util.readMPI(bytes.subarray(read)); read += d.length + 2; + d = util.leftPad(d, curve.payloadSize); + return { read, privateParams: { d } }; + } + case enums.publicKey.eddsaLegacy: { + const curve = new CurveWithOID(publicParams.oid); + let seed = util.readMPI(bytes.subarray(read)); read += seed.length + 2; + seed = util.leftPad(seed, curve.payloadSize); + return { read, privateParams: { seed } }; + } + case enums.publicKey.ed25519: { + const seed = bytes.subarray(read, read + 32); read += seed.length; + return { read, privateParams: { seed } }; + } + case enums.publicKey.x25519: { + const k = bytes.subarray(read, read + 32); read += k.length; + return { read, privateParams: { k } }; + } + default: + throw new UnsupportedError('Unknown public key encryption algorithm.'); + } + } + + /** Returns the types comprising the encrypted session key of an algorithm + * @param {module:enums.publicKey} algo - The key algorithm + * @param {Uint8Array} bytes - The key material to parse + * @returns {Object} The session key parameters referenced by name. + */ + function parseEncSessionKeyParams(algo, bytes) { + let read = 0; + switch (algo) { + // Algorithm-Specific Fields for RSA encrypted session keys: + // - MPI of RSA encrypted value m**e mod n. + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: { + const c = util.readMPI(bytes.subarray(read)); + return { c }; + } + + // Algorithm-Specific Fields for Elgamal encrypted session keys: + // - MPI of Elgamal value g**k mod p + // - MPI of Elgamal value m * y**k mod p + case enums.publicKey.elgamal: { + const c1 = util.readMPI(bytes.subarray(read)); read += c1.length + 2; + const c2 = util.readMPI(bytes.subarray(read)); + return { c1, c2 }; + } + // Algorithm-Specific Fields for ECDH encrypted session keys: + // - MPI containing the ephemeral key used to establish the shared secret + // - ECDH Symmetric Key + case enums.publicKey.ecdh: { + const V = util.readMPI(bytes.subarray(read)); read += V.length + 2; + const C = new ECDHSymmetricKey(); C.read(bytes.subarray(read)); + return { V, C }; + } + // Algorithm-Specific Fields for X25519 encrypted session keys: + // - 32 octets representing an ephemeral X25519 public key. + // - A one-octet size of the following fields. + // - The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet). + // - The encrypted session key. + case enums.publicKey.x25519: { + const ephemeralPublicKey = bytes.subarray(read, read + 32); read += ephemeralPublicKey.length; + const C = new ECDHXSymmetricKey(); C.read(bytes.subarray(read)); + return { ephemeralPublicKey, C }; + } + default: + throw new UnsupportedError('Unknown public key encryption algorithm.'); + } + } + + /** + * Convert params to MPI and serializes them in the proper order + * @param {module:enums.publicKey} algo - The public key algorithm + * @param {Object} params - The key parameters indexed by name + * @returns {Uint8Array} The array containing the MPIs. + */ + function serializeParams(algo, params) { + // Some algorithms do not rely on MPIs to store the binary params + const algosWithNativeRepresentation = new Set([enums.publicKey.ed25519, enums.publicKey.x25519]); + const orderedParams = Object.keys(params).map(name => { + const param = params[name]; + if (!util.isUint8Array(param)) return param.write(); + return algosWithNativeRepresentation.has(algo) ? param : util.uint8ArrayToMPI(param); + }); + return util.concatUint8Array(orderedParams); + } + + /** + * Generate algorithm-specific key parameters + * @param {module:enums.publicKey} algo - The public key algorithm + * @param {Integer} bits - Bit length for RSA keys + * @param {module:type/oid} oid - Object identifier for ECC keys + * @returns {Promise<{ publicParams: {Object}, privateParams: {Object} }>} The parameters referenced by name. + * @async + */ + function generateParams(algo, bits, oid) { + switch (algo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaSign: { + return publicKey.rsa.generate(bits, 65537).then(({ n, e, d, p, q, u }) => ({ + privateParams: { d, p, q, u }, + publicParams: { n, e } + })); + } + case enums.publicKey.ecdsa: + return publicKey.elliptic.generate(oid).then(({ oid, Q, secret }) => ({ + privateParams: { d: secret }, + publicParams: { oid: new OID(oid), Q } + })); + case enums.publicKey.eddsaLegacy: + return publicKey.elliptic.generate(oid).then(({ oid, Q, secret }) => ({ + privateParams: { seed: secret }, + publicParams: { oid: new OID(oid), Q } + })); + case enums.publicKey.ecdh: + return publicKey.elliptic.generate(oid).then(({ oid, Q, secret, hash, cipher }) => ({ + privateParams: { d: secret }, + publicParams: { + oid: new OID(oid), + Q, + kdfParams: new KDFParams({ hash, cipher }) + } + })); + case enums.publicKey.ed25519: + return publicKey.elliptic.eddsa.generate(algo).then(({ A, seed }) => ({ + privateParams: { seed }, + publicParams: { A } + })); + case enums.publicKey.x25519: + return publicKey.elliptic.ecdhX.generate(algo).then(({ A, k }) => ({ + privateParams: { k }, + publicParams: { A } + })); + case enums.publicKey.dsa: + case enums.publicKey.elgamal: + throw Error('Unsupported algorithm for key generation.'); + default: + throw Error('Unknown public key algorithm.'); + } + } + + /** + * Validate algorithm-specific key parameters + * @param {module:enums.publicKey} algo - The public key algorithm + * @param {Object} publicParams - Algorithm-specific public key parameters + * @param {Object} privateParams - Algorithm-specific private key parameters + * @returns {Promise} Whether the parameters are valid. + * @async + */ + async function validateParams(algo, publicParams, privateParams) { + if (!publicParams || !privateParams) { + throw Error('Missing key parameters'); + } + switch (algo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaSign: { + const { n, e } = publicParams; + const { d, p, q, u } = privateParams; + return publicKey.rsa.validateParams(n, e, d, p, q, u); + } + case enums.publicKey.dsa: { + const { p, q, g, y } = publicParams; + const { x } = privateParams; + return publicKey.dsa.validateParams(p, q, g, y, x); + } + case enums.publicKey.elgamal: { + const { p, g, y } = publicParams; + const { x } = privateParams; + return publicKey.elgamal.validateParams(p, g, y, x); + } + case enums.publicKey.ecdsa: + case enums.publicKey.ecdh: { + const algoModule = publicKey.elliptic[enums.read(enums.publicKey, algo)]; + const { oid, Q } = publicParams; + const { d } = privateParams; + return algoModule.validateParams(oid, Q, d); + } + case enums.publicKey.eddsaLegacy: { + const { Q, oid } = publicParams; + const { seed } = privateParams; + return publicKey.elliptic.eddsaLegacy.validateParams(oid, Q, seed); + } + case enums.publicKey.ed25519: { + const { A } = publicParams; + const { seed } = privateParams; + return publicKey.elliptic.eddsa.validateParams(algo, A, seed); + } + case enums.publicKey.x25519: { + const { A } = publicParams; + const { k } = privateParams; + return publicKey.elliptic.ecdhX.validateParams(algo, A, k); + } + default: + throw Error('Unknown public key algorithm.'); + } + } + + /** + * Generates a random byte prefix for the specified algorithm + * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. + * @param {module:enums.symmetric} algo - Symmetric encryption algorithm + * @returns {Promise} Random bytes with length equal to the block size of the cipher, plus the last two bytes repeated. + * @async + */ + async function getPrefixRandom(algo) { + const { blockSize } = getCipher(algo); + const prefixrandom = await getRandomBytes(blockSize); + const repeat = new Uint8Array([prefixrandom[prefixrandom.length - 2], prefixrandom[prefixrandom.length - 1]]); + return util.concat([prefixrandom, repeat]); + } + + /** + * Generating a session key for the specified symmetric algorithm + * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. + * @param {module:enums.symmetric} algo - Symmetric encryption algorithm + * @returns {Uint8Array} Random bytes as a string to be used as a key. + */ + function generateSessionKey$1(algo) { + const { keySize } = getCipher(algo); + return getRandomBytes(keySize); + } + + /** + * Get implementation of the given AEAD mode + * @param {enums.aead} algo + * @returns {Object} + * @throws {Error} on invalid algo + */ + function getAEADMode(algo) { + const algoName = enums.read(enums.aead, algo); + return mode[algoName]; + } + + /** + * Check whether the given curve OID is supported + * @param {module:type/oid} oid - EC object identifier + * @throws {UnsupportedError} if curve is not supported + */ + function checkSupportedCurve(oid) { + try { + oid.getName(); + } catch (e) { + throw new UnsupportedError('Unknown curve OID'); + } + } + + /** + * Get preferred hash algo for a given elliptic algo + * @param {module:enums.publicKey} algo - alrogithm identifier + * @param {module:type/oid} [oid] - curve OID if needed by algo + */ + function getPreferredCurveHashAlgo(algo, oid) { + switch (algo) { + case enums.publicKey.ecdsa: + case enums.publicKey.eddsaLegacy: + return publicKey.elliptic.getPreferredHashAlgo(oid); + case enums.publicKey.ed25519: + return publicKey.elliptic.eddsa.getPreferredHashAlgo(algo); + default: + throw Error('Unknown elliptic signing algo'); + } + } + + var crypto$2 = /*#__PURE__*/Object.freeze({ + __proto__: null, + publicKeyEncrypt: publicKeyEncrypt, + publicKeyDecrypt: publicKeyDecrypt, + parsePublicKeyParams: parsePublicKeyParams, + parsePrivateKeyParams: parsePrivateKeyParams, + parseEncSessionKeyParams: parseEncSessionKeyParams, + serializeParams: serializeParams, + generateParams: generateParams, + validateParams: validateParams, + getPrefixRandom: getPrefixRandom, + generateSessionKey: generateSessionKey$1, + getAEADMode: getAEADMode, + getCipher: getCipher, + getPreferredCurveHashAlgo: getPreferredCurveHashAlgo + }); + + /** + * @fileoverview Provides access to all cryptographic primitives used in OpenPGP.js + * @see module:crypto/crypto + * @see module:crypto/signature + * @see module:crypto/public_key + * @see module:crypto/cipher + * @see module:crypto/random + * @see module:crypto/hash + * @module crypto + * @private + */ + + // TODO move cfb and gcm to cipher + const mod = { + /** @see module:crypto/cipher */ + cipher: cipher, + /** @see module:crypto/hash */ + hash: hash, + /** @see module:crypto/mode */ + mode: mode, + /** @see module:crypto/public_key */ + publicKey: publicKey, + /** @see module:crypto/signature */ + signature: signature$2, + /** @see module:crypto/random */ + random: random, + /** @see module:crypto/pkcs1 */ + pkcs1: pkcs1, + /** @see module:crypto/pkcs5 */ + pkcs5: pkcs5, + /** @see module:crypto/aes_kw */ + aesKW: aesKW + }; + + Object.assign(mod, crypto$2); + + var TYPED_OK = typeof Uint8Array !== "undefined" && + typeof Uint16Array !== "undefined" && + typeof Int32Array !== "undefined"; + + + // reduce buffer size, avoiding mem copy + function shrinkBuf(buf, size) { + if (buf.length === size) { + return buf; + } + if (buf.subarray) { + return buf.subarray(0, size); + } + buf.length = size; + return buf; + } + + + const fnTyped = { + arraySet: function (dest, src, src_offs, len, dest_offs) { + if (src.subarray && dest.subarray) { + dest.set(src.subarray(src_offs, src_offs + len), dest_offs); + return; + } + // Fallback to ordinary array + for (let i = 0; i < len; i++) { + dest[dest_offs + i] = src[src_offs + i]; + } + }, + // Join array of chunks to single array. + flattenChunks: function (chunks) { + let i, l, len, pos, chunk; + + // calculate data length + len = 0; + for (i = 0, l = chunks.length; i < l; i++) { + len += chunks[i].length; + } + + // join chunks + const result = new Uint8Array(len); + pos = 0; + for (i = 0, l = chunks.length; i < l; i++) { + chunk = chunks[i]; + result.set(chunk, pos); + pos += chunk.length; + } + + return result; + } + }; + + const fnUntyped = { + arraySet: function (dest, src, src_offs, len, dest_offs) { + for (let i = 0; i < len; i++) { + dest[dest_offs + i] = src[src_offs + i]; + } + }, + // Join array of chunks to single array. + flattenChunks: function (chunks) { + return [].concat.apply([], chunks); + } + }; + + + // Enable/Disable typed arrays use, for testing + // + + let Buf8 = TYPED_OK ? Uint8Array : Array; + let Buf16 = TYPED_OK ? Uint16Array : Array; + let Buf32 = TYPED_OK ? Int32Array : Array; + let flattenChunks = TYPED_OK ? fnTyped.flattenChunks : fnUntyped.flattenChunks; + let arraySet = TYPED_OK ? fnTyped.arraySet : fnUntyped.arraySet; + + // (C) 1995-2013 Jean-loup Gailly and Mark Adler + // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. + + /* Allowed flush values; see deflate() and inflate() below for details */ + const Z_NO_FLUSH = 0; + const Z_PARTIAL_FLUSH = 1; + const Z_SYNC_FLUSH = 2; + const Z_FULL_FLUSH = 3; + const Z_FINISH = 4; + const Z_BLOCK = 5; + const Z_TREES = 6; + + /* Return codes for the compression/decompression functions. Negative values + * are errors, positive values are used for special but normal events. + */ + const Z_OK = 0; + const Z_STREAM_END = 1; + const Z_NEED_DICT = 2; + const Z_STREAM_ERROR = -2; + const Z_DATA_ERROR = -3; + //export const Z_MEM_ERROR = -4; + const Z_BUF_ERROR = -5; + const Z_DEFAULT_COMPRESSION = -1; + + + const Z_FILTERED = 1; + const Z_HUFFMAN_ONLY = 2; + const Z_RLE = 3; + const Z_FIXED = 4; + const Z_DEFAULT_STRATEGY = 0; + + /* Possible values of the data_type field (though see inflate()) */ + const Z_BINARY = 0; + const Z_TEXT = 1; + //export const Z_ASCII = 1; // = Z_TEXT (deprecated) + const Z_UNKNOWN = 2; + + /* The deflate compression method */ + const Z_DEFLATED = 8; + //export const Z_NULL = null // Use -1 or null inline, depending on var type + + /*============================================================================*/ + + + function zero$1(buf) { + let len = buf.length; while (--len >= 0) { + buf[len] = 0; + } + } + + // From zutil.h + + const STORED_BLOCK = 0; + const STATIC_TREES = 1; + const DYN_TREES = 2; + /* The three kinds of block type */ + + const MIN_MATCH$1 = 3; + const MAX_MATCH$1 = 258; + /* The minimum and maximum match lengths */ + + // From deflate.h + /* =========================================================================== + * Internal compression state. + */ + + const LENGTH_CODES$1 = 29; + /* number of length codes, not counting the special END_BLOCK code */ + + const LITERALS$1 = 256; + /* number of literal bytes 0..255 */ + + const L_CODES$1 = LITERALS$1 + 1 + LENGTH_CODES$1; + /* number of Literal or Length codes, including the END_BLOCK code */ + + const D_CODES$1 = 30; + /* number of distance codes */ + + const BL_CODES$1 = 19; + /* number of codes used to transfer the bit lengths */ + + const HEAP_SIZE$1 = 2 * L_CODES$1 + 1; + /* maximum heap size */ + + const MAX_BITS$1 = 15; + /* All codes must not exceed MAX_BITS bits */ + + const Buf_size = 16; + /* size of bit buffer in bi_buf */ + + + /* =========================================================================== + * Constants + */ + + const MAX_BL_BITS = 7; + /* Bit length codes must not exceed MAX_BL_BITS bits */ + + const END_BLOCK = 256; + /* end of block literal code */ + + const REP_3_6 = 16; + /* repeat previous bit length 3-6 times (2 bits of repeat count) */ + + const REPZ_3_10 = 17; + /* repeat a zero length 3-10 times (3 bits of repeat count) */ + + const REPZ_11_138 = 18; + /* repeat a zero length 11-138 times (7 bits of repeat count) */ + + /* eslint-disable comma-spacing,array-bracket-spacing */ + const extra_lbits = /* extra bits for each length code */ + [0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]; + + const extra_dbits = /* extra bits for each distance code */ + [0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]; + + const extra_blbits = /* extra bits for each bit length code */ + [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]; + + const bl_order = + [16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]; + /* eslint-enable comma-spacing,array-bracket-spacing */ + + /* The lengths of the bit length codes are sent in order of decreasing + * probability, to avoid transmitting the lengths for unused bit length codes. + */ + + /* =========================================================================== + * Local data. These are initialized only once. + */ + + // We pre-fill arrays with 0 to avoid uninitialized gaps + + const DIST_CODE_LEN = 512; /* see definition of array dist_code below */ + + // !!!! Use flat array instead of structure, Freq = i*2, Len = i*2+1 + const static_ltree = new Array((L_CODES$1 + 2) * 2); + zero$1(static_ltree); + /* The static literal tree. Since the bit lengths are imposed, there is no + * need for the L_CODES extra codes used during heap construction. However + * The codes 286 and 287 are needed to build a canonical tree (see _tr_init + * below). + */ + + const static_dtree = new Array(D_CODES$1 * 2); + zero$1(static_dtree); + /* The static distance tree. (Actually a trivial tree since all codes use + * 5 bits.) + */ + + const _dist_code = new Array(DIST_CODE_LEN); + zero$1(_dist_code); + /* Distance codes. The first 256 values correspond to the distances + * 3 .. 258, the last 256 values correspond to the top 8 bits of + * the 15 bit distances. + */ + + const _length_code = new Array(MAX_MATCH$1 - MIN_MATCH$1 + 1); + zero$1(_length_code); + /* length code for each normalized match length (0 == MIN_MATCH) */ + + const base_length = new Array(LENGTH_CODES$1); + zero$1(base_length); + /* First normalized length for each code (0 = MIN_MATCH) */ + + const base_dist = new Array(D_CODES$1); + zero$1(base_dist); + /* First normalized distance for each code (0 = distance of 1) */ + + + function StaticTreeDesc(static_tree, extra_bits, extra_base, elems, max_length) { + + this.static_tree = static_tree; /* static tree or NULL */ + this.extra_bits = extra_bits; /* extra bits for each code or NULL */ + this.extra_base = extra_base; /* base index for extra_bits */ + this.elems = elems; /* max number of elements in the tree */ + this.max_length = max_length; /* max bit length for the codes */ + + // show if `static_tree` has data or dummy - needed for monomorphic objects + this.has_stree = static_tree && static_tree.length; + } + + + let static_l_desc; + let static_d_desc; + let static_bl_desc; + + + function TreeDesc(dyn_tree, stat_desc) { + this.dyn_tree = dyn_tree; /* the dynamic tree */ + this.max_code = 0; /* largest code with non zero frequency */ + this.stat_desc = stat_desc; /* the corresponding static tree */ + } + + + + function d_code(dist) { + return dist < 256 ? _dist_code[dist] : _dist_code[256 + (dist >>> 7)]; + } + + + /* =========================================================================== + * Output a short LSB first on the stream. + * IN assertion: there is enough room in pendingBuf. + */ + function put_short(s, w) { + // put_byte(s, (uch)((w) & 0xff)); + // put_byte(s, (uch)((ush)(w) >> 8)); + s.pending_buf[s.pending++] = w & 0xff; + s.pending_buf[s.pending++] = w >>> 8 & 0xff; + } + + + /* =========================================================================== + * Send a value on a given number of bits. + * IN assertion: length <= 16 and value fits in length bits. + */ + function send_bits(s, value, length) { + if (s.bi_valid > Buf_size - length) { + s.bi_buf |= value << s.bi_valid & 0xffff; + put_short(s, s.bi_buf); + s.bi_buf = value >> Buf_size - s.bi_valid; + s.bi_valid += length - Buf_size; + } else { + s.bi_buf |= value << s.bi_valid & 0xffff; + s.bi_valid += length; + } + } + + + function send_code(s, c, tree) { + send_bits(s, tree[c * 2]/*.Code*/, tree[c * 2 + 1]/*.Len*/); + } + + + /* =========================================================================== + * Reverse the first len bits of a code, using straightforward code (a faster + * method would use a table) + * IN assertion: 1 <= len <= 15 + */ + function bi_reverse(code, len) { + let res = 0; + do { + res |= code & 1; + code >>>= 1; + res <<= 1; + } while (--len > 0); + return res >>> 1; + } + + + /* =========================================================================== + * Flush the bit buffer, keeping at most 7 bits in it. + */ + function bi_flush(s) { + if (s.bi_valid === 16) { + put_short(s, s.bi_buf); + s.bi_buf = 0; + s.bi_valid = 0; + + } else if (s.bi_valid >= 8) { + s.pending_buf[s.pending++] = s.bi_buf & 0xff; + s.bi_buf >>= 8; + s.bi_valid -= 8; + } + } + + + /* =========================================================================== + * Compute the optimal bit lengths for a tree and update the total bit length + * for the current block. + * IN assertion: the fields freq and dad are set, heap[heap_max] and + * above are the tree nodes sorted by increasing frequency. + * OUT assertions: the field len is set to the optimal bit length, the + * array bl_count contains the frequencies for each bit length. + * The length opt_len is updated; static_len is also updated if stree is + * not null. + */ + function gen_bitlen(s, desc) + // deflate_state *s; + // tree_desc *desc; /* the tree descriptor */ + { + const tree = desc.dyn_tree; + const max_code = desc.max_code; + const stree = desc.stat_desc.static_tree; + const has_stree = desc.stat_desc.has_stree; + const extra = desc.stat_desc.extra_bits; + const base = desc.stat_desc.extra_base; + const max_length = desc.stat_desc.max_length; + let h; /* heap index */ + let n, m; /* iterate over the tree elements */ + let bits; /* bit length */ + let xbits; /* extra bits */ + let f; /* frequency */ + let overflow = 0; /* number of elements with bit length too large */ + + for (bits = 0; bits <= MAX_BITS$1; bits++) { + s.bl_count[bits] = 0; + } + + /* In a first pass, compute the optimal bit lengths (which may + * overflow in the case of the bit length tree). + */ + tree[s.heap[s.heap_max] * 2 + 1]/*.Len*/ = 0; /* root of the heap */ + + for (h = s.heap_max + 1; h < HEAP_SIZE$1; h++) { + n = s.heap[h]; + bits = tree[tree[n * 2 + 1]/*.Dad*/ * 2 + 1]/*.Len*/ + 1; + if (bits > max_length) { + bits = max_length; + overflow++; + } + tree[n * 2 + 1]/*.Len*/ = bits; + /* We overwrite tree[n].Dad which is no longer needed */ + + if (n > max_code) { + continue; + } /* not a leaf node */ + + s.bl_count[bits]++; + xbits = 0; + if (n >= base) { + xbits = extra[n - base]; + } + f = tree[n * 2]/*.Freq*/; + s.opt_len += f * (bits + xbits); + if (has_stree) { + s.static_len += f * (stree[n * 2 + 1]/*.Len*/ + xbits); + } + } + if (overflow === 0) { + return; + } + + // Trace((stderr,"\nbit length overflow\n")); + /* This happens for example on obj2 and pic of the Calgary corpus */ + + /* Find the first bit length which could increase: */ + do { + bits = max_length - 1; + while (s.bl_count[bits] === 0) { + bits--; + } + s.bl_count[bits]--; /* move one leaf down the tree */ + s.bl_count[bits + 1] += 2; /* move one overflow item as its brother */ + s.bl_count[max_length]--; + /* The brother of the overflow item also moves one step up, + * but this does not affect bl_count[max_length] + */ + overflow -= 2; + } while (overflow > 0); + + /* Now recompute all bit lengths, scanning in increasing frequency. + * h is still equal to HEAP_SIZE. (It is simpler to reconstruct all + * lengths instead of fixing only the wrong ones. This idea is taken + * from 'ar' written by Haruhiko Okumura.) + */ + for (bits = max_length; bits !== 0; bits--) { + n = s.bl_count[bits]; + while (n !== 0) { + m = s.heap[--h]; + if (m > max_code) { + continue; + } + if (tree[m * 2 + 1]/*.Len*/ !== bits) { + // Trace((stderr,"code %d bits %d->%d\n", m, tree[m].Len, bits)); + s.opt_len += (bits - tree[m * 2 + 1]/*.Len*/) * tree[m * 2]/*.Freq*/; + tree[m * 2 + 1]/*.Len*/ = bits; + } + n--; + } + } + } + + + /* =========================================================================== + * Generate the codes for a given tree and bit counts (which need not be + * optimal). + * IN assertion: the array bl_count contains the bit length statistics for + * the given tree and the field len is set for all tree elements. + * OUT assertion: the field code is set for all tree elements of non + * zero code length. + */ + function gen_codes(tree, max_code, bl_count) + // ct_data *tree; /* the tree to decorate */ + // int max_code; /* largest code with non zero frequency */ + // ushf *bl_count; /* number of codes at each bit length */ + { + const next_code = new Array(MAX_BITS$1 + 1); /* next code value for each bit length */ + let code = 0; /* running code value */ + let bits; /* bit index */ + let n; /* code index */ + + /* The distribution counts are first used to generate the code values + * without bit reversal. + */ + for (bits = 1; bits <= MAX_BITS$1; bits++) { + next_code[bits] = code = code + bl_count[bits - 1] << 1; + } + /* Check that the bit counts in bl_count are consistent. The last code + * must be all ones. + */ + //Assert (code + bl_count[MAX_BITS]-1 == (1< length code (0..28) */ + length = 0; + for (code = 0; code < LENGTH_CODES$1 - 1; code++) { + base_length[code] = length; + for (n = 0; n < 1 << extra_lbits[code]; n++) { + _length_code[length++] = code; + } + } + //Assert (length == 256, "tr_static_init: length != 256"); + /* Note that the length 255 (match length 258) can be represented + * in two different ways: code 284 + 5 bits or code 285, so we + * overwrite length_code[255] to use the best encoding: + */ + _length_code[length - 1] = code; + + /* Initialize the mapping dist (0..32K) -> dist code (0..29) */ + dist = 0; + for (code = 0; code < 16; code++) { + base_dist[code] = dist; + for (n = 0; n < 1 << extra_dbits[code]; n++) { + _dist_code[dist++] = code; + } + } + //Assert (dist == 256, "tr_static_init: dist != 256"); + dist >>= 7; /* from now on, all distances are divided by 128 */ + for (; code < D_CODES$1; code++) { + base_dist[code] = dist << 7; + for (n = 0; n < 1 << extra_dbits[code] - 7; n++) { + _dist_code[256 + dist++] = code; + } + } + //Assert (dist == 256, "tr_static_init: 256+dist != 512"); + + /* Construct the codes of the static literal tree */ + for (bits = 0; bits <= MAX_BITS$1; bits++) { + bl_count[bits] = 0; + } + + n = 0; + while (n <= 143) { + static_ltree[n * 2 + 1]/*.Len*/ = 8; + n++; + bl_count[8]++; + } + while (n <= 255) { + static_ltree[n * 2 + 1]/*.Len*/ = 9; + n++; + bl_count[9]++; + } + while (n <= 279) { + static_ltree[n * 2 + 1]/*.Len*/ = 7; + n++; + bl_count[7]++; + } + while (n <= 287) { + static_ltree[n * 2 + 1]/*.Len*/ = 8; + n++; + bl_count[8]++; + } + /* Codes 286 and 287 do not exist, but we must include them in the + * tree construction to get a canonical Huffman tree (longest code + * all ones) + */ + gen_codes(static_ltree, L_CODES$1 + 1, bl_count); + + /* The static distance tree is trivial: */ + for (n = 0; n < D_CODES$1; n++) { + static_dtree[n * 2 + 1]/*.Len*/ = 5; + static_dtree[n * 2]/*.Code*/ = bi_reverse(n, 5); + } + + // Now data ready and we can init static trees + static_l_desc = new StaticTreeDesc(static_ltree, extra_lbits, LITERALS$1 + 1, L_CODES$1, MAX_BITS$1); + static_d_desc = new StaticTreeDesc(static_dtree, extra_dbits, 0, D_CODES$1, MAX_BITS$1); + static_bl_desc = new StaticTreeDesc(new Array(0), extra_blbits, 0, BL_CODES$1, MAX_BL_BITS); + + //static_init_done = true; + } + + + /* =========================================================================== + * Initialize a new block. + */ + function init_block(s) { + let n; /* iterates over tree elements */ + + /* Initialize the trees. */ + for (n = 0; n < L_CODES$1; n++) { + s.dyn_ltree[n * 2]/*.Freq*/ = 0; + } + for (n = 0; n < D_CODES$1; n++) { + s.dyn_dtree[n * 2]/*.Freq*/ = 0; + } + for (n = 0; n < BL_CODES$1; n++) { + s.bl_tree[n * 2]/*.Freq*/ = 0; + } + + s.dyn_ltree[END_BLOCK * 2]/*.Freq*/ = 1; + s.opt_len = s.static_len = 0; + s.last_lit = s.matches = 0; + } + + + /* =========================================================================== + * Flush the bit buffer and align the output on a byte boundary + */ + function bi_windup(s) { + if (s.bi_valid > 8) { + put_short(s, s.bi_buf); + } else if (s.bi_valid > 0) { + //put_byte(s, (Byte)s->bi_buf); + s.pending_buf[s.pending++] = s.bi_buf; + } + s.bi_buf = 0; + s.bi_valid = 0; + } + + /* =========================================================================== + * Copy a stored block, storing first the length and its + * one's complement if requested. + */ + function copy_block(s, buf, len, header) + //DeflateState *s; + //charf *buf; /* the input data */ + //unsigned len; /* its length */ + //int header; /* true if block header must be written */ + { + bi_windup(s); /* align on byte boundary */ + + if (header) { + put_short(s, len); + put_short(s, ~len); + } + // while (len--) { + // put_byte(s, *buf++); + // } + arraySet(s.pending_buf, s.window, buf, len, s.pending); + s.pending += len; + } + + /* =========================================================================== + * Compares to subtrees, using the tree depth as tie breaker when + * the subtrees have equal frequency. This minimizes the worst case length. + */ + function smaller(tree, n, m, depth) { + const _n2 = n * 2; + const _m2 = m * 2; + return tree[_n2]/*.Freq*/ < tree[_m2]/*.Freq*/ || + tree[_n2]/*.Freq*/ === tree[_m2]/*.Freq*/ && depth[n] <= depth[m]; + } + + /* =========================================================================== + * Restore the heap property by moving down the tree starting at node k, + * exchanging a node with the smallest of its two sons if necessary, stopping + * when the heap property is re-established (each father smaller than its + * two sons). + */ + function pqdownheap(s, tree, k) + // deflate_state *s; + // ct_data *tree; /* the tree to restore */ + // int k; /* node to move down */ + { + const v = s.heap[k]; + let j = k << 1; /* left son of k */ + while (j <= s.heap_len) { + /* Set j to the smallest of the two sons: */ + if (j < s.heap_len && + smaller(tree, s.heap[j + 1], s.heap[j], s.depth)) { + j++; + } + /* Exit if v is smaller than both sons */ + if (smaller(tree, v, s.heap[j], s.depth)) { + break; + } + + /* Exchange v with the smallest son */ + s.heap[k] = s.heap[j]; + k = j; + + /* And continue down the tree, setting j to the left son of k */ + j <<= 1; + } + s.heap[k] = v; + } + + + // inlined manually + // var SMALLEST = 1; + + /* =========================================================================== + * Send the block data compressed using the given Huffman trees + */ + function compress_block(s, ltree, dtree) + // deflate_state *s; + // const ct_data *ltree; /* literal tree */ + // const ct_data *dtree; /* distance tree */ + { + let dist; /* distance of matched string */ + let lc; /* match length or unmatched char (if dist == 0) */ + let lx = 0; /* running index in l_buf */ + let code; /* the code to send */ + let extra; /* number of extra bits to send */ + + if (s.last_lit !== 0) { + do { + dist = s.pending_buf[s.d_buf + lx * 2] << 8 | s.pending_buf[s.d_buf + lx * 2 + 1]; + lc = s.pending_buf[s.l_buf + lx]; + lx++; + + if (dist === 0) { + send_code(s, lc, ltree); /* send a literal byte */ + //Tracecv(isgraph(lc), (stderr," '%c' ", lc)); + } else { + /* Here, lc is the match length - MIN_MATCH */ + code = _length_code[lc]; + send_code(s, code + LITERALS$1 + 1, ltree); /* send the length code */ + extra = extra_lbits[code]; + if (extra !== 0) { + lc -= base_length[code]; + send_bits(s, lc, extra); /* send the extra length bits */ + } + dist--; /* dist is now the match distance - 1 */ + code = d_code(dist); + //Assert (code < D_CODES, "bad d_code"); + + send_code(s, code, dtree); /* send the distance code */ + extra = extra_dbits[code]; + if (extra !== 0) { + dist -= base_dist[code]; + send_bits(s, dist, extra); /* send the extra distance bits */ + } + } /* literal or match pair ? */ + + /* Check that the overlay between pending_buf and d_buf+l_buf is ok: */ + //Assert((uInt)(s->pending) < s->lit_bufsize + 2*lx, + // "pendingBuf overflow"); + + } while (lx < s.last_lit); + } + + send_code(s, END_BLOCK, ltree); + } + + + /* =========================================================================== + * Construct one Huffman tree and assigns the code bit strings and lengths. + * Update the total bit length for the current block. + * IN assertion: the field freq is set for all tree elements. + * OUT assertions: the fields len and code are set to the optimal bit length + * and corresponding code. The length opt_len is updated; static_len is + * also updated if stree is not null. The field max_code is set. + */ + function build_tree(s, desc) + // deflate_state *s; + // tree_desc *desc; /* the tree descriptor */ + { + const tree = desc.dyn_tree; + const stree = desc.stat_desc.static_tree; + const has_stree = desc.stat_desc.has_stree; + const elems = desc.stat_desc.elems; + let n, m; /* iterate over heap elements */ + let max_code = -1; /* largest code with non zero frequency */ + let node; /* new node being created */ + + /* Construct the initial heap, with least frequent element in + * heap[SMALLEST]. The sons of heap[n] are heap[2*n] and heap[2*n+1]. + * heap[0] is not used. + */ + s.heap_len = 0; + s.heap_max = HEAP_SIZE$1; + + for (n = 0; n < elems; n++) { + if (tree[n * 2]/*.Freq*/ !== 0) { + s.heap[++s.heap_len] = max_code = n; + s.depth[n] = 0; + + } else { + tree[n * 2 + 1]/*.Len*/ = 0; + } + } + + /* The pkzip format requires that at least one distance code exists, + * and that at least one bit should be sent even if there is only one + * possible code. So to avoid special checks later on we force at least + * two codes of non zero frequency. + */ + while (s.heap_len < 2) { + node = s.heap[++s.heap_len] = max_code < 2 ? ++max_code : 0; + tree[node * 2]/*.Freq*/ = 1; + s.depth[node] = 0; + s.opt_len--; + + if (has_stree) { + s.static_len -= stree[node * 2 + 1]/*.Len*/; + } + /* node is 0 or 1 so it does not have extra bits */ + } + desc.max_code = max_code; + + /* The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree, + * establish sub-heaps of increasing lengths: + */ + for (n = s.heap_len >> 1/*int /2*/; n >= 1; n--) { + pqdownheap(s, tree, n); + } + + /* Construct the Huffman tree by repeatedly combining the least two + * frequent nodes. + */ + node = elems; /* next internal node of the tree */ + do { + //pqremove(s, tree, n); /* n = node of least frequency */ + /*** pqremove ***/ + n = s.heap[1/*SMALLEST*/]; + s.heap[1/*SMALLEST*/] = s.heap[s.heap_len--]; + pqdownheap(s, tree, 1/*SMALLEST*/); + /***/ + + m = s.heap[1/*SMALLEST*/]; /* m = node of next least frequency */ + + s.heap[--s.heap_max] = n; /* keep the nodes sorted by frequency */ + s.heap[--s.heap_max] = m; + + /* Create a new node father of n and m */ + tree[node * 2]/*.Freq*/ = tree[n * 2]/*.Freq*/ + tree[m * 2]/*.Freq*/; + s.depth[node] = (s.depth[n] >= s.depth[m] ? s.depth[n] : s.depth[m]) + 1; + tree[n * 2 + 1]/*.Dad*/ = tree[m * 2 + 1]/*.Dad*/ = node; + + /* and insert the new node in the heap */ + s.heap[1/*SMALLEST*/] = node++; + pqdownheap(s, tree, 1/*SMALLEST*/); + + } while (s.heap_len >= 2); + + s.heap[--s.heap_max] = s.heap[1/*SMALLEST*/]; + + /* At this point, the fields freq and dad are set. We can now + * generate the bit lengths. + */ + gen_bitlen(s, desc); + + /* The field len is now set, we can generate the bit codes */ + gen_codes(tree, max_code, s.bl_count); + } + + + /* =========================================================================== + * Scan a literal or distance tree to determine the frequencies of the codes + * in the bit length tree. + */ + function scan_tree(s, tree, max_code) + // deflate_state *s; + // ct_data *tree; /* the tree to be scanned */ + // int max_code; /* and its largest code of non zero frequency */ + { + let n; /* iterates over all tree elements */ + let prevlen = -1; /* last emitted length */ + let curlen; /* length of current code */ + + let nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */ + + let count = 0; /* repeat count of the current code */ + let max_count = 7; /* max repeat count */ + let min_count = 4; /* min repeat count */ + + if (nextlen === 0) { + max_count = 138; + min_count = 3; + } + tree[(max_code + 1) * 2 + 1]/*.Len*/ = 0xffff; /* guard */ + + for (n = 0; n <= max_code; n++) { + curlen = nextlen; + nextlen = tree[(n + 1) * 2 + 1]/*.Len*/; + + if (++count < max_count && curlen === nextlen) { + continue; + + } else if (count < min_count) { + s.bl_tree[curlen * 2]/*.Freq*/ += count; + + } else if (curlen !== 0) { + + if (curlen !== prevlen) { + s.bl_tree[curlen * 2]/*.Freq*/++; + } + s.bl_tree[REP_3_6 * 2]/*.Freq*/++; + + } else if (count <= 10) { + s.bl_tree[REPZ_3_10 * 2]/*.Freq*/++; + + } else { + s.bl_tree[REPZ_11_138 * 2]/*.Freq*/++; + } + + count = 0; + prevlen = curlen; + + if (nextlen === 0) { + max_count = 138; + min_count = 3; + + } else if (curlen === nextlen) { + max_count = 6; + min_count = 3; + + } else { + max_count = 7; + min_count = 4; + } + } + } + + + /* =========================================================================== + * Send a literal or distance tree in compressed form, using the codes in + * bl_tree. + */ + function send_tree(s, tree, max_code) + // deflate_state *s; + // ct_data *tree; /* the tree to be scanned */ + // int max_code; /* and its largest code of non zero frequency */ + { + let n; /* iterates over all tree elements */ + let prevlen = -1; /* last emitted length */ + let curlen; /* length of current code */ + + let nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */ + + let count = 0; /* repeat count of the current code */ + let max_count = 7; /* max repeat count */ + let min_count = 4; /* min repeat count */ + + /* tree[max_code+1].Len = -1; */ /* guard already set */ + if (nextlen === 0) { + max_count = 138; + min_count = 3; + } + + for (n = 0; n <= max_code; n++) { + curlen = nextlen; + nextlen = tree[(n + 1) * 2 + 1]/*.Len*/; + + if (++count < max_count && curlen === nextlen) { + continue; + + } else if (count < min_count) { + do { + send_code(s, curlen, s.bl_tree); + } while (--count !== 0); + + } else if (curlen !== 0) { + if (curlen !== prevlen) { + send_code(s, curlen, s.bl_tree); + count--; + } + //Assert(count >= 3 && count <= 6, " 3_6?"); + send_code(s, REP_3_6, s.bl_tree); + send_bits(s, count - 3, 2); + + } else if (count <= 10) { + send_code(s, REPZ_3_10, s.bl_tree); + send_bits(s, count - 3, 3); + + } else { + send_code(s, REPZ_11_138, s.bl_tree); + send_bits(s, count - 11, 7); + } + + count = 0; + prevlen = curlen; + if (nextlen === 0) { + max_count = 138; + min_count = 3; + + } else if (curlen === nextlen) { + max_count = 6; + min_count = 3; + + } else { + max_count = 7; + min_count = 4; + } + } + } + + + /* =========================================================================== + * Construct the Huffman tree for the bit lengths and return the index in + * bl_order of the last bit length code to send. + */ + function build_bl_tree(s) { + let max_blindex; /* index of last bit length code of non zero freq */ + + /* Determine the bit length frequencies for literal and distance trees */ + scan_tree(s, s.dyn_ltree, s.l_desc.max_code); + scan_tree(s, s.dyn_dtree, s.d_desc.max_code); + + /* Build the bit length tree: */ + build_tree(s, s.bl_desc); + /* opt_len now includes the length of the tree representations, except + * the lengths of the bit lengths codes and the 5+5+4 bits for the counts. + */ + + /* Determine the number of bit length codes to send. The pkzip format + * requires that at least 4 bit length codes be sent. (appnote.txt says + * 3 but the actual value used is 4.) + */ + for (max_blindex = BL_CODES$1 - 1; max_blindex >= 3; max_blindex--) { + if (s.bl_tree[bl_order[max_blindex] * 2 + 1]/*.Len*/ !== 0) { + break; + } + } + /* Update opt_len to include the bit length tree and counts */ + s.opt_len += 3 * (max_blindex + 1) + 5 + 5 + 4; + //Tracev((stderr, "\ndyn trees: dyn %ld, stat %ld", + // s->opt_len, s->static_len)); + + return max_blindex; + } + + + /* =========================================================================== + * Send the header for a block using dynamic Huffman trees: the counts, the + * lengths of the bit length codes, the literal tree and the distance tree. + * IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4. + */ + function send_all_trees(s, lcodes, dcodes, blcodes) + // deflate_state *s; + // int lcodes, dcodes, blcodes; /* number of codes for each tree */ + { + let rank; /* index in bl_order */ + + //Assert (lcodes >= 257 && dcodes >= 1 && blcodes >= 4, "not enough codes"); + //Assert (lcodes <= L_CODES && dcodes <= D_CODES && blcodes <= BL_CODES, + // "too many codes"); + //Tracev((stderr, "\nbl counts: ")); + send_bits(s, lcodes - 257, 5); /* not +255 as stated in appnote.txt */ + send_bits(s, dcodes - 1, 5); + send_bits(s, blcodes - 4, 4); /* not -3 as stated in appnote.txt */ + for (rank = 0; rank < blcodes; rank++) { + //Tracev((stderr, "\nbl code %2d ", bl_order[rank])); + send_bits(s, s.bl_tree[bl_order[rank] * 2 + 1]/*.Len*/, 3); + } + //Tracev((stderr, "\nbl tree: sent %ld", s->bits_sent)); + + send_tree(s, s.dyn_ltree, lcodes - 1); /* literal tree */ + //Tracev((stderr, "\nlit tree: sent %ld", s->bits_sent)); + + send_tree(s, s.dyn_dtree, dcodes - 1); /* distance tree */ + //Tracev((stderr, "\ndist tree: sent %ld", s->bits_sent)); + } + + + /* =========================================================================== + * Check if the data type is TEXT or BINARY, using the following algorithm: + * - TEXT if the two conditions below are satisfied: + * a) There are no non-portable control characters belonging to the + * "black list" (0..6, 14..25, 28..31). + * b) There is at least one printable character belonging to the + * "white list" (9 {TAB}, 10 {LF}, 13 {CR}, 32..255). + * - BINARY otherwise. + * - The following partially-portable control characters form a + * "gray list" that is ignored in this detection algorithm: + * (7 {BEL}, 8 {BS}, 11 {VT}, 12 {FF}, 26 {SUB}, 27 {ESC}). + * IN assertion: the fields Freq of dyn_ltree are set. + */ + function detect_data_type(s) { + /* black_mask is the bit mask of black-listed bytes + * set bits 0..6, 14..25, and 28..31 + * 0xf3ffc07f = binary 11110011111111111100000001111111 + */ + let black_mask = 0xf3ffc07f; + let n; + + /* Check for non-textual ("black-listed") bytes. */ + for (n = 0; n <= 31; n++, black_mask >>>= 1) { + if (black_mask & 1 && s.dyn_ltree[n * 2]/*.Freq*/ !== 0) { + return Z_BINARY; + } + } + + /* Check for textual ("white-listed") bytes. */ + if (s.dyn_ltree[9 * 2]/*.Freq*/ !== 0 || s.dyn_ltree[10 * 2]/*.Freq*/ !== 0 || + s.dyn_ltree[13 * 2]/*.Freq*/ !== 0) { + return Z_TEXT; + } + for (n = 32; n < LITERALS$1; n++) { + if (s.dyn_ltree[n * 2]/*.Freq*/ !== 0) { + return Z_TEXT; + } + } + + /* There are no "black-listed" or "white-listed" bytes: + * this stream either is empty or has tolerated ("gray-listed") bytes only. + */ + return Z_BINARY; + } + + + let static_init_done = false; + + /* =========================================================================== + * Initialize the tree data structures for a new zlib stream. + */ + function _tr_init(s) { + + if (!static_init_done) { + tr_static_init(); + static_init_done = true; + } + + s.l_desc = new TreeDesc(s.dyn_ltree, static_l_desc); + s.d_desc = new TreeDesc(s.dyn_dtree, static_d_desc); + s.bl_desc = new TreeDesc(s.bl_tree, static_bl_desc); + + s.bi_buf = 0; + s.bi_valid = 0; + + /* Initialize the first block of the first file: */ + init_block(s); + } + + + /* =========================================================================== + * Send a stored block + */ + function _tr_stored_block(s, buf, stored_len, last) + //DeflateState *s; + //charf *buf; /* input block */ + //ulg stored_len; /* length of input block */ + //int last; /* one if this is the last block for a file */ + { + send_bits(s, (STORED_BLOCK << 1) + (last ? 1 : 0), 3); /* send block type */ + copy_block(s, buf, stored_len, true); /* with header */ + } + + + /* =========================================================================== + * Send one empty static block to give enough lookahead for inflate. + * This takes 10 bits, of which 7 may remain in the bit buffer. + */ + function _tr_align(s) { + send_bits(s, STATIC_TREES << 1, 3); + send_code(s, END_BLOCK, static_ltree); + bi_flush(s); + } + + + /* =========================================================================== + * Determine the best encoding for the current block: dynamic trees, static + * trees or store, and output the encoded block to the zip file. + */ + function _tr_flush_block(s, buf, stored_len, last) + //DeflateState *s; + //charf *buf; /* input block, or NULL if too old */ + //ulg stored_len; /* length of input block */ + //int last; /* one if this is the last block for a file */ + { + let opt_lenb, static_lenb; /* opt_len and static_len in bytes */ + let max_blindex = 0; /* index of last bit length code of non zero freq */ + + /* Build the Huffman trees unless a stored block is forced */ + if (s.level > 0) { + + /* Check if the file is binary or text */ + if (s.strm.data_type === Z_UNKNOWN) { + s.strm.data_type = detect_data_type(s); + } + + /* Construct the literal and distance trees */ + build_tree(s, s.l_desc); + // Tracev((stderr, "\nlit data: dyn %ld, stat %ld", s->opt_len, + // s->static_len)); + + build_tree(s, s.d_desc); + // Tracev((stderr, "\ndist data: dyn %ld, stat %ld", s->opt_len, + // s->static_len)); + /* At this point, opt_len and static_len are the total bit lengths of + * the compressed block data, excluding the tree representations. + */ + + /* Build the bit length tree for the above two trees, and get the index + * in bl_order of the last bit length code to send. + */ + max_blindex = build_bl_tree(s); + + /* Determine the best encoding. Compute the block lengths in bytes. */ + opt_lenb = s.opt_len + 3 + 7 >>> 3; + static_lenb = s.static_len + 3 + 7 >>> 3; + + // Tracev((stderr, "\nopt %lu(%lu) stat %lu(%lu) stored %lu lit %u ", + // opt_lenb, s->opt_len, static_lenb, s->static_len, stored_len, + // s->last_lit)); + + if (static_lenb <= opt_lenb) { + opt_lenb = static_lenb; + } + + } else { + // Assert(buf != (char*)0, "lost buf"); + opt_lenb = static_lenb = stored_len + 5; /* force a stored block */ + } + + if (stored_len + 4 <= opt_lenb && buf !== -1) { + /* 4: two words for the lengths */ + + /* The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE. + * Otherwise we can't have processed more than WSIZE input bytes since + * the last block flush, because compression would have been + * successful. If LIT_BUFSIZE <= WSIZE, it is never too late to + * transform a block into a stored block. + */ + _tr_stored_block(s, buf, stored_len, last); + + } else if (s.strategy === Z_FIXED || static_lenb === opt_lenb) { + + send_bits(s, (STATIC_TREES << 1) + (last ? 1 : 0), 3); + compress_block(s, static_ltree, static_dtree); + + } else { + send_bits(s, (DYN_TREES << 1) + (last ? 1 : 0), 3); + send_all_trees(s, s.l_desc.max_code + 1, s.d_desc.max_code + 1, max_blindex + 1); + compress_block(s, s.dyn_ltree, s.dyn_dtree); + } + // Assert (s->compressed_len == s->bits_sent, "bad compressed size"); + /* The above check is made mod 2^32, for files larger than 512 MB + * and uLong implemented on 32 bits. + */ + init_block(s); + + if (last) { + bi_windup(s); + } + // Tracev((stderr,"\ncomprlen %lu(%lu) ", s->compressed_len>>3, + // s->compressed_len-7*last)); + } + + /* =========================================================================== + * Save the match info and tally the frequency counts. Return true if + * the current block must be flushed. + */ + function _tr_tally(s, dist, lc) + // deflate_state *s; + // unsigned dist; /* distance of matched string */ + // unsigned lc; /* match length-MIN_MATCH or unmatched char (if dist==0) */ + { + //var out_length, in_length, dcode; + + s.pending_buf[s.d_buf + s.last_lit * 2] = dist >>> 8 & 0xff; + s.pending_buf[s.d_buf + s.last_lit * 2 + 1] = dist & 0xff; + + s.pending_buf[s.l_buf + s.last_lit] = lc & 0xff; + s.last_lit++; + + if (dist === 0) { + /* lc is the unmatched char */ + s.dyn_ltree[lc * 2]/*.Freq*/++; + } else { + s.matches++; + /* Here, lc is the match length - MIN_MATCH */ + dist--; /* dist = match distance - 1 */ + //Assert((ush)dist < (ush)MAX_DIST(s) && + // (ush)lc <= (ush)(MAX_MATCH-MIN_MATCH) && + // (ush)d_code(dist) < (ush)D_CODES, "_tr_tally: bad match"); + + s.dyn_ltree[(_length_code[lc] + LITERALS$1 + 1) * 2]/*.Freq*/++; + s.dyn_dtree[d_code(dist) * 2]/*.Freq*/++; + } + + // (!) This block is disabled in zlib defaults, + // don't enable it for binary compatibility + + //#ifdef TRUNCATE_BLOCK + // /* Try to guess if it is profitable to stop the current block here */ + // if ((s.last_lit & 0x1fff) === 0 && s.level > 2) { + // /* Compute an upper bound for the compressed length */ + // out_length = s.last_lit*8; + // in_length = s.strstart - s.block_start; + // + // for (dcode = 0; dcode < D_CODES; dcode++) { + // out_length += s.dyn_dtree[dcode*2]/*.Freq*/ * (5 + extra_dbits[dcode]); + // } + // out_length >>>= 3; + // //Tracev((stderr,"\nlast_lit %u, in %ld, out ~%ld(%ld%%) ", + // // s->last_lit, in_length, out_length, + // // 100L - out_length*100L/in_length)); + // if (s.matches < (s.last_lit>>1)/*int /2*/ && out_length < (in_length>>1)/*int /2*/) { + // return true; + // } + // } + //#endif + + return s.last_lit === s.lit_bufsize - 1; + /* We avoid equality with lit_bufsize because of wraparound at 64K + * on 16 bit machines and because stored blocks are restricted to + * 64K-1 bytes. + */ + } + + // Note: adler32 takes 12% for level 0 and 2% for level 6. + // It isn't worth it to make additional optimizations as in original. + // Small size is preferable. + + // (C) 1995-2013 Jean-loup Gailly and Mark Adler + // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. + + function adler32(adler, buf, len, pos) { + let s1 = adler & 0xffff |0, + s2 = adler >>> 16 & 0xffff |0, + n = 0; + + while (len !== 0) { + // Set limit ~ twice less than 5552, to keep + // s2 in 31-bits, because we force signed ints. + // in other case %= will fail. + n = len > 2000 ? 2000 : len; + len -= n; + + do { + s1 = s1 + buf[pos++] |0; + s2 = s2 + s1 |0; + } while (--n); + + s1 %= 65521; + s2 %= 65521; + } + + return s1 | s2 << 16 |0; + } + + // Note: we can't get significant speed boost here. + // So write code to minimize size - no pregenerated tables + // and array tools dependencies. + + // (C) 1995-2013 Jean-loup Gailly and Mark Adler + // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. + + // Use ordinary array, since untyped makes no boost here + function makeTable() { + let c; + const table = []; + + for (let n = 0; n < 256; n++) { + c = n; + for (let k = 0; k < 8; k++) { + c = c & 1 ? 0xEDB88320 ^ c >>> 1 : c >>> 1; + } + table[n] = c; + } + + return table; + } + + // Create table on load. Just 255 signed longs. Not a problem. + const crcTable = makeTable(); + + + function crc32$1(crc, buf, len, pos) { + const t = crcTable, + end = pos + len; + + crc ^= -1; + + for (let i = pos; i < end; i++) { + crc = crc >>> 8 ^ t[(crc ^ buf[i]) & 0xFF]; + } + + return crc ^ -1; // >>> 0; + } + + // (C) 1995-2013 Jean-loup Gailly and Mark Adler + // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. + + var msg = { + 2: "need dictionary", /* Z_NEED_DICT 2 */ + 1: "stream end", /* Z_STREAM_END 1 */ + 0: "", /* Z_OK 0 */ + "-1": "file error", /* Z_ERRNO (-1) */ + "-2": "stream error", /* Z_STREAM_ERROR (-2) */ + "-3": "data error", /* Z_DATA_ERROR (-3) */ + "-4": "insufficient memory", /* Z_MEM_ERROR (-4) */ + "-5": "buffer error", /* Z_BUF_ERROR (-5) */ + "-6": "incompatible version" /* Z_VERSION_ERROR (-6) */ + }; + + /*============================================================================*/ + + + const MAX_MEM_LEVEL = 9; + + + const LENGTH_CODES = 29; + /* number of length codes, not counting the special END_BLOCK code */ + const LITERALS = 256; + /* number of literal bytes 0..255 */ + const L_CODES = LITERALS + 1 + LENGTH_CODES; + /* number of Literal or Length codes, including the END_BLOCK code */ + const D_CODES = 30; + /* number of distance codes */ + const BL_CODES = 19; + /* number of codes used to transfer the bit lengths */ + const HEAP_SIZE = 2 * L_CODES + 1; + /* maximum heap size */ + const MAX_BITS = 15; + /* All codes must not exceed MAX_BITS bits */ + + const MIN_MATCH = 3; + const MAX_MATCH = 258; + const MIN_LOOKAHEAD = (MAX_MATCH + MIN_MATCH + 1); + + const PRESET_DICT = 0x20; + + const INIT_STATE = 42; + const EXTRA_STATE = 69; + const NAME_STATE = 73; + const COMMENT_STATE = 91; + const HCRC_STATE = 103; + const BUSY_STATE = 113; + const FINISH_STATE = 666; + + const BS_NEED_MORE = 1; /* block not completed, need more input or more output */ + const BS_BLOCK_DONE = 2; /* block flush performed */ + const BS_FINISH_STARTED = 3; /* finish started, need only more output at next deflate */ + const BS_FINISH_DONE = 4; /* finish done, accept no more input or output */ + + const OS_CODE = 0x03; // Unix :) . Don't detect, use this default. + + function err(strm, errorCode) { + strm.msg = msg[errorCode]; + return errorCode; + } + + function rank(f) { + return ((f) << 1) - ((f) > 4 ? 9 : 0); + } + + function zero(buf) { let len = buf.length; while (--len >= 0) { buf[len] = 0; } } + + + /* ========================================================================= + * Flush as much pending output as possible. All deflate() output goes + * through this function so some applications may wish to modify it + * to avoid allocating a large strm->output buffer and copying into it. + * (See also read_buf()). + */ + function flush_pending(strm) { + const s = strm.state; + + //_tr_flush_bits(s); + let len = s.pending; + if (len > strm.avail_out) { + len = strm.avail_out; + } + if (len === 0) { return; } + + arraySet(strm.output, s.pending_buf, s.pending_out, len, strm.next_out); + strm.next_out += len; + s.pending_out += len; + strm.total_out += len; + strm.avail_out -= len; + s.pending -= len; + if (s.pending === 0) { + s.pending_out = 0; + } + } + + + function flush_block_only(s, last) { + _tr_flush_block(s, (s.block_start >= 0 ? s.block_start : -1), s.strstart - s.block_start, last); + s.block_start = s.strstart; + flush_pending(s.strm); + } + + + function put_byte(s, b) { + s.pending_buf[s.pending++] = b; + } + + + /* ========================================================================= + * Put a short in the pending buffer. The 16-bit value is put in MSB order. + * IN assertion: the stream state is correct and there is enough room in + * pending_buf. + */ + function putShortMSB(s, b) { + // put_byte(s, (Byte)(b >> 8)); + // put_byte(s, (Byte)(b & 0xff)); + s.pending_buf[s.pending++] = (b >>> 8) & 0xff; + s.pending_buf[s.pending++] = b & 0xff; + } + + + /* =========================================================================== + * Read a new buffer from the current input stream, update the adler32 + * and total number of bytes read. All deflate() input goes through + * this function so some applications may wish to modify it to avoid + * allocating a large strm->input buffer and copying from it. + * (See also flush_pending()). + */ + function read_buf(strm, buf, start, size) { + let len = strm.avail_in; + + if (len > size) { len = size; } + if (len === 0) { return 0; } + + strm.avail_in -= len; + + // zmemcpy(buf, strm->next_in, len); + arraySet(buf, strm.input, strm.next_in, len, start); + if (strm.state.wrap === 1) { + strm.adler = adler32(strm.adler, buf, len, start); + } + + else if (strm.state.wrap === 2) { + strm.adler = crc32$1(strm.adler, buf, len, start); + } + + strm.next_in += len; + strm.total_in += len; + + return len; + } + + + /* =========================================================================== + * Set match_start to the longest match starting at the given string and + * return its length. Matches shorter or equal to prev_length are discarded, + * in which case the result is equal to prev_length and match_start is + * garbage. + * IN assertions: cur_match is the head of the hash chain for the current + * string (strstart) and its distance is <= MAX_DIST, and prev_length >= 1 + * OUT assertion: the match length is not greater than s->lookahead. + */ + function longest_match(s, cur_match) { + let chain_length = s.max_chain_length; /* max hash chain length */ + let scan = s.strstart; /* current string */ + let match; /* matched string */ + let len; /* length of current match */ + let best_len = s.prev_length; /* best match length so far */ + let nice_match = s.nice_match; /* stop if match long enough */ + const limit = (s.strstart > (s.w_size - MIN_LOOKAHEAD)) ? + s.strstart - (s.w_size - MIN_LOOKAHEAD) : 0/*NIL*/; + + const _win = s.window; // shortcut + + const wmask = s.w_mask; + const prev = s.prev; + + /* Stop when cur_match becomes <= limit. To simplify the code, + * we prevent matches with the string of window index 0. + */ + + const strend = s.strstart + MAX_MATCH; + let scan_end1 = _win[scan + best_len - 1]; + let scan_end = _win[scan + best_len]; + + /* The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16. + * It is easy to get rid of this optimization if necessary. + */ + // Assert(s->hash_bits >= 8 && MAX_MATCH == 258, "Code too clever"); + + /* Do not waste too much time if we already have a good match: */ + if (s.prev_length >= s.good_match) { + chain_length >>= 2; + } + /* Do not look for matches beyond the end of the input. This is necessary + * to make deflate deterministic. + */ + if (nice_match > s.lookahead) { nice_match = s.lookahead; } + + // Assert((ulg)s->strstart <= s->window_size-MIN_LOOKAHEAD, "need lookahead"); + + do { + // Assert(cur_match < s->strstart, "no future"); + match = cur_match; + + /* Skip to next match if the match length cannot increase + * or if the match length is less than 2. Note that the checks below + * for insufficient lookahead only occur occasionally for performance + * reasons. Therefore uninitialized memory will be accessed, and + * conditional jumps will be made that depend on those values. + * However the length of the match is limited to the lookahead, so + * the output of deflate is not affected by the uninitialized values. + */ + + if (_win[match + best_len] !== scan_end || + _win[match + best_len - 1] !== scan_end1 || + _win[match] !== _win[scan] || + _win[++match] !== _win[scan + 1]) { + continue; + } + + /* The check at best_len-1 can be removed because it will be made + * again later. (This heuristic is not always a win.) + * It is not necessary to compare scan[2] and match[2] since they + * are always equal when the other bytes match, given that + * the hash keys are equal and that HASH_BITS >= 8. + */ + scan += 2; + match++; + // Assert(*scan == *match, "match[2]?"); + + /* We check for insufficient lookahead only every 8th comparison; + * the 256th check will be made at strstart+258. + */ + do { + /*jshint noempty:false*/ + } while (_win[++scan] === _win[++match] && _win[++scan] === _win[++match] && + _win[++scan] === _win[++match] && _win[++scan] === _win[++match] && + _win[++scan] === _win[++match] && _win[++scan] === _win[++match] && + _win[++scan] === _win[++match] && _win[++scan] === _win[++match] && + scan < strend); + + // Assert(scan <= s->window+(unsigned)(s->window_size-1), "wild scan"); + + len = MAX_MATCH - (strend - scan); + scan = strend - MAX_MATCH; + + if (len > best_len) { + s.match_start = cur_match; + best_len = len; + if (len >= nice_match) { + break; + } + scan_end1 = _win[scan + best_len - 1]; + scan_end = _win[scan + best_len]; + } + } while ((cur_match = prev[cur_match & wmask]) > limit && --chain_length !== 0); + + if (best_len <= s.lookahead) { + return best_len; + } + return s.lookahead; + } + + + /* =========================================================================== + * Fill the window when the lookahead becomes insufficient. + * Updates strstart and lookahead. + * + * IN assertion: lookahead < MIN_LOOKAHEAD + * OUT assertions: strstart <= window_size-MIN_LOOKAHEAD + * At least one byte has been read, or avail_in == 0; reads are + * performed for at least two bytes (required for the zip translate_eol + * option -- not supported here). + */ + function fill_window(s) { + const _w_size = s.w_size; + let p, n, m, more, str; + + //Assert(s->lookahead < MIN_LOOKAHEAD, "already enough lookahead"); + + do { + more = s.window_size - s.lookahead - s.strstart; + + // JS ints have 32 bit, block below not needed + /* Deal with !@#$% 64K limit: */ + //if (sizeof(int) <= 2) { + // if (more == 0 && s->strstart == 0 && s->lookahead == 0) { + // more = wsize; + // + // } else if (more == (unsigned)(-1)) { + // /* Very unlikely, but possible on 16 bit machine if + // * strstart == 0 && lookahead == 1 (input done a byte at time) + // */ + // more--; + // } + //} + + + /* If the window is almost full and there is insufficient lookahead, + * move the upper half to the lower one to make room in the upper half. + */ + if (s.strstart >= _w_size + (_w_size - MIN_LOOKAHEAD)) { + + arraySet(s.window, s.window, _w_size, _w_size, 0); + s.match_start -= _w_size; + s.strstart -= _w_size; + /* we now have strstart >= MAX_DIST */ + s.block_start -= _w_size; + + /* Slide the hash table (could be avoided with 32 bit values + at the expense of memory usage). We slide even when level == 0 + to keep the hash table consistent if we switch back to level > 0 + later. (Using level 0 permanently is not an optimal usage of + zlib, so we don't care about this pathological case.) + */ + + n = s.hash_size; + p = n; + do { + m = s.head[--p]; + s.head[p] = (m >= _w_size ? m - _w_size : 0); + } while (--n); + + n = _w_size; + p = n; + do { + m = s.prev[--p]; + s.prev[p] = (m >= _w_size ? m - _w_size : 0); + /* If n is not on any hash chain, prev[n] is garbage but + * its value will never be used. + */ + } while (--n); + + more += _w_size; + } + if (s.strm.avail_in === 0) { + break; + } + + /* If there was no sliding: + * strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 && + * more == window_size - lookahead - strstart + * => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1) + * => more >= window_size - 2*WSIZE + 2 + * In the BIG_MEM or MMAP case (not yet supported), + * window_size == input_size + MIN_LOOKAHEAD && + * strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD. + * Otherwise, window_size == 2*WSIZE so more >= 2. + * If there was sliding, more >= WSIZE. So in all cases, more >= 2. + */ + //Assert(more >= 2, "more < 2"); + n = read_buf(s.strm, s.window, s.strstart + s.lookahead, more); + s.lookahead += n; + + /* Initialize the hash value now that we have some input: */ + if (s.lookahead + s.insert >= MIN_MATCH) { + str = s.strstart - s.insert; + s.ins_h = s.window[str]; + + /* UPDATE_HASH(s, s->ins_h, s->window[str + 1]); */ + s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + 1]) & s.hash_mask; + //#if MIN_MATCH != 3 + // Call update_hash() MIN_MATCH-3 more times + //#endif + while (s.insert) { + /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */ + s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + MIN_MATCH - 1]) & s.hash_mask; + + s.prev[str & s.w_mask] = s.head[s.ins_h]; + s.head[s.ins_h] = str; + str++; + s.insert--; + if (s.lookahead + s.insert < MIN_MATCH) { + break; + } + } + } + /* If the whole input has less than MIN_MATCH bytes, ins_h is garbage, + * but this is not important since only literal bytes will be emitted. + */ + + } while (s.lookahead < MIN_LOOKAHEAD && s.strm.avail_in !== 0); + + /* If the WIN_INIT bytes after the end of the current data have never been + * written, then zero those bytes in order to avoid memory check reports of + * the use of uninitialized (or uninitialised as Julian writes) bytes by + * the longest match routines. Update the high water mark for the next + * time through here. WIN_INIT is set to MAX_MATCH since the longest match + * routines allow scanning to strstart + MAX_MATCH, ignoring lookahead. + */ + // if (s.high_water < s.window_size) { + // var curr = s.strstart + s.lookahead; + // var init = 0; + // + // if (s.high_water < curr) { + // /* Previous high water mark below current data -- zero WIN_INIT + // * bytes or up to end of window, whichever is less. + // */ + // init = s.window_size - curr; + // if (init > WIN_INIT) + // init = WIN_INIT; + // zmemzero(s->window + curr, (unsigned)init); + // s->high_water = curr + init; + // } + // else if (s->high_water < (ulg)curr + WIN_INIT) { + // /* High water mark at or above current data, but below current data + // * plus WIN_INIT -- zero out to current data plus WIN_INIT, or up + // * to end of window, whichever is less. + // */ + // init = (ulg)curr + WIN_INIT - s->high_water; + // if (init > s->window_size - s->high_water) + // init = s->window_size - s->high_water; + // zmemzero(s->window + s->high_water, (unsigned)init); + // s->high_water += init; + // } + // } + // + // Assert((ulg)s->strstart <= s->window_size - MIN_LOOKAHEAD, + // "not enough room for search"); + } + + /* =========================================================================== + * Copy without compression as much as possible from the input stream, return + * the current block state. + * This function does not insert new strings in the dictionary since + * uncompressible data is probably not useful. This function is used + * only for the level=0 compression option. + * NOTE: this function should be optimized to avoid extra copying from + * window to pending_buf. + */ + function deflate_stored(s, flush) { + /* Stored blocks are limited to 0xffff bytes, pending_buf is limited + * to pending_buf_size, and each stored block has a 5 byte header: + */ + let max_block_size = 0xffff; + + if (max_block_size > s.pending_buf_size - 5) { + max_block_size = s.pending_buf_size - 5; + } + + /* Copy as much as possible from input to output: */ + for (; ;) { + /* Fill the window as much as possible: */ + if (s.lookahead <= 1) { + + //Assert(s->strstart < s->w_size+MAX_DIST(s) || + // s->block_start >= (long)s->w_size, "slide too late"); + // if (!(s.strstart < s.w_size + (s.w_size - MIN_LOOKAHEAD) || + // s.block_start >= s.w_size)) { + // throw Error("slide too late"); + // } + + fill_window(s); + if (s.lookahead === 0 && flush === Z_NO_FLUSH) { + return BS_NEED_MORE; + } + + if (s.lookahead === 0) { + break; + } + /* flush the current block */ + } + //Assert(s->block_start >= 0L, "block gone"); + // if (s.block_start < 0) throw Error("block gone"); + + s.strstart += s.lookahead; + s.lookahead = 0; + + /* Emit a stored block if pending_buf will be full: */ + const max_start = s.block_start + max_block_size; + + if (s.strstart === 0 || s.strstart >= max_start) { + /* strstart == 0 is possible when wraparound on 16-bit machine */ + s.lookahead = s.strstart - max_start; + s.strstart = max_start; + /*** FLUSH_BLOCK(s, 0); ***/ + flush_block_only(s, false); + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + /***/ + + + } + /* Flush if we may have to slide, otherwise block_start may become + * negative and the data will be gone: + */ + if (s.strstart - s.block_start >= (s.w_size - MIN_LOOKAHEAD)) { + /*** FLUSH_BLOCK(s, 0); ***/ + flush_block_only(s, false); + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + /***/ + } + } + + s.insert = 0; + + if (flush === Z_FINISH) { + /*** FLUSH_BLOCK(s, 1); ***/ + flush_block_only(s, true); + if (s.strm.avail_out === 0) { + return BS_FINISH_STARTED; + } + /***/ + return BS_FINISH_DONE; + } + + if (s.strstart > s.block_start) { + /*** FLUSH_BLOCK(s, 0); ***/ + flush_block_only(s, false); + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + /***/ + } + + return BS_NEED_MORE; + } + + /* =========================================================================== + * Compress as much as possible from the input stream, return the current + * block state. + * This function does not perform lazy evaluation of matches and inserts + * new strings in the dictionary only for unmatched strings or for short + * matches. It is used only for the fast compression options. + */ + function deflate_fast(s, flush) { + let hash_head; /* head of the hash chain */ + let bflush; /* set if current block must be flushed */ + + for (; ;) { + /* Make sure that we always have enough lookahead, except + * at the end of the input file. We need MAX_MATCH bytes + * for the next match, plus MIN_MATCH bytes to insert the + * string following the next match. + */ + if (s.lookahead < MIN_LOOKAHEAD) { + fill_window(s); + if (s.lookahead < MIN_LOOKAHEAD && flush === Z_NO_FLUSH) { + return BS_NEED_MORE; + } + if (s.lookahead === 0) { + break; /* flush the current block */ + } + } + + /* Insert the string window[strstart .. strstart+2] in the + * dictionary, and set hash_head to the head of the hash chain: + */ + hash_head = 0/*NIL*/; + if (s.lookahead >= MIN_MATCH) { + /*** INSERT_STRING(s, s.strstart, hash_head); ***/ + s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask; + hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h]; + s.head[s.ins_h] = s.strstart; + /***/ + } + + /* Find the longest match, discarding those <= prev_length. + * At this point we have always match_length < MIN_MATCH + */ + if (hash_head !== 0/*NIL*/ && ((s.strstart - hash_head) <= (s.w_size - MIN_LOOKAHEAD))) { + /* To simplify the code, we prevent matches with the string + * of window index 0 (in particular we have to avoid a match + * of the string with itself at the start of the input file). + */ + s.match_length = longest_match(s, hash_head); + /* longest_match() sets match_start */ + } + if (s.match_length >= MIN_MATCH) { + // check_match(s, s.strstart, s.match_start, s.match_length); // for debug only + + /*** _tr_tally_dist(s, s.strstart - s.match_start, + s.match_length - MIN_MATCH, bflush); ***/ + bflush = _tr_tally(s, s.strstart - s.match_start, s.match_length - MIN_MATCH); + + s.lookahead -= s.match_length; + + /* Insert new strings in the hash table only if the match length + * is not too large. This saves time but degrades compression. + */ + if (s.match_length <= s.max_lazy_match/*max_insert_length*/ && s.lookahead >= MIN_MATCH) { + s.match_length--; /* string at strstart already in table */ + do { + s.strstart++; + /*** INSERT_STRING(s, s.strstart, hash_head); ***/ + s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask; + hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h]; + s.head[s.ins_h] = s.strstart; + /***/ + /* strstart never exceeds WSIZE-MAX_MATCH, so there are + * always MIN_MATCH bytes ahead. + */ + } while (--s.match_length !== 0); + s.strstart++; + } else { + s.strstart += s.match_length; + s.match_length = 0; + s.ins_h = s.window[s.strstart]; + /* UPDATE_HASH(s, s.ins_h, s.window[s.strstart+1]); */ + s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + 1]) & s.hash_mask; + + //#if MIN_MATCH != 3 + // Call UPDATE_HASH() MIN_MATCH-3 more times + //#endif + /* If lookahead < MIN_MATCH, ins_h is garbage, but it does not + * matter since it will be recomputed at next deflate call. + */ + } + } else { + /* No match, output a literal byte */ + //Tracevv((stderr,"%c", s.window[s.strstart])); + /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/ + bflush = _tr_tally(s, 0, s.window[s.strstart]); + + s.lookahead--; + s.strstart++; + } + if (bflush) { + /*** FLUSH_BLOCK(s, 0); ***/ + flush_block_only(s, false); + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + /***/ + } + } + s.insert = ((s.strstart < (MIN_MATCH - 1)) ? s.strstart : MIN_MATCH - 1); + if (flush === Z_FINISH) { + /*** FLUSH_BLOCK(s, 1); ***/ + flush_block_only(s, true); + if (s.strm.avail_out === 0) { + return BS_FINISH_STARTED; + } + /***/ + return BS_FINISH_DONE; + } + if (s.last_lit) { + /*** FLUSH_BLOCK(s, 0); ***/ + flush_block_only(s, false); + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + /***/ + } + return BS_BLOCK_DONE; + } + + /* =========================================================================== + * Same as above, but achieves better compression. We use a lazy + * evaluation for matches: a match is finally adopted only if there is + * no better match at the next window position. + */ + function deflate_slow(s, flush) { + let hash_head; /* head of hash chain */ + let bflush; /* set if current block must be flushed */ + + let max_insert; + + /* Process the input block. */ + for (; ;) { + /* Make sure that we always have enough lookahead, except + * at the end of the input file. We need MAX_MATCH bytes + * for the next match, plus MIN_MATCH bytes to insert the + * string following the next match. + */ + if (s.lookahead < MIN_LOOKAHEAD) { + fill_window(s); + if (s.lookahead < MIN_LOOKAHEAD && flush === Z_NO_FLUSH) { + return BS_NEED_MORE; + } + if (s.lookahead === 0) { break; } /* flush the current block */ + } + + /* Insert the string window[strstart .. strstart+2] in the + * dictionary, and set hash_head to the head of the hash chain: + */ + hash_head = 0/*NIL*/; + if (s.lookahead >= MIN_MATCH) { + /*** INSERT_STRING(s, s.strstart, hash_head); ***/ + s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask; + hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h]; + s.head[s.ins_h] = s.strstart; + /***/ + } + + /* Find the longest match, discarding those <= prev_length. + */ + s.prev_length = s.match_length; + s.prev_match = s.match_start; + s.match_length = MIN_MATCH - 1; + + if (hash_head !== 0/*NIL*/ && s.prev_length < s.max_lazy_match && + s.strstart - hash_head <= (s.w_size - MIN_LOOKAHEAD)/*MAX_DIST(s)*/) { + /* To simplify the code, we prevent matches with the string + * of window index 0 (in particular we have to avoid a match + * of the string with itself at the start of the input file). + */ + s.match_length = longest_match(s, hash_head); + /* longest_match() sets match_start */ + + if (s.match_length <= 5 && + (s.strategy === Z_FILTERED || (s.match_length === MIN_MATCH && s.strstart - s.match_start > 4096/*TOO_FAR*/))) { + + /* If prev_match is also MIN_MATCH, match_start is garbage + * but we will ignore the current match anyway. + */ + s.match_length = MIN_MATCH - 1; + } + } + /* If there was a match at the previous step and the current + * match is not better, output the previous match: + */ + if (s.prev_length >= MIN_MATCH && s.match_length <= s.prev_length) { + max_insert = s.strstart + s.lookahead - MIN_MATCH; + /* Do not insert strings in hash table beyond this. */ + + //check_match(s, s.strstart-1, s.prev_match, s.prev_length); + + /***_tr_tally_dist(s, s.strstart - 1 - s.prev_match, + s.prev_length - MIN_MATCH, bflush);***/ + bflush = _tr_tally(s, s.strstart - 1 - s.prev_match, s.prev_length - MIN_MATCH); + /* Insert in hash table all strings up to the end of the match. + * strstart-1 and strstart are already inserted. If there is not + * enough lookahead, the last two strings are not inserted in + * the hash table. + */ + s.lookahead -= s.prev_length - 1; + s.prev_length -= 2; + do { + if (++s.strstart <= max_insert) { + /*** INSERT_STRING(s, s.strstart, hash_head); ***/ + s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask; + hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h]; + s.head[s.ins_h] = s.strstart; + /***/ + } + } while (--s.prev_length !== 0); + s.match_available = 0; + s.match_length = MIN_MATCH - 1; + s.strstart++; + + if (bflush) { + /*** FLUSH_BLOCK(s, 0); ***/ + flush_block_only(s, false); + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + /***/ + } + + } else if (s.match_available) { + /* If there was no match at the previous position, output a + * single literal. If there was a match but the current match + * is longer, truncate the previous match to a single literal. + */ + //Tracevv((stderr,"%c", s->window[s->strstart-1])); + /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/ + bflush = _tr_tally(s, 0, s.window[s.strstart - 1]); + + if (bflush) { + /*** FLUSH_BLOCK_ONLY(s, 0) ***/ + flush_block_only(s, false); + /***/ + } + s.strstart++; + s.lookahead--; + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + } else { + /* There is no previous match to compare with, wait for + * the next step to decide. + */ + s.match_available = 1; + s.strstart++; + s.lookahead--; + } + } + //Assert (flush != Z_NO_FLUSH, "no flush?"); + if (s.match_available) { + //Tracevv((stderr,"%c", s->window[s->strstart-1])); + /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/ + bflush = _tr_tally(s, 0, s.window[s.strstart - 1]); + + s.match_available = 0; + } + s.insert = s.strstart < MIN_MATCH - 1 ? s.strstart : MIN_MATCH - 1; + if (flush === Z_FINISH) { + /*** FLUSH_BLOCK(s, 1); ***/ + flush_block_only(s, true); + if (s.strm.avail_out === 0) { + return BS_FINISH_STARTED; + } + /***/ + return BS_FINISH_DONE; + } + if (s.last_lit) { + /*** FLUSH_BLOCK(s, 0); ***/ + flush_block_only(s, false); + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + /***/ + } + + return BS_BLOCK_DONE; + } + + + /* =========================================================================== + * For Z_RLE, simply look for runs of bytes, generate matches only of distance + * one. Do not maintain a hash table. (It will be regenerated if this run of + * deflate switches away from Z_RLE.) + */ + function deflate_rle(s, flush) { + let bflush; /* set if current block must be flushed */ + let prev; /* byte at distance one to match */ + let scan, strend; /* scan goes up to strend for length of run */ + + const _win = s.window; + + for (; ;) { + /* Make sure that we always have enough lookahead, except + * at the end of the input file. We need MAX_MATCH bytes + * for the longest run, plus one for the unrolled loop. + */ + if (s.lookahead <= MAX_MATCH) { + fill_window(s); + if (s.lookahead <= MAX_MATCH && flush === Z_NO_FLUSH) { + return BS_NEED_MORE; + } + if (s.lookahead === 0) { break; } /* flush the current block */ + } + + /* See how many times the previous byte repeats */ + s.match_length = 0; + if (s.lookahead >= MIN_MATCH && s.strstart > 0) { + scan = s.strstart - 1; + prev = _win[scan]; + if (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan]) { + strend = s.strstart + MAX_MATCH; + do { + /*jshint noempty:false*/ + } while (prev === _win[++scan] && prev === _win[++scan] && + prev === _win[++scan] && prev === _win[++scan] && + prev === _win[++scan] && prev === _win[++scan] && + prev === _win[++scan] && prev === _win[++scan] && + scan < strend); + s.match_length = MAX_MATCH - (strend - scan); + if (s.match_length > s.lookahead) { + s.match_length = s.lookahead; + } + } + //Assert(scan <= s->window+(uInt)(s->window_size-1), "wild scan"); + } + + /* Emit match if have run of MIN_MATCH or longer, else emit literal */ + if (s.match_length >= MIN_MATCH) { + //check_match(s, s.strstart, s.strstart - 1, s.match_length); + + /*** _tr_tally_dist(s, 1, s.match_length - MIN_MATCH, bflush); ***/ + bflush = _tr_tally(s, 1, s.match_length - MIN_MATCH); + + s.lookahead -= s.match_length; + s.strstart += s.match_length; + s.match_length = 0; + } else { + /* No match, output a literal byte */ + //Tracevv((stderr,"%c", s->window[s->strstart])); + /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/ + bflush = _tr_tally(s, 0, s.window[s.strstart]); + + s.lookahead--; + s.strstart++; + } + if (bflush) { + /*** FLUSH_BLOCK(s, 0); ***/ + flush_block_only(s, false); + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + /***/ + } + } + s.insert = 0; + if (flush === Z_FINISH) { + /*** FLUSH_BLOCK(s, 1); ***/ + flush_block_only(s, true); + if (s.strm.avail_out === 0) { + return BS_FINISH_STARTED; + } + /***/ + return BS_FINISH_DONE; + } + if (s.last_lit) { + /*** FLUSH_BLOCK(s, 0); ***/ + flush_block_only(s, false); + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + /***/ + } + return BS_BLOCK_DONE; + } + + /* =========================================================================== + * For Z_HUFFMAN_ONLY, do not look for matches. Do not maintain a hash table. + * (It will be regenerated if this run of deflate switches away from Huffman.) + */ + function deflate_huff(s, flush) { + let bflush; /* set if current block must be flushed */ + + for (; ;) { + /* Make sure that we have a literal to write. */ + if (s.lookahead === 0) { + fill_window(s); + if (s.lookahead === 0) { + if (flush === Z_NO_FLUSH) { + return BS_NEED_MORE; + } + break; /* flush the current block */ + } + } + + /* Output a literal byte */ + s.match_length = 0; + //Tracevv((stderr,"%c", s->window[s->strstart])); + /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/ + bflush = _tr_tally(s, 0, s.window[s.strstart]); + s.lookahead--; + s.strstart++; + if (bflush) { + /*** FLUSH_BLOCK(s, 0); ***/ + flush_block_only(s, false); + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + /***/ + } + } + s.insert = 0; + if (flush === Z_FINISH) { + /*** FLUSH_BLOCK(s, 1); ***/ + flush_block_only(s, true); + if (s.strm.avail_out === 0) { + return BS_FINISH_STARTED; + } + /***/ + return BS_FINISH_DONE; + } + if (s.last_lit) { + /*** FLUSH_BLOCK(s, 0); ***/ + flush_block_only(s, false); + if (s.strm.avail_out === 0) { + return BS_NEED_MORE; + } + /***/ + } + return BS_BLOCK_DONE; + } + + /* Values for max_lazy_match, good_match and max_chain_length, depending on + * the desired pack level (0..9). The values given below have been tuned to + * exclude worst case performance for pathological files. Better values may be + * found for specific files. + */ + class Config { + constructor(good_length, max_lazy, nice_length, max_chain, func) { + this.good_length = good_length; + this.max_lazy = max_lazy; + this.nice_length = nice_length; + this.max_chain = max_chain; + this.func = func; + } + } + const configuration_table = [ + /* good lazy nice chain */ + new Config(0, 0, 0, 0, deflate_stored), /* 0 store only */ + new Config(4, 4, 8, 4, deflate_fast), /* 1 max speed, no lazy matches */ + new Config(4, 5, 16, 8, deflate_fast), /* 2 */ + new Config(4, 6, 32, 32, deflate_fast), /* 3 */ + + new Config(4, 4, 16, 16, deflate_slow), /* 4 lazy matches */ + new Config(8, 16, 32, 32, deflate_slow), /* 5 */ + new Config(8, 16, 128, 128, deflate_slow), /* 6 */ + new Config(8, 32, 128, 256, deflate_slow), /* 7 */ + new Config(32, 128, 258, 1024, deflate_slow), /* 8 */ + new Config(32, 258, 258, 4096, deflate_slow) /* 9 max compression */ + ]; + + + /* =========================================================================== + * Initialize the "longest match" routines for a new zlib stream + */ + function lm_init(s) { + s.window_size = 2 * s.w_size; + + /*** CLEAR_HASH(s); ***/ + zero(s.head); // Fill with NIL (= 0); + + /* Set the default configuration parameters: + */ + s.max_lazy_match = configuration_table[s.level].max_lazy; + s.good_match = configuration_table[s.level].good_length; + s.nice_match = configuration_table[s.level].nice_length; + s.max_chain_length = configuration_table[s.level].max_chain; + + s.strstart = 0; + s.block_start = 0; + s.lookahead = 0; + s.insert = 0; + s.match_length = s.prev_length = MIN_MATCH - 1; + s.match_available = 0; + s.ins_h = 0; + } + + class DeflateState { + constructor() { + this.strm = null; /* pointer back to this zlib stream */ + this.status = 0; /* as the name implies */ + this.pending_buf = null; /* output still pending */ + this.pending_buf_size = 0; /* size of pending_buf */ + this.pending_out = 0; /* next pending byte to output to the stream */ + this.pending = 0; /* nb of bytes in the pending buffer */ + this.wrap = 0; /* bit 0 true for zlib, bit 1 true for gzip */ + this.gzhead = null; /* gzip header information to write */ + this.gzindex = 0; /* where in extra, name, or comment */ + this.method = Z_DEFLATED; /* can only be DEFLATED */ + this.last_flush = -1; /* value of flush param for previous deflate call */ + + this.w_size = 0; /* LZ77 window size (32K by default) */ + this.w_bits = 0; /* log2(w_size) (8..16) */ + this.w_mask = 0; /* w_size - 1 */ + + this.window = null; + /* Sliding window. Input bytes are read into the second half of the window, + * and move to the first half later to keep a dictionary of at least wSize + * bytes. With this organization, matches are limited to a distance of + * wSize-MAX_MATCH bytes, but this ensures that IO is always + * performed with a length multiple of the block size. + */ + + this.window_size = 0; + /* Actual size of window: 2*wSize, except when the user input buffer + * is directly used as sliding window. + */ + + this.prev = null; + /* Link to older string with same hash index. To limit the size of this + * array to 64K, this link is maintained only for the last 32K strings. + * An index in this array is thus a window index modulo 32K. + */ + + this.head = null; /* Heads of the hash chains or NIL. */ + + this.ins_h = 0; /* hash index of string to be inserted */ + this.hash_size = 0; /* number of elements in hash table */ + this.hash_bits = 0; /* log2(hash_size) */ + this.hash_mask = 0; /* hash_size-1 */ + + this.hash_shift = 0; + /* Number of bits by which ins_h must be shifted at each input + * step. It must be such that after MIN_MATCH steps, the oldest + * byte no longer takes part in the hash key, that is: + * hash_shift * MIN_MATCH >= hash_bits + */ + + this.block_start = 0; + /* Window position at the beginning of the current output block. Gets + * negative when the window is moved backwards. + */ + + this.match_length = 0; /* length of best match */ + this.prev_match = 0; /* previous match */ + this.match_available = 0; /* set if previous match exists */ + this.strstart = 0; /* start of string to insert */ + this.match_start = 0; /* start of matching string */ + this.lookahead = 0; /* number of valid bytes ahead in window */ + + this.prev_length = 0; + /* Length of the best match at previous step. Matches not greater than this + * are discarded. This is used in the lazy match evaluation. + */ + + this.max_chain_length = 0; + /* To speed up deflation, hash chains are never searched beyond this + * length. A higher limit improves compression ratio but degrades the + * speed. + */ + + this.max_lazy_match = 0; + /* Attempt to find a better match only when the current match is strictly + * smaller than this value. This mechanism is used only for compression + * levels >= 4. + */ + // That's alias to max_lazy_match, don't use directly + //this.max_insert_length = 0; + /* Insert new strings in the hash table only if the match length is not + * greater than this length. This saves time but degrades compression. + * max_insert_length is used only for compression levels <= 3. + */ + + this.level = 0; /* compression level (1..9) */ + this.strategy = 0; /* favor or force Huffman coding*/ + + this.good_match = 0; + /* Use a faster search when the previous match is longer than this */ + + this.nice_match = 0; /* Stop searching when current match exceeds this */ + + /* used by trees.c: */ + + /* Didn't use ct_data typedef below to suppress compiler warning */ + + // struct ct_data_s dyn_ltree[HEAP_SIZE]; /* literal and length tree */ + // struct ct_data_s dyn_dtree[2*D_CODES+1]; /* distance tree */ + // struct ct_data_s bl_tree[2*BL_CODES+1]; /* Huffman tree for bit lengths */ + + // Use flat array of DOUBLE size, with interleaved fata, + // because JS does not support effective + this.dyn_ltree = new Buf16(HEAP_SIZE * 2); + this.dyn_dtree = new Buf16((2 * D_CODES + 1) * 2); + this.bl_tree = new Buf16((2 * BL_CODES + 1) * 2); + zero(this.dyn_ltree); + zero(this.dyn_dtree); + zero(this.bl_tree); + + this.l_desc = null; /* desc. for literal tree */ + this.d_desc = null; /* desc. for distance tree */ + this.bl_desc = null; /* desc. for bit length tree */ + + //ush bl_count[MAX_BITS+1]; + this.bl_count = new Buf16(MAX_BITS + 1); + /* number of codes at each bit length for an optimal tree */ + + //int heap[2*L_CODES+1]; /* heap used to build the Huffman trees */ + this.heap = new Buf16(2 * L_CODES + 1); /* heap used to build the Huffman trees */ + zero(this.heap); + + this.heap_len = 0; /* number of elements in the heap */ + this.heap_max = 0; /* element of largest frequency */ + /* The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used. + * The same heap array is used to build all trees. + */ + + this.depth = new Buf16(2 * L_CODES + 1); //uch depth[2*L_CODES+1]; + zero(this.depth); + /* Depth of each subtree used as tie breaker for trees of equal frequency + */ + + this.l_buf = 0; /* buffer index for literals or lengths */ + + this.lit_bufsize = 0; + /* Size of match buffer for literals/lengths. There are 4 reasons for + * limiting lit_bufsize to 64K: + * - frequencies can be kept in 16 bit counters + * - if compression is not successful for the first block, all input + * data is still in the window so we can still emit a stored block even + * when input comes from standard input. (This can also be done for + * all blocks if lit_bufsize is not greater than 32K.) + * - if compression is not successful for a file smaller than 64K, we can + * even emit a stored file instead of a stored block (saving 5 bytes). + * This is applicable only for zip (not gzip or zlib). + * - creating new Huffman trees less frequently may not provide fast + * adaptation to changes in the input data statistics. (Take for + * example a binary file with poorly compressible code followed by + * a highly compressible string table.) Smaller buffer sizes give + * fast adaptation but have of course the overhead of transmitting + * trees more frequently. + * - I can't count above 4 + */ + + this.last_lit = 0; /* running index in l_buf */ + + this.d_buf = 0; + /* Buffer index for distances. To simplify the code, d_buf and l_buf have + * the same number of elements. To use different lengths, an extra flag + * array would be necessary. + */ + + this.opt_len = 0; /* bit length of current block with optimal trees */ + this.static_len = 0; /* bit length of current block with static trees */ + this.matches = 0; /* number of string matches in current block */ + this.insert = 0; /* bytes at end of window left to insert */ + + + this.bi_buf = 0; + /* Output buffer. bits are inserted starting at the bottom (least + * significant bits). + */ + this.bi_valid = 0; + /* Number of valid bits in bi_buf. All bits above the last valid bit + * are always zero. + */ + + // Used for window memory init. We safely ignore it for JS. That makes + // sense only for pointers and memory check tools. + //this.high_water = 0; + /* High water mark offset in window for initialized bytes -- bytes above + * this are set to zero in order to avoid memory check warnings when + * longest match routines access bytes past the input. This is then + * updated to the new high water mark. + */ + } + } + + function deflateResetKeep(strm) { + let s; + + if (!strm || !strm.state) { + return err(strm, Z_STREAM_ERROR); + } + + strm.total_in = strm.total_out = 0; + strm.data_type = Z_UNKNOWN; + + s = strm.state; + s.pending = 0; + s.pending_out = 0; + + if (s.wrap < 0) { + s.wrap = -s.wrap; + /* was made negative by deflate(..., Z_FINISH); */ + } + s.status = (s.wrap ? INIT_STATE : BUSY_STATE); + strm.adler = (s.wrap === 2) ? + 0 // crc32(0, Z_NULL, 0) + : + 1; // adler32(0, Z_NULL, 0) + s.last_flush = Z_NO_FLUSH; + _tr_init(s); + return Z_OK; + } + + + function deflateReset(strm) { + const ret = deflateResetKeep(strm); + if (ret === Z_OK) { + lm_init(strm.state); + } + return ret; + } + + + function deflateSetHeader(strm, head) { + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + if (strm.state.wrap !== 2) { return Z_STREAM_ERROR; } + strm.state.gzhead = head; + return Z_OK; + } + + + function deflateInit2(strm, level, method, windowBits, memLevel, strategy) { + if (!strm) { // === Z_NULL + return Z_STREAM_ERROR; + } + let wrap = 1; + + if (level === Z_DEFAULT_COMPRESSION) { + level = 6; + } + + if (windowBits < 0) { /* suppress zlib wrapper */ + wrap = 0; + windowBits = -windowBits; + } + + else if (windowBits > 15) { + wrap = 2; /* write gzip wrapper instead */ + windowBits -= 16; + } + + + if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || method !== Z_DEFLATED || + windowBits < 8 || windowBits > 15 || level < 0 || level > 9 || + strategy < 0 || strategy > Z_FIXED) { + return err(strm, Z_STREAM_ERROR); + } + + + if (windowBits === 8) { + windowBits = 9; + } + /* until 256-byte window bug fixed */ + + const s = new DeflateState(); + + strm.state = s; + s.strm = strm; + + s.wrap = wrap; + s.gzhead = null; + s.w_bits = windowBits; + s.w_size = 1 << s.w_bits; + s.w_mask = s.w_size - 1; + + s.hash_bits = memLevel + 7; + s.hash_size = 1 << s.hash_bits; + s.hash_mask = s.hash_size - 1; + s.hash_shift = ~~((s.hash_bits + MIN_MATCH - 1) / MIN_MATCH); + s.window = new Buf8(s.w_size * 2); + s.head = new Buf16(s.hash_size); + s.prev = new Buf16(s.w_size); + + // Don't need mem init magic for JS. + //s.high_water = 0; /* nothing written to s->window yet */ + + s.lit_bufsize = 1 << (memLevel + 6); /* 16K elements by default */ + + s.pending_buf_size = s.lit_bufsize * 4; + + //overlay = (ushf *) ZALLOC(strm, s->lit_bufsize, sizeof(ush)+2); + //s->pending_buf = (uchf *) overlay; + s.pending_buf = new Buf8(s.pending_buf_size); + + // It is offset from `s.pending_buf` (size is `s.lit_bufsize * 2`) + //s->d_buf = overlay + s->lit_bufsize/sizeof(ush); + s.d_buf = 1 * s.lit_bufsize; + + //s->l_buf = s->pending_buf + (1+sizeof(ush))*s->lit_bufsize; + s.l_buf = (1 + 2) * s.lit_bufsize; + + s.level = level; + s.strategy = strategy; + s.method = method; + + return deflateReset(strm); + } + + + function deflate(strm, flush) { + let old_flush, s; + let beg, val; // for gzip header write only + + if (!strm || !strm.state || + flush > Z_BLOCK || flush < 0) { + return strm ? err(strm, Z_STREAM_ERROR) : Z_STREAM_ERROR; + } + + s = strm.state; + + if (!strm.output || + (!strm.input && strm.avail_in !== 0) || + (s.status === FINISH_STATE && flush !== Z_FINISH)) { + return err(strm, (strm.avail_out === 0) ? Z_BUF_ERROR : Z_STREAM_ERROR); + } + + s.strm = strm; /* just in case */ + old_flush = s.last_flush; + s.last_flush = flush; + + /* Write the header */ + if (s.status === INIT_STATE) { + + if (s.wrap === 2) { // GZIP header + strm.adler = 0; //crc32(0L, Z_NULL, 0); + put_byte(s, 31); + put_byte(s, 139); + put_byte(s, 8); + if (!s.gzhead) { // s->gzhead == Z_NULL + put_byte(s, 0); + put_byte(s, 0); + put_byte(s, 0); + put_byte(s, 0); + put_byte(s, 0); + put_byte(s, s.level === 9 ? 2 : + (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2 ? + 4 : 0)); + put_byte(s, OS_CODE); + s.status = BUSY_STATE; + } + else { + put_byte(s, (s.gzhead.text ? 1 : 0) + + (s.gzhead.hcrc ? 2 : 0) + + (!s.gzhead.extra ? 0 : 4) + + (!s.gzhead.name ? 0 : 8) + + (!s.gzhead.comment ? 0 : 16) + ); + put_byte(s, s.gzhead.time & 0xff); + put_byte(s, (s.gzhead.time >> 8) & 0xff); + put_byte(s, (s.gzhead.time >> 16) & 0xff); + put_byte(s, (s.gzhead.time >> 24) & 0xff); + put_byte(s, s.level === 9 ? 2 : + (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2 ? + 4 : 0)); + put_byte(s, s.gzhead.os & 0xff); + if (s.gzhead.extra && s.gzhead.extra.length) { + put_byte(s, s.gzhead.extra.length & 0xff); + put_byte(s, (s.gzhead.extra.length >> 8) & 0xff); + } + if (s.gzhead.hcrc) { + strm.adler = crc32$1(strm.adler, s.pending_buf, s.pending, 0); + } + s.gzindex = 0; + s.status = EXTRA_STATE; + } + } + else // DEFLATE header + { + let header = (Z_DEFLATED + ((s.w_bits - 8) << 4)) << 8; + let level_flags = -1; + + if (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2) { + level_flags = 0; + } else if (s.level < 6) { + level_flags = 1; + } else if (s.level === 6) { + level_flags = 2; + } else { + level_flags = 3; + } + header |= (level_flags << 6); + if (s.strstart !== 0) { header |= PRESET_DICT; } + header += 31 - (header % 31); + + s.status = BUSY_STATE; + putShortMSB(s, header); + + /* Save the adler32 of the preset dictionary: */ + if (s.strstart !== 0) { + putShortMSB(s, strm.adler >>> 16); + putShortMSB(s, strm.adler & 0xffff); + } + strm.adler = 1; // adler32(0L, Z_NULL, 0); + } + } + + //#ifdef GZIP + if (s.status === EXTRA_STATE) { + if (s.gzhead.extra/* != Z_NULL*/) { + beg = s.pending; /* start of bytes to update crc */ + + while (s.gzindex < (s.gzhead.extra.length & 0xffff)) { + if (s.pending === s.pending_buf_size) { + if (s.gzhead.hcrc && s.pending > beg) { + strm.adler = crc32$1(strm.adler, s.pending_buf, s.pending - beg, beg); + } + flush_pending(strm); + beg = s.pending; + if (s.pending === s.pending_buf_size) { + break; + } + } + put_byte(s, s.gzhead.extra[s.gzindex] & 0xff); + s.gzindex++; + } + if (s.gzhead.hcrc && s.pending > beg) { + strm.adler = crc32$1(strm.adler, s.pending_buf, s.pending - beg, beg); + } + if (s.gzindex === s.gzhead.extra.length) { + s.gzindex = 0; + s.status = NAME_STATE; + } + } + else { + s.status = NAME_STATE; + } + } + if (s.status === NAME_STATE) { + if (s.gzhead.name/* != Z_NULL*/) { + beg = s.pending; /* start of bytes to update crc */ + //int val; + + do { + if (s.pending === s.pending_buf_size) { + if (s.gzhead.hcrc && s.pending > beg) { + strm.adler = crc32$1(strm.adler, s.pending_buf, s.pending - beg, beg); + } + flush_pending(strm); + beg = s.pending; + if (s.pending === s.pending_buf_size) { + val = 1; + break; + } + } + // JS specific: little magic to add zero terminator to end of string + if (s.gzindex < s.gzhead.name.length) { + val = s.gzhead.name.charCodeAt(s.gzindex++) & 0xff; + } else { + val = 0; + } + put_byte(s, val); + } while (val !== 0); + + if (s.gzhead.hcrc && s.pending > beg) { + strm.adler = crc32$1(strm.adler, s.pending_buf, s.pending - beg, beg); + } + if (val === 0) { + s.gzindex = 0; + s.status = COMMENT_STATE; + } + } + else { + s.status = COMMENT_STATE; + } + } + if (s.status === COMMENT_STATE) { + if (s.gzhead.comment/* != Z_NULL*/) { + beg = s.pending; /* start of bytes to update crc */ + //int val; + + do { + if (s.pending === s.pending_buf_size) { + if (s.gzhead.hcrc && s.pending > beg) { + strm.adler = crc32$1(strm.adler, s.pending_buf, s.pending - beg, beg); + } + flush_pending(strm); + beg = s.pending; + if (s.pending === s.pending_buf_size) { + val = 1; + break; + } + } + // JS specific: little magic to add zero terminator to end of string + if (s.gzindex < s.gzhead.comment.length) { + val = s.gzhead.comment.charCodeAt(s.gzindex++) & 0xff; + } else { + val = 0; + } + put_byte(s, val); + } while (val !== 0); + + if (s.gzhead.hcrc && s.pending > beg) { + strm.adler = crc32$1(strm.adler, s.pending_buf, s.pending - beg, beg); + } + if (val === 0) { + s.status = HCRC_STATE; + } + } + else { + s.status = HCRC_STATE; + } + } + if (s.status === HCRC_STATE) { + if (s.gzhead.hcrc) { + if (s.pending + 2 > s.pending_buf_size) { + flush_pending(strm); + } + if (s.pending + 2 <= s.pending_buf_size) { + put_byte(s, strm.adler & 0xff); + put_byte(s, (strm.adler >> 8) & 0xff); + strm.adler = 0; //crc32(0L, Z_NULL, 0); + s.status = BUSY_STATE; + } + } + else { + s.status = BUSY_STATE; + } + } + //#endif + + /* Flush as much pending output as possible */ + if (s.pending !== 0) { + flush_pending(strm); + if (strm.avail_out === 0) { + /* Since avail_out is 0, deflate will be called again with + * more output space, but possibly with both pending and + * avail_in equal to zero. There won't be anything to do, + * but this is not an error situation so make sure we + * return OK instead of BUF_ERROR at next call of deflate: + */ + s.last_flush = -1; + return Z_OK; + } + + /* Make sure there is something to do and avoid duplicate consecutive + * flushes. For repeated and useless calls with Z_FINISH, we keep + * returning Z_STREAM_END instead of Z_BUF_ERROR. + */ + } else if (strm.avail_in === 0 && rank(flush) <= rank(old_flush) && + flush !== Z_FINISH) { + return err(strm, Z_BUF_ERROR); + } + + /* User must not provide more input after the first FINISH: */ + if (s.status === FINISH_STATE && strm.avail_in !== 0) { + return err(strm, Z_BUF_ERROR); + } + + /* Start a new block or continue the current one. + */ + if (strm.avail_in !== 0 || s.lookahead !== 0 || + (flush !== Z_NO_FLUSH && s.status !== FINISH_STATE)) { + var bstate = (s.strategy === Z_HUFFMAN_ONLY) ? deflate_huff(s, flush) : + (s.strategy === Z_RLE ? deflate_rle(s, flush) : + configuration_table[s.level].func(s, flush)); + + if (bstate === BS_FINISH_STARTED || bstate === BS_FINISH_DONE) { + s.status = FINISH_STATE; + } + if (bstate === BS_NEED_MORE || bstate === BS_FINISH_STARTED) { + if (strm.avail_out === 0) { + s.last_flush = -1; + /* avoid BUF_ERROR next call, see above */ + } + return Z_OK; + /* If flush != Z_NO_FLUSH && avail_out == 0, the next call + * of deflate should use the same flush parameter to make sure + * that the flush is complete. So we don't have to output an + * empty block here, this will be done at next call. This also + * ensures that for a very small output buffer, we emit at most + * one empty block. + */ + } + if (bstate === BS_BLOCK_DONE) { + if (flush === Z_PARTIAL_FLUSH) { + _tr_align(s); + } + else if (flush !== Z_BLOCK) { /* FULL_FLUSH or SYNC_FLUSH */ + + _tr_stored_block(s, 0, 0, false); + /* For a full flush, this empty block will be recognized + * as a special marker by inflate_sync(). + */ + if (flush === Z_FULL_FLUSH) { + /*** CLEAR_HASH(s); ***/ /* forget history */ + zero(s.head); // Fill with NIL (= 0); + + if (s.lookahead === 0) { + s.strstart = 0; + s.block_start = 0; + s.insert = 0; + } + } + } + flush_pending(strm); + if (strm.avail_out === 0) { + s.last_flush = -1; /* avoid BUF_ERROR at next call, see above */ + return Z_OK; + } + } + } + //Assert(strm->avail_out > 0, "bug2"); + //if (strm.avail_out <= 0) { throw Error("bug2");} + + if (flush !== Z_FINISH) { return Z_OK; } + if (s.wrap <= 0) { return Z_STREAM_END; } + + /* Write the trailer */ + if (s.wrap === 2) { + put_byte(s, strm.adler & 0xff); + put_byte(s, (strm.adler >> 8) & 0xff); + put_byte(s, (strm.adler >> 16) & 0xff); + put_byte(s, (strm.adler >> 24) & 0xff); + put_byte(s, strm.total_in & 0xff); + put_byte(s, (strm.total_in >> 8) & 0xff); + put_byte(s, (strm.total_in >> 16) & 0xff); + put_byte(s, (strm.total_in >> 24) & 0xff); + } + else { + putShortMSB(s, strm.adler >>> 16); + putShortMSB(s, strm.adler & 0xffff); + } + + flush_pending(strm); + /* If avail_out is zero, the application will call deflate again + * to flush the rest. + */ + if (s.wrap > 0) { s.wrap = -s.wrap; } + /* write the trailer only once! */ + return s.pending !== 0 ? Z_OK : Z_STREAM_END; + } + + function deflateEnd(strm) { + let status; + + if (!strm/*== Z_NULL*/ || !strm.state/*== Z_NULL*/) { + return Z_STREAM_ERROR; + } + + status = strm.state.status; + if (status !== INIT_STATE && + status !== EXTRA_STATE && + status !== NAME_STATE && + status !== COMMENT_STATE && + status !== HCRC_STATE && + status !== BUSY_STATE && + status !== FINISH_STATE + ) { + return err(strm, Z_STREAM_ERROR); + } + + strm.state = null; + + return status === BUSY_STATE ? err(strm, Z_DATA_ERROR) : Z_OK; + } + + + /* ========================================================================= + * Initializes the compression dictionary from the given byte + * sequence without producing any compressed output. + */ + function deflateSetDictionary(strm, dictionary) { + let dictLength = dictionary.length; + + let s; + let str, n; + let wrap; + let avail; + let next; + let input; + let tmpDict; + + if (!strm/*== Z_NULL*/ || !strm.state/*== Z_NULL*/) { + return Z_STREAM_ERROR; + } + + s = strm.state; + wrap = s.wrap; + + if (wrap === 2 || (wrap === 1 && s.status !== INIT_STATE) || s.lookahead) { + return Z_STREAM_ERROR; + } + + /* when using zlib wrappers, compute Adler-32 for provided dictionary */ + if (wrap === 1) { + /* adler32(strm->adler, dictionary, dictLength); */ + strm.adler = adler32(strm.adler, dictionary, dictLength, 0); + } + + s.wrap = 0; /* avoid computing Adler-32 in read_buf */ + + /* if dictionary would fill window, just replace the history */ + if (dictLength >= s.w_size) { + if (wrap === 0) { /* already empty otherwise */ + /*** CLEAR_HASH(s); ***/ + zero(s.head); // Fill with NIL (= 0); + s.strstart = 0; + s.block_start = 0; + s.insert = 0; + } + /* use the tail */ + // dictionary = dictionary.slice(dictLength - s.w_size); + tmpDict = new Buf8(s.w_size); + arraySet(tmpDict, dictionary, dictLength - s.w_size, s.w_size, 0); + dictionary = tmpDict; + dictLength = s.w_size; + } + /* insert dictionary into window and hash */ + avail = strm.avail_in; + next = strm.next_in; + input = strm.input; + strm.avail_in = dictLength; + strm.next_in = 0; + strm.input = dictionary; + fill_window(s); + while (s.lookahead >= MIN_MATCH) { + str = s.strstart; + n = s.lookahead - (MIN_MATCH - 1); + do { + /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */ + s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + MIN_MATCH - 1]) & s.hash_mask; + + s.prev[str & s.w_mask] = s.head[s.ins_h]; + + s.head[s.ins_h] = str; + str++; + } while (--n); + s.strstart = str; + s.lookahead = MIN_MATCH - 1; + fill_window(s); + } + s.strstart += s.lookahead; + s.block_start = s.strstart; + s.insert = s.lookahead; + s.lookahead = 0; + s.match_length = s.prev_length = MIN_MATCH - 1; + s.match_available = 0; + strm.next_in = next; + strm.input = input; + strm.avail_in = avail; + s.wrap = wrap; + return Z_OK; + } + + /* Not implemented + exports.deflateBound = deflateBound; + exports.deflateCopy = deflateCopy; + exports.deflateParams = deflateParams; + exports.deflatePending = deflatePending; + exports.deflatePrime = deflatePrime; + exports.deflateTune = deflateTune; + */ + + // String encode/decode helpers + + try { + String.fromCharCode.apply(null, [ 0 ]); + } catch (__) { + } + try { + String.fromCharCode.apply(null, new Uint8Array(1)); + } catch (__) { + } + + + // Table with utf8 lengths (calculated by first byte of sequence) + // Note, that 5 & 6-byte values and some 4-byte values can not be represented in JS, + // because max possible codepoint is 0x10ffff + const _utf8len = new Buf8(256); + for (let q = 0; q < 256; q++) { + _utf8len[q] = q >= 252 ? 6 : q >= 248 ? 5 : q >= 240 ? 4 : q >= 224 ? 3 : q >= 192 ? 2 : 1; + } + _utf8len[254] = _utf8len[254] = 1; // Invalid sequence start + + + // convert string to array (typed, when possible) + function string2buf (str) { + let c, c2, m_pos, i, buf_len = 0; + const str_len = str.length; + + // count binary size + for (m_pos = 0; m_pos < str_len; m_pos++) { + c = str.charCodeAt(m_pos); + if ((c & 0xfc00) === 0xd800 && m_pos + 1 < str_len) { + c2 = str.charCodeAt(m_pos + 1); + if ((c2 & 0xfc00) === 0xdc00) { + c = 0x10000 + (c - 0xd800 << 10) + (c2 - 0xdc00); + m_pos++; + } + } + buf_len += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4; + } + + // allocate buffer + const buf = new Buf8(buf_len); + + // convert + for (i = 0, m_pos = 0; i < buf_len; m_pos++) { + c = str.charCodeAt(m_pos); + if ((c & 0xfc00) === 0xd800 && m_pos + 1 < str_len) { + c2 = str.charCodeAt(m_pos + 1); + if ((c2 & 0xfc00) === 0xdc00) { + c = 0x10000 + (c - 0xd800 << 10) + (c2 - 0xdc00); + m_pos++; + } + } + if (c < 0x80) { + /* one byte */ + buf[i++] = c; + } else if (c < 0x800) { + /* two bytes */ + buf[i++] = 0xC0 | c >>> 6; + buf[i++] = 0x80 | c & 0x3f; + } else if (c < 0x10000) { + /* three bytes */ + buf[i++] = 0xE0 | c >>> 12; + buf[i++] = 0x80 | c >>> 6 & 0x3f; + buf[i++] = 0x80 | c & 0x3f; + } else { + /* four bytes */ + buf[i++] = 0xf0 | c >>> 18; + buf[i++] = 0x80 | c >>> 12 & 0x3f; + buf[i++] = 0x80 | c >>> 6 & 0x3f; + buf[i++] = 0x80 | c & 0x3f; + } + } + + return buf; + } + + + // Convert binary string (typed, when possible) + function binstring2buf (str) { + const buf = new Buf8(str.length); + for (let i = 0, len = buf.length; i < len; i++) { + buf[i] = str.charCodeAt(i); + } + return buf; + } + + // (C) 1995-2013 Jean-loup Gailly and Mark Adler + // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. + + class ZStream { + constructor() { + /* next input byte */ + this.input = null; // JS specific, because we have no pointers + this.next_in = 0; + /* number of bytes available at input */ + this.avail_in = 0; + /* total number of input bytes read so far */ + this.total_in = 0; + /* next output byte should be put there */ + this.output = null; // JS specific, because we have no pointers + this.next_out = 0; + /* remaining free space at output */ + this.avail_out = 0; + /* total number of bytes output so far */ + this.total_out = 0; + /* last error message, NULL if no error */ + this.msg = ''/*Z_NULL*/; + /* not visible by applications */ + this.state = null; + /* best guess about the data type: binary or text */ + this.data_type = 2/*Z_UNKNOWN*/; + /* adler32 value of the uncompressed data */ + this.adler = 0; + } + } + + /* ===========================================================================*/ + + + /** + * class Deflate + * + * Generic JS-style wrapper for zlib calls. If you don't need + * streaming behaviour - use more simple functions: [[deflate]], + * [[deflateRaw]] and [[gzip]]. + **/ + + /* internal + * Deflate.chunks -> Array + * + * Chunks of output data, if [[Deflate#onData]] not overridden. + **/ + + /** + * Deflate.result -> Uint8Array|Array + * + * Compressed result, generated by default [[Deflate#onData]] + * and [[Deflate#onEnd]] handlers. Filled after you push last chunk + * (call [[Deflate#push]] with `Z_FINISH` / `true` param) or if you + * push a chunk with explicit flush (call [[Deflate#push]] with + * `Z_SYNC_FLUSH` param). + **/ + + /** + * Deflate.err -> Number + * + * Error code after deflate finished. 0 (Z_OK) on success. + * You will not need it in real life, because deflate errors + * are possible only on wrong options or bad `onData` / `onEnd` + * custom handlers. + **/ + + /** + * Deflate.msg -> String + * + * Error message, if [[Deflate.err]] != 0 + **/ + + + /** + * new Deflate(options) + * - options (Object): zlib deflate options. + * + * Creates new deflator instance with specified params. Throws exception + * on bad params. Supported options: + * + * - `level` + * - `windowBits` + * - `memLevel` + * - `strategy` + * - `dictionary` + * + * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced) + * for more information on these. + * + * Additional options, for internal needs: + * + * - `chunkSize` - size of generated data chunks (16K by default) + * - `raw` (Boolean) - do raw deflate + * - `gzip` (Boolean) - create gzip wrapper + * - `to` (String) - if equal to 'string', then result will be "binary string" + * (each char code [0..255]) + * - `header` (Object) - custom header for gzip + * - `text` (Boolean) - true if compressed data believed to be text + * - `time` (Number) - modification time, unix timestamp + * - `os` (Number) - operation system code + * - `extra` (Array) - array of bytes with extra data (max 65536) + * - `name` (String) - file name (binary string) + * - `comment` (String) - comment (binary string) + * - `hcrc` (Boolean) - true if header crc should be added + * + * ##### Example: + * + * ```javascript + * var pako = void('pako') + * , chunk1 = Uint8Array([1,2,3,4,5,6,7,8,9]) + * , chunk2 = Uint8Array([10,11,12,13,14,15,16,17,18,19]); + * + * var deflate = new pako.Deflate({ level: 3}); + * + * deflate.push(chunk1, false); + * deflate.push(chunk2, true); // true -> last chunk + * + * if (deflate.err) { throw Error(deflate.err); } + * + * console.log(deflate.result); + * ``` + **/ + + class Deflate { + constructor(options) { + this.options = { + level: Z_DEFAULT_COMPRESSION, + method: Z_DEFLATED, + chunkSize: 16384, + windowBits: 15, + memLevel: 8, + strategy: Z_DEFAULT_STRATEGY, + ...(options || {}) + }; + + const opt = this.options; + + if (opt.raw && (opt.windowBits > 0)) { + opt.windowBits = -opt.windowBits; + } + + else if (opt.gzip && (opt.windowBits > 0) && (opt.windowBits < 16)) { + opt.windowBits += 16; + } + + this.err = 0; // error code, if happens (0 = Z_OK) + this.msg = ''; // error message + this.ended = false; // used to avoid multiple onEnd() calls + this.chunks = []; // chunks of compressed data + + this.strm = new ZStream(); + this.strm.avail_out = 0; + + var status = deflateInit2( + this.strm, + opt.level, + opt.method, + opt.windowBits, + opt.memLevel, + opt.strategy + ); + + if (status !== Z_OK) { + throw Error(msg[status]); + } + + if (opt.header) { + deflateSetHeader(this.strm, opt.header); + } + + if (opt.dictionary) { + let dict; + // Convert data if needed + if (typeof opt.dictionary === 'string') { + // If we need to compress text, change encoding to utf8. + dict = string2buf(opt.dictionary); + } else if (opt.dictionary instanceof ArrayBuffer) { + dict = new Uint8Array(opt.dictionary); + } else { + dict = opt.dictionary; + } + + status = deflateSetDictionary(this.strm, dict); + + if (status !== Z_OK) { + throw Error(msg[status]); + } + + this._dict_set = true; + } + } + + /** + * Deflate#push(data[, mode]) -> Boolean + * - data (Uint8Array|Array|ArrayBuffer|String): input data. Strings will be + * converted to utf8 byte sequence. + * - mode (Number|Boolean): 0..6 for corresponding Z_NO_FLUSH..Z_TREE modes. + * See constants. Skipped or `false` means Z_NO_FLUSH, `true` means Z_FINISH. + * + * Sends input data to deflate pipe, generating [[Deflate#onData]] calls with + * new compressed chunks. Returns `true` on success. The last data block must have + * mode Z_FINISH (or `true`). That will flush internal pending buffers and call + * [[Deflate#onEnd]]. For interim explicit flushes (without ending the stream) you + * can use mode Z_SYNC_FLUSH, keeping the compression context. + * + * On fail call [[Deflate#onEnd]] with error code and return false. + * + * We strongly recommend to use `Uint8Array` on input for best speed (output + * array format is detected automatically). Also, don't skip last param and always + * use the same type in your code (boolean or number). That will improve JS speed. + * + * For regular `Array`-s make sure all elements are [0..255]. + * + * ##### Example + * + * ```javascript + * push(chunk, false); // push one of data chunks + * ... + * push(chunk, true); // push last chunk + * ``` + **/ + push(data, mode) { + const { strm, options: { chunkSize } } = this; + var status, _mode; + + if (this.ended) { return false; } + + _mode = (mode === ~~mode) ? mode : ((mode === true) ? Z_FINISH : Z_NO_FLUSH); + + // Convert data if needed + if (typeof data === 'string') { + // If we need to compress text, change encoding to utf8. + strm.input = string2buf(data); + } else if (data instanceof ArrayBuffer) { + strm.input = new Uint8Array(data); + } else { + strm.input = data; + } + + strm.next_in = 0; + strm.avail_in = strm.input.length; + + do { + if (strm.avail_out === 0) { + strm.output = new Buf8(chunkSize); + strm.next_out = 0; + strm.avail_out = chunkSize; + } + status = deflate(strm, _mode); /* no bad return value */ + + if (status !== Z_STREAM_END && status !== Z_OK) { + this.onEnd(status); + this.ended = true; + return false; + } + if (strm.avail_out === 0 || (strm.avail_in === 0 && (_mode === Z_FINISH || _mode === Z_SYNC_FLUSH))) { + this.onData(shrinkBuf(strm.output, strm.next_out)); + } + } while ((strm.avail_in > 0 || strm.avail_out === 0) && status !== Z_STREAM_END); + + // Finalize on the last chunk. + if (_mode === Z_FINISH) { + status = deflateEnd(this.strm); + this.onEnd(status); + this.ended = true; + return status === Z_OK; + } + + // callback interim results if Z_SYNC_FLUSH. + if (_mode === Z_SYNC_FLUSH) { + this.onEnd(Z_OK); + strm.avail_out = 0; + return true; + } + + return true; + }; + /** + * Deflate#onData(chunk) -> Void + * - chunk (Uint8Array|Array|String): output data. Type of array depends + * on js engine support. When string output requested, each chunk + * will be string. + * + * By default, stores data blocks in `chunks[]` property and glue + * those in `onEnd`. Override this handler, if you need another behaviour. + **/ + onData(chunk) { + this.chunks.push(chunk); + }; + + /** + * Deflate#onEnd(status) -> Void + * - status (Number): deflate status. 0 (Z_OK) on success, + * other if not. + * + * Called once after you tell deflate that the input stream is + * complete (Z_FINISH) or should be flushed (Z_SYNC_FLUSH) + * or if an error happened. By default - join collected chunks, + * free memory and fill `results` / `err` properties. + **/ + onEnd(status) { + // On success - join + if (status === Z_OK) { + this.result = flattenChunks(this.chunks); + } + this.chunks = []; + this.err = status; + this.msg = this.strm.msg; + }; + } + + // (C) 1995-2013 Jean-loup Gailly and Mark Adler + // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. + + // See state defs from inflate.js + const BAD$1 = 30; /* got a data error -- remain here until reset */ + const TYPE$1 = 12; /* i: waiting for type bits, including last-flag bit */ + + /* + Decode literal, length, and distance codes and write out the resulting + literal and match bytes until either not enough input or output is + available, an end-of-block is encountered, or a data error is encountered. + When large enough input and output buffers are supplied to inflate(), for + example, a 16K input buffer and a 64K output buffer, more than 95% of the + inflate execution time is spent in this routine. + + Entry assumptions: + + state.mode === LEN + strm.avail_in >= 6 + strm.avail_out >= 258 + start >= strm.avail_out + state.bits < 8 + + On return, state.mode is one of: + + LEN -- ran out of enough output space or enough available input + TYPE -- reached end of block code, inflate() to interpret next block + BAD -- error in block data + + Notes: + + - The maximum input bits used by a length/distance pair is 15 bits for the + length code, 5 bits for the length extra, 15 bits for the distance code, + and 13 bits for the distance extra. This totals 48 bits, or six bytes. + Therefore if strm.avail_in >= 6, then there is enough input to avoid + checking for available input while decoding. + + - The maximum bytes that a single length/distance pair can output is 258 + bytes, which is the maximum length that can be coded. inflate_fast() + requires strm.avail_out >= 258 for each loop to avoid checking for + output space. + */ + function inflate_fast(strm, start) { + let _in; /* local strm.input */ + let _out; /* local strm.output */ + // Use `s_window` instead `window`, avoid conflict with instrumentation tools + let hold; /* local strm.hold */ + let bits; /* local strm.bits */ + let here; /* retrieved table entry */ + let op; /* code bits, operation, extra bits, or */ + /* window position, window bytes to copy */ + let len; /* match length, unused bytes */ + let dist; /* match distance */ + let from; /* where to copy match from */ + let from_source; + + + + /* copy state to local variables */ + const state = strm.state; + //here = state.here; + _in = strm.next_in; + const input = strm.input; + const last = _in + (strm.avail_in - 5); + _out = strm.next_out; + const output = strm.output; + const beg = _out - (start - strm.avail_out); + const end = _out + (strm.avail_out - 257); + //#ifdef INFLATE_STRICT + const dmax = state.dmax; + //#endif + const wsize = state.wsize; + const whave = state.whave; + const wnext = state.wnext; + const s_window = state.window; + hold = state.hold; + bits = state.bits; + const lcode = state.lencode; + const dcode = state.distcode; + const lmask = (1 << state.lenbits) - 1; + const dmask = (1 << state.distbits) - 1; + + + /* decode literals and length/distances until end-of-block or not enough + input data or output space */ + + top: + do { + if (bits < 15) { + hold += input[_in++] << bits; + bits += 8; + hold += input[_in++] << bits; + bits += 8; + } + + here = lcode[hold & lmask]; + + dolen: + for (;;) { // Goto emulation + op = here >>> 24/*here.bits*/; + hold >>>= op; + bits -= op; + op = here >>> 16 & 0xff/*here.op*/; + if (op === 0) { /* literal */ + //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ? + // "inflate: literal '%c'\n" : + // "inflate: literal 0x%02x\n", here.val)); + output[_out++] = here & 0xffff/*here.val*/; + } else if (op & 16) { /* length base */ + len = here & 0xffff/*here.val*/; + op &= 15; /* number of extra bits */ + if (op) { + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + } + len += hold & (1 << op) - 1; + hold >>>= op; + bits -= op; + } + //Tracevv((stderr, "inflate: length %u\n", len)); + if (bits < 15) { + hold += input[_in++] << bits; + bits += 8; + hold += input[_in++] << bits; + bits += 8; + } + here = dcode[hold & dmask]; + + dodist: + for (;;) { // goto emulation + op = here >>> 24/*here.bits*/; + hold >>>= op; + bits -= op; + op = here >>> 16 & 0xff/*here.op*/; + + if (op & 16) { /* distance base */ + dist = here & 0xffff/*here.val*/; + op &= 15; /* number of extra bits */ + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + } + } + dist += hold & (1 << op) - 1; + //#ifdef INFLATE_STRICT + if (dist > dmax) { + strm.msg = "invalid distance too far back"; + state.mode = BAD$1; + break top; + } + //#endif + hold >>>= op; + bits -= op; + //Tracevv((stderr, "inflate: distance %u\n", dist)); + op = _out - beg; /* max distance in output */ + if (dist > op) { /* see if copy from window */ + op = dist - op; /* distance back in window */ + if (op > whave) { + if (state.sane) { + strm.msg = "invalid distance too far back"; + state.mode = BAD$1; + break top; + } + + // (!) This block is disabled in zlib defaults, + // don't enable it for binary compatibility + //#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR + // if (len <= op - whave) { + // do { + // output[_out++] = 0; + // } while (--len); + // continue top; + // } + // len -= op - whave; + // do { + // output[_out++] = 0; + // } while (--op > whave); + // if (op === 0) { + // from = _out - dist; + // do { + // output[_out++] = output[from++]; + // } while (--len); + // continue top; + // } + //#endif + } + from = 0; // window index + from_source = s_window; + if (wnext === 0) { /* very common case */ + from += wsize - op; + if (op < len) { /* some from window */ + len -= op; + do { + output[_out++] = s_window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } else if (wnext < op) { /* wrap around window */ + from += wsize + wnext - op; + op -= wnext; + if (op < len) { /* some from end of window */ + len -= op; + do { + output[_out++] = s_window[from++]; + } while (--op); + from = 0; + if (wnext < len) { /* some from start of window */ + op = wnext; + len -= op; + do { + output[_out++] = s_window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } + } else { /* contiguous in window */ + from += wnext - op; + if (op < len) { /* some from window */ + len -= op; + do { + output[_out++] = s_window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } + while (len > 2) { + output[_out++] = from_source[from++]; + output[_out++] = from_source[from++]; + output[_out++] = from_source[from++]; + len -= 3; + } + if (len) { + output[_out++] = from_source[from++]; + if (len > 1) { + output[_out++] = from_source[from++]; + } + } + } else { + from = _out - dist; /* copy direct from output */ + do { /* minimum length is three */ + output[_out++] = output[from++]; + output[_out++] = output[from++]; + output[_out++] = output[from++]; + len -= 3; + } while (len > 2); + if (len) { + output[_out++] = output[from++]; + if (len > 1) { + output[_out++] = output[from++]; + } + } + } + } else if ((op & 64) === 0) { /* 2nd level distance code */ + here = dcode[(here & 0xffff)/*here.val*/ + (hold & (1 << op) - 1)]; + continue dodist; + } else { + strm.msg = "invalid distance code"; + state.mode = BAD$1; + break top; + } + + break; // need to emulate goto via "continue" + } + } else if ((op & 64) === 0) { /* 2nd level length code */ + here = lcode[(here & 0xffff)/*here.val*/ + (hold & (1 << op) - 1)]; + continue dolen; + } else if (op & 32) { /* end-of-block */ + //Tracevv((stderr, "inflate: end of block\n")); + state.mode = TYPE$1; + break top; + } else { + strm.msg = "invalid literal/length code"; + state.mode = BAD$1; + break top; + } + + break; // need to emulate goto via "continue" + } + } while (_in < last && _out < end); + + /* return unused bytes (on entry, bits < 8, so in won't go too far back) */ + len = bits >> 3; + _in -= len; + bits -= len << 3; + hold &= (1 << bits) - 1; + + /* update state and return */ + strm.next_in = _in; + strm.next_out = _out; + strm.avail_in = _in < last ? 5 + (last - _in) : 5 - (_in - last); + strm.avail_out = _out < end ? 257 + (end - _out) : 257 - (_out - end); + state.hold = hold; + state.bits = bits; + return; + } + + const MAXBITS = 15; + const ENOUGH_LENS$1 = 852; + const ENOUGH_DISTS$1 = 592; + //var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS); + + const CODES$1 = 0; + const LENS$1 = 1; + const DISTS$1 = 2; + + const lbase = [ /* Length codes 257..285 base */ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 + ]; + + const lext = [ /* Length codes 257..285 extra */ + 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, + 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78 + ]; + + const dbase = [ /* Distance codes 0..29 base */ + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577, 0, 0 + ]; + + const dext = [ /* Distance codes 0..29 extra */ + 16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, + 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, + 28, 28, 29, 29, 64, 64 + ]; + + function inflate_table(type, lens, lens_index, codes, table, table_index, work, opts) { + const bits = opts.bits; + //here = opts.here; /* table entry for duplication */ + + let len = 0; /* a code's length in bits */ + let sym = 0; /* index of code symbols */ + let min = 0, max = 0; /* minimum and maximum code lengths */ + let root = 0; /* number of index bits for root table */ + let curr = 0; /* number of index bits for current table */ + let drop = 0; /* code bits to drop for sub-table */ + let left = 0; /* number of prefix codes available */ + let used = 0; /* code entries in table used */ + let huff = 0; /* Huffman code */ + let incr; /* for incrementing code, index */ + let fill; /* index for replicating entries */ + let low; /* low bits for current root entry */ + let next; /* next available space in table */ + let base = null; /* base value table to use */ + let base_index = 0; + // var shoextra; /* extra bits table to use */ + let end; /* use base and extra for symbol > end */ + const count = new Buf16(MAXBITS + 1); //[MAXBITS+1]; /* number of codes of each length */ + const offs = new Buf16(MAXBITS + 1); //[MAXBITS+1]; /* offsets in table for each length */ + let extra = null; + let extra_index = 0; + + let here_bits, here_op, here_val; + + /* + Process a set of code lengths to create a canonical Huffman code. The + code lengths are lens[0..codes-1]. Each length corresponds to the + symbols 0..codes-1. The Huffman code is generated by first sorting the + symbols by length from short to long, and retaining the symbol order + for codes with equal lengths. Then the code starts with all zero bits + for the first code of the shortest length, and the codes are integer + increments for the same length, and zeros are appended as the length + increases. For the deflate format, these bits are stored backwards + from their more natural integer increment ordering, and so when the + decoding tables are built in the large loop below, the integer codes + are incremented backwards. + + This routine assumes, but does not check, that all of the entries in + lens[] are in the range 0..MAXBITS. The caller must assure this. + 1..MAXBITS is interpreted as that code length. zero means that that + symbol does not occur in this code. + + The codes are sorted by computing a count of codes for each length, + creating from that a table of starting indices for each length in the + sorted table, and then entering the symbols in order in the sorted + table. The sorted table is work[], with that space being provided by + the caller. + + The length counts are used for other purposes as well, i.e. finding + the minimum and maximum length codes, determining if there are any + codes at all, checking for a valid set of lengths, and looking ahead + at length counts to determine sub-table sizes when building the + decoding tables. + */ + + /* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */ + for (len = 0; len <= MAXBITS; len++) { + count[len] = 0; + } + for (sym = 0; sym < codes; sym++) { + count[lens[lens_index + sym]]++; + } + + /* bound code lengths, force root to be within code lengths */ + root = bits; + for (max = MAXBITS; max >= 1; max--) { + if (count[max] !== 0) { + break; + } + } + if (root > max) { + root = max; + } + if (max === 0) { /* no symbols to code at all */ + //table.op[opts.table_index] = 64; //here.op = (var char)64; /* invalid code marker */ + //table.bits[opts.table_index] = 1; //here.bits = (var char)1; + //table.val[opts.table_index++] = 0; //here.val = (var short)0; + table[table_index++] = 1 << 24 | 64 << 16 | 0; + + + //table.op[opts.table_index] = 64; + //table.bits[opts.table_index] = 1; + //table.val[opts.table_index++] = 0; + table[table_index++] = 1 << 24 | 64 << 16 | 0; + + opts.bits = 1; + return 0; /* no symbols, but wait for decoding to report error */ + } + for (min = 1; min < max; min++) { + if (count[min] !== 0) { + break; + } + } + if (root < min) { + root = min; + } + + /* check for an over-subscribed or incomplete set of lengths */ + left = 1; + for (len = 1; len <= MAXBITS; len++) { + left <<= 1; + left -= count[len]; + if (left < 0) { + return -1; + } /* over-subscribed */ + } + if (left > 0 && (type === CODES$1 || max !== 1)) { + return -1; /* incomplete set */ + } + + /* generate offsets into symbol table for each length for sorting */ + offs[1] = 0; + for (len = 1; len < MAXBITS; len++) { + offs[len + 1] = offs[len] + count[len]; + } + + /* sort symbols by length, by symbol order within each length */ + for (sym = 0; sym < codes; sym++) { + if (lens[lens_index + sym] !== 0) { + work[offs[lens[lens_index + sym]]++] = sym; + } + } + + /* + Create and fill in decoding tables. In this loop, the table being + filled is at next and has curr index bits. The code being used is huff + with length len. That code is converted to an index by dropping drop + bits off of the bottom. For codes where len is less than drop + curr, + those top drop + curr - len bits are incremented through all values to + fill the table with replicated entries. + + root is the number of index bits for the root table. When len exceeds + root, sub-tables are created pointed to by the root entry with an index + of the low root bits of huff. This is saved in low to check for when a + new sub-table should be started. drop is zero when the root table is + being filled, and drop is root when sub-tables are being filled. + + When a new sub-table is needed, it is necessary to look ahead in the + code lengths to determine what size sub-table is needed. The length + counts are used for this, and so count[] is decremented as codes are + entered in the tables. + + used keeps track of how many table entries have been allocated from the + provided *table space. It is checked for LENS and DIST tables against + the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in + the initial root table size constants. See the comments in inftrees.h + for more information. + + sym increments through all symbols, and the loop terminates when + all codes of length max, i.e. all codes, have been processed. This + routine permits incomplete codes, so another loop after this one fills + in the rest of the decoding tables with invalid code markers. + */ + + /* set up for code type */ + // poor man optimization - use if-else instead of switch, + // to avoid deopts in old v8 + if (type === CODES$1) { + base = extra = work; /* dummy value--not used */ + end = 19; + + } else if (type === LENS$1) { + base = lbase; + base_index -= 257; + extra = lext; + extra_index -= 257; + end = 256; + + } else { /* DISTS */ + base = dbase; + extra = dext; + end = -1; + } + + /* initialize opts for loop */ + huff = 0; /* starting code */ + sym = 0; /* starting code symbol */ + len = min; /* starting code length */ + next = table_index; /* current table to fill in */ + curr = root; /* current table index bits */ + drop = 0; /* current bits to drop from code for index */ + low = -1; /* trigger new sub-table when len > root */ + used = 1 << root; /* use root table entries */ + const mask = used - 1; /* mask for comparing low */ + + /* check available table space */ + if (type === LENS$1 && used > ENOUGH_LENS$1 || + type === DISTS$1 && used > ENOUGH_DISTS$1) { + return 1; + } + + /* process all codes and make table entries */ + for (;;) { + /* create table entry */ + here_bits = len - drop; + if (work[sym] < end) { + here_op = 0; + here_val = work[sym]; + } else if (work[sym] > end) { + here_op = extra[extra_index + work[sym]]; + here_val = base[base_index + work[sym]]; + } else { + here_op = 32 + 64; /* end of block */ + here_val = 0; + } + + /* replicate for those indices with low len bits equal to huff */ + incr = 1 << len - drop; + fill = 1 << curr; + min = fill; /* save offset to next table */ + do { + fill -= incr; + table[next + (huff >> drop) + fill] = here_bits << 24 | here_op << 16 | here_val |0; + } while (fill !== 0); + + /* backwards increment the len-bit code huff */ + incr = 1 << len - 1; + while (huff & incr) { + incr >>= 1; + } + if (incr !== 0) { + huff &= incr - 1; + huff += incr; + } else { + huff = 0; + } + + /* go to next symbol, update count, len */ + sym++; + if (--count[len] === 0) { + if (len === max) { + break; + } + len = lens[lens_index + work[sym]]; + } + + /* create new sub-table if needed */ + if (len > root && (huff & mask) !== low) { + /* if first time, transition to sub-tables */ + if (drop === 0) { + drop = root; + } + + /* increment past last table */ + next += min; /* here min is 1 << curr */ + + /* determine length of next table */ + curr = len - drop; + left = 1 << curr; + while (curr + drop < max) { + left -= count[curr + drop]; + if (left <= 0) { + break; + } + curr++; + left <<= 1; + } + + /* check for enough space */ + used += 1 << curr; + if (type === LENS$1 && used > ENOUGH_LENS$1 || + type === DISTS$1 && used > ENOUGH_DISTS$1) { + return 1; + } + + /* point entry in root table to sub-table */ + low = huff & mask; + /*table.op[low] = curr; + table.bits[low] = root; + table.val[low] = next - opts.table_index;*/ + table[low] = root << 24 | curr << 16 | next - table_index |0; + } + } + + /* fill in remaining table entry if code is incomplete (guaranteed to have + at most one remaining entry, since if the code is incomplete, the + maximum code length that was allowed to get this far is one bit) */ + if (huff !== 0) { + //table.op[next + huff] = 64; /* invalid code marker */ + //table.bits[next + huff] = len - drop; + //table.val[next + huff] = 0; + table[next + huff] = len - drop << 24 | 64 << 16 |0; + } + + /* set return parameters */ + //opts.table_index += used; + opts.bits = root; + return 0; + } + + const CODES = 0; + const LENS = 1; + const DISTS = 2; + + /* STATES ====================================================================*/ + /* ===========================================================================*/ + + + const HEAD = 1; /* i: waiting for magic header */ + const FLAGS = 2; /* i: waiting for method and flags (gzip) */ + const TIME = 3; /* i: waiting for modification time (gzip) */ + const OS = 4; /* i: waiting for extra flags and operating system (gzip) */ + const EXLEN = 5; /* i: waiting for extra length (gzip) */ + const EXTRA = 6; /* i: waiting for extra bytes (gzip) */ + const NAME = 7; /* i: waiting for end of file name (gzip) */ + const COMMENT = 8; /* i: waiting for end of comment (gzip) */ + const HCRC = 9; /* i: waiting for header crc (gzip) */ + const DICTID = 10; /* i: waiting for dictionary check value */ + const DICT = 11; /* waiting for inflateSetDictionary() call */ + const TYPE = 12; /* i: waiting for type bits, including last-flag bit */ + const TYPEDO = 13; /* i: same, but skip check to exit inflate on new block */ + const STORED = 14; /* i: waiting for stored size (length and complement) */ + const COPY_ = 15; /* i/o: same as COPY below, but only first time in */ + const COPY = 16; /* i/o: waiting for input or output to copy stored block */ + const TABLE = 17; /* i: waiting for dynamic block table lengths */ + const LENLENS = 18; /* i: waiting for code length code lengths */ + const CODELENS = 19; /* i: waiting for length/lit and distance code lengths */ + const LEN_ = 20; /* i: same as LEN below, but only first time in */ + const LEN = 21; /* i: waiting for length/lit/eob code */ + const LENEXT = 22; /* i: waiting for length extra bits */ + const DIST = 23; /* i: waiting for distance code */ + const DISTEXT = 24; /* i: waiting for distance extra bits */ + const MATCH = 25; /* o: waiting for output space to copy string */ + const LIT = 26; /* o: waiting for output space to write literal */ + const CHECK = 27; /* i: waiting for 32-bit check value */ + const LENGTH = 28; /* i: waiting for 32-bit length (gzip) */ + const DONE = 29; /* finished check, done -- remain here until reset */ + const BAD = 30; /* got a data error -- remain here until reset */ + //const MEM = 31; /* got an inflate() memory error -- remain here until reset */ + const SYNC = 32; /* looking for synchronization bytes to restart inflate() */ + + /* ===========================================================================*/ + + + + const ENOUGH_LENS = 852; + const ENOUGH_DISTS = 592; + + + function zswap32(q) { + return (((q >>> 24) & 0xff) + + ((q >>> 8) & 0xff00) + + ((q & 0xff00) << 8) + + ((q & 0xff) << 24)); + } + + + class InflateState { + constructor() { + this.mode = 0; /* current inflate mode */ + this.last = false; /* true if processing last block */ + this.wrap = 0; /* bit 0 true for zlib, bit 1 true for gzip */ + this.havedict = false; /* true if dictionary provided */ + this.flags = 0; /* gzip header method and flags (0 if zlib) */ + this.dmax = 0; /* zlib header max distance (INFLATE_STRICT) */ + this.check = 0; /* protected copy of check value */ + this.total = 0; /* protected copy of output count */ + // TODO: may be {} + this.head = null; /* where to save gzip header information */ + + /* sliding window */ + this.wbits = 0; /* log base 2 of requested window size */ + this.wsize = 0; /* window size or zero if not using window */ + this.whave = 0; /* valid bytes in the window */ + this.wnext = 0; /* window write index */ + this.window = null; /* allocated sliding window, if needed */ + + /* bit accumulator */ + this.hold = 0; /* input bit accumulator */ + this.bits = 0; /* number of bits in "in" */ + + /* for string and stored block copying */ + this.length = 0; /* literal or length of data to copy */ + this.offset = 0; /* distance back to copy string from */ + + /* for table and code decoding */ + this.extra = 0; /* extra bits needed */ + + /* fixed and dynamic code tables */ + this.lencode = null; /* starting table for length/literal codes */ + this.distcode = null; /* starting table for distance codes */ + this.lenbits = 0; /* index bits for lencode */ + this.distbits = 0; /* index bits for distcode */ + + /* dynamic table building */ + this.ncode = 0; /* number of code length code lengths */ + this.nlen = 0; /* number of length code lengths */ + this.ndist = 0; /* number of distance code lengths */ + this.have = 0; /* number of code lengths in lens[] */ + this.next = null; /* next available space in codes[] */ + + this.lens = new Buf16(320); /* temporary storage for code lengths */ + this.work = new Buf16(288); /* work area for code table building */ + + /* + because we don't have pointers in js, we use lencode and distcode directly + as buffers so we don't need codes + */ + //this.codes = new utils.Buf32(ENOUGH); /* space for code tables */ + this.lendyn = null; /* dynamic table for length/literal codes (JS specific) */ + this.distdyn = null; /* dynamic table for distance codes (JS specific) */ + this.sane = 0; /* if false, allow invalid distance too far */ + this.back = 0; /* bits back of last unprocessed length/lit */ + this.was = 0; /* initial length of match */ + } + } + + function inflateResetKeep(strm) { + let state; + + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + strm.total_in = strm.total_out = state.total = 0; + strm.msg = ''; /*Z_NULL*/ + if (state.wrap) { /* to support ill-conceived Java test suite */ + strm.adler = state.wrap & 1; + } + state.mode = HEAD; + state.last = 0; + state.havedict = 0; + state.dmax = 32768; + state.head = null/*Z_NULL*/; + state.hold = 0; + state.bits = 0; + //state.lencode = state.distcode = state.next = state.codes; + state.lencode = state.lendyn = new Buf32(ENOUGH_LENS); + state.distcode = state.distdyn = new Buf32(ENOUGH_DISTS); + + state.sane = 1; + state.back = -1; + //Tracev((stderr, "inflate: reset\n")); + return Z_OK; + } + + function inflateReset(strm) { + let state; + + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + state.wsize = 0; + state.whave = 0; + state.wnext = 0; + return inflateResetKeep(strm); + + } + + function inflateReset2(strm, windowBits) { + let wrap; + let state; + + /* get the state */ + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + + /* extract wrap request from windowBits parameter */ + if (windowBits < 0) { + wrap = 0; + windowBits = -windowBits; + } + else { + wrap = (windowBits >> 4) + 1; + if (windowBits < 48) { + windowBits &= 15; + } + } + + /* set number of window bits, free window if different */ + if (windowBits && (windowBits < 8 || windowBits > 15)) { + return Z_STREAM_ERROR; + } + if (state.window !== null && state.wbits !== windowBits) { + state.window = null; + } + + /* update state and reset the rest of it */ + state.wrap = wrap; + state.wbits = windowBits; + return inflateReset(strm); + } + + function inflateInit2(strm, windowBits) { + let ret; + let state; + + if (!strm) { return Z_STREAM_ERROR; } + //strm.msg = Z_NULL; /* in case we return an error */ + + state = new InflateState(); + + //if (state === Z_NULL) return Z_MEM_ERROR; + //Tracev((stderr, "inflate: allocated\n")); + strm.state = state; + state.window = null/*Z_NULL*/; + ret = inflateReset2(strm, windowBits); + if (ret !== Z_OK) { + strm.state = null/*Z_NULL*/; + } + return ret; + } + + + /* + Return state with length and distance decoding tables and index sizes set to + fixed code decoding. Normally this returns fixed tables from inffixed.h. + If BUILDFIXED is defined, then instead this routine builds the tables the + first time it's called, and returns those tables the first time and + thereafter. This reduces the size of the code by about 2K bytes, in + exchange for a little execution time. However, BUILDFIXED should not be + used for threaded applications, since the rewriting of the tables and virgin + may not be thread-safe. + */ + let virgin = true; + + let lenfix, distfix; // We have no pointers in JS, so keep tables separate + + function fixedtables(state) { + /* build fixed huffman tables if first call (may not be thread safe) */ + if (virgin) { + let sym; + + lenfix = new Buf32(512); + distfix = new Buf32(32); + + /* literal/length table */ + sym = 0; + while (sym < 144) { state.lens[sym++] = 8; } + while (sym < 256) { state.lens[sym++] = 9; } + while (sym < 280) { state.lens[sym++] = 7; } + while (sym < 288) { state.lens[sym++] = 8; } + + inflate_table(LENS, state.lens, 0, 288, lenfix, 0, state.work, { bits: 9 }); + + /* distance table */ + sym = 0; + while (sym < 32) { state.lens[sym++] = 5; } + + inflate_table(DISTS, state.lens, 0, 32, distfix, 0, state.work, { bits: 5 }); + + /* do this just once */ + virgin = false; + } + + state.lencode = lenfix; + state.lenbits = 9; + state.distcode = distfix; + state.distbits = 5; + } + + + /* + Update the window with the last wsize (normally 32K) bytes written before + returning. If window does not exist yet, create it. This is only called + when a window is already in use, or when output has been written during this + inflate call, but the end of the deflate stream has not been reached yet. + It is also called to create a window for dictionary data when a dictionary + is loaded. + + Providing output buffers larger than 32K to inflate() should provide a speed + advantage, since only the last 32K of output is copied to the sliding window + upon return from inflate(), and since all distances after the first 32K of + output will fall in the output data, making match copies simpler and faster. + The advantage may be dependent on the size of the processor's data caches. + */ + function updatewindow(strm, src, end, copy) { + let dist; + const state = strm.state; + + /* if it hasn't been done already, allocate space for the window */ + if (state.window === null) { + state.wsize = 1 << state.wbits; + state.wnext = 0; + state.whave = 0; + + state.window = new Buf8(state.wsize); + } + + /* copy state->wsize or less output bytes into the circular window */ + if (copy >= state.wsize) { + arraySet(state.window, src, end - state.wsize, state.wsize, 0); + state.wnext = 0; + state.whave = state.wsize; + } + else { + dist = state.wsize - state.wnext; + if (dist > copy) { + dist = copy; + } + //zmemcpy(state->window + state->wnext, end - copy, dist); + arraySet(state.window, src, end - copy, dist, state.wnext); + copy -= dist; + if (copy) { + //zmemcpy(state->window, end - copy, copy); + arraySet(state.window, src, end - copy, copy, 0); + state.wnext = copy; + state.whave = state.wsize; + } + else { + state.wnext += dist; + if (state.wnext === state.wsize) { state.wnext = 0; } + if (state.whave < state.wsize) { state.whave += dist; } + } + } + return 0; + } + + function inflate(strm, flush) { + let state; + let input, output; // input/output buffers + let next; /* next input INDEX */ + let put; /* next output INDEX */ + let have, left; /* available input and output */ + let hold; /* bit buffer */ + let bits; /* bits in bit buffer */ + let _in, _out; /* save starting available input and output */ + let copy; /* number of stored or match bytes to copy */ + let from; /* where to copy match bytes from */ + let from_source; + let here = 0; /* current decoding table entry */ + let here_bits, here_op, here_val; // paked "here" denormalized (JS specific) + //var last; /* parent table entry */ + let last_bits, last_op, last_val; // paked "last" denormalized (JS specific) + let len; /* length to copy for repeats, bits to drop */ + let ret; /* return code */ + let hbuf = new Buf8(4); /* buffer for gzip header crc calculation */ + let opts; + + let n; // temporary var for NEED_BITS + + const order = /* permutation of code lengths */ + [ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ]; + + + if (!strm || !strm.state || !strm.output || + (!strm.input && strm.avail_in !== 0)) { + return Z_STREAM_ERROR; + } + + state = strm.state; + if (state.mode === TYPE) { state.mode = TYPEDO; } /* skip check */ + + + //--- LOAD() --- + put = strm.next_out; + output = strm.output; + left = strm.avail_out; + next = strm.next_in; + input = strm.input; + have = strm.avail_in; + hold = state.hold; + bits = state.bits; + //--- + + _in = have; + _out = left; + ret = Z_OK; + + inf_leave: // goto emulation + for (;;) { + switch (state.mode) { + case HEAD: + if (state.wrap === 0) { + state.mode = TYPEDO; + break; + } + //=== NEEDBITS(16); + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if ((state.wrap & 2) && hold === 0x8b1f) { /* gzip header */ + state.check = 0/*crc32(0L, Z_NULL, 0)*/; + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32$1(state.check, hbuf, 2, 0); + //===// + + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = FLAGS; + break; + } + state.flags = 0; /* expect zlib header */ + if (state.head) { + state.head.done = false; + } + if (!(state.wrap & 1) || /* check if zlib header allowed */ + (((hold & 0xff)/*BITS(8)*/ << 8) + (hold >> 8)) % 31) { + strm.msg = 'incorrect header check'; + state.mode = BAD; + break; + } + if ((hold & 0x0f)/*BITS(4)*/ !== Z_DEFLATED) { + strm.msg = 'unknown compression method'; + state.mode = BAD; + break; + } + //--- DROPBITS(4) ---// + hold >>>= 4; + bits -= 4; + //---// + len = (hold & 0x0f)/*BITS(4)*/ + 8; + if (state.wbits === 0) { + state.wbits = len; + } + else if (len > state.wbits) { + strm.msg = 'invalid window size'; + state.mode = BAD; + break; + } + state.dmax = 1 << len; + //Tracev((stderr, "inflate: zlib header ok\n")); + strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/; + state.mode = hold & 0x200 ? DICTID : TYPE; + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + break; + case FLAGS: + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.flags = hold; + if ((state.flags & 0xff) !== Z_DEFLATED) { + strm.msg = 'unknown compression method'; + state.mode = BAD; + break; + } + if (state.flags & 0xe000) { + strm.msg = 'unknown header flags set'; + state.mode = BAD; + break; + } + if (state.head) { + state.head.text = ((hold >> 8) & 1); + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32$1(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = TIME; + /* falls through */ + case TIME: + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (state.head) { + state.head.time = hold; + } + if (state.flags & 0x0200) { + //=== CRC4(state.check, hold) + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + hbuf[2] = (hold >>> 16) & 0xff; + hbuf[3] = (hold >>> 24) & 0xff; + state.check = crc32$1(state.check, hbuf, 4, 0); + //=== + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = OS; + /* falls through */ + case OS: + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (state.head) { + state.head.xflags = (hold & 0xff); + state.head.os = (hold >> 8); + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32$1(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = EXLEN; + /* falls through */ + case EXLEN: + if (state.flags & 0x0400) { + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.length = hold; + if (state.head) { + state.head.extra_len = hold; + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32$1(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + } + else if (state.head) { + state.head.extra = null/*Z_NULL*/; + } + state.mode = EXTRA; + /* falls through */ + case EXTRA: + if (state.flags & 0x0400) { + copy = state.length; + if (copy > have) { copy = have; } + if (copy) { + if (state.head) { + len = state.head.extra_len - state.length; + if (!state.head.extra) { + // Use untyped array for more convenient processing later + state.head.extra = new Array(state.head.extra_len); + } + arraySet( + state.head.extra, + input, + next, + // extra field is limited to 65536 bytes + // - no need for additional size check + copy, + /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/ + len + ); + //zmemcpy(state.head.extra + len, next, + // len + copy > state.head.extra_max ? + // state.head.extra_max - len : copy); + } + if (state.flags & 0x0200) { + state.check = crc32$1(state.check, input, copy, next); + } + have -= copy; + next += copy; + state.length -= copy; + } + if (state.length) { break inf_leave; } + } + state.length = 0; + state.mode = NAME; + /* falls through */ + case NAME: + if (state.flags & 0x0800) { + if (have === 0) { break inf_leave; } + copy = 0; + do { + // TODO: 2 or 1 bytes? + len = input[next + copy++]; + /* use constant limit because in js we should not preallocate memory */ + if (state.head && len && + (state.length < 65536 /*state.head.name_max*/)) { + state.head.name += String.fromCharCode(len); + } + } while (len && copy < have); + + if (state.flags & 0x0200) { + state.check = crc32$1(state.check, input, copy, next); + } + have -= copy; + next += copy; + if (len) { break inf_leave; } + } + else if (state.head) { + state.head.name = null; + } + state.length = 0; + state.mode = COMMENT; + /* falls through */ + case COMMENT: + if (state.flags & 0x1000) { + if (have === 0) { break inf_leave; } + copy = 0; + do { + len = input[next + copy++]; + /* use constant limit because in js we should not preallocate memory */ + if (state.head && len && + (state.length < 65536 /*state.head.comm_max*/)) { + state.head.comment += String.fromCharCode(len); + } + } while (len && copy < have); + if (state.flags & 0x0200) { + state.check = crc32$1(state.check, input, copy, next); + } + have -= copy; + next += copy; + if (len) { break inf_leave; } + } + else if (state.head) { + state.head.comment = null; + } + state.mode = HCRC; + /* falls through */ + case HCRC: + if (state.flags & 0x0200) { + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (hold !== (state.check & 0xffff)) { + strm.msg = 'header crc mismatch'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + } + if (state.head) { + state.head.hcrc = ((state.flags >> 9) & 1); + state.head.done = true; + } + strm.adler = state.check = 0; + state.mode = TYPE; + break; + case DICTID: + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + strm.adler = state.check = zswap32(hold); + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = DICT; + /* falls through */ + case DICT: + if (state.havedict === 0) { + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + return Z_NEED_DICT; + } + strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/; + state.mode = TYPE; + /* falls through */ + case TYPE: + if (flush === Z_BLOCK || flush === Z_TREES) { break inf_leave; } + /* falls through */ + case TYPEDO: + if (state.last) { + //--- BYTEBITS() ---// + hold >>>= bits & 7; + bits -= bits & 7; + //---// + state.mode = CHECK; + break; + } + //=== NEEDBITS(3); */ + while (bits < 3) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.last = (hold & 0x01)/*BITS(1)*/; + //--- DROPBITS(1) ---// + hold >>>= 1; + bits -= 1; + //---// + + switch ((hold & 0x03)/*BITS(2)*/) { + case 0: /* stored block */ + //Tracev((stderr, "inflate: stored block%s\n", + // state.last ? " (last)" : "")); + state.mode = STORED; + break; + case 1: /* fixed block */ + fixedtables(state); + //Tracev((stderr, "inflate: fixed codes block%s\n", + // state.last ? " (last)" : "")); + state.mode = LEN_; /* decode codes */ + if (flush === Z_TREES) { + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + break inf_leave; + } + break; + case 2: /* dynamic block */ + //Tracev((stderr, "inflate: dynamic codes block%s\n", + // state.last ? " (last)" : "")); + state.mode = TABLE; + break; + case 3: + strm.msg = 'invalid block type'; + state.mode = BAD; + } + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + break; + case STORED: + //--- BYTEBITS() ---// /* go to byte boundary */ + hold >>>= bits & 7; + bits -= bits & 7; + //---// + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if ((hold & 0xffff) !== ((hold >>> 16) ^ 0xffff)) { + strm.msg = 'invalid stored block lengths'; + state.mode = BAD; + break; + } + state.length = hold & 0xffff; + //Tracev((stderr, "inflate: stored length %u\n", + // state.length)); + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = COPY_; + if (flush === Z_TREES) { break inf_leave; } + /* falls through */ + case COPY_: + state.mode = COPY; + /* falls through */ + case COPY: + copy = state.length; + if (copy) { + if (copy > have) { copy = have; } + if (copy > left) { copy = left; } + if (copy === 0) { break inf_leave; } + //--- zmemcpy(put, next, copy); --- + arraySet(output, input, next, copy, put); + //---// + have -= copy; + next += copy; + left -= copy; + put += copy; + state.length -= copy; + break; + } + //Tracev((stderr, "inflate: stored end\n")); + state.mode = TYPE; + break; + case TABLE: + //=== NEEDBITS(14); */ + while (bits < 14) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.nlen = (hold & 0x1f)/*BITS(5)*/ + 257; + //--- DROPBITS(5) ---// + hold >>>= 5; + bits -= 5; + //---// + state.ndist = (hold & 0x1f)/*BITS(5)*/ + 1; + //--- DROPBITS(5) ---// + hold >>>= 5; + bits -= 5; + //---// + state.ncode = (hold & 0x0f)/*BITS(4)*/ + 4; + //--- DROPBITS(4) ---// + hold >>>= 4; + bits -= 4; + //---// + //#ifndef PKZIP_BUG_WORKAROUND + if (state.nlen > 286 || state.ndist > 30) { + strm.msg = 'too many length or distance symbols'; + state.mode = BAD; + break; + } + //#endif + //Tracev((stderr, "inflate: table sizes ok\n")); + state.have = 0; + state.mode = LENLENS; + /* falls through */ + case LENLENS: + while (state.have < state.ncode) { + //=== NEEDBITS(3); + while (bits < 3) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.lens[order[state.have++]] = (hold & 0x07);//BITS(3); + //--- DROPBITS(3) ---// + hold >>>= 3; + bits -= 3; + //---// + } + while (state.have < 19) { + state.lens[order[state.have++]] = 0; + } + // We have separate tables & no pointers. 2 commented lines below not needed. + //state.next = state.codes; + //state.lencode = state.next; + // Switch to use dynamic table + state.lencode = state.lendyn; + state.lenbits = 7; + + opts = { bits: state.lenbits }; + ret = inflate_table(CODES, state.lens, 0, 19, state.lencode, 0, state.work, opts); + state.lenbits = opts.bits; + + if (ret) { + strm.msg = 'invalid code lengths set'; + state.mode = BAD; + break; + } + //Tracev((stderr, "inflate: code lengths ok\n")); + state.have = 0; + state.mode = CODELENS; + /* falls through */ + case CODELENS: + while (state.have < state.nlen + state.ndist) { + for (;;) { + here = state.lencode[hold & ((1 << state.lenbits) - 1)];/*BITS(state.lenbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if (here_val < 16) { + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.lens[state.have++] = here_val; + } + else { + if (here_val === 16) { + //=== NEEDBITS(here.bits + 2); + n = here_bits + 2; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + if (state.have === 0) { + strm.msg = 'invalid bit length repeat'; + state.mode = BAD; + break; + } + len = state.lens[state.have - 1]; + copy = 3 + (hold & 0x03);//BITS(2); + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + } + else if (here_val === 17) { + //=== NEEDBITS(here.bits + 3); + n = here_bits + 3; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + len = 0; + copy = 3 + (hold & 0x07);//BITS(3); + //--- DROPBITS(3) ---// + hold >>>= 3; + bits -= 3; + //---// + } + else { + //=== NEEDBITS(here.bits + 7); + n = here_bits + 7; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + len = 0; + copy = 11 + (hold & 0x7f);//BITS(7); + //--- DROPBITS(7) ---// + hold >>>= 7; + bits -= 7; + //---// + } + if (state.have + copy > state.nlen + state.ndist) { + strm.msg = 'invalid bit length repeat'; + state.mode = BAD; + break; + } + while (copy--) { + state.lens[state.have++] = len; + } + } + } + + /* handle error breaks in while */ + if (state.mode === BAD) { break; } + + /* check for end-of-block code (better have one) */ + if (state.lens[256] === 0) { + strm.msg = 'invalid code -- missing end-of-block'; + state.mode = BAD; + break; + } + + /* build code tables -- note: do not change the lenbits or distbits + values here (9 and 6) without reading the comments in inftrees.h + concerning the ENOUGH constants, which depend on those values */ + state.lenbits = 9; + + opts = { bits: state.lenbits }; + ret = inflate_table(LENS, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts); + // We have separate tables & no pointers. 2 commented lines below not needed. + // state.next_index = opts.table_index; + state.lenbits = opts.bits; + // state.lencode = state.next; + + if (ret) { + strm.msg = 'invalid literal/lengths set'; + state.mode = BAD; + break; + } + + state.distbits = 6; + //state.distcode.copy(state.codes); + // Switch to use dynamic table + state.distcode = state.distdyn; + opts = { bits: state.distbits }; + ret = inflate_table(DISTS, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts); + // We have separate tables & no pointers. 2 commented lines below not needed. + // state.next_index = opts.table_index; + state.distbits = opts.bits; + // state.distcode = state.next; + + if (ret) { + strm.msg = 'invalid distances set'; + state.mode = BAD; + break; + } + //Tracev((stderr, 'inflate: codes ok\n')); + state.mode = LEN_; + if (flush === Z_TREES) { break inf_leave; } + /* falls through */ + case LEN_: + state.mode = LEN; + /* falls through */ + case LEN: + if (have >= 6 && left >= 258) { + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + inflate_fast(strm, _out); + //--- LOAD() --- + put = strm.next_out; + output = strm.output; + left = strm.avail_out; + next = strm.next_in; + input = strm.input; + have = strm.avail_in; + hold = state.hold; + bits = state.bits; + //--- + + if (state.mode === TYPE) { + state.back = -1; + } + break; + } + state.back = 0; + for (;;) { + here = state.lencode[hold & ((1 << state.lenbits) - 1)]; /*BITS(state.lenbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if (here_bits <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if (here_op && (here_op & 0xf0) === 0) { + last_bits = here_bits; + last_op = here_op; + last_val = here_val; + for (;;) { + here = state.lencode[last_val + + ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)]; + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((last_bits + here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + //--- DROPBITS(last.bits) ---// + hold >>>= last_bits; + bits -= last_bits; + //---// + state.back += last_bits; + } + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.back += here_bits; + state.length = here_val; + if (here_op === 0) { + //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ? + // "inflate: literal '%c'\n" : + // "inflate: literal 0x%02x\n", here.val)); + state.mode = LIT; + break; + } + if (here_op & 32) { + //Tracevv((stderr, "inflate: end of block\n")); + state.back = -1; + state.mode = TYPE; + break; + } + if (here_op & 64) { + strm.msg = 'invalid literal/length code'; + state.mode = BAD; + break; + } + state.extra = here_op & 15; + state.mode = LENEXT; + /* falls through */ + case LENEXT: + if (state.extra) { + //=== NEEDBITS(state.extra); + n = state.extra; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.length += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/; + //--- DROPBITS(state.extra) ---// + hold >>>= state.extra; + bits -= state.extra; + //---// + state.back += state.extra; + } + //Tracevv((stderr, "inflate: length %u\n", state.length)); + state.was = state.length; + state.mode = DIST; + /* falls through */ + case DIST: + for (;;) { + here = state.distcode[hold & ((1 << state.distbits) - 1)];/*BITS(state.distbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if ((here_op & 0xf0) === 0) { + last_bits = here_bits; + last_op = here_op; + last_val = here_val; + for (;;) { + here = state.distcode[last_val + + ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)]; + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((last_bits + here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + //--- DROPBITS(last.bits) ---// + hold >>>= last_bits; + bits -= last_bits; + //---// + state.back += last_bits; + } + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.back += here_bits; + if (here_op & 64) { + strm.msg = 'invalid distance code'; + state.mode = BAD; + break; + } + state.offset = here_val; + state.extra = (here_op) & 15; + state.mode = DISTEXT; + /* falls through */ + case DISTEXT: + if (state.extra) { + //=== NEEDBITS(state.extra); + n = state.extra; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.offset += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/; + //--- DROPBITS(state.extra) ---// + hold >>>= state.extra; + bits -= state.extra; + //---// + state.back += state.extra; + } + //#ifdef INFLATE_STRICT + if (state.offset > state.dmax) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break; + } + //#endif + //Tracevv((stderr, "inflate: distance %u\n", state.offset)); + state.mode = MATCH; + /* falls through */ + case MATCH: + if (left === 0) { break inf_leave; } + copy = _out - left; + if (state.offset > copy) { /* copy from window */ + copy = state.offset - copy; + if (copy > state.whave) { + if (state.sane) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break; + } + // (!) This block is disabled in zlib defaults, + // don't enable it for binary compatibility + //#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR + // Trace((stderr, "inflate.c too far\n")); + // copy -= state.whave; + // if (copy > state.length) { copy = state.length; } + // if (copy > left) { copy = left; } + // left -= copy; + // state.length -= copy; + // do { + // output[put++] = 0; + // } while (--copy); + // if (state.length === 0) { state.mode = LEN; } + // break; + //#endif + } + if (copy > state.wnext) { + copy -= state.wnext; + from = state.wsize - copy; + } + else { + from = state.wnext - copy; + } + if (copy > state.length) { copy = state.length; } + from_source = state.window; + } + else { /* copy from output */ + from_source = output; + from = put - state.offset; + copy = state.length; + } + if (copy > left) { copy = left; } + left -= copy; + state.length -= copy; + do { + output[put++] = from_source[from++]; + } while (--copy); + if (state.length === 0) { state.mode = LEN; } + break; + case LIT: + if (left === 0) { break inf_leave; } + output[put++] = state.length; + left--; + state.mode = LEN; + break; + case CHECK: + if (state.wrap) { + //=== NEEDBITS(32); + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + // Use '|' instead of '+' to make sure that result is signed + hold |= input[next++] << bits; + bits += 8; + } + //===// + _out -= left; + strm.total_out += _out; + state.total += _out; + if (_out) { + strm.adler = state.check = + /*UPDATE(state.check, put - _out, _out);*/ + (state.flags ? crc32$1(state.check, output, _out, put - _out) : adler32(state.check, output, _out, put - _out)); + + } + _out = left; + // NB: crc32 stored as signed 32-bit int, zswap32 returns signed too + if ((state.flags ? hold : zswap32(hold)) !== state.check) { + strm.msg = 'incorrect data check'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + //Tracev((stderr, "inflate: check matches trailer\n")); + } + state.mode = LENGTH; + /* falls through */ + case LENGTH: + if (state.wrap && state.flags) { + //=== NEEDBITS(32); + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (hold !== (state.total & 0xffffffff)) { + strm.msg = 'incorrect length check'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + //Tracev((stderr, "inflate: length matches trailer\n")); + } + state.mode = DONE; + /* falls through */ + case DONE: + ret = Z_STREAM_END; + break inf_leave; + case BAD: + ret = Z_DATA_ERROR; + break inf_leave; + // case MEM: + // return Z_MEM_ERROR; + case SYNC: + /* falls through */ + default: + return Z_STREAM_ERROR; + } + } + + // inf_leave <- here is real place for "goto inf_leave", emulated via "break inf_leave" + + /* + Return from inflate(), updating the total counts and the check value. + If there was no progress during the inflate() call, return a buffer + error. Call updatewindow() to create and/or update the window state. + Note: a memory error from inflate() is non-recoverable. + */ + + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + + if (state.wsize || (_out !== strm.avail_out && state.mode < BAD && + (state.mode < CHECK || flush !== Z_FINISH))) { + if (updatewindow(strm, strm.output, strm.next_out, _out - strm.avail_out)) ; + } + _in -= strm.avail_in; + _out -= strm.avail_out; + strm.total_in += _in; + strm.total_out += _out; + state.total += _out; + if (state.wrap && _out) { + strm.adler = state.check = /*UPDATE(state.check, strm.next_out - _out, _out);*/ + (state.flags ? crc32$1(state.check, output, _out, strm.next_out - _out) : adler32(state.check, output, _out, strm.next_out - _out)); + } + strm.data_type = state.bits + (state.last ? 64 : 0) + + (state.mode === TYPE ? 128 : 0) + + (state.mode === LEN_ || state.mode === COPY_ ? 256 : 0); + if (((_in === 0 && _out === 0) || flush === Z_FINISH) && ret === Z_OK) { + ret = Z_BUF_ERROR; + } + return ret; + } + + function inflateEnd(strm) { + + if (!strm || !strm.state /*|| strm->zfree == (free_func)0*/) { + return Z_STREAM_ERROR; + } + + const state = strm.state; + if (state.window) { + state.window = null; + } + strm.state = null; + return Z_OK; + } + + function inflateGetHeader(strm, head) { + let state; + + /* check state */ + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + if ((state.wrap & 2) === 0) { return Z_STREAM_ERROR; } + + /* save header structure */ + state.head = head; + head.done = false; + return Z_OK; + } + + function inflateSetDictionary(strm, dictionary) { + const dictLength = dictionary.length; + + let state; + let dictid; + + /* check state */ + if (!strm /* == Z_NULL */ || !strm.state /* == Z_NULL */) { return Z_STREAM_ERROR; } + state = strm.state; + + if (state.wrap !== 0 && state.mode !== DICT) { + return Z_STREAM_ERROR; + } + + /* check for correct dictionary identifier */ + if (state.mode === DICT) { + dictid = 1; /* adler32(0, null, 0)*/ + /* dictid = adler32(dictid, dictionary, dictLength); */ + dictid = adler32(dictid, dictionary, dictLength, 0); + if (dictid !== state.check) { + return Z_DATA_ERROR; + } + } + /* copy dictionary to window using updatewindow(), which will amend the + existing dictionary if appropriate */ + updatewindow(strm, dictionary, dictLength, dictLength); + // if (ret) { + // state.mode = MEM; + // return Z_MEM_ERROR; + // } + state.havedict = 1; + // Tracev((stderr, "inflate: dictionary set\n")); + return Z_OK; + } + + /* Not implemented + exports.inflateCopy = inflateCopy; + exports.inflateGetDictionary = inflateGetDictionary; + exports.inflateMark = inflateMark; + exports.inflatePrime = inflatePrime; + exports.inflateSync = inflateSync; + exports.inflateSyncPoint = inflateSyncPoint; + exports.inflateUndermine = inflateUndermine; + */ + + // (C) 1995-2013 Jean-loup Gailly and Mark Adler + // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // 3. This notice may not be removed or altered from any source distribution. + + class GZheader { + constructor() { + /* true if compressed data believed to be text */ + this.text = 0; + /* modification time */ + this.time = 0; + /* extra flags (not used when writing a gzip file) */ + this.xflags = 0; + /* operating system */ + this.os = 0; + /* pointer to extra field or Z_NULL if none */ + this.extra = null; + /* extra field length (valid if extra != Z_NULL) */ + this.extra_len = 0; // Actually, we don't need it in JS, + // but leave for few code modifications + + // + // Setup limits is not necessary because in js we should not preallocate memory + // for inflate use constant limit in 65536 bytes + // + + /* space at extra (only when reading header) */ + // this.extra_max = 0; + /* pointer to zero-terminated file name or Z_NULL */ + this.name = ''; + /* space at name (only when reading header) */ + // this.name_max = 0; + /* pointer to zero-terminated comment or Z_NULL */ + this.comment = ''; + /* space at comment (only when reading header) */ + // this.comm_max = 0; + /* true if there was or will be a header crc */ + this.hcrc = 0; + /* true when done reading gzip header (not used when writing a gzip file) */ + this.done = false; + } + } + + /** + * class Inflate + * + * Generic JS-style wrapper for zlib calls. If you don't need + * streaming behaviour - use more simple functions: [[inflate]] + * and [[inflateRaw]]. + **/ + + /* internal + * inflate.chunks -> Array + * + * Chunks of output data, if [[Inflate#onData]] not overridden. + **/ + + /** + * Inflate.result -> Uint8Array|Array|String + * + * Uncompressed result, generated by default [[Inflate#onData]] + * and [[Inflate#onEnd]] handlers. Filled after you push last chunk + * (call [[Inflate#push]] with `Z_FINISH` / `true` param) or if you + * push a chunk with explicit flush (call [[Inflate#push]] with + * `Z_SYNC_FLUSH` param). + **/ + + /** + * Inflate.err -> Number + * + * Error code after inflate finished. 0 (Z_OK) on success. + * Should be checked if broken data possible. + **/ + + /** + * Inflate.msg -> String + * + * Error message, if [[Inflate.err]] != 0 + **/ + + + /** + * new Inflate(options) + * - options (Object): zlib inflate options. + * + * Creates new inflator instance with specified params. Throws exception + * on bad params. Supported options: + * + * - `windowBits` + * - `dictionary` + * + * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced) + * for more information on these. + * + * Additional options, for internal needs: + * + * - `chunkSize` - size of generated data chunks (16K by default) + * - `raw` (Boolean) - do raw inflate + * - `to` (String) - if equal to 'string', then result will be converted + * from utf8 to utf16 (javascript) string. When string output requested, + * chunk length can differ from `chunkSize`, depending on content. + * + * By default, when no options set, autodetect deflate/gzip data format via + * wrapper header. + * + * ##### Example: + * + * ```javascript + * var pako = void('pako') + * , chunk1 = Uint8Array([1,2,3,4,5,6,7,8,9]) + * , chunk2 = Uint8Array([10,11,12,13,14,15,16,17,18,19]); + * + * var inflate = new pako.Inflate({ level: 3}); + * + * inflate.push(chunk1, false); + * inflate.push(chunk2, true); // true -> last chunk + * + * if (inflate.err) { throw Error(inflate.err); } + * + * console.log(inflate.result); + * ``` + **/ + class Inflate { + constructor(options) { + this.options = { + chunkSize: 16384, + windowBits: 0, + ...(options || {}) + }; + + const opt = this.options; + + // Force window size for `raw` data, if not set directly, + // because we have no header for autodetect. + if (opt.raw && (opt.windowBits >= 0) && (opt.windowBits < 16)) { + opt.windowBits = -opt.windowBits; + if (opt.windowBits === 0) { opt.windowBits = -15; } + } + + // If `windowBits` not defined (and mode not raw) - set autodetect flag for gzip/deflate + if ((opt.windowBits >= 0) && (opt.windowBits < 16) && + !(options && options.windowBits)) { + opt.windowBits += 32; + } + + // Gzip header has no info about windows size, we can do autodetect only + // for deflate. So, if window size not set, force it to max when gzip possible + if ((opt.windowBits > 15) && (opt.windowBits < 48)) { + // bit 3 (16) -> gzipped data + // bit 4 (32) -> autodetect gzip/deflate + if ((opt.windowBits & 15) === 0) { + opt.windowBits |= 15; + } + } + + this.err = 0; // error code, if happens (0 = Z_OK) + this.msg = ''; // error message + this.ended = false; // used to avoid multiple onEnd() calls + this.chunks = []; // chunks of compressed data + + this.strm = new ZStream(); + this.strm.avail_out = 0; + + let status = inflateInit2( + this.strm, + opt.windowBits + ); + + if (status !== Z_OK) { + throw Error(msg[status]); + } + + this.header = new GZheader(); + + inflateGetHeader(this.strm, this.header); + + // Setup dictionary + if (opt.dictionary) { + // Convert data if needed + if (typeof opt.dictionary === 'string') { + opt.dictionary = string2buf(opt.dictionary); + } else if (opt.dictionary instanceof ArrayBuffer) { + opt.dictionary = new Uint8Array(opt.dictionary); + } + if (opt.raw) { //In raw mode we need to set the dictionary early + status = inflateSetDictionary(this.strm, opt.dictionary); + if (status !== Z_OK) { + throw Error(msg[status]); + } + } + } + } + /** + * Inflate#push(data[, mode]) -> Boolean + * - data (Uint8Array|Array|ArrayBuffer|String): input data + * - mode (Number|Boolean): 0..6 for corresponding Z_NO_FLUSH..Z_TREE modes. + * See constants. Skipped or `false` means Z_NO_FLUSH, `true` means Z_FINISH. + * + * Sends input data to inflate pipe, generating [[Inflate#onData]] calls with + * new output chunks. Returns `true` on success. The last data block must have + * mode Z_FINISH (or `true`). That will flush internal pending buffers and call + * [[Inflate#onEnd]]. For interim explicit flushes (without ending the stream) you + * can use mode Z_SYNC_FLUSH, keeping the decompression context. + * + * On fail call [[Inflate#onEnd]] with error code and return false. + * + * We strongly recommend to use `Uint8Array` on input for best speed (output + * format is detected automatically). Also, don't skip last param and always + * use the same type in your code (boolean or number). That will improve JS speed. + * + * For regular `Array`-s make sure all elements are [0..255]. + * + * ##### Example + * + * ```javascript + * push(chunk, false); // push one of data chunks + * ... + * push(chunk, true); // push last chunk + * ``` + **/ + push(data, mode) { + const { strm, options: { chunkSize, dictionary } } = this; + let status, _mode; + + // Flag to properly process Z_BUF_ERROR on testing inflate call + // when we check that all output data was flushed. + let allowBufError = false; + + if (this.ended) { return false; } + _mode = (mode === ~~mode) ? mode : ((mode === true) ? Z_FINISH : Z_NO_FLUSH); + + // Convert data if needed + if (typeof data === 'string') { + // Only binary strings can be decompressed on practice + strm.input = binstring2buf(data); + } else if (data instanceof ArrayBuffer) { + strm.input = new Uint8Array(data); + } else { + strm.input = data; + } + + strm.next_in = 0; + strm.avail_in = strm.input.length; + + do { + if (strm.avail_out === 0) { + strm.output = new Buf8(chunkSize); + strm.next_out = 0; + strm.avail_out = chunkSize; + } + + status = inflate(strm, Z_NO_FLUSH); /* no bad return value */ + + if (status === Z_NEED_DICT && dictionary) { + status = inflateSetDictionary(this.strm, dictionary); + } + + if (status === Z_BUF_ERROR && allowBufError === true) { + status = Z_OK; + allowBufError = false; + } + + if (status !== Z_STREAM_END && status !== Z_OK) { + this.onEnd(status); + this.ended = true; + return false; + } + + if (strm.next_out) { + if (strm.avail_out === 0 || status === Z_STREAM_END || (strm.avail_in === 0 && (_mode === Z_FINISH || _mode === Z_SYNC_FLUSH))) { + this.onData(shrinkBuf(strm.output, strm.next_out)); + } + } + + // When no more input data, we should check that internal inflate buffers + // are flushed. The only way to do it when avail_out = 0 - run one more + // inflate pass. But if output data not exists, inflate return Z_BUF_ERROR. + // Here we set flag to process this error properly. + // + // NOTE. Deflate does not return error in this case and does not needs such + // logic. + if (strm.avail_in === 0 && strm.avail_out === 0) { + allowBufError = true; + } + + } while ((strm.avail_in > 0 || strm.avail_out === 0) && status !== Z_STREAM_END); + + if (status === Z_STREAM_END) { + _mode = Z_FINISH; + } + + // Finalize on the last chunk. + if (_mode === Z_FINISH) { + status = inflateEnd(this.strm); + this.onEnd(status); + this.ended = true; + return status === Z_OK; + } + + // callback interim results if Z_SYNC_FLUSH. + if (_mode === Z_SYNC_FLUSH) { + this.onEnd(Z_OK); + strm.avail_out = 0; + return true; + } + + return true; + }; + + /** + * Inflate#onData(chunk) -> Void + * - chunk (Uint8Array|Array|String): output data. Type of array depends + * on js engine support. When string output requested, each chunk + * will be string. + * + * By default, stores data blocks in `chunks[]` property and glue + * those in `onEnd`. Override this handler, if you need another behaviour. + **/ + onData(chunk) { + this.chunks.push(chunk); + }; + + + + /** + * Inflate#onEnd(status) -> Void + * - status (Number): inflate status. 0 (Z_OK) on success, + * other if not. + * + * Called either after you tell inflate that the input stream is + * complete (Z_FINISH) or should be flushed (Z_SYNC_FLUSH) + * or if an error happened. By default - join collected chunks, + * free memory and fill `results` / `err` properties. + **/ + onEnd(status) { + // On success - join + if (status === Z_OK) { + this.result = flattenChunks(this.chunks); + } + this.chunks = []; + this.err = status; + this.msg = this.strm.msg; + }; + } + + /* + node-bzip - a pure-javascript Node.JS module for decoding bzip2 data + + Copyright (C) 2012 Eli Skeggs + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see + http://www.gnu.org/licenses/lgpl-2.1.html + + Adapted from bzip2.js, copyright 2011 antimatter15 (antimatter15@gmail.com). + + Based on micro-bunzip by Rob Landley (rob@landley.net). + + Based on bzip2 decompression code by Julian R Seward (jseward@acm.org), + which also acknowledges contributions by Mike Burrows, David Wheeler, + Peter Fenwick, Alistair Moffat, Radford Neal, Ian H. Witten, + Robert Sedgewick, and Jon L. Bentley. + */ + + var BITMASK = [0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF]; + + // offset in bytes + var BitReader = function(stream) { + this.stream = stream; + this.bitOffset = 0; + this.curByte = 0; + this.hasByte = false; + }; + + BitReader.prototype._ensureByte = function() { + if (!this.hasByte) { + this.curByte = this.stream.readByte(); + this.hasByte = true; + } + }; + + // reads bits from the buffer + BitReader.prototype.read = function(bits) { + var result = 0; + while (bits > 0) { + this._ensureByte(); + var remaining = 8 - this.bitOffset; + // if we're in a byte + if (bits >= remaining) { + result <<= remaining; + result |= BITMASK[remaining] & this.curByte; + this.hasByte = false; + this.bitOffset = 0; + bits -= remaining; + } else { + result <<= bits; + var shift = remaining - bits; + result |= (this.curByte & (BITMASK[bits] << shift)) >> shift; + this.bitOffset += bits; + bits = 0; + } + } + return result; + }; + + // seek to an arbitrary point in the buffer (expressed in bits) + BitReader.prototype.seek = function(pos) { + var n_bit = pos % 8; + var n_byte = (pos - n_bit) / 8; + this.bitOffset = n_bit; + this.stream.seek(n_byte); + this.hasByte = false; + }; + + // reads 6 bytes worth of data using the read method + BitReader.prototype.pi = function() { + var buf = new Uint8Array(6), i; + for (i = 0; i < buf.length; i++) { + buf[i] = this.read(8); + } + return bufToHex(buf); + }; + + function bufToHex(buf) { + return Array.prototype.map.call(buf, x => ('00' + x.toString(16)).slice(-2)).join(''); + } + + var bitreader = BitReader; + + /* very simple input/output stream interface */ + var Stream = function() { + }; + + // input streams ////////////// + /** Returns the next byte, or -1 for EOF. */ + Stream.prototype.readByte = function() { + throw Error("abstract method readByte() not implemented"); + }; + /** Attempts to fill the buffer; returns number of bytes read, or + * -1 for EOF. */ + Stream.prototype.read = function(buffer, bufOffset, length) { + var bytesRead = 0; + while (bytesRead < length) { + var c = this.readByte(); + if (c < 0) { // EOF + return (bytesRead===0) ? -1 : bytesRead; + } + buffer[bufOffset++] = c; + bytesRead++; + } + return bytesRead; + }; + Stream.prototype.seek = function(new_pos) { + throw Error("abstract method seek() not implemented"); + }; + + // output streams /////////// + Stream.prototype.writeByte = function(_byte) { + throw Error("abstract method readByte() not implemented"); + }; + Stream.prototype.write = function(buffer, bufOffset, length) { + var i; + for (i=0; i>> 0; // return an unsigned value + }; + + /** + * Update the CRC with a single byte + * @param value The value to update the CRC with + */ + this.updateCRC = function(value) { + crc = (crc << 8) ^ crc32Lookup[((crc >>> 24) ^ value) & 0xff]; + }; + + /** + * Update the CRC with a sequence of identical bytes + * @param value The value to update the CRC with + * @param count The number of bytes + */ + this.updateCRCRun = function(value, count) { + while (count-- > 0) { + crc = (crc << 8) ^ crc32Lookup[((crc >>> 24) ^ value) & 0xff]; + } + }; + }; + return CRC32; + })(); + + /* + seek-bzip - a pure-javascript module for seeking within bzip2 data + + Copyright (C) 2013 C. Scott Ananian + Copyright (C) 2012 Eli Skeggs + Copyright (C) 2011 Kevin Kwok + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see + http://www.gnu.org/licenses/lgpl-2.1.html + + Adapted from node-bzip, copyright 2012 Eli Skeggs. + Adapted from bzip2.js, copyright 2011 Kevin Kwok (antimatter15@gmail.com). + + Based on micro-bunzip by Rob Landley (rob@landley.net). + + Based on bzip2 decompression code by Julian R Seward (jseward@acm.org), + which also acknowledges contributions by Mike Burrows, David Wheeler, + Peter Fenwick, Alistair Moffat, Radford Neal, Ian H. Witten, + Robert Sedgewick, and Jon L. Bentley. + */ + + + + + + var MAX_HUFCODE_BITS = 20; + var MAX_SYMBOLS = 258; + var SYMBOL_RUNA = 0; + var SYMBOL_RUNB = 1; + var MIN_GROUPS = 2; + var MAX_GROUPS = 6; + var GROUP_SIZE = 50; + + var WHOLEPI = "314159265359"; + var SQRTPI = "177245385090"; + + var mtf = function(array, index) { + var src = array[index], i; + for (i = index; i > 0; i--) { + array[i] = array[i-1]; + } + array[0] = src; + return src; + }; + + var Err = { + OK: 0, + LAST_BLOCK: -1, + NOT_BZIP_DATA: -2, + UNEXPECTED_INPUT_EOF: -3, + UNEXPECTED_OUTPUT_EOF: -4, + DATA_ERROR: -5, + OUT_OF_MEMORY: -6, + OBSOLETE_INPUT: -7, + END_OF_BLOCK: -8 + }; + var ErrorMessages = {}; + ErrorMessages[Err.LAST_BLOCK] = "Bad file checksum"; + ErrorMessages[Err.NOT_BZIP_DATA] = "Not bzip data"; + ErrorMessages[Err.UNEXPECTED_INPUT_EOF] = "Unexpected input EOF"; + ErrorMessages[Err.UNEXPECTED_OUTPUT_EOF] = "Unexpected output EOF"; + ErrorMessages[Err.DATA_ERROR] = "Data error"; + ErrorMessages[Err.OUT_OF_MEMORY] = "Out of memory"; + ErrorMessages[Err.OBSOLETE_INPUT] = "Obsolete (pre 0.9.5) bzip format not supported."; + + var _throw = function(status, optDetail) { + var msg = ErrorMessages[status] || 'unknown error'; + if (optDetail) { msg += ': '+optDetail; } + var e = new TypeError(msg); + e.errorCode = status; + throw e; + }; + + var Bunzip = function(inputStream, outputStream) { + this.writePos = this.writeCurrent = this.writeCount = 0; + + this._start_bunzip(inputStream, outputStream); + }; + Bunzip.prototype._init_block = function() { + var moreBlocks = this._get_next_block(); + if ( !moreBlocks ) { + this.writeCount = -1; + return false; /* no more blocks */ + } + this.blockCRC = new crc32(); + return true; + }; + /* XXX micro-bunzip uses (inputStream, inputBuffer, len) as arguments */ + Bunzip.prototype._start_bunzip = function(inputStream, outputStream) { + /* Ensure that file starts with "BZh['1'-'9']." */ + var buf = new Uint8Array(4); + if (inputStream.read(buf, 0, 4) !== 4 || + String.fromCharCode(buf[0], buf[1], buf[2]) !== 'BZh') + _throw(Err.NOT_BZIP_DATA, 'bad magic'); + + var level = buf[3] - 0x30; + if (level < 1 || level > 9) + _throw(Err.NOT_BZIP_DATA, 'level out of range'); + + this.reader = new bitreader(inputStream); + + /* Fourth byte (ascii '1'-'9'), indicates block size in units of 100k of + uncompressed data. Allocate intermediate buffer for block. */ + this.dbufSize = 100000 * level; + this.nextoutput = 0; + this.outputStream = outputStream; + this.streamCRC = 0; + }; + Bunzip.prototype._get_next_block = function() { + var i, j, k; + var reader = this.reader; + // this is get_next_block() function from micro-bunzip: + /* Read in header signature and CRC, then validate signature. + (last block signature means CRC is for whole file, return now) */ + var h = reader.pi(); + if (h === SQRTPI) { // last block + return false; /* no more blocks */ + } + if (h !== WHOLEPI) + _throw(Err.NOT_BZIP_DATA); + this.targetBlockCRC = reader.read(32) >>> 0; // (convert to unsigned) + this.streamCRC = (this.targetBlockCRC ^ + ((this.streamCRC << 1) | (this.streamCRC>>>31))) >>> 0; + /* We can add support for blockRandomised if anybody complains. There was + some code for this in busybox 1.0.0-pre3, but nobody ever noticed that + it didn't actually work. */ + if (reader.read(1)) + _throw(Err.OBSOLETE_INPUT); + var origPointer = reader.read(24); + if (origPointer > this.dbufSize) + _throw(Err.DATA_ERROR, 'initial position out of bounds'); + /* mapping table: if some byte values are never used (encoding things + like ascii text), the compression code removes the gaps to have fewer + symbols to deal with, and writes a sparse bitfield indicating which + values were present. We make a translation table to convert the symbols + back to the corresponding bytes. */ + var t = reader.read(16); + var symToByte = new Uint8Array(256), symTotal = 0; + for (i = 0; i < 16; i++) { + if (t & (1 << (0xF - i))) { + var o = i * 16; + k = reader.read(16); + for (j = 0; j < 16; j++) + if (k & (1 << (0xF - j))) + symToByte[symTotal++] = o + j; + } + } + + /* How many different huffman coding groups does this block use? */ + var groupCount = reader.read(3); + if (groupCount < MIN_GROUPS || groupCount > MAX_GROUPS) + _throw(Err.DATA_ERROR); + /* nSelectors: Every GROUP_SIZE many symbols we select a new huffman coding + group. Read in the group selector list, which is stored as MTF encoded + bit runs. (MTF=Move To Front, as each value is used it's moved to the + start of the list.) */ + var nSelectors = reader.read(15); + if (nSelectors === 0) + _throw(Err.DATA_ERROR); + + var mtfSymbol = new Uint8Array(256); + for (i = 0; i < groupCount; i++) + mtfSymbol[i] = i; + + var selectors = new Uint8Array(nSelectors); // was 32768... + + for (i = 0; i < nSelectors; i++) { + /* Get next value */ + for (j = 0; reader.read(1); j++) + if (j >= groupCount) _throw(Err.DATA_ERROR); + /* Decode MTF to get the next selector */ + selectors[i] = mtf(mtfSymbol, j); + } + + /* Read the huffman coding tables for each group, which code for symTotal + literal symbols, plus two run symbols (RUNA, RUNB) */ + var symCount = symTotal + 2; + var groups = [], hufGroup; + for (j = 0; j < groupCount; j++) { + var length = new Uint8Array(symCount), temp = new Uint16Array(MAX_HUFCODE_BITS + 1); + /* Read huffman code lengths for each symbol. They're stored in + a way similar to mtf; record a starting value for the first symbol, + and an offset from the previous value for everys symbol after that. */ + t = reader.read(5); // lengths + for (i = 0; i < symCount; i++) { + for (;;) { + if (t < 1 || t > MAX_HUFCODE_BITS) _throw(Err.DATA_ERROR); + /* If first bit is 0, stop. Else second bit indicates whether + to increment or decrement the value. */ + if(!reader.read(1)) + break; + if(!reader.read(1)) + t++; + else + t--; + } + length[i] = t; + } + + /* Find largest and smallest lengths in this group */ + var minLen, maxLen; + minLen = maxLen = length[0]; + for (i = 1; i < symCount; i++) { + if (length[i] > maxLen) + maxLen = length[i]; + else if (length[i] < minLen) + minLen = length[i]; + } + + /* Calculate permute[], base[], and limit[] tables from length[]. + * + * permute[] is the lookup table for converting huffman coded symbols + * into decoded symbols. base[] is the amount to subtract from the + * value of a huffman symbol of a given length when using permute[]. + * + * limit[] indicates the largest numerical value a symbol with a given + * number of bits can have. This is how the huffman codes can vary in + * length: each code with a value>limit[length] needs another bit. + */ + hufGroup = {}; + groups.push(hufGroup); + hufGroup.permute = new Uint16Array(MAX_SYMBOLS); + hufGroup.limit = new Uint32Array(MAX_HUFCODE_BITS + 2); + hufGroup.base = new Uint32Array(MAX_HUFCODE_BITS + 1); + hufGroup.minLen = minLen; + hufGroup.maxLen = maxLen; + /* Calculate permute[]. Concurently, initialize temp[] and limit[]. */ + var pp = 0; + for (i = minLen; i <= maxLen; i++) { + temp[i] = hufGroup.limit[i] = 0; + for (t = 0; t < symCount; t++) + if (length[t] === i) + hufGroup.permute[pp++] = t; + } + /* Count symbols coded for at each bit length */ + for (i = 0; i < symCount; i++) + temp[length[i]]++; + /* Calculate limit[] (the largest symbol-coding value at each bit + * length, which is (previous limit<<1)+symbols at this level), and + * base[] (number of symbols to ignore at each bit length, which is + * limit minus the cumulative count of symbols coded for already). */ + pp = t = 0; + for (i = minLen; i < maxLen; i++) { + pp += temp[i]; + /* We read the largest possible symbol size and then unget bits + after determining how many we need, and those extra bits could + be set to anything. (They're noise from future symbols.) At + each level we're really only interested in the first few bits, + so here we set all the trailing to-be-ignored bits to 1 so they + don't affect the value>limit[length] comparison. */ + hufGroup.limit[i] = pp - 1; + pp <<= 1; + t += temp[i]; + hufGroup.base[i + 1] = pp - t; + } + hufGroup.limit[maxLen + 1] = Number.MAX_VALUE; /* Sentinal value for reading next sym. */ + hufGroup.limit[maxLen] = pp + temp[maxLen] - 1; + hufGroup.base[minLen] = 0; + } + /* We've finished reading and digesting the block header. Now read this + block's huffman coded symbols from the file and undo the huffman coding + and run length encoding, saving the result into dbuf[dbufCount++]=uc */ + + /* Initialize symbol occurrence counters and symbol Move To Front table */ + var byteCount = new Uint32Array(256); + for (i = 0; i < 256; i++) + mtfSymbol[i] = i; + /* Loop through compressed symbols. */ + var runPos = 0, dbufCount = 0, selector = 0, uc; + var dbuf = this.dbuf = new Uint32Array(this.dbufSize); + symCount = 0; + for (;;) { + /* Determine which huffman coding group to use. */ + if (!(symCount--)) { + symCount = GROUP_SIZE - 1; + if (selector >= nSelectors) { _throw(Err.DATA_ERROR); } + hufGroup = groups[selectors[selector++]]; + } + /* Read next huffman-coded symbol. */ + i = hufGroup.minLen; + j = reader.read(i); + for (;;i++) { + if (i > hufGroup.maxLen) { _throw(Err.DATA_ERROR); } + if (j <= hufGroup.limit[i]) + break; + j = (j << 1) | reader.read(1); + } + /* Huffman decode value to get nextSym (with bounds checking) */ + j -= hufGroup.base[i]; + if (j < 0 || j >= MAX_SYMBOLS) { _throw(Err.DATA_ERROR); } + var nextSym = hufGroup.permute[j]; + /* We have now decoded the symbol, which indicates either a new literal + byte, or a repeated run of the most recent literal byte. First, + check if nextSym indicates a repeated run, and if so loop collecting + how many times to repeat the last literal. */ + if (nextSym === SYMBOL_RUNA || nextSym === SYMBOL_RUNB) { + /* If this is the start of a new run, zero out counter */ + if (!runPos){ + runPos = 1; + t = 0; + } + /* Neat trick that saves 1 symbol: instead of or-ing 0 or 1 at + each bit position, add 1 or 2 instead. For example, + 1011 is 1<<0 + 1<<1 + 2<<2. 1010 is 2<<0 + 2<<1 + 1<<2. + You can make any bit pattern that way using 1 less symbol than + the basic or 0/1 method (except all bits 0, which would use no + symbols, but a run of length 0 doesn't mean anything in this + context). Thus space is saved. */ + if (nextSym === SYMBOL_RUNA) + t += runPos; + else + t += 2 * runPos; + runPos <<= 1; + continue; + } + /* When we hit the first non-run symbol after a run, we now know + how many times to repeat the last literal, so append that many + copies to our buffer of decoded symbols (dbuf) now. (The last + literal used is the one at the head of the mtfSymbol array.) */ + if (runPos){ + runPos = 0; + if (dbufCount + t > this.dbufSize) { _throw(Err.DATA_ERROR); } + uc = symToByte[mtfSymbol[0]]; + byteCount[uc] += t; + while (t--) + dbuf[dbufCount++] = uc; + } + /* Is this the terminating symbol? */ + if (nextSym > symTotal) + break; + /* At this point, nextSym indicates a new literal character. Subtract + one to get the position in the MTF array at which this literal is + currently to be found. (Note that the result can't be -1 or 0, + because 0 and 1 are RUNA and RUNB. But another instance of the + first symbol in the mtf array, position 0, would have been handled + as part of a run above. Therefore 1 unused mtf position minus + 2 non-literal nextSym values equals -1.) */ + if (dbufCount >= this.dbufSize) { _throw(Err.DATA_ERROR); } + i = nextSym - 1; + uc = mtf(mtfSymbol, i); + uc = symToByte[uc]; + /* We have our literal byte. Save it into dbuf. */ + byteCount[uc]++; + dbuf[dbufCount++] = uc; + } + /* At this point, we've read all the huffman-coded symbols (and repeated + runs) for this block from the input stream, and decoded them into the + intermediate buffer. There are dbufCount many decoded bytes in dbuf[]. + Now undo the Burrows-Wheeler transform on dbuf. + See http://dogma.net/markn/articles/bwt/bwt.htm + */ + if (origPointer < 0 || origPointer >= dbufCount) { _throw(Err.DATA_ERROR); } + /* Turn byteCount into cumulative occurrence counts of 0 to n-1. */ + j = 0; + for (i = 0; i < 256; i++) { + k = j + byteCount[i]; + byteCount[i] = j; + j = k; + } + /* Figure out what order dbuf would be in if we sorted it. */ + for (i = 0; i < dbufCount; i++) { + uc = dbuf[i] & 0xff; + dbuf[byteCount[uc]] |= (i << 8); + byteCount[uc]++; + } + /* Decode first byte by hand to initialize "previous" byte. Note that it + doesn't get output, and if the first three characters are identical + it doesn't qualify as a run (hence writeRunCountdown=5). */ + var pos = 0, current = 0, run = 0; + if (dbufCount) { + pos = dbuf[origPointer]; + current = (pos & 0xff); + pos >>= 8; + run = -1; + } + this.writePos = pos; + this.writeCurrent = current; + this.writeCount = dbufCount; + this.writeRun = run; + + return true; /* more blocks to come */ + }; + /* Undo burrows-wheeler transform on intermediate buffer to produce output. + If start_bunzip was initialized with out_fd=-1, then up to len bytes of + data are written to outbuf. Return value is number of bytes written or + error (all errors are negative numbers). If out_fd!=-1, outbuf and len + are ignored, data is written to out_fd and return is RETVAL_OK or error. + */ + Bunzip.prototype._read_bunzip = function(outputBuffer, len) { + var copies, previous, outbyte; + /* james@jamestaylor.org: writeCount goes to -1 when the buffer is fully + decoded, which results in this returning RETVAL_LAST_BLOCK, also + equal to -1... Confusing, I'm returning 0 here to indicate no + bytes written into the buffer */ + if (this.writeCount < 0) { return 0; } + var dbuf = this.dbuf, pos = this.writePos, current = this.writeCurrent; + var dbufCount = this.writeCount; this.outputsize; + var run = this.writeRun; + + while (dbufCount) { + dbufCount--; + previous = current; + pos = dbuf[pos]; + current = pos & 0xff; + pos >>= 8; + if (run++ === 3){ + copies = current; + outbyte = previous; + current = -1; + } else { + copies = 1; + outbyte = current; + } + this.blockCRC.updateCRCRun(outbyte, copies); + while (copies--) { + this.outputStream.writeByte(outbyte); + this.nextoutput++; + } + if (current != previous) + run = 0; + } + this.writeCount = dbufCount; + // check CRC + if (this.blockCRC.getCRC() !== this.targetBlockCRC) { + _throw(Err.DATA_ERROR, "Bad block CRC "+ + "(got "+this.blockCRC.getCRC().toString(16)+ + " expected "+this.targetBlockCRC.toString(16)+")"); + } + return this.nextoutput; + }; + + var coerceInputStream = function(input) { + if ('readByte' in input) { return input; } + var inputStream = new stream(); + inputStream.pos = 0; + inputStream.readByte = function() { return input[this.pos++]; }; + inputStream.seek = function(pos) { this.pos = pos; }; + inputStream.eof = function() { return this.pos >= input.length; }; + return inputStream; + }; + var coerceOutputStream = function(output) { + var outputStream = new stream(); + var resizeOk = true; + if (output) { + if (typeof(output)==='number') { + outputStream.buffer = new Uint8Array(output); + resizeOk = false; + } else if ('writeByte' in output) { + return output; + } else { + outputStream.buffer = output; + resizeOk = false; + } + } else { + outputStream.buffer = new Uint8Array(16384); + } + outputStream.pos = 0; + outputStream.writeByte = function(_byte) { + if (resizeOk && this.pos >= this.buffer.length) { + var newBuffer = new Uint8Array(this.buffer.length*2); + newBuffer.set(this.buffer); + this.buffer = newBuffer; + } + this.buffer[this.pos++] = _byte; + }; + outputStream.getBuffer = function() { + // trim buffer + if (this.pos !== this.buffer.length) { + if (!resizeOk) + throw new TypeError('outputsize does not match decoded input'); + var newBuffer = new Uint8Array(this.pos); + newBuffer.set(this.buffer.subarray(0, this.pos)); + this.buffer = newBuffer; + } + return this.buffer; + }; + outputStream._coerced = true; + return outputStream; + }; + + /* Static helper functions */ + // 'input' can be a stream or a buffer + // 'output' can be a stream or a buffer or a number (buffer size) + const decode = function(input, output, multistream) { + // make a stream from a buffer, if necessary + var inputStream = coerceInputStream(input); + var outputStream = coerceOutputStream(output); + + var bz = new Bunzip(inputStream, outputStream); + while (true) { + if ('eof' in inputStream && inputStream.eof()) break; + if (bz._init_block()) { + bz._read_bunzip(); + } else { + var targetStreamCRC = bz.reader.read(32) >>> 0; // (convert to unsigned) + if (targetStreamCRC !== bz.streamCRC) { + _throw(Err.DATA_ERROR, "Bad stream CRC "+ + "(got "+bz.streamCRC.toString(16)+ + " expected "+targetStreamCRC.toString(16)+")"); + } + if (multistream && + 'eof' in inputStream && + !inputStream.eof()) { + // note that start_bunzip will also resync the bit reader to next byte + bz._start_bunzip(inputStream, outputStream); + } else break; + } + } + if ('getBuffer' in outputStream) + return outputStream.getBuffer(); + }; + const decodeBlock = function(input, pos, output) { + // make a stream from a buffer, if necessary + var inputStream = coerceInputStream(input); + var outputStream = coerceOutputStream(output); + var bz = new Bunzip(inputStream, outputStream); + bz.reader.seek(pos); + /* Fill the decode buffer for the block */ + var moreBlocks = bz._get_next_block(); + if (moreBlocks) { + /* Init the CRC for writing */ + bz.blockCRC = new crc32(); + + /* Zero this so the current byte from before the seek is not written */ + bz.writeCopies = 0; + + /* Decompress the block and write to stdout */ + bz._read_bunzip(); + // XXX keep writing? + } + if ('getBuffer' in outputStream) + return outputStream.getBuffer(); + }; + /* Reads bzip2 file from stream or buffer `input`, and invoke + * `callback(position, size)` once for each bzip2 block, + * where position gives the starting position (in *bits*) + * and size gives uncompressed size of the block (in *bytes*). */ + const table = function(input, callback, multistream) { + // make a stream from a buffer, if necessary + var inputStream = new stream(); + inputStream.delegate = coerceInputStream(input); + inputStream.pos = 0; + inputStream.readByte = function() { + this.pos++; + return this.delegate.readByte(); + }; + if (inputStream.delegate.eof) { + inputStream.eof = inputStream.delegate.eof.bind(inputStream.delegate); + } + var outputStream = new stream(); + outputStream.pos = 0; + outputStream.writeByte = function() { this.pos++; }; + + var bz = new Bunzip(inputStream, outputStream); + var blockSize = bz.dbufSize; + while (true) { + if ('eof' in inputStream && inputStream.eof()) break; + + var position = inputStream.pos*8 + bz.reader.bitOffset; + if (bz.reader.hasByte) { position -= 8; } + + if (bz._init_block()) { + var start = outputStream.pos; + bz._read_bunzip(); + callback(position, outputStream.pos - start); + } else { + bz.reader.read(32); // (but we ignore the crc) + if (multistream && + 'eof' in inputStream && + !inputStream.eof()) { + // note that start_bunzip will also resync the bit reader to next byte + bz._start_bunzip(inputStream, outputStream); + console.assert(bz.dbufSize === blockSize, + "shouldn't change block size within multistream file"); + } else break; + } + } + }; + + var lib = { + Bunzip, + Stream: stream, + Err, + decode, + decodeBlock, + table + }; + var lib_4 = lib.decode; + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * Implementation of the Literal Data Packet (Tag 11) + * + * {@link https://tools.ietf.org/html/rfc4880#section-5.9|RFC4880 5.9}: + * A Literal Data packet contains the body of a message; data that is not to be + * further interpreted. + */ + class LiteralDataPacket { + static get tag() { + return enums.packet.literalData; + } + + /** + * @param {Date} date - The creation date of the literal package + */ + constructor(date = new Date()) { + this.format = enums.literal.utf8; // default format for literal data packets + this.date = util.normalizeDate(date); + this.text = null; // textual data representation + this.data = null; // literal data representation + this.filename = ''; + } + + /** + * Set the packet data to a javascript native string, end of line + * will be normalized to \r\n and by default text is converted to UTF8 + * @param {String | ReadableStream} text - Any native javascript string + * @param {enums.literal} [format] - The format of the string of bytes + */ + setText(text, format = enums.literal.utf8) { + this.format = format; + this.text = text; + this.data = null; + } + + /** + * Returns literal data packets as native JavaScript string + * with normalized end of line to \n + * @param {Boolean} [clone] - Whether to return a clone so that getBytes/getText can be called again + * @returns {String | ReadableStream} Literal data as text. + */ + getText(clone = false) { + if (this.text === null || util.isStream(this.text)) { // Assume that this.text has been read + this.text = util.decodeUTF8(util.nativeEOL(this.getBytes(clone))); + } + return this.text; + } + + /** + * Set the packet data to value represented by the provided string of bytes. + * @param {Uint8Array | ReadableStream} bytes - The string of bytes + * @param {enums.literal} format - The format of the string of bytes + */ + setBytes(bytes, format) { + this.format = format; + this.data = bytes; + this.text = null; + } + + + /** + * Get the byte sequence representing the literal packet data + * @param {Boolean} [clone] - Whether to return a clone so that getBytes/getText can be called again + * @returns {Uint8Array | ReadableStream} A sequence of bytes. + */ + getBytes(clone = false) { + if (this.data === null) { + // encode UTF8 and normalize EOL to \r\n + this.data = util.canonicalizeEOL(util.encodeUTF8(this.text)); + } + if (clone) { + return passiveClone(this.data); + } + return this.data; + } + + + /** + * Sets the filename of the literal packet data + * @param {String} filename - Any native javascript string + */ + setFilename(filename) { + this.filename = filename; + } + + + /** + * Get the filename of the literal packet data + * @returns {String} Filename. + */ + getFilename() { + return this.filename; + } + + /** + * Parsing function for a literal data packet (tag 11). + * + * @param {Uint8Array | ReadableStream} input - Payload of a tag 11 packet + * @returns {Promise} Object representation. + * @async + */ + async read(bytes) { + await parse(bytes, async reader => { + // - A one-octet field that describes how the data is formatted. + const format = await reader.readByte(); // enums.literal + + const filename_len = await reader.readByte(); + this.filename = util.decodeUTF8(await reader.readBytes(filename_len)); + + this.date = util.readDate(await reader.readBytes(4)); + + let data = reader.remainder(); + if (isArrayStream(data)) data = await readToEnd(data); + this.setBytes(data, format); + }); + } + + /** + * Creates a Uint8Array representation of the packet, excluding the data + * + * @returns {Uint8Array} Uint8Array representation of the packet. + */ + writeHeader() { + const filename = util.encodeUTF8(this.filename); + const filename_length = new Uint8Array([filename.length]); + + const format = new Uint8Array([this.format]); + const date = util.writeDate(this.date); + + return util.concatUint8Array([format, filename_length, filename, date]); + } + + /** + * Creates a Uint8Array representation of the packet + * + * @returns {Uint8Array | ReadableStream} Uint8Array representation of the packet. + */ + write() { + const header = this.writeHeader(); + const data = this.getBytes(); + + return util.concat([header, data]); + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + // Symbol to store cryptographic validity of the signature, to avoid recomputing multiple times on verification. + const verified = Symbol('verified'); + + // GPG puts the Issuer and Signature subpackets in the unhashed area. + // Tampering with those invalidates the signature, so we still trust them and parse them. + // All other unhashed subpackets are ignored. + const allowedUnhashedSubpackets = new Set([ + enums.signatureSubpacket.issuer, + enums.signatureSubpacket.issuerFingerprint, + enums.signatureSubpacket.embeddedSignature + ]); + + /** + * Implementation of the Signature Packet (Tag 2) + * + * {@link https://tools.ietf.org/html/rfc4880#section-5.2|RFC4480 5.2}: + * A Signature packet describes a binding between some public key and + * some data. The most common signatures are a signature of a file or a + * block of text, and a signature that is a certification of a User ID. + */ + class SignaturePacket { + static get tag() { + return enums.packet.signature; + } + + constructor() { + this.version = null; + /** @type {enums.signature} */ + this.signatureType = null; + /** @type {enums.hash} */ + this.hashAlgorithm = null; + /** @type {enums.publicKey} */ + this.publicKeyAlgorithm = null; + + this.signatureData = null; + this.unhashedSubpackets = []; + this.signedHashValue = null; + + this.created = null; + this.signatureExpirationTime = null; + this.signatureNeverExpires = true; + this.exportable = null; + this.trustLevel = null; + this.trustAmount = null; + this.regularExpression = null; + this.revocable = null; + this.keyExpirationTime = null; + this.keyNeverExpires = null; + this.preferredSymmetricAlgorithms = null; + this.revocationKeyClass = null; + this.revocationKeyAlgorithm = null; + this.revocationKeyFingerprint = null; + this.issuerKeyID = new KeyID(); + this.rawNotations = []; + this.notations = {}; + this.preferredHashAlgorithms = null; + this.preferredCompressionAlgorithms = null; + this.keyServerPreferences = null; + this.preferredKeyServer = null; + this.isPrimaryUserID = null; + this.policyURI = null; + this.keyFlags = null; + this.signersUserID = null; + this.reasonForRevocationFlag = null; + this.reasonForRevocationString = null; + this.features = null; + this.signatureTargetPublicKeyAlgorithm = null; + this.signatureTargetHashAlgorithm = null; + this.signatureTargetHash = null; + this.embeddedSignature = null; + this.issuerKeyVersion = null; + this.issuerFingerprint = null; + this.preferredAEADAlgorithms = null; + + this.revoked = null; + this[verified] = null; + } + + /** + * parsing function for a signature packet (tag 2). + * @param {String} bytes - Payload of a tag 2 packet + * @returns {SignaturePacket} Object representation. + */ + read(bytes) { + let i = 0; + this.version = bytes[i++]; + + if (this.version !== 4 && this.version !== 5) { + throw new UnsupportedError(`Version ${this.version} of the signature packet is unsupported.`); + } + + this.signatureType = bytes[i++]; + this.publicKeyAlgorithm = bytes[i++]; + this.hashAlgorithm = bytes[i++]; + + // hashed subpackets + i += this.readSubPackets(bytes.subarray(i, bytes.length), true); + if (!this.created) { + throw Error('Missing signature creation time subpacket.'); + } + + // A V4 signature hashes the packet body + // starting from its first field, the version number, through the end + // of the hashed subpacket data. Thus, the fields hashed are the + // signature version, the signature type, the public-key algorithm, the + // hash algorithm, the hashed subpacket length, and the hashed + // subpacket body. + this.signatureData = bytes.subarray(0, i); + + // unhashed subpackets + i += this.readSubPackets(bytes.subarray(i, bytes.length), false); + + // Two-octet field holding left 16 bits of signed hash value. + this.signedHashValue = bytes.subarray(i, i + 2); + i += 2; + + this.params = mod.signature.parseSignatureParams(this.publicKeyAlgorithm, bytes.subarray(i, bytes.length)); + } + + /** + * @returns {Uint8Array | ReadableStream} + */ + writeParams() { + if (this.params instanceof Promise) { + return fromAsync( + async () => mod.serializeParams(this.publicKeyAlgorithm, await this.params) + ); + } + return mod.serializeParams(this.publicKeyAlgorithm, this.params); + } + + write() { + const arr = []; + arr.push(this.signatureData); + arr.push(this.writeUnhashedSubPackets()); + arr.push(this.signedHashValue); + arr.push(this.writeParams()); + return util.concat(arr); + } + + /** + * Signs provided data. This needs to be done prior to serialization. + * @param {SecretKeyPacket} key - Private key used to sign the message. + * @param {Object} data - Contains packets to be signed. + * @param {Date} [date] - The signature creation time. + * @param {Boolean} [detached] - Whether to create a detached signature + * @throws {Error} if signing failed + * @async + */ + async sign(key, data, date = new Date(), detached = false) { + if (key.version === 5) { + this.version = 5; + } else { + this.version = 4; + } + const arr = [new Uint8Array([this.version, this.signatureType, this.publicKeyAlgorithm, this.hashAlgorithm])]; + + this.created = util.normalizeDate(date); + this.issuerKeyVersion = key.version; + this.issuerFingerprint = key.getFingerprintBytes(); + this.issuerKeyID = key.getKeyID(); + + // Add hashed subpackets + arr.push(this.writeHashedSubPackets()); + + // Remove unhashed subpackets, in case some allowed unhashed + // subpackets existed, in order not to duplicate them (in both + // the hashed and unhashed subpackets) when re-signing. + this.unhashedSubpackets = []; + + this.signatureData = util.concat(arr); + + const toHash = this.toHash(this.signatureType, data, detached); + const hash = await this.hash(this.signatureType, data, toHash, detached); + + this.signedHashValue = slice(clone(hash), 0, 2); + const signed = async () => mod.signature.sign( + this.publicKeyAlgorithm, this.hashAlgorithm, key.publicParams, key.privateParams, toHash, await readToEnd(hash) + ); + if (util.isStream(hash)) { + this.params = signed(); + } else { + this.params = await signed(); + + // Store the fact that this signature is valid, e.g. for when we call `await + // getLatestValidSignature(this.revocationSignatures, key, data)` later. + // Note that this only holds up if the key and data passed to verify are the + // same as the ones passed to sign. + this[verified] = true; + } + } + + /** + * Creates Uint8Array of bytes of all subpacket data except Issuer and Embedded Signature subpackets + * @returns {Uint8Array} Subpacket data. + */ + writeHashedSubPackets() { + const sub = enums.signatureSubpacket; + const arr = []; + let bytes; + if (this.created === null) { + throw Error('Missing signature creation time'); + } + arr.push(writeSubPacket(sub.signatureCreationTime, true, util.writeDate(this.created))); + if (this.signatureExpirationTime !== null) { + arr.push(writeSubPacket(sub.signatureExpirationTime, true, util.writeNumber(this.signatureExpirationTime, 4))); + } + if (this.exportable !== null) { + arr.push(writeSubPacket(sub.exportableCertification, true, new Uint8Array([this.exportable ? 1 : 0]))); + } + if (this.trustLevel !== null) { + bytes = new Uint8Array([this.trustLevel, this.trustAmount]); + arr.push(writeSubPacket(sub.trustSignature, true, bytes)); + } + if (this.regularExpression !== null) { + arr.push(writeSubPacket(sub.regularExpression, true, this.regularExpression)); + } + if (this.revocable !== null) { + arr.push(writeSubPacket(sub.revocable, true, new Uint8Array([this.revocable ? 1 : 0]))); + } + if (this.keyExpirationTime !== null) { + arr.push(writeSubPacket(sub.keyExpirationTime, true, util.writeNumber(this.keyExpirationTime, 4))); + } + if (this.preferredSymmetricAlgorithms !== null) { + bytes = util.stringToUint8Array(util.uint8ArrayToString(this.preferredSymmetricAlgorithms)); + arr.push(writeSubPacket(sub.preferredSymmetricAlgorithms, false, bytes)); + } + if (this.revocationKeyClass !== null) { + bytes = new Uint8Array([this.revocationKeyClass, this.revocationKeyAlgorithm]); + bytes = util.concat([bytes, this.revocationKeyFingerprint]); + arr.push(writeSubPacket(sub.revocationKey, false, bytes)); + } + if (!this.issuerKeyID.isNull() && this.issuerKeyVersion !== 5) { + // If the version of [the] key is greater than 4, this subpacket + // MUST NOT be included in the signature. + arr.push(writeSubPacket(sub.issuer, true, this.issuerKeyID.write())); + } + this.rawNotations.forEach(({ name, value, humanReadable, critical }) => { + bytes = [new Uint8Array([humanReadable ? 0x80 : 0, 0, 0, 0])]; + const encodedName = util.encodeUTF8(name); + // 2 octets of name length + bytes.push(util.writeNumber(encodedName.length, 2)); + // 2 octets of value length + bytes.push(util.writeNumber(value.length, 2)); + bytes.push(encodedName); + bytes.push(value); + bytes = util.concat(bytes); + arr.push(writeSubPacket(sub.notationData, critical, bytes)); + }); + if (this.preferredHashAlgorithms !== null) { + bytes = util.stringToUint8Array(util.uint8ArrayToString(this.preferredHashAlgorithms)); + arr.push(writeSubPacket(sub.preferredHashAlgorithms, false, bytes)); + } + if (this.preferredCompressionAlgorithms !== null) { + bytes = util.stringToUint8Array(util.uint8ArrayToString(this.preferredCompressionAlgorithms)); + arr.push(writeSubPacket(sub.preferredCompressionAlgorithms, false, bytes)); + } + if (this.keyServerPreferences !== null) { + bytes = util.stringToUint8Array(util.uint8ArrayToString(this.keyServerPreferences)); + arr.push(writeSubPacket(sub.keyServerPreferences, false, bytes)); + } + if (this.preferredKeyServer !== null) { + arr.push(writeSubPacket(sub.preferredKeyServer, false, util.encodeUTF8(this.preferredKeyServer))); + } + if (this.isPrimaryUserID !== null) { + arr.push(writeSubPacket(sub.primaryUserID, false, new Uint8Array([this.isPrimaryUserID ? 1 : 0]))); + } + if (this.policyURI !== null) { + arr.push(writeSubPacket(sub.policyURI, false, util.encodeUTF8(this.policyURI))); + } + if (this.keyFlags !== null) { + bytes = util.stringToUint8Array(util.uint8ArrayToString(this.keyFlags)); + arr.push(writeSubPacket(sub.keyFlags, true, bytes)); + } + if (this.signersUserID !== null) { + arr.push(writeSubPacket(sub.signersUserID, false, util.encodeUTF8(this.signersUserID))); + } + if (this.reasonForRevocationFlag !== null) { + bytes = util.stringToUint8Array(String.fromCharCode(this.reasonForRevocationFlag) + this.reasonForRevocationString); + arr.push(writeSubPacket(sub.reasonForRevocation, true, bytes)); + } + if (this.features !== null) { + bytes = util.stringToUint8Array(util.uint8ArrayToString(this.features)); + arr.push(writeSubPacket(sub.features, false, bytes)); + } + if (this.signatureTargetPublicKeyAlgorithm !== null) { + bytes = [new Uint8Array([this.signatureTargetPublicKeyAlgorithm, this.signatureTargetHashAlgorithm])]; + bytes.push(util.stringToUint8Array(this.signatureTargetHash)); + bytes = util.concat(bytes); + arr.push(writeSubPacket(sub.signatureTarget, true, bytes)); + } + if (this.embeddedSignature !== null) { + arr.push(writeSubPacket(sub.embeddedSignature, true, this.embeddedSignature.write())); + } + if (this.issuerFingerprint !== null) { + bytes = [new Uint8Array([this.issuerKeyVersion]), this.issuerFingerprint]; + bytes = util.concat(bytes); + arr.push(writeSubPacket(sub.issuerFingerprint, this.version === 5, bytes)); + } + if (this.preferredAEADAlgorithms !== null) { + bytes = util.stringToUint8Array(util.uint8ArrayToString(this.preferredAEADAlgorithms)); + arr.push(writeSubPacket(sub.preferredAEADAlgorithms, false, bytes)); + } + + const result = util.concat(arr); + const length = util.writeNumber(result.length, 2); + + return util.concat([length, result]); + } + + /** + * Creates an Uint8Array containing the unhashed subpackets + * @returns {Uint8Array} Subpacket data. + */ + writeUnhashedSubPackets() { + const arr = []; + this.unhashedSubpackets.forEach(data => { + arr.push(writeSimpleLength(data.length)); + arr.push(data); + }); + + const result = util.concat(arr); + const length = util.writeNumber(result.length, 2); + + return util.concat([length, result]); + } + + // V4 signature sub packets + readSubPacket(bytes, hashed = true) { + let mypos = 0; + + // The leftmost bit denotes a "critical" packet + const critical = !!(bytes[mypos] & 0x80); + const type = bytes[mypos] & 0x7F; + + if (!hashed) { + this.unhashedSubpackets.push(bytes.subarray(mypos, bytes.length)); + if (!allowedUnhashedSubpackets.has(type)) { + return; + } + } + + mypos++; + + // subpacket type + switch (type) { + case enums.signatureSubpacket.signatureCreationTime: + // Signature Creation Time + this.created = util.readDate(bytes.subarray(mypos, bytes.length)); + break; + case enums.signatureSubpacket.signatureExpirationTime: { + // Signature Expiration Time in seconds + const seconds = util.readNumber(bytes.subarray(mypos, bytes.length)); + + this.signatureNeverExpires = seconds === 0; + this.signatureExpirationTime = seconds; + + break; + } + case enums.signatureSubpacket.exportableCertification: + // Exportable Certification + this.exportable = bytes[mypos++] === 1; + break; + case enums.signatureSubpacket.trustSignature: + // Trust Signature + this.trustLevel = bytes[mypos++]; + this.trustAmount = bytes[mypos++]; + break; + case enums.signatureSubpacket.regularExpression: + // Regular Expression + this.regularExpression = bytes[mypos]; + break; + case enums.signatureSubpacket.revocable: + // Revocable + this.revocable = bytes[mypos++] === 1; + break; + case enums.signatureSubpacket.keyExpirationTime: { + // Key Expiration Time in seconds + const seconds = util.readNumber(bytes.subarray(mypos, bytes.length)); + + this.keyExpirationTime = seconds; + this.keyNeverExpires = seconds === 0; + + break; + } + case enums.signatureSubpacket.preferredSymmetricAlgorithms: + // Preferred Symmetric Algorithms + this.preferredSymmetricAlgorithms = [...bytes.subarray(mypos, bytes.length)]; + break; + case enums.signatureSubpacket.revocationKey: + // Revocation Key + // (1 octet of class, 1 octet of public-key algorithm ID, 20 + // octets of + // fingerprint) + this.revocationKeyClass = bytes[mypos++]; + this.revocationKeyAlgorithm = bytes[mypos++]; + this.revocationKeyFingerprint = bytes.subarray(mypos, mypos + 20); + break; + + case enums.signatureSubpacket.issuer: + // Issuer + this.issuerKeyID.read(bytes.subarray(mypos, bytes.length)); + break; + + case enums.signatureSubpacket.notationData: { + // Notation Data + const humanReadable = !!(bytes[mypos] & 0x80); + + // We extract key/value tuple from the byte stream. + mypos += 4; + const m = util.readNumber(bytes.subarray(mypos, mypos + 2)); + mypos += 2; + const n = util.readNumber(bytes.subarray(mypos, mypos + 2)); + mypos += 2; + + const name = util.decodeUTF8(bytes.subarray(mypos, mypos + m)); + const value = bytes.subarray(mypos + m, mypos + m + n); + + this.rawNotations.push({ name, humanReadable, value, critical }); + + if (humanReadable) { + this.notations[name] = util.decodeUTF8(value); + } + break; + } + case enums.signatureSubpacket.preferredHashAlgorithms: + // Preferred Hash Algorithms + this.preferredHashAlgorithms = [...bytes.subarray(mypos, bytes.length)]; + break; + case enums.signatureSubpacket.preferredCompressionAlgorithms: + // Preferred Compression Algorithms + this.preferredCompressionAlgorithms = [...bytes.subarray(mypos, bytes.length)]; + break; + case enums.signatureSubpacket.keyServerPreferences: + // Key Server Preferences + this.keyServerPreferences = [...bytes.subarray(mypos, bytes.length)]; + break; + case enums.signatureSubpacket.preferredKeyServer: + // Preferred Key Server + this.preferredKeyServer = util.decodeUTF8(bytes.subarray(mypos, bytes.length)); + break; + case enums.signatureSubpacket.primaryUserID: + // Primary User ID + this.isPrimaryUserID = bytes[mypos++] !== 0; + break; + case enums.signatureSubpacket.policyURI: + // Policy URI + this.policyURI = util.decodeUTF8(bytes.subarray(mypos, bytes.length)); + break; + case enums.signatureSubpacket.keyFlags: + // Key Flags + this.keyFlags = [...bytes.subarray(mypos, bytes.length)]; + break; + case enums.signatureSubpacket.signersUserID: + // Signer's User ID + this.signersUserID = util.decodeUTF8(bytes.subarray(mypos, bytes.length)); + break; + case enums.signatureSubpacket.reasonForRevocation: + // Reason for Revocation + this.reasonForRevocationFlag = bytes[mypos++]; + this.reasonForRevocationString = util.decodeUTF8(bytes.subarray(mypos, bytes.length)); + break; + case enums.signatureSubpacket.features: + // Features + this.features = [...bytes.subarray(mypos, bytes.length)]; + break; + case enums.signatureSubpacket.signatureTarget: { + // Signature Target + // (1 octet public-key algorithm, 1 octet hash algorithm, N octets hash) + this.signatureTargetPublicKeyAlgorithm = bytes[mypos++]; + this.signatureTargetHashAlgorithm = bytes[mypos++]; + + const len = mod.getHashByteLength(this.signatureTargetHashAlgorithm); + + this.signatureTargetHash = util.uint8ArrayToString(bytes.subarray(mypos, mypos + len)); + break; + } + case enums.signatureSubpacket.embeddedSignature: + // Embedded Signature + this.embeddedSignature = new SignaturePacket(); + this.embeddedSignature.read(bytes.subarray(mypos, bytes.length)); + break; + case enums.signatureSubpacket.issuerFingerprint: + // Issuer Fingerprint + this.issuerKeyVersion = bytes[mypos++]; + this.issuerFingerprint = bytes.subarray(mypos, bytes.length); + if (this.issuerKeyVersion === 5) { + this.issuerKeyID.read(this.issuerFingerprint); + } else { + this.issuerKeyID.read(this.issuerFingerprint.subarray(-8)); + } + break; + case enums.signatureSubpacket.preferredAEADAlgorithms: + // Preferred AEAD Algorithms + this.preferredAEADAlgorithms = [...bytes.subarray(mypos, bytes.length)]; + break; + default: { + const err = Error(`Unknown signature subpacket type ${type}`); + if (critical) { + throw err; + } else { + console.log(err); + } + } + } + } + + readSubPackets(bytes, trusted = true, config) { + // Two-octet scalar octet count for following subpacket data. + const subpacketLength = util.readNumber(bytes.subarray(0, 2)); + + let i = 2; + + // subpacket data set (zero or more subpackets) + while (i < 2 + subpacketLength) { + const len = readSimpleLength(bytes.subarray(i, bytes.length)); + i += len.offset; + + this.readSubPacket(bytes.subarray(i, i + len.len), trusted, config); + + i += len.len; + } + + return i; + } + + // Produces data to produce signature on + toSign(type, data) { + const t = enums.signature; + + switch (type) { + case t.binary: + if (data.text !== null) { + return util.encodeUTF8(data.getText(true)); + } + return data.getBytes(true); + + case t.text: { + const bytes = data.getBytes(true); + // normalize EOL to \r\n + return util.canonicalizeEOL(bytes); + } + case t.standalone: + return new Uint8Array(0); + + case t.certGeneric: + case t.certPersona: + case t.certCasual: + case t.certPositive: + case t.certRevocation: { + let packet; + let tag; + + if (data.userID) { + tag = 0xB4; + packet = data.userID; + } else if (data.userAttribute) { + tag = 0xD1; + packet = data.userAttribute; + } else { + throw Error('Either a userID or userAttribute packet needs to be ' + + 'supplied for certification.'); + } + + const bytes = packet.write(); + + return util.concat([this.toSign(t.key, data), + new Uint8Array([tag]), + util.writeNumber(bytes.length, 4), + bytes]); + } + case t.subkeyBinding: + case t.subkeyRevocation: + case t.keyBinding: + return util.concat([this.toSign(t.key, data), this.toSign(t.key, { + key: data.bind + })]); + + case t.key: + if (data.key === undefined) { + throw Error('Key packet is required for this signature.'); + } + return data.key.writeForHash(this.version); + + case t.keyRevocation: + return this.toSign(t.key, data); + case t.timestamp: + return new Uint8Array(0); + case t.thirdParty: + throw Error('Not implemented'); + default: + throw Error('Unknown signature type.'); + } + } + + calculateTrailer(data, detached) { + let length = 0; + return transform(clone(this.signatureData), value => { + length += value.length; + }, () => { + const arr = []; + if (this.version === 5 && (this.signatureType === enums.signature.binary || this.signatureType === enums.signature.text)) { + if (detached) { + arr.push(new Uint8Array(6)); + } else { + arr.push(data.writeHeader()); + } + } + arr.push(new Uint8Array([this.version, 0xFF])); + if (this.version === 5) { + arr.push(new Uint8Array(4)); + } + arr.push(util.writeNumber(length, 4)); + // For v5, this should really be writeNumber(length, 8) rather than the + // hardcoded 4 zero bytes above + return util.concat(arr); + }); + } + + toHash(signatureType, data, detached = false) { + const bytes = this.toSign(signatureType, data); + + return util.concat([bytes, this.signatureData, this.calculateTrailer(data, detached)]); + } + + async hash(signatureType, data, toHash, detached = false) { + if (!toHash) toHash = this.toHash(signatureType, data, detached); + return mod.hash.digest(this.hashAlgorithm, toHash); + } + + /** + * verifies the signature packet. Note: not all signature types are implemented + * @param {PublicSubkeyPacket|PublicKeyPacket| + * SecretSubkeyPacket|SecretKeyPacket} key - the public key to verify the signature + * @param {module:enums.signature} signatureType - Expected signature type + * @param {Uint8Array|Object} data - Data which on the signature applies + * @param {Date} [date] - Use the given date instead of the current time to check for signature validity and expiration + * @param {Boolean} [detached] - Whether to verify a detached signature + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @throws {Error} if signature validation failed + * @async + */ + async verify(key, signatureType, data, date = new Date(), detached = false, config$1 = config) { + if (!this.issuerKeyID.equals(key.getKeyID())) { + throw Error('Signature was not issued by the given public key'); + } + if (this.publicKeyAlgorithm !== key.algorithm) { + throw Error('Public key algorithm used to sign signature does not match issuer key algorithm.'); + } + + const isMessageSignature = signatureType === enums.signature.binary || signatureType === enums.signature.text; + // Cryptographic validity is cached after one successful verification. + // However, for message signatures, we always re-verify, since the passed `data` can change + const skipVerify = this[verified] && !isMessageSignature; + if (!skipVerify) { + let toHash; + let hash; + if (this.hashed) { + hash = await this.hashed; + } else { + toHash = this.toHash(signatureType, data, detached); + hash = await this.hash(signatureType, data, toHash); + } + hash = await readToEnd(hash); + if (this.signedHashValue[0] !== hash[0] || + this.signedHashValue[1] !== hash[1]) { + throw Error('Signed digest did not match'); + } + + this.params = await this.params; + + this[verified] = await mod.signature.verify( + this.publicKeyAlgorithm, this.hashAlgorithm, this.params, key.publicParams, + toHash, hash + ); + + if (!this[verified]) { + throw Error('Signature verification failed'); + } + } + + const normDate = util.normalizeDate(date); + if (normDate && this.created > normDate) { + throw Error('Signature creation time is in the future'); + } + if (normDate && normDate >= this.getExpirationTime()) { + throw Error('Signature is expired'); + } + if (config$1.rejectHashAlgorithms.has(this.hashAlgorithm)) { + throw Error('Insecure hash algorithm: ' + enums.read(enums.hash, this.hashAlgorithm).toUpperCase()); + } + if (config$1.rejectMessageHashAlgorithms.has(this.hashAlgorithm) && + [enums.signature.binary, enums.signature.text].includes(this.signatureType)) { + throw Error('Insecure message hash algorithm: ' + enums.read(enums.hash, this.hashAlgorithm).toUpperCase()); + } + this.rawNotations.forEach(({ name, critical }) => { + if (critical && (config$1.knownNotations.indexOf(name) < 0)) { + throw Error(`Unknown critical notation: ${name}`); + } + }); + if (this.revocationKeyClass !== null) { + throw Error('This key is intended to be revoked with an authorized key, which OpenPGP.js does not support.'); + } + } + + /** + * Verifies signature expiration date + * @param {Date} [date] - Use the given date for verification instead of the current time + * @returns {Boolean} True if expired. + */ + isExpired(date = new Date()) { + const normDate = util.normalizeDate(date); + if (normDate !== null) { + return !(this.created <= normDate && normDate < this.getExpirationTime()); + } + return false; + } + + /** + * Returns the expiration time of the signature or Infinity if signature does not expire + * @returns {Date | Infinity} Expiration time. + */ + getExpirationTime() { + return this.signatureNeverExpires ? Infinity : new Date(this.created.getTime() + this.signatureExpirationTime * 1000); + } + } + + /** + * Creates a Uint8Array representation of a sub signature packet + * @see {@link https://tools.ietf.org/html/rfc4880#section-5.2.3.1|RFC4880 5.2.3.1} + * @see {@link https://tools.ietf.org/html/rfc4880#section-5.2.3.2|RFC4880 5.2.3.2} + * @param {Integer} type - Subpacket signature type. + * @param {Boolean} critical - Whether the subpacket should be critical. + * @param {String} data - Data to be included + * @returns {Uint8Array} The signature subpacket. + * @private + */ + function writeSubPacket(type, critical, data) { + const arr = []; + arr.push(writeSimpleLength(data.length + 1)); + arr.push(new Uint8Array([(critical ? 0x80 : 0) | type])); + arr.push(data); + return util.concat(arr); + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + const VERSION$3 = 3; + + /** + * Implementation of the One-Pass Signature Packets (Tag 4) + * + * {@link https://tools.ietf.org/html/rfc4880#section-5.4|RFC4880 5.4}: + * The One-Pass Signature packet precedes the signed data and contains + * enough information to allow the receiver to begin calculating any + * hashes needed to verify the signature. It allows the Signature + * packet to be placed at the end of the message, so that the signer + * can compute the entire signed message in one pass. + */ + class OnePassSignaturePacket { + static get tag() { + return enums.packet.onePassSignature; + } + + constructor() { + /** A one-octet version number. The current version is 3. */ + this.version = null; + /** + * A one-octet signature type. + * Signature types are described in + * {@link https://tools.ietf.org/html/rfc4880#section-5.2.1|RFC4880 Section 5.2.1}. + * @type {enums.signature} + + */ + this.signatureType = null; + /** + * A one-octet number describing the hash algorithm used. + * @see {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC4880 9.4} + * @type {enums.hash} + */ + this.hashAlgorithm = null; + /** + * A one-octet number describing the public-key algorithm used. + * @see {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC4880 9.1} + * @type {enums.publicKey} + */ + this.publicKeyAlgorithm = null; + /** An eight-octet number holding the Key ID of the signing key. */ + this.issuerKeyID = null; + /** + * A one-octet number holding a flag showing whether the signature is nested. + * A zero value indicates that the next packet is another One-Pass Signature packet + * that describes another signature to be applied to the same message data. + */ + this.flags = null; + } + + /** + * parsing function for a one-pass signature packet (tag 4). + * @param {Uint8Array} bytes - Payload of a tag 4 packet + * @returns {OnePassSignaturePacket} Object representation. + */ + read(bytes) { + let mypos = 0; + // A one-octet version number. The current version is 3. + this.version = bytes[mypos++]; + if (this.version !== VERSION$3) { + throw new UnsupportedError(`Version ${this.version} of the one-pass signature packet is unsupported.`); + } + + // A one-octet signature type. Signature types are described in + // Section 5.2.1. + this.signatureType = bytes[mypos++]; + + // A one-octet number describing the hash algorithm used. + this.hashAlgorithm = bytes[mypos++]; + + // A one-octet number describing the public-key algorithm used. + this.publicKeyAlgorithm = bytes[mypos++]; + + // An eight-octet number holding the Key ID of the signing key. + this.issuerKeyID = new KeyID(); + this.issuerKeyID.read(bytes.subarray(mypos, mypos + 8)); + mypos += 8; + + // A one-octet number holding a flag showing whether the signature + // is nested. A zero value indicates that the next packet is + // another One-Pass Signature packet that describes another + // signature to be applied to the same message data. + this.flags = bytes[mypos++]; + return this; + } + + /** + * creates a string representation of a one-pass signature packet + * @returns {Uint8Array} A Uint8Array representation of a one-pass signature packet. + */ + write() { + const start = new Uint8Array([VERSION$3, this.signatureType, this.hashAlgorithm, this.publicKeyAlgorithm]); + + const end = new Uint8Array([this.flags]); + + return util.concatUint8Array([start, this.issuerKeyID.write(), end]); + } + + calculateTrailer(...args) { + return fromAsync(async () => SignaturePacket.prototype.calculateTrailer.apply(await this.correspondingSig, args)); + } + + async verify() { + const correspondingSig = await this.correspondingSig; + if (!correspondingSig || correspondingSig.constructor.tag !== enums.packet.signature) { + throw Error('Corresponding signature packet missing'); + } + if ( + correspondingSig.signatureType !== this.signatureType || + correspondingSig.hashAlgorithm !== this.hashAlgorithm || + correspondingSig.publicKeyAlgorithm !== this.publicKeyAlgorithm || + !correspondingSig.issuerKeyID.equals(this.issuerKeyID) + ) { + throw Error('Corresponding signature packet does not match one-pass signature packet'); + } + correspondingSig.hashed = this.hashed; + return correspondingSig.verify.apply(correspondingSig, arguments); + } + } + + OnePassSignaturePacket.prototype.hash = SignaturePacket.prototype.hash; + OnePassSignaturePacket.prototype.toHash = SignaturePacket.prototype.toHash; + OnePassSignaturePacket.prototype.toSign = SignaturePacket.prototype.toSign; + + /** + * Instantiate a new packet given its tag + * @function newPacketFromTag + * @param {module:enums.packet} tag - Property value from {@link module:enums.packet} + * @param {Object} allowedPackets - mapping where keys are allowed packet tags, pointing to their Packet class + * @returns {Object} New packet object with type based on tag + * @throws {Error|UnsupportedError} for disallowed or unknown packets + */ + function newPacketFromTag(tag, allowedPackets) { + if (!allowedPackets[tag]) { + // distinguish between disallowed packets and unknown ones + let packetType; + try { + packetType = enums.read(enums.packet, tag); + } catch (e) { + throw new UnsupportedError(`Unknown packet type with tag: ${tag}`); + } + throw Error(`Packet not allowed in this context: ${packetType}`); + } + return new allowedPackets[tag](); + } + + /** + * This class represents a list of openpgp packets. + * Take care when iterating over it - the packets themselves + * are stored as numerical indices. + * @extends Array + */ + class PacketList extends Array { + /** + * Parses the given binary data and returns a list of packets. + * Equivalent to calling `read` on an empty PacketList instance. + * @param {Uint8Array | ReadableStream} bytes - binary data to parse + * @param {Object} allowedPackets - mapping where keys are allowed packet tags, pointing to their Packet class + * @param {Object} [config] - full configuration, defaults to openpgp.config + * @returns {PacketList} parsed list of packets + * @throws on parsing errors + * @async + */ + static async fromBinary(bytes, allowedPackets, config$1 = config) { + const packets = new PacketList(); + await packets.read(bytes, allowedPackets, config$1); + return packets; + } + + /** + * Reads a stream of binary data and interprets it as a list of packets. + * @param {Uint8Array | ReadableStream} bytes - binary data to parse + * @param {Object} allowedPackets - mapping where keys are allowed packet tags, pointing to their Packet class + * @param {Object} [config] - full configuration, defaults to openpgp.config + * @throws on parsing errors + * @async + */ + async read(bytes, allowedPackets, config$1 = config) { + if (config$1.additionalAllowedPackets.length) { + allowedPackets = { ...allowedPackets, ...util.constructAllowedPackets(config$1.additionalAllowedPackets) }; + } + this.stream = transformPair(bytes, async (readable, writable) => { + const writer = getWriter(writable); + try { + while (true) { + await writer.ready; + const done = await readPackets(readable, async parsed => { + try { + if (parsed.tag === enums.packet.marker || parsed.tag === enums.packet.trust) { + // According to the spec, these packet types should be ignored and not cause parsing errors, even if not esplicitly allowed: + // - Marker packets MUST be ignored when received: https://github.com/openpgpjs/openpgpjs/issues/1145 + // - Trust packets SHOULD be ignored outside of keyrings (unsupported): https://datatracker.ietf.org/doc/html/rfc4880#section-5.10 + return; + } + const packet = newPacketFromTag(parsed.tag, allowedPackets); + packet.packets = new PacketList(); + packet.fromStream = util.isStream(parsed.packet); + await packet.read(parsed.packet, config$1); + await writer.write(packet); + } catch (e) { + const throwUnsupportedError = !config$1.ignoreUnsupportedPackets && e instanceof UnsupportedError; + const throwMalformedError = !config$1.ignoreMalformedPackets && !(e instanceof UnsupportedError); + if (throwUnsupportedError || throwMalformedError || supportsStreaming(parsed.tag)) { + // The packets that support streaming are the ones that contain message data. + // Those are also the ones we want to be more strict about and throw on parse errors + // (since we likely cannot process the message without these packets anyway). + await writer.abort(e); + } else { + const unparsedPacket = new UnparseablePacket(parsed.tag, parsed.packet); + await writer.write(unparsedPacket); + } + console.error(e); + } + }); + if (done) { + await writer.ready; + await writer.close(); + return; + } + } + } catch (e) { + await writer.abort(e); + } + }); + + // Wait until first few packets have been read + const reader = getReader(this.stream); + while (true) { + const { done, value } = await reader.read(); + if (!done) { + this.push(value); + } else { + this.stream = null; + } + if (done || supportsStreaming(value.constructor.tag)) { + break; + } + } + reader.releaseLock(); + } + + /** + * Creates a binary representation of openpgp objects contained within the + * class instance. + * @returns {Uint8Array} A Uint8Array containing valid openpgp packets. + */ + write() { + const arr = []; + + for (let i = 0; i < this.length; i++) { + const tag = this[i] instanceof UnparseablePacket ? this[i].tag : this[i].constructor.tag; + const packetbytes = this[i].write(); + if (util.isStream(packetbytes) && supportsStreaming(this[i].constructor.tag)) { + let buffer = []; + let bufferLength = 0; + const minLength = 512; + arr.push(writeTag(tag)); + arr.push(transform(packetbytes, value => { + buffer.push(value); + bufferLength += value.length; + if (bufferLength >= minLength) { + const powerOf2 = Math.min(Math.log(bufferLength) / Math.LN2 | 0, 30); + const chunkSize = 2 ** powerOf2; + const bufferConcat = util.concat([writePartialLength(powerOf2)].concat(buffer)); + buffer = [bufferConcat.subarray(1 + chunkSize)]; + bufferLength = buffer[0].length; + return bufferConcat.subarray(0, 1 + chunkSize); + } + }, () => util.concat([writeSimpleLength(bufferLength)].concat(buffer)))); + } else { + if (util.isStream(packetbytes)) { + let length = 0; + arr.push(transform(clone(packetbytes), value => { + length += value.length; + }, () => writeHeader(tag, length))); + } else { + arr.push(writeHeader(tag, packetbytes.length)); + } + arr.push(packetbytes); + } + } + + return util.concat(arr); + } + + /** + * Creates a new PacketList with all packets matching the given tag(s) + * @param {...module:enums.packet} tags - packet tags to look for + * @returns {PacketList} + */ + filterByTag(...tags) { + const filtered = new PacketList(); + + const handle = tag => packetType => tag === packetType; + + for (let i = 0; i < this.length; i++) { + if (tags.some(handle(this[i].constructor.tag))) { + filtered.push(this[i]); + } + } + + return filtered; + } + + /** + * Traverses packet list and returns first packet with matching tag + * @param {module:enums.packet} tag - The packet tag + * @returns {Packet|undefined} + */ + findPacket(tag) { + return this.find(packet => packet.constructor.tag === tag); + } + + /** + * Find indices of packets with the given tag(s) + * @param {...module:enums.packet} tags - packet tags to look for + * @returns {Integer[]} packet indices + */ + indexOfTag(...tags) { + const tagIndex = []; + const that = this; + + const handle = tag => packetType => tag === packetType; + + for (let i = 0; i < this.length; i++) { + if (tags.some(handle(that[i].constructor.tag))) { + tagIndex.push(i); + } + } + return tagIndex; + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + // A Compressed Data packet can contain the following packet types + const allowedPackets$5 = /*#__PURE__*/ util.constructAllowedPackets([ + LiteralDataPacket, + OnePassSignaturePacket, + SignaturePacket + ]); + + /** + * Implementation of the Compressed Data Packet (Tag 8) + * + * {@link https://tools.ietf.org/html/rfc4880#section-5.6|RFC4880 5.6}: + * The Compressed Data packet contains compressed data. Typically, + * this packet is found as the contents of an encrypted packet, or following + * a Signature or One-Pass Signature packet, and contains a literal data packet. + */ + class CompressedDataPacket { + static get tag() { + return enums.packet.compressedData; + } + + /** + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + constructor(config$1 = config) { + /** + * List of packets + * @type {PacketList} + */ + this.packets = null; + /** + * Compression algorithm + * @type {enums.compression} + */ + this.algorithm = config$1.preferredCompressionAlgorithm; + + /** + * Compressed packet data + * @type {Uint8Array | ReadableStream} + */ + this.compressed = null; + + /** + * zip/zlib compression level, between 1 and 9 + */ + this.deflateLevel = config$1.deflateLevel; + } + + /** + * Parsing function for the packet. + * @param {Uint8Array | ReadableStream} bytes - Payload of a tag 8 packet + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + async read(bytes, config$1 = config) { + await parse(bytes, async reader => { + + // One octet that gives the algorithm used to compress the packet. + this.algorithm = await reader.readByte(); + + // Compressed data, which makes up the remainder of the packet. + this.compressed = reader.remainder(); + + await this.decompress(config$1); + }); + } + + + /** + * Return the compressed packet. + * @returns {Uint8Array | ReadableStream} Binary compressed packet. + */ + write() { + if (this.compressed === null) { + this.compress(); + } + + return util.concat([new Uint8Array([this.algorithm]), this.compressed]); + } + + + /** + * Decompression method for decompressing the compressed data + * read by read_packet + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + async decompress(config$1 = config) { + const compressionName = enums.read(enums.compression, this.algorithm); + const decompressionFn = decompress_fns[compressionName]; + if (!decompressionFn) { + throw Error(`${compressionName} decompression not supported`); + } + + this.packets = await PacketList.fromBinary(decompressionFn(this.compressed), allowedPackets$5, config$1); + } + + /** + * Compress the packet data (member decompressedData) + */ + compress() { + const compressionName = enums.read(enums.compression, this.algorithm); + const compressionFn = compress_fns[compressionName]; + if (!compressionFn) { + throw Error(`${compressionName} compression not supported`); + } + + this.compressed = compressionFn(this.packets.write(), this.deflateLevel); + } + } + + ////////////////////////// + // // + // Helper functions // + // // + ////////////////////////// + + + function uncompressed(data) { + return data; + } + + function pako_zlib(constructor, options = {}) { + return function(data) { + const obj = new constructor(options); + return transform(data, value => { + if (value.length) { + obj.push(value, Z_SYNC_FLUSH); + return obj.result; + } + }, () => { + if (constructor === Deflate) { + obj.push([], Z_FINISH); + return obj.result; + } + }); + }; + } + + function bzip2(func) { + return function(data) { + return fromAsync(async () => func(await readToEnd(data))); + }; + } + + const compress_fns = { + zip: (compressed, level) => pako_zlib(Deflate, { raw: true, level })(compressed), + zlib: (compressed, level) => pako_zlib(Deflate, { level })(compressed) + }; + + const decompress_fns = { + uncompressed: uncompressed, + zip: /*#__PURE__*/ pako_zlib(Inflate, { raw: true }), + zlib: /*#__PURE__*/ pako_zlib(Inflate), + bzip2: /*#__PURE__*/ bzip2(lib_4) + }; + + // GPG4Browsers - An OpenPGP implementation in javascript + + // A SEIP packet can contain the following packet types + const allowedPackets$4 = /*#__PURE__*/ util.constructAllowedPackets([ + LiteralDataPacket, + CompressedDataPacket, + OnePassSignaturePacket, + SignaturePacket + ]); + + const VERSION$2 = 1; // A one-octet version number of the data packet. + + /** + * Implementation of the Sym. Encrypted Integrity Protected Data Packet (Tag 18) + * + * {@link https://tools.ietf.org/html/rfc4880#section-5.13|RFC4880 5.13}: + * The Symmetrically Encrypted Integrity Protected Data packet is + * a variant of the Symmetrically Encrypted Data packet. It is a new feature + * created for OpenPGP that addresses the problem of detecting a modification to + * encrypted data. It is used in combination with a Modification Detection Code + * packet. + */ + class SymEncryptedIntegrityProtectedDataPacket { + static get tag() { + return enums.packet.symEncryptedIntegrityProtectedData; + } + + constructor() { + this.version = VERSION$2; + this.encrypted = null; + this.packets = null; + } + + async read(bytes) { + await parse(bytes, async reader => { + const version = await reader.readByte(); + // - A one-octet version number. The only currently defined value is 1. + if (version !== VERSION$2) { + throw new UnsupportedError(`Version ${version} of the SEIP packet is unsupported.`); + } + + // - Encrypted data, the output of the selected symmetric-key cipher + // operating in Cipher Feedback mode with shift amount equal to the + // block size of the cipher (CFB-n where n is the block size). + this.encrypted = reader.remainder(); + }); + } + + write() { + return util.concat([new Uint8Array([VERSION$2]), this.encrypted]); + } + + /** + * Encrypt the payload in the packet. + * @param {enums.symmetric} sessionKeyAlgorithm - The symmetric encryption algorithm to use + * @param {Uint8Array} key - The key of cipher blocksize length to be used + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} + * @throws {Error} on encryption failure + * @async + */ + async encrypt(sessionKeyAlgorithm, key, config$1 = config) { + const { blockSize } = mod.getCipher(sessionKeyAlgorithm); + + let bytes = this.packets.write(); + if (isArrayStream(bytes)) bytes = await readToEnd(bytes); + const prefix = await mod.getPrefixRandom(sessionKeyAlgorithm); + const mdc = new Uint8Array([0xD3, 0x14]); // modification detection code packet + + const tohash = util.concat([prefix, bytes, mdc]); + const hash = await mod.hash.sha1(passiveClone(tohash)); + const plaintext = util.concat([tohash, hash]); + + this.encrypted = await mod.mode.cfb.encrypt(sessionKeyAlgorithm, key, plaintext, new Uint8Array(blockSize), config$1); + return true; + } + + /** + * Decrypts the encrypted data contained in the packet. + * @param {enums.symmetric} sessionKeyAlgorithm - The selected symmetric encryption algorithm to be used + * @param {Uint8Array} key - The key of cipher blocksize length to be used + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} + * @throws {Error} on decryption failure + * @async + */ + async decrypt(sessionKeyAlgorithm, key, config$1 = config) { + const { blockSize } = mod.getCipher(sessionKeyAlgorithm); + let encrypted = clone(this.encrypted); + if (isArrayStream(encrypted)) encrypted = await readToEnd(encrypted); + const decrypted = await mod.mode.cfb.decrypt(sessionKeyAlgorithm, key, encrypted, new Uint8Array(blockSize)); + + // there must be a modification detection code packet as the + // last packet and everything gets hashed except the hash itself + const realHash = slice(passiveClone(decrypted), -20); + const tohash = slice(decrypted, 0, -20); + const verifyHash = Promise.all([ + readToEnd(await mod.hash.sha1(passiveClone(tohash))), + readToEnd(realHash) + ]).then(([hash, mdc]) => { + if (!util.equalsUint8Array(hash, mdc)) { + throw Error('Modification detected.'); + } + return new Uint8Array(); + }); + const bytes = slice(tohash, blockSize + 2); // Remove random prefix + let packetbytes = slice(bytes, 0, -2); // Remove MDC packet + packetbytes = concat([packetbytes, fromAsync(() => verifyHash)]); + if (!util.isStream(encrypted) || !config$1.allowUnauthenticatedStream) { + packetbytes = await readToEnd(packetbytes); + } + this.packets = await PacketList.fromBinary(packetbytes, allowedPackets$4, config$1); + return true; + } + } + + // OpenPGP.js - An OpenPGP implementation in javascript + + // An AEAD-encrypted Data packet can contain the following packet types + const allowedPackets$3 = /*#__PURE__*/ util.constructAllowedPackets([ + LiteralDataPacket, + CompressedDataPacket, + OnePassSignaturePacket, + SignaturePacket + ]); + + const VERSION$1 = 1; // A one-octet version number of the data packet. + + /** + * Implementation of the Symmetrically Encrypted Authenticated Encryption with + * Additional Data (AEAD) Protected Data Packet + * + * {@link https://tools.ietf.org/html/draft-ford-openpgp-format-00#section-2.1}: + * AEAD Protected Data Packet + */ + class AEADEncryptedDataPacket { + static get tag() { + return enums.packet.aeadEncryptedData; + } + + constructor() { + this.version = VERSION$1; + /** @type {enums.symmetric} */ + this.cipherAlgorithm = null; + /** @type {enums.aead} */ + this.aeadAlgorithm = enums.aead.eax; + this.chunkSizeByte = null; + this.iv = null; + this.encrypted = null; + this.packets = null; + } + + /** + * Parse an encrypted payload of bytes in the order: version, IV, ciphertext (see specification) + * @param {Uint8Array | ReadableStream} bytes + * @throws {Error} on parsing failure + */ + async read(bytes) { + await parse(bytes, async reader => { + const version = await reader.readByte(); + if (version !== VERSION$1) { // The only currently defined value is 1. + throw new UnsupportedError(`Version ${version} of the AEAD-encrypted data packet is not supported.`); + } + this.cipherAlgorithm = await reader.readByte(); + this.aeadAlgorithm = await reader.readByte(); + this.chunkSizeByte = await reader.readByte(); + + const mode = mod.getAEADMode(this.aeadAlgorithm); + this.iv = await reader.readBytes(mode.ivLength); + this.encrypted = reader.remainder(); + }); + } + + /** + * Write the encrypted payload of bytes in the order: version, IV, ciphertext (see specification) + * @returns {Uint8Array | ReadableStream} The encrypted payload. + */ + write() { + return util.concat([new Uint8Array([this.version, this.cipherAlgorithm, this.aeadAlgorithm, this.chunkSizeByte]), this.iv, this.encrypted]); + } + + /** + * Decrypt the encrypted payload. + * @param {enums.symmetric} sessionKeyAlgorithm - The session key's cipher algorithm + * @param {Uint8Array} key - The session key used to encrypt the payload + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @throws {Error} if decryption was not successful + * @async + */ + async decrypt(sessionKeyAlgorithm, key, config$1 = config) { + this.packets = await PacketList.fromBinary( + await this.crypt('decrypt', key, clone(this.encrypted)), + allowedPackets$3, + config$1 + ); + } + + /** + * Encrypt the packet payload. + * @param {enums.symmetric} sessionKeyAlgorithm - The session key's cipher algorithm + * @param {Uint8Array} key - The session key used to encrypt the payload + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @throws {Error} if encryption was not successful + * @async + */ + async encrypt(sessionKeyAlgorithm, key, config$1 = config) { + this.cipherAlgorithm = sessionKeyAlgorithm; + + const { ivLength } = mod.getAEADMode(this.aeadAlgorithm); + this.iv = mod.random.getRandomBytes(ivLength); // generate new random IV + this.chunkSizeByte = config$1.aeadChunkSizeByte; + const data = this.packets.write(); + this.encrypted = await this.crypt('encrypt', key, data); + } + + /** + * En/decrypt the payload. + * @param {encrypt|decrypt} fn - Whether to encrypt or decrypt + * @param {Uint8Array} key - The session key used to en/decrypt the payload + * @param {Uint8Array | ReadableStream} data - The data to en/decrypt + * @returns {Promise>} + * @async + */ + async crypt(fn, key, data) { + const mode = mod.getAEADMode(this.aeadAlgorithm); + const modeInstance = await mode(this.cipherAlgorithm, key); + const tagLengthIfDecrypting = fn === 'decrypt' ? mode.tagLength : 0; + const tagLengthIfEncrypting = fn === 'encrypt' ? mode.tagLength : 0; + const chunkSize = 2 ** (this.chunkSizeByte + 6) + tagLengthIfDecrypting; // ((uint64_t)1 << (c + 6)) + const adataBuffer = new ArrayBuffer(21); + const adataArray = new Uint8Array(adataBuffer, 0, 13); + const adataTagArray = new Uint8Array(adataBuffer); + const adataView = new DataView(adataBuffer); + const chunkIndexArray = new Uint8Array(adataBuffer, 5, 8); + adataArray.set([0xC0 | AEADEncryptedDataPacket.tag, this.version, this.cipherAlgorithm, this.aeadAlgorithm, this.chunkSizeByte], 0); + let chunkIndex = 0; + let latestPromise = Promise.resolve(); + let cryptedBytes = 0; + let queuedBytes = 0; + const iv = this.iv; + return transformPair(data, async (readable, writable) => { + if (util.isStream(readable) !== 'array') { + const buffer = new TransformStream$1({}, { + highWaterMark: util.getHardwareConcurrency() * 2 ** (this.chunkSizeByte + 6), + size: array => array.length + }); + pipe(buffer.readable, writable); + writable = buffer.writable; + } + const reader = getReader(readable); + const writer = getWriter(writable); + try { + while (true) { + let chunk = await reader.readBytes(chunkSize + tagLengthIfDecrypting) || new Uint8Array(); + const finalChunk = chunk.subarray(chunk.length - tagLengthIfDecrypting); + chunk = chunk.subarray(0, chunk.length - tagLengthIfDecrypting); + let cryptedPromise; + let done; + if (!chunkIndex || chunk.length) { + reader.unshift(finalChunk); + cryptedPromise = modeInstance[fn](chunk, mode.getNonce(iv, chunkIndexArray), adataArray); + queuedBytes += chunk.length - tagLengthIfDecrypting + tagLengthIfEncrypting; + } else { + // After the last chunk, we either encrypt a final, empty + // data chunk to get the final authentication tag or + // validate that final authentication tag. + adataView.setInt32(13 + 4, cryptedBytes); // Should be setInt64(13, ...) + cryptedPromise = modeInstance[fn](finalChunk, mode.getNonce(iv, chunkIndexArray), adataTagArray); + queuedBytes += tagLengthIfEncrypting; + done = true; + } + cryptedBytes += chunk.length - tagLengthIfDecrypting; + // eslint-disable-next-line no-loop-func + latestPromise = latestPromise.then(() => cryptedPromise).then(async crypted => { + await writer.ready; + await writer.write(crypted); + queuedBytes -= crypted.length; + }).catch(err => writer.abort(err)); + if (done || queuedBytes > writer.desiredSize) { + await latestPromise; // Respect backpressure + } + if (!done) { + adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...) + } else { + await writer.close(); + break; + } + } + } catch (e) { + await writer.abort(e); + } + }); + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + const VERSION = 3; + + /** + * Public-Key Encrypted Session Key Packets (Tag 1) + * + * {@link https://tools.ietf.org/html/rfc4880#section-5.1|RFC4880 5.1}: + * A Public-Key Encrypted Session Key packet holds the session key + * used to encrypt a message. Zero or more Public-Key Encrypted Session Key + * packets and/or Symmetric-Key Encrypted Session Key packets may precede a + * Symmetrically Encrypted Data Packet, which holds an encrypted message. The + * message is encrypted with the session key, and the session key is itself + * encrypted and stored in the Encrypted Session Key packet(s). The + * Symmetrically Encrypted Data Packet is preceded by one Public-Key Encrypted + * Session Key packet for each OpenPGP key to which the message is encrypted. + * The recipient of the message finds a session key that is encrypted to their + * public key, decrypts the session key, and then uses the session key to + * decrypt the message. + */ + class PublicKeyEncryptedSessionKeyPacket { + static get tag() { + return enums.packet.publicKeyEncryptedSessionKey; + } + + constructor() { + this.version = 3; + + this.publicKeyID = new KeyID(); + this.publicKeyAlgorithm = null; + + this.sessionKey = null; + /** + * Algorithm to encrypt the message with + * @type {enums.symmetric} + */ + this.sessionKeyAlgorithm = null; + + /** @type {Object} */ + this.encrypted = {}; + } + + /** + * Parsing function for a publickey encrypted session key packet (tag 1). + * + * @param {Uint8Array} bytes - Payload of a tag 1 packet + */ + read(bytes) { + let i = 0; + this.version = bytes[i++]; + if (this.version !== VERSION) { + throw new UnsupportedError(`Version ${this.version} of the PKESK packet is unsupported.`); + } + i += this.publicKeyID.read(bytes.subarray(i)); + this.publicKeyAlgorithm = bytes[i++]; + this.encrypted = mod.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(i), this.version); + if (this.publicKeyAlgorithm === enums.publicKey.x25519) { + this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm); + } + } + + /** + * Create a binary representation of a tag 1 packet + * + * @returns {Uint8Array} The Uint8Array representation. + */ + write() { + const arr = [ + new Uint8Array([this.version]), + this.publicKeyID.write(), + new Uint8Array([this.publicKeyAlgorithm]), + mod.serializeParams(this.publicKeyAlgorithm, this.encrypted) + ]; + + return util.concatUint8Array(arr); + } + + /** + * Encrypt session key packet + * @param {PublicKeyPacket} key - Public key + * @throws {Error} if encryption failed + * @async + */ + async encrypt(key) { + const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); + const encoded = encodeSessionKey(this.version, algo, this.sessionKeyAlgorithm, this.sessionKey); + this.encrypted = await mod.publicKeyEncrypt( + algo, this.sessionKeyAlgorithm, key.publicParams, encoded, key.getFingerprintBytes()); + } + + /** + * Decrypts the session key (only for public key encrypted session key packets (tag 1) + * @param {SecretKeyPacket} key - decrypted private key + * @param {Object} [randomSessionKey] - Bogus session key to use in case of sensitive decryption error, or if the decrypted session key is of a different type/size. + * This is needed for constant-time processing. Expected object of the form: { sessionKey: Uint8Array, sessionKeyAlgorithm: enums.symmetric } + * @throws {Error} if decryption failed, unless `randomSessionKey` is given + * @async + */ + async decrypt(key, randomSessionKey) { + // check that session key algo matches the secret key algo + if (this.publicKeyAlgorithm !== key.algorithm) { + throw Error('Decryption error'); + } + + const randomPayload = randomSessionKey ? + encodeSessionKey(this.version, this.publicKeyAlgorithm, randomSessionKey.sessionKeyAlgorithm, randomSessionKey.sessionKey) : + null; + const decryptedData = await mod.publicKeyDecrypt(this.publicKeyAlgorithm, key.publicParams, key.privateParams, this.encrypted, key.getFingerprintBytes(), randomPayload); + + const { sessionKey, sessionKeyAlgorithm } = decodeSessionKey(this.version, this.publicKeyAlgorithm, decryptedData, randomSessionKey); + + // v3 Montgomery curves have cleartext cipher algo + if (this.publicKeyAlgorithm !== enums.publicKey.x25519) { + this.sessionKeyAlgorithm = sessionKeyAlgorithm; + } + this.sessionKey = sessionKey; + } + } + + + function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) { + switch (keyAlgo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.elgamal: + case enums.publicKey.ecdh: { + // add checksum + return util.concatUint8Array([ + new Uint8Array([cipherAlgo]), + sessionKeyData, + util.writeChecksum(sessionKeyData.subarray(sessionKeyData.length % 8)) + ]); + } + case enums.publicKey.x25519: + return sessionKeyData; + default: + throw Error('Unsupported public key algorithm'); + } + } + + + function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) { + switch (keyAlgo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.elgamal: + case enums.publicKey.ecdh: { + // verify checksum in constant time + const result = decryptedData.subarray(0, decryptedData.length - 2); + const checksum = decryptedData.subarray(decryptedData.length - 2); + const computedChecksum = util.writeChecksum(result.subarray(result.length % 8)); + const isValidChecksum = computedChecksum[0] === checksum[0] & computedChecksum[1] === checksum[1]; + const decryptedSessionKey = { sessionKeyAlgorithm: result[0], sessionKey: result.subarray(1) }; + if (randomSessionKey) { + // We must not leak info about the validity of the decrypted checksum or cipher algo. + // The decrypted session key must be of the same algo and size as the random session key, otherwise we discard it and use the random data. + const isValidPayload = isValidChecksum & + decryptedSessionKey.sessionKeyAlgorithm === randomSessionKey.sessionKeyAlgorithm & + decryptedSessionKey.sessionKey.length === randomSessionKey.sessionKey.length; + return { + sessionKey: util.selectUint8Array(isValidPayload, decryptedSessionKey.sessionKey, randomSessionKey.sessionKey), + sessionKeyAlgorithm: util.selectUint8( + isValidPayload, + decryptedSessionKey.sessionKeyAlgorithm, + randomSessionKey.sessionKeyAlgorithm + ) + }; + } else { + const isValidPayload = isValidChecksum && enums.read(enums.symmetric, decryptedSessionKey.sessionKeyAlgorithm); + if (isValidPayload) { + return decryptedSessionKey; + } else { + throw Error('Decryption error'); + } + } + } + case enums.publicKey.x25519: + return { + sessionKey: decryptedData + }; + default: + throw Error('Unsupported public key algorithm'); + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + class S2K { + /** + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + constructor(config$1 = config) { + /** + * Hash function identifier, or 0 for gnu-dummy keys + * @type {module:enums.hash | 0} + */ + this.algorithm = enums.hash.sha256; + /** + * enums.s2k identifier or 'gnu-dummy' + * @type {String} + */ + this.type = 'iterated'; + /** @type {Integer} */ + this.c = config$1.s2kIterationCountByte; + /** Eight bytes of salt in a binary string. + * @type {Uint8Array} + */ + this.salt = null; + } + + getCount() { + // Exponent bias, defined in RFC4880 + const expbias = 6; + + return (16 + (this.c & 15)) << ((this.c >> 4) + expbias); + } + + /** + * Parsing function for a string-to-key specifier ({@link https://tools.ietf.org/html/rfc4880#section-3.7|RFC 4880 3.7}). + * @param {Uint8Array} bytes - Payload of string-to-key specifier + * @returns {Integer} Actual length of the object. + */ + read(bytes) { + let i = 0; + try { + this.type = enums.read(enums.s2k, bytes[i++]); + } catch (err) { + throw new UnsupportedError('Unknown S2K type.'); + } + this.algorithm = bytes[i++]; + + switch (this.type) { + case 'simple': + break; + + case 'salted': + this.salt = bytes.subarray(i, i + 8); + i += 8; + break; + + case 'iterated': + this.salt = bytes.subarray(i, i + 8); + i += 8; + + // Octet 10: count, a one-octet, coded value + this.c = bytes[i++]; + break; + + case 'gnu': + if (util.uint8ArrayToString(bytes.subarray(i, i + 3)) === 'GNU') { + i += 3; // GNU + const gnuExtType = 1000 + bytes[i++]; + if (gnuExtType === 1001) { + this.type = 'gnu-dummy'; + // GnuPG extension mode 1001 -- don't write secret key at all + } else { + throw new UnsupportedError('Unknown s2k gnu protection mode.'); + } + } else { + throw new UnsupportedError('Unknown s2k type.'); + } + break; + + default: + throw new UnsupportedError('Unknown s2k type.'); // unreachable + } + + return i; + } + + /** + * Serializes s2k information + * @returns {Uint8Array} Binary representation of s2k. + */ + write() { + if (this.type === 'gnu-dummy') { + return new Uint8Array([101, 0, ...util.stringToUint8Array('GNU'), 1]); + } + const arr = [new Uint8Array([enums.write(enums.s2k, this.type), this.algorithm])]; + + switch (this.type) { + case 'simple': + break; + case 'salted': + arr.push(this.salt); + break; + case 'iterated': + arr.push(this.salt); + arr.push(new Uint8Array([this.c])); + break; + case 'gnu': + throw Error('GNU s2k type not supported.'); + default: + throw Error('Unknown s2k type.'); + } + + return util.concatUint8Array(arr); + } + + /** + * Produces a key using the specified passphrase and the defined + * hashAlgorithm + * @param {String} passphrase - Passphrase containing user input + * @returns {Promise} Produced key with a length corresponding to. + * hashAlgorithm hash length + * @async + */ + async produceKey(passphrase, numBytes) { + passphrase = util.encodeUTF8(passphrase); + + const arr = []; + let rlength = 0; + + let prefixlen = 0; + while (rlength < numBytes) { + let toHash; + switch (this.type) { + case 'simple': + toHash = util.concatUint8Array([new Uint8Array(prefixlen), passphrase]); + break; + case 'salted': + toHash = util.concatUint8Array([new Uint8Array(prefixlen), this.salt, passphrase]); + break; + case 'iterated': { + const data = util.concatUint8Array([this.salt, passphrase]); + let datalen = data.length; + const count = Math.max(this.getCount(), datalen); + toHash = new Uint8Array(prefixlen + count); + toHash.set(data, prefixlen); + for (let pos = prefixlen + datalen; pos < count; pos += datalen, datalen *= 2) { + toHash.copyWithin(pos, prefixlen, pos); + } + break; + } + case 'gnu': + throw Error('GNU s2k type not supported.'); + default: + throw Error('Unknown s2k type.'); + } + const result = await mod.hash.digest(this.algorithm, toHash); + arr.push(result); + rlength += result.length; + prefixlen++; + } + + return util.concatUint8Array(arr).subarray(0, numBytes); + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * Symmetric-Key Encrypted Session Key Packets (Tag 3) + * + * {@link https://tools.ietf.org/html/rfc4880#section-5.3|RFC4880 5.3}: + * The Symmetric-Key Encrypted Session Key packet holds the + * symmetric-key encryption of a session key used to encrypt a message. + * Zero or more Public-Key Encrypted Session Key packets and/or + * Symmetric-Key Encrypted Session Key packets may precede a + * Symmetrically Encrypted Data packet that holds an encrypted message. + * The message is encrypted with a session key, and the session key is + * itself encrypted and stored in the Encrypted Session Key packet or + * the Symmetric-Key Encrypted Session Key packet. + */ + class SymEncryptedSessionKeyPacket { + static get tag() { + return enums.packet.symEncryptedSessionKey; + } + + /** + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + constructor(config$1 = config) { + this.version = config$1.aeadProtect ? 5 : 4; + this.sessionKey = null; + /** + * Algorithm to encrypt the session key with + * @type {enums.symmetric} + */ + this.sessionKeyEncryptionAlgorithm = null; + /** + * Algorithm to encrypt the message with + * @type {enums.symmetric} + */ + this.sessionKeyAlgorithm = enums.symmetric.aes256; + /** + * AEAD mode to encrypt the session key with (if AEAD protection is enabled) + * @type {enums.aead} + */ + this.aeadAlgorithm = enums.write(enums.aead, config$1.preferredAEADAlgorithm); + this.encrypted = null; + this.s2k = null; + this.iv = null; + } + + /** + * Parsing function for a symmetric encrypted session key packet (tag 3). + * + * @param {Uint8Array} bytes - Payload of a tag 3 packet + */ + read(bytes) { + let offset = 0; + + // A one-octet version number. The only currently defined version is 4. + this.version = bytes[offset++]; + if (this.version !== 4 && this.version !== 5) { + throw new UnsupportedError(`Version ${this.version} of the SKESK packet is unsupported.`); + } + + // A one-octet number describing the symmetric algorithm used. + const algo = bytes[offset++]; + + if (this.version === 5) { + // A one-octet AEAD algorithm. + this.aeadAlgorithm = bytes[offset++]; + } + + // A string-to-key (S2K) specifier, length as defined above. + this.s2k = new S2K(); + offset += this.s2k.read(bytes.subarray(offset, bytes.length)); + + if (this.version === 5) { + const mode = mod.getAEADMode(this.aeadAlgorithm); + + // A starting initialization vector of size specified by the AEAD + // algorithm. + this.iv = bytes.subarray(offset, offset += mode.ivLength); + } + + // The encrypted session key itself, which is decrypted with the + // string-to-key object. This is optional in version 4. + if (this.version === 5 || offset < bytes.length) { + this.encrypted = bytes.subarray(offset, bytes.length); + this.sessionKeyEncryptionAlgorithm = algo; + } else { + this.sessionKeyAlgorithm = algo; + } + } + + /** + * Create a binary representation of a tag 3 packet + * + * @returns {Uint8Array} The Uint8Array representation. + */ + write() { + const algo = this.encrypted === null ? + this.sessionKeyAlgorithm : + this.sessionKeyEncryptionAlgorithm; + + let bytes; + + if (this.version === 5) { + bytes = util.concatUint8Array([new Uint8Array([this.version, algo, this.aeadAlgorithm]), this.s2k.write(), this.iv, this.encrypted]); + } else { + bytes = util.concatUint8Array([new Uint8Array([this.version, algo]), this.s2k.write()]); + + if (this.encrypted !== null) { + bytes = util.concatUint8Array([bytes, this.encrypted]); + } + } + + return bytes; + } + + /** + * Decrypts the session key with the given passphrase + * @param {String} passphrase - The passphrase in string form + * @throws {Error} if decryption was not successful + * @async + */ + async decrypt(passphrase) { + const algo = this.sessionKeyEncryptionAlgorithm !== null ? + this.sessionKeyEncryptionAlgorithm : + this.sessionKeyAlgorithm; + + const { blockSize, keySize } = mod.getCipher(algo); + const key = await this.s2k.produceKey(passphrase, keySize); + + if (this.version === 5) { + const mode = mod.getAEADMode(this.aeadAlgorithm); + const adata = new Uint8Array([0xC0 | SymEncryptedSessionKeyPacket.tag, this.version, this.sessionKeyEncryptionAlgorithm, this.aeadAlgorithm]); + const modeInstance = await mode(algo, key); + this.sessionKey = await modeInstance.decrypt(this.encrypted, this.iv, adata); + } else if (this.encrypted !== null) { + const decrypted = await mod.mode.cfb.decrypt(algo, key, this.encrypted, new Uint8Array(blockSize)); + + this.sessionKeyAlgorithm = enums.write(enums.symmetric, decrypted[0]); + this.sessionKey = decrypted.subarray(1, decrypted.length); + } else { + this.sessionKey = key; + } + } + + /** + * Encrypts the session key with the given passphrase + * @param {String} passphrase - The passphrase in string form + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @throws {Error} if encryption was not successful + * @async + */ + async encrypt(passphrase, config$1 = config) { + const algo = this.sessionKeyEncryptionAlgorithm !== null ? + this.sessionKeyEncryptionAlgorithm : + this.sessionKeyAlgorithm; + + this.sessionKeyEncryptionAlgorithm = algo; + + this.s2k = new S2K(config$1); + this.s2k.salt = mod.random.getRandomBytes(8); + + const { blockSize, keySize } = mod.getCipher(algo); + const encryptionKey = await this.s2k.produceKey(passphrase, keySize); + + if (this.sessionKey === null) { + this.sessionKey = mod.generateSessionKey(this.sessionKeyAlgorithm); + } + + if (this.version === 5) { + const mode = mod.getAEADMode(this.aeadAlgorithm); + this.iv = mod.random.getRandomBytes(mode.ivLength); // generate new random IV + const associatedData = new Uint8Array([0xC0 | SymEncryptedSessionKeyPacket.tag, this.version, this.sessionKeyEncryptionAlgorithm, this.aeadAlgorithm]); + const modeInstance = await mode(algo, encryptionKey); + this.encrypted = await modeInstance.encrypt(this.sessionKey, this.iv, associatedData); + } else { + const toEncrypt = util.concatUint8Array([ + new Uint8Array([this.sessionKeyAlgorithm]), + this.sessionKey + ]); + this.encrypted = await mod.mode.cfb.encrypt(algo, encryptionKey, toEncrypt, new Uint8Array(blockSize), config$1); + } + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * Implementation of the Key Material Packet (Tag 5,6,7,14) + * + * {@link https://tools.ietf.org/html/rfc4880#section-5.5|RFC4480 5.5}: + * A key material packet contains all the information about a public or + * private key. There are four variants of this packet type, and two + * major versions. + * + * A Public-Key packet starts a series of packets that forms an OpenPGP + * key (sometimes called an OpenPGP certificate). + */ + class PublicKeyPacket { + static get tag() { + return enums.packet.publicKey; + } + + /** + * @param {Date} [date] - Creation date + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + constructor(date = new Date(), config$1 = config) { + /** + * Packet version + * @type {Integer} + */ + this.version = config$1.v5Keys ? 5 : 4; + /** + * Key creation date. + * @type {Date} + */ + this.created = util.normalizeDate(date); + /** + * Public key algorithm. + * @type {enums.publicKey} + */ + this.algorithm = null; + /** + * Algorithm specific public params + * @type {Object} + */ + this.publicParams = null; + /** + * Time until expiration in days (V3 only) + * @type {Integer} + */ + this.expirationTimeV3 = 0; + /** + * Fingerprint bytes + * @type {Uint8Array} + */ + this.fingerprint = null; + /** + * KeyID + * @type {module:type/keyid~KeyID} + */ + this.keyID = null; + } + + /** + * Create a PublicKeyPacket from a SecretKeyPacket + * @param {SecretKeyPacket} secretKeyPacket - key packet to convert + * @returns {PublicKeyPacket} public key packet + * @static + */ + static fromSecretKeyPacket(secretKeyPacket) { + const keyPacket = new PublicKeyPacket(); + const { version, created, algorithm, publicParams, keyID, fingerprint } = secretKeyPacket; + keyPacket.version = version; + keyPacket.created = created; + keyPacket.algorithm = algorithm; + keyPacket.publicParams = publicParams; + keyPacket.keyID = keyID; + keyPacket.fingerprint = fingerprint; + return keyPacket; + } + + /** + * Internal Parser for public keys as specified in {@link https://tools.ietf.org/html/rfc4880#section-5.5.2|RFC 4880 section 5.5.2 Public-Key Packet Formats} + * @param {Uint8Array} bytes - Input array to read the packet from + * @returns {Object} This object with attributes set by the parser + * @async + */ + async read(bytes) { + let pos = 0; + // A one-octet version number (3, 4 or 5). + this.version = bytes[pos++]; + + if (this.version === 4 || this.version === 5) { + // - A four-octet number denoting the time that the key was created. + this.created = util.readDate(bytes.subarray(pos, pos + 4)); + pos += 4; + + // - A one-octet number denoting the public-key algorithm of this key. + this.algorithm = bytes[pos++]; + + if (this.version === 5) { + // - A four-octet scalar octet count for the following key material. + pos += 4; + } + + // - A series of values comprising the key material. + const { read, publicParams } = mod.parsePublicKeyParams(this.algorithm, bytes.subarray(pos)); + this.publicParams = publicParams; + pos += read; + + // we set the fingerprint and keyID already to make it possible to put together the key packets directly in the Key constructor + await this.computeFingerprintAndKeyID(); + return pos; + } + throw new UnsupportedError(`Version ${this.version} of the key packet is unsupported.`); + } + + /** + * Creates an OpenPGP public key packet for the given key. + * @returns {Uint8Array} Bytes encoding the public key OpenPGP packet. + */ + write() { + const arr = []; + // Version + arr.push(new Uint8Array([this.version])); + arr.push(util.writeDate(this.created)); + // A one-octet number denoting the public-key algorithm of this key + arr.push(new Uint8Array([this.algorithm])); + + const params = mod.serializeParams(this.algorithm, this.publicParams); + if (this.version === 5) { + // A four-octet scalar octet count for the following key material + arr.push(util.writeNumber(params.length, 4)); + } + // Algorithm-specific params + arr.push(params); + return util.concatUint8Array(arr); + } + + /** + * Write packet in order to be hashed; either for a signature or a fingerprint + * @param {Integer} version - target version of signature or key + */ + writeForHash(version) { + const bytes = this.writePublicKey(); + + if (version === 5) { + return util.concatUint8Array([new Uint8Array([0x9A]), util.writeNumber(bytes.length, 4), bytes]); + } + return util.concatUint8Array([new Uint8Array([0x99]), util.writeNumber(bytes.length, 2), bytes]); + } + + /** + * Check whether secret-key data is available in decrypted form. Returns null for public keys. + * @returns {Boolean|null} + */ + isDecrypted() { + return null; + } + + /** + * Returns the creation time of the key + * @returns {Date} + */ + getCreationTime() { + return this.created; + } + + /** + * Return the key ID of the key + * @returns {module:type/keyid~KeyID} The 8-byte key ID + */ + getKeyID() { + return this.keyID; + } + + /** + * Computes and set the key ID and fingerprint of the key + * @async + */ + async computeFingerprintAndKeyID() { + await this.computeFingerprint(); + this.keyID = new KeyID(); + + if (this.version === 5) { + this.keyID.read(this.fingerprint.subarray(0, 8)); + } else if (this.version === 4) { + this.keyID.read(this.fingerprint.subarray(12, 20)); + } else { + throw Error('Unsupported key version'); + } + } + + /** + * Computes and set the fingerprint of the key + */ + async computeFingerprint() { + const toHash = this.writeForHash(this.version); + + if (this.version === 5) { + this.fingerprint = await mod.hash.sha256(toHash); + } else if (this.version === 4) { + this.fingerprint = await mod.hash.sha1(toHash); + } else { + throw Error('Unsupported key version'); + } + } + + /** + * Returns the fingerprint of the key, as an array of bytes + * @returns {Uint8Array} A Uint8Array containing the fingerprint + */ + getFingerprintBytes() { + return this.fingerprint; + } + + /** + * Calculates and returns the fingerprint of the key, as a string + * @returns {String} A string containing the fingerprint in lowercase hex + */ + getFingerprint() { + return util.uint8ArrayToHex(this.getFingerprintBytes()); + } + + /** + * Calculates whether two keys have the same fingerprint without actually calculating the fingerprint + * @returns {Boolean} Whether the two keys have the same version and public key data. + */ + hasSameFingerprintAs(other) { + return this.version === other.version && util.equalsUint8Array(this.writePublicKey(), other.writePublicKey()); + } + + /** + * Returns algorithm information + * @returns {Object} An object of the form {algorithm: String, bits:int, curve:String}. + */ + getAlgorithmInfo() { + const result = {}; + result.algorithm = enums.read(enums.publicKey, this.algorithm); + // RSA, DSA or ElGamal public modulo + const modulo = this.publicParams.n || this.publicParams.p; + if (modulo) { + result.bits = util.uint8ArrayBitLength(modulo); + } else if (this.publicParams.oid) { + result.curve = this.publicParams.oid.getName(); + } + return result; + } + } + + /** + * Alias of read() + * @see PublicKeyPacket#read + */ + PublicKeyPacket.prototype.readPublicKey = PublicKeyPacket.prototype.read; + + /** + * Alias of write() + * @see PublicKeyPacket#write + */ + PublicKeyPacket.prototype.writePublicKey = PublicKeyPacket.prototype.write; + + // GPG4Browsers - An OpenPGP implementation in javascript + + // A SE packet can contain the following packet types + const allowedPackets$2 = /*#__PURE__*/ util.constructAllowedPackets([ + LiteralDataPacket, + CompressedDataPacket, + OnePassSignaturePacket, + SignaturePacket + ]); + + /** + * Implementation of the Symmetrically Encrypted Data Packet (Tag 9) + * + * {@link https://tools.ietf.org/html/rfc4880#section-5.7|RFC4880 5.7}: + * The Symmetrically Encrypted Data packet contains data encrypted with a + * symmetric-key algorithm. When it has been decrypted, it contains other + * packets (usually a literal data packet or compressed data packet, but in + * theory other Symmetrically Encrypted Data packets or sequences of packets + * that form whole OpenPGP messages). + */ + class SymmetricallyEncryptedDataPacket { + static get tag() { + return enums.packet.symmetricallyEncryptedData; + } + + constructor() { + /** + * Encrypted secret-key data + */ + this.encrypted = null; + /** + * Decrypted packets contained within. + * @type {PacketList} + */ + this.packets = null; + } + + read(bytes) { + this.encrypted = bytes; + } + + write() { + return this.encrypted; + } + + /** + * Decrypt the symmetrically-encrypted packet data + * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. + * @param {module:enums.symmetric} sessionKeyAlgorithm - Symmetric key algorithm to use + * @param {Uint8Array} key - The key of cipher blocksize length to be used + * @param {Object} [config] - Full configuration, defaults to openpgp.config + + * @throws {Error} if decryption was not successful + * @async + */ + async decrypt(sessionKeyAlgorithm, key, config$1 = config) { + // If MDC errors are not being ignored, all missing MDC packets in symmetrically encrypted data should throw an error + if (!config$1.allowUnauthenticatedMessages) { + throw Error('Message is not authenticated.'); + } + + const { blockSize } = mod.getCipher(sessionKeyAlgorithm); + const encrypted = await readToEnd(clone(this.encrypted)); + const decrypted = await mod.mode.cfb.decrypt(sessionKeyAlgorithm, key, + encrypted.subarray(blockSize + 2), + encrypted.subarray(2, blockSize + 2) + ); + + this.packets = await PacketList.fromBinary(decrypted, allowedPackets$2, config$1); + } + + /** + * Encrypt the symmetrically-encrypted packet data + * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. + * @param {module:enums.symmetric} sessionKeyAlgorithm - Symmetric key algorithm to use + * @param {Uint8Array} key - The key of cipher blocksize length to be used + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @throws {Error} if encryption was not successful + * @async + */ + async encrypt(sessionKeyAlgorithm, key, config$1 = config) { + const data = this.packets.write(); + const { blockSize } = mod.getCipher(sessionKeyAlgorithm); + + const prefix = await mod.getPrefixRandom(sessionKeyAlgorithm); + const FRE = await mod.mode.cfb.encrypt(sessionKeyAlgorithm, key, prefix, new Uint8Array(blockSize), config$1); + const ciphertext = await mod.mode.cfb.encrypt(sessionKeyAlgorithm, key, data, FRE.subarray(2), config$1); + this.encrypted = util.concat([FRE, ciphertext]); + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * Implementation of the strange "Marker packet" (Tag 10) + * + * {@link https://tools.ietf.org/html/rfc4880#section-5.8|RFC4880 5.8}: + * An experimental version of PGP used this packet as the Literal + * packet, but no released version of PGP generated Literal packets with this + * tag. With PGP 5.x, this packet has been reassigned and is reserved for use as + * the Marker packet. + * + * The body of this packet consists of: + * The three octets 0x50, 0x47, 0x50 (which spell "PGP" in UTF-8). + * + * Such a packet MUST be ignored when received. It may be placed at the + * beginning of a message that uses features not available in PGP + * version 2.6 in order to cause that version to report that newer + * software is necessary to process the message. + */ + class MarkerPacket { + static get tag() { + return enums.packet.marker; + } + + /** + * Parsing function for a marker data packet (tag 10). + * @param {Uint8Array} bytes - Payload of a tag 10 packet + * @returns {Boolean} whether the packet payload contains "PGP" + */ + read(bytes) { + if (bytes[0] === 0x50 && // P + bytes[1] === 0x47 && // G + bytes[2] === 0x50) { // P + return true; + } + return false; + } + + write() { + return new Uint8Array([0x50, 0x47, 0x50]); + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * A Public-Subkey packet (tag 14) has exactly the same format as a + * Public-Key packet, but denotes a subkey. One or more subkeys may be + * associated with a top-level key. By convention, the top-level key + * provides signature services, and the subkeys provide encryption + * services. + * @extends PublicKeyPacket + */ + class PublicSubkeyPacket extends PublicKeyPacket { + static get tag() { + return enums.packet.publicSubkey; + } + + /** + * @param {Date} [date] - Creation date + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + // eslint-disable-next-line no-useless-constructor + constructor(date, config) { + super(date, config); + } + + /** + * Create a PublicSubkeyPacket from a SecretSubkeyPacket + * @param {SecretSubkeyPacket} secretSubkeyPacket - subkey packet to convert + * @returns {SecretSubkeyPacket} public key packet + * @static + */ + static fromSecretSubkeyPacket(secretSubkeyPacket) { + const keyPacket = new PublicSubkeyPacket(); + const { version, created, algorithm, publicParams, keyID, fingerprint } = secretSubkeyPacket; + keyPacket.version = version; + keyPacket.created = created; + keyPacket.algorithm = algorithm; + keyPacket.publicParams = publicParams; + keyPacket.keyID = keyID; + keyPacket.fingerprint = fingerprint; + return keyPacket; + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * Implementation of the User Attribute Packet (Tag 17) + * + * The User Attribute packet is a variation of the User ID packet. It + * is capable of storing more types of data than the User ID packet, + * which is limited to text. Like the User ID packet, a User Attribute + * packet may be certified by the key owner ("self-signed") or any other + * key owner who cares to certify it. Except as noted, a User Attribute + * packet may be used anywhere that a User ID packet may be used. + * + * While User Attribute packets are not a required part of the OpenPGP + * standard, implementations SHOULD provide at least enough + * compatibility to properly handle a certification signature on the + * User Attribute packet. A simple way to do this is by treating the + * User Attribute packet as a User ID packet with opaque contents, but + * an implementation may use any method desired. + */ + class UserAttributePacket { + static get tag() { + return enums.packet.userAttribute; + } + + constructor() { + this.attributes = []; + } + + /** + * parsing function for a user attribute packet (tag 17). + * @param {Uint8Array} input - Payload of a tag 17 packet + */ + read(bytes) { + let i = 0; + while (i < bytes.length) { + const len = readSimpleLength(bytes.subarray(i, bytes.length)); + i += len.offset; + + this.attributes.push(util.uint8ArrayToString(bytes.subarray(i, i + len.len))); + i += len.len; + } + } + + /** + * Creates a binary representation of the user attribute packet + * @returns {Uint8Array} String representation. + */ + write() { + const arr = []; + for (let i = 0; i < this.attributes.length; i++) { + arr.push(writeSimpleLength(this.attributes[i].length)); + arr.push(util.stringToUint8Array(this.attributes[i])); + } + return util.concatUint8Array(arr); + } + + /** + * Compare for equality + * @param {UserAttributePacket} usrAttr + * @returns {Boolean} True if equal. + */ + equals(usrAttr) { + if (!usrAttr || !(usrAttr instanceof UserAttributePacket)) { + return false; + } + return this.attributes.every(function(attr, index) { + return attr === usrAttr.attributes[index]; + }); + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * A Secret-Key packet contains all the information that is found in a + * Public-Key packet, including the public-key material, but also + * includes the secret-key material after all the public-key fields. + * @extends PublicKeyPacket + */ + class SecretKeyPacket extends PublicKeyPacket { + static get tag() { + return enums.packet.secretKey; + } + + /** + * @param {Date} [date] - Creation date + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + constructor(date = new Date(), config$1 = config) { + super(date, config$1); + /** + * Secret-key data + */ + this.keyMaterial = null; + /** + * Indicates whether secret-key data is encrypted. `this.isEncrypted === false` means data is available in decrypted form. + */ + this.isEncrypted = null; + /** + * S2K usage + * @type {enums.symmetric} + */ + this.s2kUsage = 0; + /** + * S2K object + * @type {type/s2k} + */ + this.s2k = null; + /** + * Symmetric algorithm to encrypt the key with + * @type {enums.symmetric} + */ + this.symmetric = null; + /** + * AEAD algorithm to encrypt the key with (if AEAD protection is enabled) + * @type {enums.aead} + */ + this.aead = null; + /** + * Decrypted private parameters, referenced by name + * @type {Object} + */ + this.privateParams = null; + } + + // 5.5.3. Secret-Key Packet Formats + + /** + * Internal parser for private keys as specified in + * {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.5.3|RFC4880bis-04 section 5.5.3} + * @param {Uint8Array} bytes - Input string to read the packet from + * @async + */ + async read(bytes) { + // - A Public-Key or Public-Subkey packet, as described above. + let i = await this.readPublicKey(bytes); + const startOfSecretKeyData = i; + + // - One octet indicating string-to-key usage conventions. Zero + // indicates that the secret-key data is not encrypted. 255 or 254 + // indicates that a string-to-key specifier is being given. Any + // other value is a symmetric-key encryption algorithm identifier. + this.s2kUsage = bytes[i++]; + + // - Only for a version 5 packet, a one-octet scalar octet count of + // the next 4 optional fields. + if (this.version === 5) { + i++; + } + + try { + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a + // one-octet symmetric encryption algorithm. + if (this.s2kUsage === 255 || this.s2kUsage === 254 || this.s2kUsage === 253) { + this.symmetric = bytes[i++]; + + // - [Optional] If string-to-key usage octet was 253, a one-octet + // AEAD algorithm. + if (this.s2kUsage === 253) { + this.aead = bytes[i++]; + } + + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a + // string-to-key specifier. The length of the string-to-key + // specifier is implied by its type, as described above. + this.s2k = new S2K(); + i += this.s2k.read(bytes.subarray(i, bytes.length)); + + if (this.s2k.type === 'gnu-dummy') { + return; + } + } else if (this.s2kUsage) { + this.symmetric = this.s2kUsage; + } + + // - [Optional] If secret data is encrypted (string-to-key usage octet + // not zero), an Initial Vector (IV) of the same length as the + // cipher's block size. + if (this.s2kUsage) { + this.iv = bytes.subarray( + i, + i + mod.getCipher(this.symmetric).blockSize + ); + + i += this.iv.length; + } + } catch (e) { + // if the s2k is unsupported, we still want to support encrypting and verifying with the given key + if (!this.s2kUsage) throw e; // always throw for decrypted keys + this.unparseableKeyMaterial = bytes.subarray(startOfSecretKeyData); + this.isEncrypted = true; + } + + // - Only for a version 5 packet, a four-octet scalar octet count for + // the following key material. + if (this.version === 5) { + i += 4; + } + + // - Plain or encrypted multiprecision integers comprising the secret + // key data. These algorithm-specific fields are as described + // below. + this.keyMaterial = bytes.subarray(i); + this.isEncrypted = !!this.s2kUsage; + + if (!this.isEncrypted) { + const cleartext = this.keyMaterial.subarray(0, -2); + if (!util.equalsUint8Array(util.writeChecksum(cleartext), this.keyMaterial.subarray(-2))) { + throw Error('Key checksum mismatch'); + } + try { + const { privateParams } = mod.parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); + this.privateParams = privateParams; + } catch (err) { + if (err instanceof UnsupportedError) throw err; + // avoid throwing potentially sensitive errors + throw Error('Error reading MPIs'); + } + } + } + + /** + * Creates an OpenPGP key packet for the given key. + * @returns {Uint8Array} A string of bytes containing the secret key OpenPGP packet. + */ + write() { + const serializedPublicKey = this.writePublicKey(); + if (this.unparseableKeyMaterial) { + return util.concatUint8Array([ + serializedPublicKey, + this.unparseableKeyMaterial + ]); + } + + const arr = [serializedPublicKey]; + arr.push(new Uint8Array([this.s2kUsage])); + + const optionalFieldsArr = []; + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a + // one- octet symmetric encryption algorithm. + if (this.s2kUsage === 255 || this.s2kUsage === 254 || this.s2kUsage === 253) { + optionalFieldsArr.push(this.symmetric); + + // - [Optional] If string-to-key usage octet was 253, a one-octet + // AEAD algorithm. + if (this.s2kUsage === 253) { + optionalFieldsArr.push(this.aead); + } + + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a + // string-to-key specifier. The length of the string-to-key + // specifier is implied by its type, as described above. + optionalFieldsArr.push(...this.s2k.write()); + } + + // - [Optional] If secret data is encrypted (string-to-key usage octet + // not zero), an Initial Vector (IV) of the same length as the + // cipher's block size. + if (this.s2kUsage && this.s2k.type !== 'gnu-dummy') { + optionalFieldsArr.push(...this.iv); + } + + if (this.version === 5) { + arr.push(new Uint8Array([optionalFieldsArr.length])); + } + arr.push(new Uint8Array(optionalFieldsArr)); + + if (!this.isDummy()) { + if (!this.s2kUsage) { + this.keyMaterial = mod.serializeParams(this.algorithm, this.privateParams); + } + + if (this.version === 5) { + arr.push(util.writeNumber(this.keyMaterial.length, 4)); + } + arr.push(this.keyMaterial); + + if (!this.s2kUsage) { + arr.push(util.writeChecksum(this.keyMaterial)); + } + } + + return util.concatUint8Array(arr); + } + + /** + * Check whether secret-key data is available in decrypted form. + * Returns false for gnu-dummy keys and null for public keys. + * @returns {Boolean|null} + */ + isDecrypted() { + return this.isEncrypted === false; + } + + /** + * Check whether the key includes secret key material. + * Some secret keys do not include it, and can thus only be used + * for public-key operations (encryption and verification). + * Such keys are: + * - GNU-dummy keys, where the secret material has been stripped away + * - encrypted keys with unsupported S2K or cipher + */ + isMissingSecretKeyMaterial() { + return this.unparseableKeyMaterial !== undefined || this.isDummy(); + } + + /** + * Check whether this is a gnu-dummy key + * @returns {Boolean} + */ + isDummy() { + return !!(this.s2k && this.s2k.type === 'gnu-dummy'); + } + + /** + * Remove private key material, converting the key to a dummy one. + * The resulting key cannot be used for signing/decrypting but can still verify signatures. + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + makeDummy(config$1 = config) { + if (this.isDummy()) { + return; + } + if (this.isDecrypted()) { + this.clearPrivateParams(); + } + delete this.unparseableKeyMaterial; + this.isEncrypted = null; + this.keyMaterial = null; + this.s2k = new S2K(config$1); + this.s2k.algorithm = 0; + this.s2k.c = 0; + this.s2k.type = 'gnu-dummy'; + this.s2kUsage = 254; + this.symmetric = enums.symmetric.aes256; + } + + /** + * Encrypt the payload. By default, we use aes256 and iterated, salted string + * to key specifier. If the key is in a decrypted state (isEncrypted === false) + * and the passphrase is empty or undefined, the key will be set as not encrypted. + * This can be used to remove passphrase protection after calling decrypt(). + * @param {String} passphrase + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @throws {Error} if encryption was not successful + * @async + */ + async encrypt(passphrase, config$1 = config) { + if (this.isDummy()) { + return; + } + + if (!this.isDecrypted()) { + throw Error('Key packet is already encrypted'); + } + + if (!passphrase) { + throw Error('A non-empty passphrase is required for key encryption.'); + } + + this.s2k = new S2K(config$1); + this.s2k.salt = mod.random.getRandomBytes(8); + const cleartext = mod.serializeParams(this.algorithm, this.privateParams); + this.symmetric = enums.symmetric.aes256; + const key = await produceEncryptionKey(this.s2k, passphrase, this.symmetric); + + const { blockSize } = mod.getCipher(this.symmetric); + this.iv = mod.random.getRandomBytes(blockSize); + + if (config$1.aeadProtect) { + this.s2kUsage = 253; + this.aead = enums.aead.eax; + const mode = mod.getAEADMode(this.aead); + const modeInstance = await mode(this.symmetric, key); + this.keyMaterial = await modeInstance.encrypt(cleartext, this.iv.subarray(0, mode.ivLength), new Uint8Array()); + } else { + this.s2kUsage = 254; + this.keyMaterial = await mod.mode.cfb.encrypt(this.symmetric, key, util.concatUint8Array([ + cleartext, + await mod.hash.sha1(cleartext, config$1) + ]), this.iv, config$1); + } + } + + /** + * Decrypts the private key params which are needed to use the key. + * Successful decryption does not imply key integrity, call validate() to confirm that. + * {@link SecretKeyPacket.isDecrypted} should be false, as + * otherwise calls to this function will throw an error. + * @param {String} passphrase - The passphrase for this private key as string + * @throws {Error} if the key is already decrypted, or if decryption was not successful + * @async + */ + async decrypt(passphrase) { + if (this.isDummy()) { + return false; + } + + if (this.unparseableKeyMaterial) { + throw Error('Key packet cannot be decrypted: unsupported S2K or cipher algo'); + } + + if (this.isDecrypted()) { + throw Error('Key packet is already decrypted.'); + } + + let key; + if (this.s2kUsage === 254 || this.s2kUsage === 253) { + key = await produceEncryptionKey(this.s2k, passphrase, this.symmetric); + } else if (this.s2kUsage === 255) { + throw Error('Encrypted private key is authenticated using an insecure two-byte hash'); + } else { + throw Error('Private key is encrypted using an insecure S2K function: unsalted MD5'); + } + + let cleartext; + if (this.s2kUsage === 253) { + const mode = mod.getAEADMode(this.aead); + const modeInstance = await mode(this.symmetric, key); + try { + cleartext = await modeInstance.decrypt(this.keyMaterial, this.iv.subarray(0, mode.ivLength), new Uint8Array()); + } catch (err) { + if (err.message === 'Authentication tag mismatch') { + throw Error('Incorrect key passphrase: ' + err.message); + } + throw err; + } + } else { + const cleartextWithHash = await mod.mode.cfb.decrypt(this.symmetric, key, this.keyMaterial, this.iv); + + cleartext = cleartextWithHash.subarray(0, -20); + const hash = await mod.hash.sha1(cleartext); + + if (!util.equalsUint8Array(hash, cleartextWithHash.subarray(-20))) { + throw Error('Incorrect key passphrase'); + } + } + + try { + const { privateParams } = mod.parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); + this.privateParams = privateParams; + } catch (err) { + throw Error('Error reading MPIs'); + } + this.isEncrypted = false; + this.keyMaterial = null; + this.s2kUsage = 0; + } + + /** + * Checks that the key parameters are consistent + * @throws {Error} if validation was not successful + * @async + */ + async validate() { + if (this.isDummy()) { + return; + } + + if (!this.isDecrypted()) { + throw Error('Key is not decrypted'); + } + + let validParams; + try { + // this can throw if some parameters are undefined + validParams = await mod.validateParams(this.algorithm, this.publicParams, this.privateParams); + } catch (_) { + validParams = false; + } + if (!validParams) { + throw Error('Key is invalid'); + } + } + + async generate(bits, curve) { + const { privateParams, publicParams } = await mod.generateParams(this.algorithm, bits, curve); + this.privateParams = privateParams; + this.publicParams = publicParams; + this.isEncrypted = false; + } + + /** + * Clear private key parameters + */ + clearPrivateParams() { + if (this.isMissingSecretKeyMaterial()) { + return; + } + + Object.keys(this.privateParams).forEach(name => { + const param = this.privateParams[name]; + param.fill(0); + delete this.privateParams[name]; + }); + this.privateParams = null; + this.isEncrypted = true; + } + } + + async function produceEncryptionKey(s2k, passphrase, algorithm) { + const { keySize } = mod.getCipher(algorithm); + return s2k.produceKey(passphrase, keySize); + } + + var emailAddresses = createCommonjsModule(function (module) { + // email-addresses.js - RFC 5322 email address parser + // v 3.1.0 + // + // http://tools.ietf.org/html/rfc5322 + // + // This library does not validate email addresses. + // emailAddresses attempts to parse addresses using the (fairly liberal) + // grammar specified in RFC 5322. + // + // email-addresses returns { + // ast: , + // addresses: [{ + // node: , + // name: , + // address: , + // local: , + // domain: + // }, ...] + // } + // + // emailAddresses.parseOneAddress and emailAddresses.parseAddressList + // work as you might expect. Try it out. + // + // Many thanks to Dominic Sayers and his documentation on the is_email function, + // http://code.google.com/p/isemail/ , which helped greatly in writing this parser. + + (function (global) { + + function parse5322(opts) { + + // tokenizing functions + + function inStr() { return pos < len; } + function curTok() { return parseString[pos]; } + function getPos() { return pos; } + function setPos(i) { pos = i; } + function nextTok() { pos += 1; } + function initialize() { + pos = 0; + len = parseString.length; + } + + // parser helper functions + + function o(name, value) { + return { + name: name, + tokens: value || "", + semantic: value || "", + children: [] + }; + } + + function wrap(name, ast) { + var n; + if (ast === null) { return null; } + n = o(name); + n.tokens = ast.tokens; + n.semantic = ast.semantic; + n.children.push(ast); + return n; + } + + function add(parent, child) { + if (child !== null) { + parent.tokens += child.tokens; + parent.semantic += child.semantic; + } + parent.children.push(child); + return parent; + } + + function compareToken(fxnCompare) { + var tok; + if (!inStr()) { return null; } + tok = curTok(); + if (fxnCompare(tok)) { + nextTok(); + return o('token', tok); + } + return null; + } + + function literal(lit) { + return function literalFunc() { + return wrap('literal', compareToken(function (tok) { + return tok === lit; + })); + }; + } + + function and() { + var args = arguments; + return function andFunc() { + var i, s, result, start; + start = getPos(); + s = o('and'); + for (i = 0; i < args.length; i += 1) { + result = args[i](); + if (result === null) { + setPos(start); + return null; + } + add(s, result); + } + return s; + }; + } + + function or() { + var args = arguments; + return function orFunc() { + var i, result, start; + start = getPos(); + for (i = 0; i < args.length; i += 1) { + result = args[i](); + if (result !== null) { + return result; + } + setPos(start); + } + return null; + }; + } + + function opt(prod) { + return function optFunc() { + var result, start; + start = getPos(); + result = prod(); + if (result !== null) { + return result; + } + else { + setPos(start); + return o('opt'); + } + }; + } + + function invis(prod) { + return function invisFunc() { + var result = prod(); + if (result !== null) { + result.semantic = ""; + } + return result; + }; + } + + function colwsp(prod) { + return function collapseSemanticWhitespace() { + var result = prod(); + if (result !== null && result.semantic.length > 0) { + result.semantic = " "; + } + return result; + }; + } + + function star(prod, minimum) { + return function starFunc() { + var s, result, count, start, min; + start = getPos(); + s = o('star'); + count = 0; + min = minimum === undefined ? 0 : minimum; + while ((result = prod()) !== null) { + count = count + 1; + add(s, result); + } + if (count >= min) { + return s; + } + else { + setPos(start); + return null; + } + }; + } + + // One expects names to get normalized like this: + // " First Last " -> "First Last" + // "First Last" -> "First Last" + // "First Last" -> "First Last" + function collapseWhitespace(s) { + return s.replace(/([ \t]|\r\n)+/g, ' ').replace(/^\s*/, '').replace(/\s*$/, ''); + } + + // UTF-8 pseudo-production (RFC 6532) + // RFC 6532 extends RFC 5322 productions to include UTF-8 + // using the following productions: + // UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4 + // UTF8-2 = + // UTF8-3 = + // UTF8-4 = + // + // For reference, the extended RFC 5322 productions are: + // VCHAR =/ UTF8-non-ascii + // ctext =/ UTF8-non-ascii + // atext =/ UTF8-non-ascii + // qtext =/ UTF8-non-ascii + // dtext =/ UTF8-non-ascii + function isUTF8NonAscii(tok) { + // In JavaScript, we just deal directly with Unicode code points, + // so we aren't checking individual bytes for UTF-8 encoding. + // Just check that the character is non-ascii. + return tok.charCodeAt(0) >= 128; + } + + + // common productions (RFC 5234) + // http://tools.ietf.org/html/rfc5234 + // B.1. Core Rules + + // CR = %x0D + // ; carriage return + function cr() { return wrap('cr', literal('\r')()); } + + // CRLF = CR LF + // ; Internet standard newline + function crlf() { return wrap('crlf', and(cr, lf)()); } + + // DQUOTE = %x22 + // ; " (Double Quote) + function dquote() { return wrap('dquote', literal('"')()); } + + // HTAB = %x09 + // ; horizontal tab + function htab() { return wrap('htab', literal('\t')()); } + + // LF = %x0A + // ; linefeed + function lf() { return wrap('lf', literal('\n')()); } + + // SP = %x20 + function sp() { return wrap('sp', literal(' ')()); } + + // VCHAR = %x21-7E + // ; visible (printing) characters + function vchar() { + return wrap('vchar', compareToken(function vcharFunc(tok) { + var code = tok.charCodeAt(0); + var accept = (0x21 <= code && code <= 0x7E); + if (opts.rfc6532) { + accept = accept || isUTF8NonAscii(tok); + } + return accept; + })); + } + + // WSP = SP / HTAB + // ; white space + function wsp() { return wrap('wsp', or(sp, htab)()); } + + + // email productions (RFC 5322) + // http://tools.ietf.org/html/rfc5322 + // 3.2.1. Quoted characters + + // quoted-pair = ("\" (VCHAR / WSP)) / obs-qp + function quotedPair() { + var qp = wrap('quoted-pair', + or( + and(literal('\\'), or(vchar, wsp)), + obsQP + )()); + if (qp === null) { return null; } + // a quoted pair will be two characters, and the "\" character + // should be semantically "invisible" (RFC 5322 3.2.1) + qp.semantic = qp.semantic[1]; + return qp; + } + + // 3.2.2. Folding White Space and Comments + + // FWS = ([*WSP CRLF] 1*WSP) / obs-FWS + function fws() { + return wrap('fws', or( + obsFws, + and( + opt(and( + star(wsp), + invis(crlf) + )), + star(wsp, 1) + ) + )()); + } + + // ctext = %d33-39 / ; Printable US-ASCII + // %d42-91 / ; characters not including + // %d93-126 / ; "(", ")", or "\" + // obs-ctext + function ctext() { + return wrap('ctext', or( + function ctextFunc1() { + return compareToken(function ctextFunc2(tok) { + var code = tok.charCodeAt(0); + var accept = + (33 <= code && code <= 39) || + (42 <= code && code <= 91) || + (93 <= code && code <= 126); + if (opts.rfc6532) { + accept = accept || isUTF8NonAscii(tok); + } + return accept; + }); + }, + obsCtext + )()); + } + + // ccontent = ctext / quoted-pair / comment + function ccontent() { + return wrap('ccontent', or(ctext, quotedPair, comment)()); + } + + // comment = "(" *([FWS] ccontent) [FWS] ")" + function comment() { + return wrap('comment', and( + literal('('), + star(and(opt(fws), ccontent)), + opt(fws), + literal(')') + )()); + } + + // CFWS = (1*([FWS] comment) [FWS]) / FWS + function cfws() { + return wrap('cfws', or( + and( + star( + and(opt(fws), comment), + 1 + ), + opt(fws) + ), + fws + )()); + } + + // 3.2.3. Atom + + //atext = ALPHA / DIGIT / ; Printable US-ASCII + // "!" / "#" / ; characters not including + // "$" / "%" / ; specials. Used for atoms. + // "&" / "'" / + // "*" / "+" / + // "-" / "/" / + // "=" / "?" / + // "^" / "_" / + // "`" / "{" / + // "|" / "}" / + // "~" + function atext() { + return wrap('atext', compareToken(function atextFunc(tok) { + var accept = + ('a' <= tok && tok <= 'z') || + ('A' <= tok && tok <= 'Z') || + ('0' <= tok && tok <= '9') || + (['!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', + '=', '?', '^', '_', '`', '{', '|', '}', '~'].indexOf(tok) >= 0); + if (opts.rfc6532) { + accept = accept || isUTF8NonAscii(tok); + } + return accept; + })); + } + + // atom = [CFWS] 1*atext [CFWS] + function atom() { + return wrap('atom', and(colwsp(opt(cfws)), star(atext, 1), colwsp(opt(cfws)))()); + } + + // dot-atom-text = 1*atext *("." 1*atext) + function dotAtomText() { + var s, maybeText; + s = wrap('dot-atom-text', star(atext, 1)()); + if (s === null) { return s; } + maybeText = star(and(literal('.'), star(atext, 1)))(); + if (maybeText !== null) { + add(s, maybeText); + } + return s; + } + + // dot-atom = [CFWS] dot-atom-text [CFWS] + function dotAtom() { + return wrap('dot-atom', and(invis(opt(cfws)), dotAtomText, invis(opt(cfws)))()); + } + + // 3.2.4. Quoted Strings + + // qtext = %d33 / ; Printable US-ASCII + // %d35-91 / ; characters not including + // %d93-126 / ; "\" or the quote character + // obs-qtext + function qtext() { + return wrap('qtext', or( + function qtextFunc1() { + return compareToken(function qtextFunc2(tok) { + var code = tok.charCodeAt(0); + var accept = + (33 === code) || + (35 <= code && code <= 91) || + (93 <= code && code <= 126); + if (opts.rfc6532) { + accept = accept || isUTF8NonAscii(tok); + } + return accept; + }); + }, + obsQtext + )()); + } + + // qcontent = qtext / quoted-pair + function qcontent() { + return wrap('qcontent', or(qtext, quotedPair)()); + } + + // quoted-string = [CFWS] + // DQUOTE *([FWS] qcontent) [FWS] DQUOTE + // [CFWS] + function quotedString() { + return wrap('quoted-string', and( + invis(opt(cfws)), + invis(dquote), star(and(opt(colwsp(fws)), qcontent)), opt(invis(fws)), invis(dquote), + invis(opt(cfws)) + )()); + } + + // 3.2.5 Miscellaneous Tokens + + // word = atom / quoted-string + function word() { + return wrap('word', or(atom, quotedString)()); + } + + // phrase = 1*word / obs-phrase + function phrase() { + return wrap('phrase', or(obsPhrase, star(word, 1))()); + } + + // 3.4. Address Specification + // address = mailbox / group + function address() { + return wrap('address', or(mailbox, group)()); + } + + // mailbox = name-addr / addr-spec + function mailbox() { + return wrap('mailbox', or(nameAddr, addrSpec)()); + } + + // name-addr = [display-name] angle-addr + function nameAddr() { + return wrap('name-addr', and(opt(displayName), angleAddr)()); + } + + // angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / + // obs-angle-addr + function angleAddr() { + return wrap('angle-addr', or( + and( + invis(opt(cfws)), + literal('<'), + addrSpec, + literal('>'), + invis(opt(cfws)) + ), + obsAngleAddr + )()); + } + + // group = display-name ":" [group-list] ";" [CFWS] + function group() { + return wrap('group', and( + displayName, + literal(':'), + opt(groupList), + literal(';'), + invis(opt(cfws)) + )()); + } + + // display-name = phrase + function displayName() { + return wrap('display-name', function phraseFixedSemantic() { + var result = phrase(); + if (result !== null) { + result.semantic = collapseWhitespace(result.semantic); + } + return result; + }()); + } + + // mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list + function mailboxList() { + return wrap('mailbox-list', or( + and( + mailbox, + star(and(literal(','), mailbox)) + ), + obsMboxList + )()); + } + + // address-list = (address *("," address)) / obs-addr-list + function addressList() { + return wrap('address-list', or( + and( + address, + star(and(literal(','), address)) + ), + obsAddrList + )()); + } + + // group-list = mailbox-list / CFWS / obs-group-list + function groupList() { + return wrap('group-list', or( + mailboxList, + invis(cfws), + obsGroupList + )()); + } + + // 3.4.1 Addr-Spec Specification + + // local-part = dot-atom / quoted-string / obs-local-part + function localPart() { + // note: quoted-string, dotAtom are proper subsets of obs-local-part + // so we really just have to look for obsLocalPart, if we don't care about the exact parse tree + return wrap('local-part', or(obsLocalPart, dotAtom, quotedString)()); + } + + // dtext = %d33-90 / ; Printable US-ASCII + // %d94-126 / ; characters not including + // obs-dtext ; "[", "]", or "\" + function dtext() { + return wrap('dtext', or( + function dtextFunc1() { + return compareToken(function dtextFunc2(tok) { + var code = tok.charCodeAt(0); + var accept = + (33 <= code && code <= 90) || + (94 <= code && code <= 126); + if (opts.rfc6532) { + accept = accept || isUTF8NonAscii(tok); + } + return accept; + }); + }, + obsDtext + )() + ); + } + + // domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS] + function domainLiteral() { + return wrap('domain-literal', and( + invis(opt(cfws)), + literal('['), + star(and(opt(fws), dtext)), + opt(fws), + literal(']'), + invis(opt(cfws)) + )()); + } + + // domain = dot-atom / domain-literal / obs-domain + function domain() { + return wrap('domain', function domainCheckTLD() { + var result = or(obsDomain, dotAtom, domainLiteral)(); + if (opts.rejectTLD) { + if (result && result.semantic && result.semantic.indexOf('.') < 0) { + return null; + } + } + // strip all whitespace from domains + if (result) { + result.semantic = result.semantic.replace(/\s+/g, ''); + } + return result; + }()); + } + + // addr-spec = local-part "@" domain + function addrSpec() { + return wrap('addr-spec', and( + localPart, literal('@'), domain + )()); + } + + // 3.6.2 Originator Fields + // Below we only parse the field body, not the name of the field + // like "From:", "Sender:", or "Reply-To:". Other libraries that + // parse email headers can parse those and defer to these productions + // for the "RFC 5322" part. + + // RFC 6854 2.1. Replacement of RFC 5322, Section 3.6.2. Originator Fields + // from = "From:" (mailbox-list / address-list) CRLF + function fromSpec() { + return wrap('from', or( + mailboxList, + addressList + )()); + } + + // RFC 6854 2.1. Replacement of RFC 5322, Section 3.6.2. Originator Fields + // sender = "Sender:" (mailbox / address) CRLF + function senderSpec() { + return wrap('sender', or( + mailbox, + address + )()); + } + + // RFC 6854 2.1. Replacement of RFC 5322, Section 3.6.2. Originator Fields + // reply-to = "Reply-To:" address-list CRLF + function replyToSpec() { + return wrap('reply-to', addressList()); + } + + // 4.1. Miscellaneous Obsolete Tokens + + // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control + // %d11 / ; characters that do not + // %d12 / ; include the carriage + // %d14-31 / ; return, line feed, and + // %d127 ; white space characters + function obsNoWsCtl() { + return opts.strict ? null : wrap('obs-NO-WS-CTL', compareToken(function (tok) { + var code = tok.charCodeAt(0); + return ((1 <= code && code <= 8) || + (11 === code || 12 === code) || + (14 <= code && code <= 31) || + (127 === code)); + })); + } + + // obs-ctext = obs-NO-WS-CTL + function obsCtext() { return opts.strict ? null : wrap('obs-ctext', obsNoWsCtl()); } + + // obs-qtext = obs-NO-WS-CTL + function obsQtext() { return opts.strict ? null : wrap('obs-qtext', obsNoWsCtl()); } + + // obs-qp = "\" (%d0 / obs-NO-WS-CTL / LF / CR) + function obsQP() { + return opts.strict ? null : wrap('obs-qp', and( + literal('\\'), + or(literal('\0'), obsNoWsCtl, lf, cr) + )()); + } + + // obs-phrase = word *(word / "." / CFWS) + function obsPhrase() { + if (opts.strict ) return null; + return opts.atInDisplayName ? wrap('obs-phrase', and( + word, + star(or(word, literal('.'), literal('@'), colwsp(cfws))) + )()) : + wrap('obs-phrase', and( + word, + star(or(word, literal('.'), colwsp(cfws))) + )()); + } + + // 4.2. Obsolete Folding White Space + + // NOTE: read the errata http://www.rfc-editor.org/errata_search.php?rfc=5322&eid=1908 + // obs-FWS = 1*([CRLF] WSP) + function obsFws() { + return opts.strict ? null : wrap('obs-FWS', star( + and(invis(opt(crlf)), wsp), + 1 + )()); + } + + // 4.4. Obsolete Addressing + + // obs-angle-addr = [CFWS] "<" obs-route addr-spec ">" [CFWS] + function obsAngleAddr() { + return opts.strict ? null : wrap('obs-angle-addr', and( + invis(opt(cfws)), + literal('<'), + obsRoute, + addrSpec, + literal('>'), + invis(opt(cfws)) + )()); + } + + // obs-route = obs-domain-list ":" + function obsRoute() { + return opts.strict ? null : wrap('obs-route', and( + obsDomainList, + literal(':') + )()); + } + + // obs-domain-list = *(CFWS / ",") "@" domain + // *("," [CFWS] ["@" domain]) + function obsDomainList() { + return opts.strict ? null : wrap('obs-domain-list', and( + star(or(invis(cfws), literal(','))), + literal('@'), + domain, + star(and( + literal(','), + invis(opt(cfws)), + opt(and(literal('@'), domain)) + )) + )()); + } + + // obs-mbox-list = *([CFWS] ",") mailbox *("," [mailbox / CFWS]) + function obsMboxList() { + return opts.strict ? null : wrap('obs-mbox-list', and( + star(and( + invis(opt(cfws)), + literal(',') + )), + mailbox, + star(and( + literal(','), + opt(and( + mailbox, + invis(cfws) + )) + )) + )()); + } + + // obs-addr-list = *([CFWS] ",") address *("," [address / CFWS]) + function obsAddrList() { + return opts.strict ? null : wrap('obs-addr-list', and( + star(and( + invis(opt(cfws)), + literal(',') + )), + address, + star(and( + literal(','), + opt(and( + address, + invis(cfws) + )) + )) + )()); + } + + // obs-group-list = 1*([CFWS] ",") [CFWS] + function obsGroupList() { + return opts.strict ? null : wrap('obs-group-list', and( + star(and( + invis(opt(cfws)), + literal(',') + ), 1), + invis(opt(cfws)) + )()); + } + + // obs-local-part = word *("." word) + function obsLocalPart() { + return opts.strict ? null : wrap('obs-local-part', and(word, star(and(literal('.'), word)))()); + } + + // obs-domain = atom *("." atom) + function obsDomain() { + return opts.strict ? null : wrap('obs-domain', and(atom, star(and(literal('.'), atom)))()); + } + + // obs-dtext = obs-NO-WS-CTL / quoted-pair + function obsDtext() { + return opts.strict ? null : wrap('obs-dtext', or(obsNoWsCtl, quotedPair)()); + } + + ///////////////////////////////////////////////////// + + // ast analysis + + function findNode(name, root) { + var i, stack, node; + if (root === null || root === undefined) { return null; } + stack = [root]; + while (stack.length > 0) { + node = stack.pop(); + if (node.name === name) { + return node; + } + for (i = node.children.length - 1; i >= 0; i -= 1) { + stack.push(node.children[i]); + } + } + return null; + } + + function findAllNodes(name, root) { + var i, stack, node, result; + if (root === null || root === undefined) { return null; } + stack = [root]; + result = []; + while (stack.length > 0) { + node = stack.pop(); + if (node.name === name) { + result.push(node); + } + for (i = node.children.length - 1; i >= 0; i -= 1) { + stack.push(node.children[i]); + } + } + return result; + } + + function findAllNodesNoChildren(names, root) { + var i, stack, node, result, namesLookup; + if (root === null || root === undefined) { return null; } + stack = [root]; + result = []; + namesLookup = {}; + for (i = 0; i < names.length; i += 1) { + namesLookup[names[i]] = true; + } + + while (stack.length > 0) { + node = stack.pop(); + if (node.name in namesLookup) { + result.push(node); + // don't look at children (hence findAllNodesNoChildren) + } else { + for (i = node.children.length - 1; i >= 0; i -= 1) { + stack.push(node.children[i]); + } + } + } + return result; + } + + function giveResult(ast) { + var addresses, groupsAndMailboxes, i, groupOrMailbox, result; + if (ast === null) { + return null; + } + addresses = []; + + // An address is a 'group' (i.e. a list of mailboxes) or a 'mailbox'. + groupsAndMailboxes = findAllNodesNoChildren(['group', 'mailbox'], ast); + for (i = 0; i < groupsAndMailboxes.length; i += 1) { + groupOrMailbox = groupsAndMailboxes[i]; + if (groupOrMailbox.name === 'group') { + addresses.push(giveResultGroup(groupOrMailbox)); + } else if (groupOrMailbox.name === 'mailbox') { + addresses.push(giveResultMailbox(groupOrMailbox)); + } + } + + result = { + ast: ast, + addresses: addresses, + }; + if (opts.simple) { + result = simplifyResult(result); + } + if (opts.oneResult) { + return oneResult(result); + } + if (opts.simple) { + return result && result.addresses; + } else { + return result; + } + } + + function giveResultGroup(group) { + var i; + var groupName = findNode('display-name', group); + var groupResultMailboxes = []; + var mailboxes = findAllNodesNoChildren(['mailbox'], group); + for (i = 0; i < mailboxes.length; i += 1) { + groupResultMailboxes.push(giveResultMailbox(mailboxes[i])); + } + return { + node: group, + parts: { + name: groupName, + }, + type: group.name, // 'group' + name: grabSemantic(groupName), + addresses: groupResultMailboxes, + }; + } + + function giveResultMailbox(mailbox) { + var name = findNode('display-name', mailbox); + var aspec = findNode('addr-spec', mailbox); + var cfws = findAllNodes('cfws', mailbox); + var comments = findAllNodesNoChildren(['comment'], mailbox); + + + var local = findNode('local-part', aspec); + var domain = findNode('domain', aspec); + return { + node: mailbox, + parts: { + name: name, + address: aspec, + local: local, + domain: domain, + comments: cfws + }, + type: mailbox.name, // 'mailbox' + name: grabSemantic(name), + address: grabSemantic(aspec), + local: grabSemantic(local), + domain: grabSemantic(domain), + comments: concatComments(comments), + groupName: grabSemantic(mailbox.groupName), + }; + } + + function grabSemantic(n) { + return n !== null && n !== undefined ? n.semantic : null; + } + + function simplifyResult(result) { + var i; + if (result && result.addresses) { + for (i = 0; i < result.addresses.length; i += 1) { + delete result.addresses[i].node; + } + } + return result; + } + + function concatComments(comments) { + var result = ''; + if (comments) { + for (var i = 0; i < comments.length; i += 1) { + result += grabSemantic(comments[i]); + } + } + return result; + } + + function oneResult(result) { + if (!result) { return null; } + if (!opts.partial && result.addresses.length > 1) { return null; } + return result.addresses && result.addresses[0]; + } + + ///////////////////////////////////////////////////// + + var parseString, pos, len, parsed, startProduction; + + opts = handleOpts(opts, {}); + if (opts === null) { return null; } + + parseString = opts.input; + + startProduction = { + 'address': address, + 'address-list': addressList, + 'angle-addr': angleAddr, + 'from': fromSpec, + 'group': group, + 'mailbox': mailbox, + 'mailbox-list': mailboxList, + 'reply-to': replyToSpec, + 'sender': senderSpec, + }[opts.startAt] || addressList; + + if (!opts.strict) { + initialize(); + opts.strict = true; + parsed = startProduction(parseString); + if (opts.partial || !inStr()) { + return giveResult(parsed); + } + opts.strict = false; + } + + initialize(); + parsed = startProduction(parseString); + if (!opts.partial && inStr()) { return null; } + return giveResult(parsed); + } + + function parseOneAddressSimple(opts) { + return parse5322(handleOpts(opts, { + oneResult: true, + rfc6532: true, + simple: true, + startAt: 'address-list', + })); + } + + function parseAddressListSimple(opts) { + return parse5322(handleOpts(opts, { + rfc6532: true, + simple: true, + startAt: 'address-list', + })); + } + + function parseFromSimple(opts) { + return parse5322(handleOpts(opts, { + rfc6532: true, + simple: true, + startAt: 'from', + })); + } + + function parseSenderSimple(opts) { + return parse5322(handleOpts(opts, { + oneResult: true, + rfc6532: true, + simple: true, + startAt: 'sender', + })); + } + + function parseReplyToSimple(opts) { + return parse5322(handleOpts(opts, { + rfc6532: true, + simple: true, + startAt: 'reply-to', + })); + } + + function handleOpts(opts, defs) { + function isString(str) { + return Object.prototype.toString.call(str) === '[object String]'; + } + + function isObject(o) { + return o === Object(o); + } + + function isNullUndef(o) { + return o === null || o === undefined; + } + + var defaults, o; + + if (isString(opts)) { + opts = { input: opts }; + } else if (!isObject(opts)) { + return null; + } + + if (!isString(opts.input)) { return null; } + if (!defs) { return null; } + + defaults = { + oneResult: false, + partial: false, + rejectTLD: false, + rfc6532: false, + simple: false, + startAt: 'address-list', + strict: false, + atInDisplayName: false + }; + + for (o in defaults) { + if (isNullUndef(opts[o])) { + opts[o] = !isNullUndef(defs[o]) ? defs[o] : defaults[o]; + } + } + return opts; + } + + parse5322.parseOneAddress = parseOneAddressSimple; + parse5322.parseAddressList = parseAddressListSimple; + parse5322.parseFrom = parseFromSimple; + parse5322.parseSender = parseSenderSimple; + parse5322.parseReplyTo = parseReplyToSimple; + + { + module.exports = parse5322; + } + + }()); + }); + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * Implementation of the User ID Packet (Tag 13) + * + * A User ID packet consists of UTF-8 text that is intended to represent + * the name and email address of the key holder. By convention, it + * includes an RFC 2822 [RFC2822] mail name-addr, but there are no + * restrictions on its content. The packet length in the header + * specifies the length of the User ID. + */ + class UserIDPacket { + static get tag() { + return enums.packet.userID; + } + + constructor() { + /** A string containing the user id. Usually in the form + * John Doe + * @type {String} + */ + this.userID = ''; + + this.name = ''; + this.email = ''; + this.comment = ''; + } + + /** + * Create UserIDPacket instance from object + * @param {Object} userID - Object specifying userID name, email and comment + * @returns {UserIDPacket} + * @static + */ + static fromObject(userID) { + if (util.isString(userID) || + (userID.name && !util.isString(userID.name)) || + (userID.email && !util.isEmailAddress(userID.email)) || + (userID.comment && !util.isString(userID.comment))) { + throw Error('Invalid user ID format'); + } + const packet = new UserIDPacket(); + Object.assign(packet, userID); + const components = []; + if (packet.name) components.push(packet.name); + if (packet.comment) components.push(`(${packet.comment})`); + if (packet.email) components.push(`<${packet.email}>`); + packet.userID = components.join(' '); + return packet; + } + + /** + * Parsing function for a user id packet (tag 13). + * @param {Uint8Array} input - Payload of a tag 13 packet + */ + read(bytes, config$1 = config) { + const userID = util.decodeUTF8(bytes); + if (userID.length > config$1.maxUserIDLength) { + throw Error('User ID string is too long'); + } + try { + const { name, address: email, comments } = emailAddresses.parseOneAddress({ input: userID, atInDisplayName: true }); + this.comment = comments.replace(/^\(|\)$/g, ''); + this.name = name; + this.email = email; + } catch (e) {} + this.userID = userID; + } + + /** + * Creates a binary representation of the user id packet + * @returns {Uint8Array} Binary representation. + */ + write() { + return util.encodeUTF8(this.userID); + } + + equals(otherUserID) { + return otherUserID && otherUserID.userID === this.userID; + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + /** + * A Secret-Subkey packet (tag 7) is the subkey analog of the Secret + * Key packet and has exactly the same format. + * @extends SecretKeyPacket + */ + class SecretSubkeyPacket extends SecretKeyPacket { + static get tag() { + return enums.packet.secretSubkey; + } + + /** + * @param {Date} [date] - Creation date + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + constructor(date = new Date(), config$1 = config) { + super(date, config$1); + } + } + + /** + * Implementation of the Trust Packet (Tag 12) + * + * {@link https://tools.ietf.org/html/rfc4880#section-5.10|RFC4880 5.10}: + * The Trust packet is used only within keyrings and is not normally + * exported. Trust packets contain data that record the user's + * specifications of which key holders are trustworthy introducers, + * along with other information that implementing software uses for + * trust information. The format of Trust packets is defined by a given + * implementation. + * + * Trust packets SHOULD NOT be emitted to output streams that are + * transferred to other users, and they SHOULD be ignored on any input + * other than local keyring files. + */ + class TrustPacket { + static get tag() { + return enums.packet.trust; + } + + /** + * Parsing function for a trust packet (tag 12). + * Currently not implemented as we ignore trust packets + */ + read() { + throw new UnsupportedError('Trust packets are not supported'); + } + + write() { + throw new UnsupportedError('Trust packets are not supported'); + } + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + // A Signature can contain the following packets + const allowedPackets$1 = /*#__PURE__*/ util.constructAllowedPackets([SignaturePacket]); + + /** + * Class that represents an OpenPGP signature. + */ + class Signature$2 { + /** + * @param {PacketList} packetlist - The signature packets + */ + constructor(packetlist) { + this.packets = packetlist || new PacketList(); + } + + /** + * Returns binary encoded signature + * @returns {ReadableStream} Binary signature. + */ + write() { + return this.packets.write(); + } + + /** + * Returns ASCII armored text of signature + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {ReadableStream} ASCII armor. + */ + armor(config$1 = config) { + return armor(enums.armor.signature, this.write(), undefined, undefined, undefined, config$1); + } + + /** + * Returns an array of KeyIDs of all of the issuers who created this signature + * @returns {Array} The Key IDs of the signing keys + */ + getSigningKeyIDs() { + return this.packets.map(packet => packet.issuerKeyID); + } + } + + /** + * reads an (optionally armored) OpenPGP signature and returns a signature object + * @param {Object} options + * @param {String} [options.armoredSignature] - Armored signature to be parsed + * @param {Uint8Array} [options.binarySignature] - Binary signature to be parsed + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} New signature object. + * @async + * @static + */ + async function readSignature({ armoredSignature, binarySignature, config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; + let input = armoredSignature || binarySignature; + if (!input) { + throw Error('readSignature: must pass options object containing `armoredSignature` or `binarySignature`'); + } + if (armoredSignature && !util.isString(armoredSignature)) { + throw Error('readSignature: options.armoredSignature must be a string'); + } + if (binarySignature && !util.isUint8Array(binarySignature)) { + throw Error('readSignature: options.binarySignature must be a Uint8Array'); + } + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + if (armoredSignature) { + const { type, data } = await unarmor(input, config$1); + if (type !== enums.armor.signature) { + throw Error('Armored text not of type signature'); + } + input = data; + } + const packetlist = await PacketList.fromBinary(input, allowedPackets$1, config$1); + return new Signature$2(packetlist); + } + + /** + * @fileoverview Provides helpers methods for key module + * @module key/helper + * @private + */ + + async function generateSecretSubkey(options, config) { + const secretSubkeyPacket = new SecretSubkeyPacket(options.date, config); + secretSubkeyPacket.packets = null; + secretSubkeyPacket.algorithm = enums.write(enums.publicKey, options.algorithm); + await secretSubkeyPacket.generate(options.rsaBits, options.curve); + await secretSubkeyPacket.computeFingerprintAndKeyID(); + return secretSubkeyPacket; + } + + async function generateSecretKey(options, config) { + const secretKeyPacket = new SecretKeyPacket(options.date, config); + secretKeyPacket.packets = null; + secretKeyPacket.algorithm = enums.write(enums.publicKey, options.algorithm); + await secretKeyPacket.generate(options.rsaBits, options.curve, options.config); + await secretKeyPacket.computeFingerprintAndKeyID(); + return secretKeyPacket; + } + + /** + * Returns the valid and non-expired signature that has the latest creation date, while ignoring signatures created in the future. + * @param {Array} signatures - List of signatures + * @param {PublicKeyPacket|PublicSubkeyPacket} publicKey - Public key packet to verify the signature + * @param {Date} date - Use the given date instead of the current time + * @param {Object} config - full configuration + * @returns {Promise} The latest valid signature. + * @async + */ + async function getLatestValidSignature(signatures, publicKey, signatureType, dataToVerify, date = new Date(), config) { + let latestValid; + let exception; + for (let i = signatures.length - 1; i >= 0; i--) { + try { + if ( + (!latestValid || signatures[i].created >= latestValid.created) + ) { + await signatures[i].verify(publicKey, signatureType, dataToVerify, date, undefined, config); + latestValid = signatures[i]; + } + } catch (e) { + exception = e; + } + } + if (!latestValid) { + throw util.wrapError( + `Could not find valid ${enums.read(enums.signature, signatureType)} signature in key ${publicKey.getKeyID().toHex()}` + .replace('certGeneric ', 'self-') + .replace(/([a-z])([A-Z])/g, (_, $1, $2) => $1 + ' ' + $2.toLowerCase()), + exception); + } + return latestValid; + } + + function isDataExpired(keyPacket, signature, date = new Date()) { + const normDate = util.normalizeDate(date); + if (normDate !== null) { + const expirationTime = getKeyExpirationTime(keyPacket, signature); + return !(keyPacket.created <= normDate && normDate < expirationTime); + } + return false; + } + + /** + * Create Binding signature to the key according to the {@link https://tools.ietf.org/html/rfc4880#section-5.2.1} + * @param {SecretSubkeyPacket} subkey - Subkey key packet + * @param {SecretKeyPacket} primaryKey - Primary key packet + * @param {Object} options + * @param {Object} config - Full configuration + */ + async function createBindingSignature(subkey, primaryKey, options, config) { + const dataToSign = {}; + dataToSign.key = primaryKey; + dataToSign.bind = subkey; + const signatureProperties = { signatureType: enums.signature.subkeyBinding }; + if (options.sign) { + signatureProperties.keyFlags = [enums.keyFlags.signData]; + signatureProperties.embeddedSignature = await createSignaturePacket(dataToSign, null, subkey, { + signatureType: enums.signature.keyBinding + }, options.date, undefined, undefined, undefined, config); + } else { + signatureProperties.keyFlags = [enums.keyFlags.encryptCommunication | enums.keyFlags.encryptStorage]; + } + if (options.keyExpirationTime > 0) { + signatureProperties.keyExpirationTime = options.keyExpirationTime; + signatureProperties.keyNeverExpires = false; + } + const subkeySignaturePacket = await createSignaturePacket(dataToSign, null, primaryKey, signatureProperties, options.date, undefined, undefined, undefined, config); + return subkeySignaturePacket; + } + + /** + * Returns the preferred signature hash algorithm of a key + * @param {Key} [key] - The key to get preferences from + * @param {SecretKeyPacket|SecretSubkeyPacket} keyPacket - key packet used for signing + * @param {Date} [date] - Use the given date for verification instead of the current time + * @param {Object} [userID] - User ID + * @param {Object} config - full configuration + * @returns {Promise} + * @async + */ + async function getPreferredHashAlgo(key, keyPacket, date = new Date(), userID = {}, config) { + let hashAlgo = config.preferredHashAlgorithm; + let prefAlgo = hashAlgo; + if (key) { + const primaryUser = await key.getPrimaryUser(date, userID, config); + if (primaryUser.selfCertification.preferredHashAlgorithms) { + [prefAlgo] = primaryUser.selfCertification.preferredHashAlgorithms; + hashAlgo = mod.hash.getHashByteLength(hashAlgo) <= mod.hash.getHashByteLength(prefAlgo) ? + prefAlgo : hashAlgo; + } + } + switch (keyPacket.algorithm) { + case enums.publicKey.ecdsa: + case enums.publicKey.eddsaLegacy: + case enums.publicKey.ed25519: + prefAlgo = mod.getPreferredCurveHashAlgo(keyPacket.algorithm, keyPacket.publicParams.oid); + } + return mod.hash.getHashByteLength(hashAlgo) <= mod.hash.getHashByteLength(prefAlgo) ? + prefAlgo : hashAlgo; + } + + /** + * Returns the preferred symmetric/aead/compression algorithm for a set of keys + * @param {'symmetric'|'aead'|'compression'} type - Type of preference to return + * @param {Array} [keys] - Set of keys + * @param {Date} [date] - Use the given date for verification instead of the current time + * @param {Array} [userIDs] - User IDs + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} Preferred algorithm + * @async + */ + async function getPreferredAlgo(type, keys = [], date = new Date(), userIDs = [], config$1 = config) { + const defaultAlgo = { // these are all must-implement in rfc4880bis + 'symmetric': enums.symmetric.aes128, + 'aead': enums.aead.eax, + 'compression': enums.compression.uncompressed + }[type]; + const preferredSenderAlgo = { + 'symmetric': config$1.preferredSymmetricAlgorithm, + 'aead': config$1.preferredAEADAlgorithm, + 'compression': config$1.preferredCompressionAlgorithm + }[type]; + const prefPropertyName = { + 'symmetric': 'preferredSymmetricAlgorithms', + 'aead': 'preferredAEADAlgorithms', + 'compression': 'preferredCompressionAlgorithms' + }[type]; + + // if preferredSenderAlgo appears in the prefs of all recipients, we pick it + // otherwise we use the default algo + // if no keys are available, preferredSenderAlgo is returned + const senderAlgoSupport = await Promise.all(keys.map(async function(key, i) { + const primaryUser = await key.getPrimaryUser(date, userIDs[i], config$1); + const recipientPrefs = primaryUser.selfCertification[prefPropertyName]; + return !!recipientPrefs && recipientPrefs.indexOf(preferredSenderAlgo) >= 0; + })); + return senderAlgoSupport.every(Boolean) ? preferredSenderAlgo : defaultAlgo; + } + + /** + * Create signature packet + * @param {Object} dataToSign - Contains packets to be signed + * @param {PrivateKey} privateKey - key to get preferences from + * @param {SecretKeyPacket| + * SecretSubkeyPacket} signingKeyPacket secret key packet for signing + * @param {Object} [signatureProperties] - Properties to write on the signature packet before signing + * @param {Date} [date] - Override the creationtime of the signature + * @param {Object} [userID] - User ID + * @param {Array} [notations] - Notation Data to add to the signature, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }] + * @param {Object} [detached] - Whether to create a detached signature packet + * @param {Object} config - full configuration + * @returns {Promise} Signature packet. + */ + async function createSignaturePacket(dataToSign, privateKey, signingKeyPacket, signatureProperties, date, userID, notations = [], detached = false, config) { + if (signingKeyPacket.isDummy()) { + throw Error('Cannot sign with a gnu-dummy key.'); + } + if (!signingKeyPacket.isDecrypted()) { + throw Error('Signing key is not decrypted.'); + } + const signaturePacket = new SignaturePacket(); + Object.assign(signaturePacket, signatureProperties); + signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm; + signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey, signingKeyPacket, date, userID, config); + signaturePacket.rawNotations = notations; + await signaturePacket.sign(signingKeyPacket, dataToSign, date, detached); + return signaturePacket; + } + + /** + * Merges signatures from source[attr] to dest[attr] + * @param {Object} source + * @param {Object} dest + * @param {String} attr + * @param {Date} [date] - date to use for signature expiration check, instead of the current time + * @param {Function} [checkFn] - signature only merged if true + */ + async function mergeSignatures(source, dest, attr, date = new Date(), checkFn) { + source = source[attr]; + if (source) { + if (!dest[attr].length) { + dest[attr] = source; + } else { + await Promise.all(source.map(async function(sourceSig) { + if (!sourceSig.isExpired(date) && (!checkFn || await checkFn(sourceSig)) && + !dest[attr].some(function(destSig) { + return util.equalsUint8Array(destSig.writeParams(), sourceSig.writeParams()); + })) { + dest[attr].push(sourceSig); + } + })); + } + } + } + + /** + * Checks if a given certificate or binding signature is revoked + * @param {SecretKeyPacket| + * PublicKeyPacket} primaryKey The primary key packet + * @param {Object} dataToVerify - The data to check + * @param {Array} revocations - The revocation signatures to check + * @param {SignaturePacket} signature - The certificate or signature to check + * @param {PublicSubkeyPacket| + * SecretSubkeyPacket| + * PublicKeyPacket| + * SecretKeyPacket} key, optional The key packet to verify the signature, instead of the primary key + * @param {Date} date - Use the given date instead of the current time + * @param {Object} config - Full configuration + * @returns {Promise} True if the signature revokes the data. + * @async + */ + async function isDataRevoked(primaryKey, signatureType, dataToVerify, revocations, signature, key, date = new Date(), config) { + key = key || primaryKey; + const revocationKeyIDs = []; + await Promise.all(revocations.map(async function(revocationSignature) { + try { + if ( + // Note: a third-party revocation signature could legitimately revoke a + // self-signature if the signature has an authorized revocation key. + // However, we don't support passing authorized revocation keys, nor + // verifying such revocation signatures. Instead, we indicate an error + // when parsing a key with an authorized revocation key, and ignore + // third-party revocation signatures here. (It could also be revoking a + // third-party key certification, which should only affect + // `verifyAllCertifications`.) + !signature || revocationSignature.issuerKeyID.equals(signature.issuerKeyID) + ) { + await revocationSignature.verify( + key, signatureType, dataToVerify, config.revocationsExpire ? date : null, false, config + ); + + // TODO get an identifier of the revoked object instead + revocationKeyIDs.push(revocationSignature.issuerKeyID); + } + } catch (e) {} + })); + // TODO further verify that this is the signature that should be revoked + if (signature) { + signature.revoked = revocationKeyIDs.some(keyID => keyID.equals(signature.issuerKeyID)) ? true : + signature.revoked || false; + return signature.revoked; + } + return revocationKeyIDs.length > 0; + } + + /** + * Returns key expiration time based on the given certification signature. + * The expiration time of the signature is ignored. + * @param {PublicSubkeyPacket|PublicKeyPacket} keyPacket - key to check + * @param {SignaturePacket} signature - signature to process + * @returns {Date|Infinity} expiration time or infinity if the key does not expire + */ + function getKeyExpirationTime(keyPacket, signature) { + let expirationTime; + // check V4 expiration time + if (signature.keyNeverExpires === false) { + expirationTime = keyPacket.created.getTime() + signature.keyExpirationTime * 1000; + } + return expirationTime ? new Date(expirationTime) : Infinity; + } + + /** + * Returns whether aead is supported by all keys in the set + * @param {Array} keys - Set of keys + * @param {Date} [date] - Use the given date for verification instead of the current time + * @param {Array} [userIDs] - User IDs + * @param {Object} config - full configuration + * @returns {Promise} + * @async + */ + async function isAEADSupported(keys, date = new Date(), userIDs = [], config$1 = config) { + let supported = true; + // TODO replace when Promise.some or Promise.any are implemented + await Promise.all(keys.map(async function(key, i) { + const primaryUser = await key.getPrimaryUser(date, userIDs[i], config$1); + if (!primaryUser.selfCertification.features || + !(primaryUser.selfCertification.features[0] & enums.features.aead)) { + supported = false; + } + })); + return supported; + } + + function sanitizeKeyOptions(options, subkeyDefaults = {}) { + options.type = options.type || subkeyDefaults.type; + options.curve = options.curve || subkeyDefaults.curve; + options.rsaBits = options.rsaBits || subkeyDefaults.rsaBits; + options.keyExpirationTime = options.keyExpirationTime !== undefined ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime; + options.passphrase = util.isString(options.passphrase) ? options.passphrase : subkeyDefaults.passphrase; + options.date = options.date || subkeyDefaults.date; + + options.sign = options.sign || false; + + switch (options.type) { + case 'ecc': + try { + options.curve = enums.write(enums.curve, options.curve); + } catch (e) { + throw Error('Unknown curve'); + } + if (options.curve === enums.curve.ed25519Legacy || options.curve === enums.curve.curve25519Legacy) { + options.curve = options.sign ? enums.curve.ed25519Legacy : enums.curve.curve25519Legacy; + } + if (options.sign) { + options.algorithm = options.curve === enums.curve.ed25519Legacy ? enums.publicKey.eddsaLegacy : enums.publicKey.ecdsa; + } else { + options.algorithm = enums.publicKey.ecdh; + } + break; + case 'rsa': + options.algorithm = enums.publicKey.rsaEncryptSign; + break; + default: + throw Error(`Unsupported key type ${options.type}`); + } + return options; + } + + function isValidSigningKeyPacket(keyPacket, signature) { + const keyAlgo = keyPacket.algorithm; + return keyAlgo !== enums.publicKey.rsaEncrypt && + keyAlgo !== enums.publicKey.elgamal && + keyAlgo !== enums.publicKey.ecdh && + keyAlgo !== enums.publicKey.x25519 && + (!signature.keyFlags || + (signature.keyFlags[0] & enums.keyFlags.signData) !== 0); + } + + function isValidEncryptionKeyPacket(keyPacket, signature) { + const keyAlgo = keyPacket.algorithm; + return keyAlgo !== enums.publicKey.dsa && + keyAlgo !== enums.publicKey.rsaSign && + keyAlgo !== enums.publicKey.ecdsa && + keyAlgo !== enums.publicKey.eddsaLegacy && + keyAlgo !== enums.publicKey.ed25519 && + (!signature.keyFlags || + (signature.keyFlags[0] & enums.keyFlags.encryptCommunication) !== 0 || + (signature.keyFlags[0] & enums.keyFlags.encryptStorage) !== 0); + } + + function isValidDecryptionKeyPacket(signature, config) { + if (config.allowInsecureDecryptionWithSigningKeys) { + // This is only relevant for RSA keys, all other signing algorithms cannot decrypt + return true; + } + + return !signature.keyFlags || + (signature.keyFlags[0] & enums.keyFlags.encryptCommunication) !== 0 || + (signature.keyFlags[0] & enums.keyFlags.encryptStorage) !== 0; + } + + /** + * Check key against blacklisted algorithms and minimum strength requirements. + * @param {SecretKeyPacket|PublicKeyPacket| + * SecretSubkeyPacket|PublicSubkeyPacket} keyPacket + * @param {Config} config + * @throws {Error} if the key packet does not meet the requirements + */ + function checkKeyRequirements(keyPacket, config) { + const keyAlgo = enums.write(enums.publicKey, keyPacket.algorithm); + const algoInfo = keyPacket.getAlgorithmInfo(); + if (config.rejectPublicKeyAlgorithms.has(keyAlgo)) { + throw Error(`${algoInfo.algorithm} keys are considered too weak.`); + } + switch (keyAlgo) { + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaSign: + case enums.publicKey.rsaEncrypt: + if (algoInfo.bits < config.minRSABits) { + throw Error(`RSA keys shorter than ${config.minRSABits} bits are considered too weak.`); + } + break; + case enums.publicKey.ecdsa: + case enums.publicKey.eddsaLegacy: + case enums.publicKey.ecdh: + if (config.rejectCurves.has(algoInfo.curve)) { + throw Error(`Support for ${algoInfo.algorithm} keys using curve ${algoInfo.curve} is disabled.`); + } + break; + } + } + + /** + * @module key/User + * @private + */ + + /** + * Class that represents an user ID or attribute packet and the relevant signatures. + * @param {UserIDPacket|UserAttributePacket} userPacket - packet containing the user info + * @param {Key} mainKey - reference to main Key object containing the primary key and subkeys that the user is associated with + */ + class User { + constructor(userPacket, mainKey) { + this.userID = userPacket.constructor.tag === enums.packet.userID ? userPacket : null; + this.userAttribute = userPacket.constructor.tag === enums.packet.userAttribute ? userPacket : null; + this.selfCertifications = []; + this.otherCertifications = []; + this.revocationSignatures = []; + this.mainKey = mainKey; + } + + /** + * Transforms structured user data to packetlist + * @returns {PacketList} + */ + toPacketList() { + const packetlist = new PacketList(); + packetlist.push(this.userID || this.userAttribute); + packetlist.push(...this.revocationSignatures); + packetlist.push(...this.selfCertifications); + packetlist.push(...this.otherCertifications); + return packetlist; + } + + /** + * Shallow clone + * @returns {User} + */ + clone() { + const user = new User(this.userID || this.userAttribute, this.mainKey); + user.selfCertifications = [...this.selfCertifications]; + user.otherCertifications = [...this.otherCertifications]; + user.revocationSignatures = [...this.revocationSignatures]; + return user; + } + + /** + * Generate third-party certifications over this user and its primary key + * @param {Array} signingKeys - Decrypted private keys for signing + * @param {Date} [date] - Date to use as creation date of the certificate, instead of the current time + * @param {Object} config - Full configuration + * @returns {Promise} New user with new certifications. + * @async + */ + async certify(signingKeys, date, config) { + const primaryKey = this.mainKey.keyPacket; + const dataToSign = { + userID: this.userID, + userAttribute: this.userAttribute, + key: primaryKey + }; + const user = new User(dataToSign.userID || dataToSign.userAttribute, this.mainKey); + user.otherCertifications = await Promise.all(signingKeys.map(async function(privateKey) { + if (!privateKey.isPrivate()) { + throw Error('Need private key for signing'); + } + if (privateKey.hasSameFingerprintAs(primaryKey)) { + throw Error("The user's own key can only be used for self-certifications"); + } + const signingKey = await privateKey.getSigningKey(undefined, date, undefined, config); + return createSignaturePacket(dataToSign, privateKey, signingKey.keyPacket, { + // Most OpenPGP implementations use generic certification (0x10) + signatureType: enums.signature.certGeneric, + keyFlags: [enums.keyFlags.certifyKeys | enums.keyFlags.signData] + }, date, undefined, undefined, undefined, config); + })); + await user.update(this, date, config); + return user; + } + + /** + * Checks if a given certificate of the user is revoked + * @param {SignaturePacket} certificate - The certificate to verify + * @param {PublicSubkeyPacket| + * SecretSubkeyPacket| + * PublicKeyPacket| + * SecretKeyPacket} [keyPacket] The key packet to verify the signature, instead of the primary key + * @param {Date} [date] - Use the given date for verification instead of the current time + * @param {Object} config - Full configuration + * @returns {Promise} True if the certificate is revoked. + * @async + */ + async isRevoked(certificate, keyPacket, date = new Date(), config$1 = config) { + const primaryKey = this.mainKey.keyPacket; + return isDataRevoked(primaryKey, enums.signature.certRevocation, { + key: primaryKey, + userID: this.userID, + userAttribute: this.userAttribute + }, this.revocationSignatures, certificate, keyPacket, date, config$1); + } + + /** + * Verifies the user certificate. + * @param {SignaturePacket} certificate - A certificate of this user + * @param {Array} verificationKeys - Array of keys to verify certificate signatures + * @param {Date} [date] - Use the given date instead of the current time + * @param {Object} config - Full configuration + * @returns {Promise} true if the certificate could be verified, or null if the verification keys do not correspond to the certificate + * @throws if the user certificate is invalid. + * @async + */ + async verifyCertificate(certificate, verificationKeys, date = new Date(), config) { + const that = this; + const primaryKey = this.mainKey.keyPacket; + const dataToVerify = { + userID: this.userID, + userAttribute: this.userAttribute, + key: primaryKey + }; + const { issuerKeyID } = certificate; + const issuerKeys = verificationKeys.filter(key => key.getKeys(issuerKeyID).length > 0); + if (issuerKeys.length === 0) { + return null; + } + await Promise.all(issuerKeys.map(async key => { + const signingKey = await key.getSigningKey(issuerKeyID, certificate.created, undefined, config); + if (certificate.revoked || await that.isRevoked(certificate, signingKey.keyPacket, date, config)) { + throw Error('User certificate is revoked'); + } + try { + await certificate.verify(signingKey.keyPacket, enums.signature.certGeneric, dataToVerify, date, undefined, config); + } catch (e) { + throw util.wrapError('User certificate is invalid', e); + } + })); + return true; + } + + /** + * Verifies all user certificates + * @param {Array} verificationKeys - Array of keys to verify certificate signatures + * @param {Date} [date] - Use the given date instead of the current time + * @param {Object} config - Full configuration + * @returns {Promise>} List of signer's keyID and validity of signature. + * Signature validity is null if the verification keys do not correspond to the certificate. + * @async + */ + async verifyAllCertifications(verificationKeys, date = new Date(), config) { + const that = this; + const certifications = this.selfCertifications.concat(this.otherCertifications); + return Promise.all(certifications.map(async certification => ({ + keyID: certification.issuerKeyID, + valid: await that.verifyCertificate(certification, verificationKeys, date, config).catch(() => false) + }))); + } + + /** + * Verify User. Checks for existence of self signatures, revocation signatures + * and validity of self signature. + * @param {Date} date - Use the given date instead of the current time + * @param {Object} config - Full configuration + * @returns {Promise} Status of user. + * @throws {Error} if there are no valid self signatures. + * @async + */ + async verify(date = new Date(), config) { + if (!this.selfCertifications.length) { + throw Error('No self-certifications found'); + } + const that = this; + const primaryKey = this.mainKey.keyPacket; + const dataToVerify = { + userID: this.userID, + userAttribute: this.userAttribute, + key: primaryKey + }; + // TODO replace when Promise.some or Promise.any are implemented + let exception; + for (let i = this.selfCertifications.length - 1; i >= 0; i--) { + try { + const selfCertification = this.selfCertifications[i]; + if (selfCertification.revoked || await that.isRevoked(selfCertification, undefined, date, config)) { + throw Error('Self-certification is revoked'); + } + try { + await selfCertification.verify(primaryKey, enums.signature.certGeneric, dataToVerify, date, undefined, config); + } catch (e) { + throw util.wrapError('Self-certification is invalid', e); + } + return true; + } catch (e) { + exception = e; + } + } + throw exception; + } + + /** + * Update user with new components from specified user + * @param {User} sourceUser - Source user to merge + * @param {Date} date - Date to verify the validity of signatures + * @param {Object} config - Full configuration + * @returns {Promise} + * @async + */ + async update(sourceUser, date, config) { + const primaryKey = this.mainKey.keyPacket; + const dataToVerify = { + userID: this.userID, + userAttribute: this.userAttribute, + key: primaryKey + }; + // self signatures + await mergeSignatures(sourceUser, this, 'selfCertifications', date, async function(srcSelfSig) { + try { + await srcSelfSig.verify(primaryKey, enums.signature.certGeneric, dataToVerify, date, false, config); + return true; + } catch (e) { + return false; + } + }); + // other signatures + await mergeSignatures(sourceUser, this, 'otherCertifications', date); + // revocation signatures + await mergeSignatures(sourceUser, this, 'revocationSignatures', date, function(srcRevSig) { + return isDataRevoked(primaryKey, enums.signature.certRevocation, dataToVerify, [srcRevSig], undefined, undefined, date, config); + }); + } + + /** + * Revokes the user + * @param {SecretKeyPacket} primaryKey - decrypted private primary key for revocation + * @param {Object} reasonForRevocation - optional, object indicating the reason for revocation + * @param {module:enums.reasonForRevocation} reasonForRevocation.flag optional, flag indicating the reason for revocation + * @param {String} reasonForRevocation.string optional, string explaining the reason for revocation + * @param {Date} date - optional, override the creationtime of the revocation signature + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} New user with revocation signature. + * @async + */ + async revoke( + primaryKey, + { + flag: reasonForRevocationFlag = enums.reasonForRevocation.noReason, + string: reasonForRevocationString = '' + } = {}, + date = new Date(), + config$1 = config + ) { + const dataToSign = { + userID: this.userID, + userAttribute: this.userAttribute, + key: primaryKey + }; + const user = new User(dataToSign.userID || dataToSign.userAttribute, this.mainKey); + user.revocationSignatures.push(await createSignaturePacket(dataToSign, null, primaryKey, { + signatureType: enums.signature.certRevocation, + reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), + reasonForRevocationString + }, date, undefined, undefined, false, config$1)); + await user.update(this); + return user; + } + } + + /** + * @module key/Subkey + * @private + */ + + /** + * Class that represents a subkey packet and the relevant signatures. + * @borrows PublicSubkeyPacket#getKeyID as Subkey#getKeyID + * @borrows PublicSubkeyPacket#getFingerprint as Subkey#getFingerprint + * @borrows PublicSubkeyPacket#hasSameFingerprintAs as Subkey#hasSameFingerprintAs + * @borrows PublicSubkeyPacket#getAlgorithmInfo as Subkey#getAlgorithmInfo + * @borrows PublicSubkeyPacket#getCreationTime as Subkey#getCreationTime + * @borrows PublicSubkeyPacket#isDecrypted as Subkey#isDecrypted + */ + class Subkey { + /** + * @param {SecretSubkeyPacket|PublicSubkeyPacket} subkeyPacket - subkey packet to hold in the Subkey + * @param {Key} mainKey - reference to main Key object, containing the primary key packet corresponding to the subkey + */ + constructor(subkeyPacket, mainKey) { + this.keyPacket = subkeyPacket; + this.bindingSignatures = []; + this.revocationSignatures = []; + this.mainKey = mainKey; + } + + /** + * Transforms structured subkey data to packetlist + * @returns {PacketList} + */ + toPacketList() { + const packetlist = new PacketList(); + packetlist.push(this.keyPacket); + packetlist.push(...this.revocationSignatures); + packetlist.push(...this.bindingSignatures); + return packetlist; + } + + /** + * Shallow clone + * @return {Subkey} + */ + clone() { + const subkey = new Subkey(this.keyPacket, this.mainKey); + subkey.bindingSignatures = [...this.bindingSignatures]; + subkey.revocationSignatures = [...this.revocationSignatures]; + return subkey; + } + + /** + * Checks if a binding signature of a subkey is revoked + * @param {SignaturePacket} signature - The binding signature to verify + * @param {PublicSubkeyPacket| + * SecretSubkeyPacket| + * PublicKeyPacket| + * SecretKeyPacket} key, optional The key to verify the signature + * @param {Date} [date] - Use the given date for verification instead of the current time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} True if the binding signature is revoked. + * @async + */ + async isRevoked(signature, key, date = new Date(), config$1 = config) { + const primaryKey = this.mainKey.keyPacket; + return isDataRevoked( + primaryKey, enums.signature.subkeyRevocation, { + key: primaryKey, + bind: this.keyPacket + }, this.revocationSignatures, signature, key, date, config$1 + ); + } + + /** + * Verify subkey. Checks for revocation signatures, expiration time + * and valid binding signature. + * @param {Date} date - Use the given date instead of the current time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} + * @throws {Error} if the subkey is invalid. + * @async + */ + async verify(date = new Date(), config$1 = config) { + const primaryKey = this.mainKey.keyPacket; + const dataToVerify = { key: primaryKey, bind: this.keyPacket }; + // check subkey binding signatures + const bindingSignature = await getLatestValidSignature(this.bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date, config$1); + // check binding signature is not revoked + if (bindingSignature.revoked || await this.isRevoked(bindingSignature, null, date, config$1)) { + throw Error('Subkey is revoked'); + } + // check for expiration time + if (isDataExpired(this.keyPacket, bindingSignature, date)) { + throw Error('Subkey is expired'); + } + return bindingSignature; + } + + /** + * Returns the expiration time of the subkey or Infinity if key does not expire. + * Returns null if the subkey is invalid. + * @param {Date} date - Use the given date instead of the current time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} + * @async + */ + async getExpirationTime(date = new Date(), config$1 = config) { + const primaryKey = this.mainKey.keyPacket; + const dataToVerify = { key: primaryKey, bind: this.keyPacket }; + let bindingSignature; + try { + bindingSignature = await getLatestValidSignature(this.bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date, config$1); + } catch (e) { + return null; + } + const keyExpiry = getKeyExpirationTime(this.keyPacket, bindingSignature); + const sigExpiry = bindingSignature.getExpirationTime(); + return keyExpiry < sigExpiry ? keyExpiry : sigExpiry; + } + + /** + * Update subkey with new components from specified subkey + * @param {Subkey} subkey - Source subkey to merge + * @param {Date} [date] - Date to verify validity of signatures + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @throws {Error} if update failed + * @async + */ + async update(subkey, date = new Date(), config$1 = config) { + const primaryKey = this.mainKey.keyPacket; + if (!this.hasSameFingerprintAs(subkey)) { + throw Error('Subkey update method: fingerprints of subkeys not equal'); + } + // key packet + if (this.keyPacket.constructor.tag === enums.packet.publicSubkey && + subkey.keyPacket.constructor.tag === enums.packet.secretSubkey) { + this.keyPacket = subkey.keyPacket; + } + // update missing binding signatures + const that = this; + const dataToVerify = { key: primaryKey, bind: that.keyPacket }; + await mergeSignatures(subkey, this, 'bindingSignatures', date, async function(srcBindSig) { + for (let i = 0; i < that.bindingSignatures.length; i++) { + if (that.bindingSignatures[i].issuerKeyID.equals(srcBindSig.issuerKeyID)) { + if (srcBindSig.created > that.bindingSignatures[i].created) { + that.bindingSignatures[i] = srcBindSig; + } + return false; + } + } + try { + await srcBindSig.verify(primaryKey, enums.signature.subkeyBinding, dataToVerify, date, undefined, config$1); + return true; + } catch (e) { + return false; + } + }); + // revocation signatures + await mergeSignatures(subkey, this, 'revocationSignatures', date, function(srcRevSig) { + return isDataRevoked(primaryKey, enums.signature.subkeyRevocation, dataToVerify, [srcRevSig], undefined, undefined, date, config$1); + }); + } + + /** + * Revokes the subkey + * @param {SecretKeyPacket} primaryKey - decrypted private primary key for revocation + * @param {Object} reasonForRevocation - optional, object indicating the reason for revocation + * @param {module:enums.reasonForRevocation} reasonForRevocation.flag optional, flag indicating the reason for revocation + * @param {String} reasonForRevocation.string optional, string explaining the reason for revocation + * @param {Date} date - optional, override the creationtime of the revocation signature + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} New subkey with revocation signature. + * @async + */ + async revoke( + primaryKey, + { + flag: reasonForRevocationFlag = enums.reasonForRevocation.noReason, + string: reasonForRevocationString = '' + } = {}, + date = new Date(), + config$1 = config + ) { + const dataToSign = { key: primaryKey, bind: this.keyPacket }; + const subkey = new Subkey(this.keyPacket, this.mainKey); + subkey.revocationSignatures.push(await createSignaturePacket(dataToSign, null, primaryKey, { + signatureType: enums.signature.subkeyRevocation, + reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), + reasonForRevocationString + }, date, undefined, undefined, false, config$1)); + await subkey.update(this); + return subkey; + } + + hasSameFingerprintAs(other) { + return this.keyPacket.hasSameFingerprintAs(other.keyPacket || other); + } + } + + ['getKeyID', 'getFingerprint', 'getAlgorithmInfo', 'getCreationTime', 'isDecrypted'].forEach(name => { + Subkey.prototype[name] = + function() { + return this.keyPacket[name](); + }; + }); + + // GPG4Browsers - An OpenPGP implementation in javascript + + // A key revocation certificate can contain the following packets + const allowedRevocationPackets = /*#__PURE__*/ util.constructAllowedPackets([SignaturePacket]); + const mainKeyPacketTags = new Set([enums.packet.publicKey, enums.packet.privateKey]); + const keyPacketTags = new Set([ + enums.packet.publicKey, enums.packet.privateKey, + enums.packet.publicSubkey, enums.packet.privateSubkey + ]); + + /** + * Abstract class that represents an OpenPGP key. Must contain a primary key. + * Can contain additional subkeys, signatures, user ids, user attributes. + * @borrows PublicKeyPacket#getKeyID as Key#getKeyID + * @borrows PublicKeyPacket#getFingerprint as Key#getFingerprint + * @borrows PublicKeyPacket#hasSameFingerprintAs as Key#hasSameFingerprintAs + * @borrows PublicKeyPacket#getAlgorithmInfo as Key#getAlgorithmInfo + * @borrows PublicKeyPacket#getCreationTime as Key#getCreationTime + */ + class Key { + /** + * Transforms packetlist to structured key data + * @param {PacketList} packetlist - The packets that form a key + * @param {Set} disallowedPackets - disallowed packet tags + */ + packetListToStructure(packetlist, disallowedPackets = new Set()) { + let user; + let primaryKeyID; + let subkey; + let ignoreUntil; + + for (const packet of packetlist) { + + if (packet instanceof UnparseablePacket) { + const isUnparseableKeyPacket = keyPacketTags.has(packet.tag); + if (isUnparseableKeyPacket && !ignoreUntil) { + // Since non-key packets apply to the preceding key packet, if a (sub)key is Unparseable we must + // discard all non-key packets that follow, until another (sub)key packet is found. + if (mainKeyPacketTags.has(packet.tag)) { + ignoreUntil = mainKeyPacketTags; + } else { + ignoreUntil = keyPacketTags; + } + } + continue; + } + + const tag = packet.constructor.tag; + if (ignoreUntil) { + if (!ignoreUntil.has(tag)) continue; + ignoreUntil = null; + } + if (disallowedPackets.has(tag)) { + throw Error(`Unexpected packet type: ${tag}`); + } + switch (tag) { + case enums.packet.publicKey: + case enums.packet.secretKey: + if (this.keyPacket) { + throw Error('Key block contains multiple keys'); + } + this.keyPacket = packet; + primaryKeyID = this.getKeyID(); + if (!primaryKeyID) { + throw Error('Missing Key ID'); + } + break; + case enums.packet.userID: + case enums.packet.userAttribute: + user = new User(packet, this); + this.users.push(user); + break; + case enums.packet.publicSubkey: + case enums.packet.secretSubkey: + user = null; + subkey = new Subkey(packet, this); + this.subkeys.push(subkey); + break; + case enums.packet.signature: + switch (packet.signatureType) { + case enums.signature.certGeneric: + case enums.signature.certPersona: + case enums.signature.certCasual: + case enums.signature.certPositive: + if (!user) { + console.log('Dropping certification signatures without preceding user packet'); + continue; + } + if (packet.issuerKeyID.equals(primaryKeyID)) { + user.selfCertifications.push(packet); + } else { + user.otherCertifications.push(packet); + } + break; + case enums.signature.certRevocation: + if (user) { + user.revocationSignatures.push(packet); + } else { + this.directSignatures.push(packet); + } + break; + case enums.signature.key: + this.directSignatures.push(packet); + break; + case enums.signature.subkeyBinding: + if (!subkey) { + console.log('Dropping subkey binding signature without preceding subkey packet'); + continue; + } + subkey.bindingSignatures.push(packet); + break; + case enums.signature.keyRevocation: + this.revocationSignatures.push(packet); + break; + case enums.signature.subkeyRevocation: + if (!subkey) { + console.log('Dropping subkey revocation signature without preceding subkey packet'); + continue; + } + subkey.revocationSignatures.push(packet); + break; + } + break; + } + } + } + + /** + * Transforms structured key data to packetlist + * @returns {PacketList} The packets that form a key. + */ + toPacketList() { + const packetlist = new PacketList(); + packetlist.push(this.keyPacket); + packetlist.push(...this.revocationSignatures); + packetlist.push(...this.directSignatures); + this.users.map(user => packetlist.push(...user.toPacketList())); + this.subkeys.map(subkey => packetlist.push(...subkey.toPacketList())); + return packetlist; + } + + /** + * Clones the key object. The copy is shallow, as it references the same packet objects as the original. However, if the top-level API is used, the two key instances are effectively independent. + * @param {Boolean} [clonePrivateParams=false] Only relevant for private keys: whether the secret key paramenters should be deeply copied. This is needed if e.g. `encrypt()` is to be called either on the clone or the original key. + * @returns {Promise} Clone of the key. + */ + clone(clonePrivateParams = false) { + const key = new this.constructor(this.toPacketList()); + if (clonePrivateParams) { + key.getKeys().forEach(k => { + // shallow clone the key packets + k.keyPacket = Object.create( + Object.getPrototypeOf(k.keyPacket), + Object.getOwnPropertyDescriptors(k.keyPacket) + ); + if (!k.keyPacket.isDecrypted()) return; + // deep clone the private params, which are cleared during encryption + const privateParams = {}; + Object.keys(k.keyPacket.privateParams).forEach(name => { + privateParams[name] = new Uint8Array(k.keyPacket.privateParams[name]); + }); + k.keyPacket.privateParams = privateParams; + }); + } + return key; + } + + /** + * Returns an array containing all public or private subkeys matching keyID; + * If no keyID is given, returns all subkeys. + * @param {type/keyID} [keyID] - key ID to look for + * @returns {Array} array of subkeys + */ + getSubkeys(keyID = null) { + const subkeys = this.subkeys.filter(subkey => ( + !keyID || subkey.getKeyID().equals(keyID, true) + )); + return subkeys; + } + + /** + * Returns an array containing all public or private keys matching keyID. + * If no keyID is given, returns all keys, starting with the primary key. + * @param {type/keyid~KeyID} [keyID] - key ID to look for + * @returns {Array} array of keys + */ + getKeys(keyID = null) { + const keys = []; + if (!keyID || this.getKeyID().equals(keyID, true)) { + keys.push(this); + } + return keys.concat(this.getSubkeys(keyID)); + } + + /** + * Returns key IDs of all keys + * @returns {Array} + */ + getKeyIDs() { + return this.getKeys().map(key => key.getKeyID()); + } + + /** + * Returns userIDs + * @returns {Array} Array of userIDs. + */ + getUserIDs() { + return this.users.map(user => { + return user.userID ? user.userID.userID : null; + }).filter(userID => userID !== null); + } + + /** + * Returns binary encoded key + * @returns {Uint8Array} Binary key. + */ + write() { + return this.toPacketList().write(); + } + + /** + * Returns last created key or key by given keyID that is available for signing and verification + * @param {module:type/keyid~KeyID} [keyID] - key ID of a specific key to retrieve + * @param {Date} [date] - use the fiven date date to to check key validity instead of the current date + * @param {Object} [userID] - filter keys for the given user ID + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} signing key + * @throws if no valid signing key was found + * @async + */ + async getSigningKey(keyID = null, date = new Date(), userID = {}, config$1 = config) { + await this.verifyPrimaryKey(date, userID, config$1); + const primaryKey = this.keyPacket; + const subkeys = this.subkeys.slice().sort((a, b) => b.keyPacket.created - a.keyPacket.created); + let exception; + for (const subkey of subkeys) { + if (!keyID || subkey.getKeyID().equals(keyID)) { + try { + await subkey.verify(date, config$1); + const dataToVerify = { key: primaryKey, bind: subkey.keyPacket }; + const bindingSignature = await getLatestValidSignature( + subkey.bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date, config$1 + ); + if (!isValidSigningKeyPacket(subkey.keyPacket, bindingSignature)) { + continue; + } + if (!bindingSignature.embeddedSignature) { + throw Error('Missing embedded signature'); + } + // verify embedded signature + await getLatestValidSignature( + [bindingSignature.embeddedSignature], subkey.keyPacket, enums.signature.keyBinding, dataToVerify, date, config$1 + ); + checkKeyRequirements(subkey.keyPacket, config$1); + return subkey; + } catch (e) { + exception = e; + } + } + } + + try { + const primaryUser = await this.getPrimaryUser(date, userID, config$1); + if ((!keyID || primaryKey.getKeyID().equals(keyID)) && + isValidSigningKeyPacket(primaryKey, primaryUser.selfCertification, config$1)) { + checkKeyRequirements(primaryKey, config$1); + return this; + } + } catch (e) { + exception = e; + } + throw util.wrapError('Could not find valid signing key packet in key ' + this.getKeyID().toHex(), exception); + } + + /** + * Returns last created key or key by given keyID that is available for encryption or decryption + * @param {module:type/keyid~KeyID} [keyID] - key ID of a specific key to retrieve + * @param {Date} [date] - use the fiven date date to to check key validity instead of the current date + * @param {Object} [userID] - filter keys for the given user ID + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} encryption key + * @throws if no valid encryption key was found + * @async + */ + async getEncryptionKey(keyID, date = new Date(), userID = {}, config$1 = config) { + await this.verifyPrimaryKey(date, userID, config$1); + const primaryKey = this.keyPacket; + // V4: by convention subkeys are preferred for encryption service + const subkeys = this.subkeys.slice().sort((a, b) => b.keyPacket.created - a.keyPacket.created); + let exception; + for (const subkey of subkeys) { + if (!keyID || subkey.getKeyID().equals(keyID)) { + try { + await subkey.verify(date, config$1); + const dataToVerify = { key: primaryKey, bind: subkey.keyPacket }; + const bindingSignature = await getLatestValidSignature(subkey.bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date, config$1); + if (isValidEncryptionKeyPacket(subkey.keyPacket, bindingSignature)) { + checkKeyRequirements(subkey.keyPacket, config$1); + return subkey; + } + } catch (e) { + exception = e; + } + } + } + + try { + // if no valid subkey for encryption, evaluate primary key + const primaryUser = await this.getPrimaryUser(date, userID, config$1); + if ((!keyID || primaryKey.getKeyID().equals(keyID)) && + isValidEncryptionKeyPacket(primaryKey, primaryUser.selfCertification)) { + checkKeyRequirements(primaryKey, config$1); + return this; + } + } catch (e) { + exception = e; + } + throw util.wrapError('Could not find valid encryption key packet in key ' + this.getKeyID().toHex(), exception); + } + + /** + * Checks if a signature on a key is revoked + * @param {SignaturePacket} signature - The signature to verify + * @param {PublicSubkeyPacket| + * SecretSubkeyPacket| + * PublicKeyPacket| + * SecretKeyPacket} key, optional The key to verify the signature + * @param {Date} [date] - Use the given date for verification, instead of the current time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} True if the certificate is revoked. + * @async + */ + async isRevoked(signature, key, date = new Date(), config$1 = config) { + return isDataRevoked( + this.keyPacket, enums.signature.keyRevocation, { key: this.keyPacket }, this.revocationSignatures, signature, key, date, config$1 + ); + } + + /** + * Verify primary key. Checks for revocation signatures, expiration time + * and valid self signature. Throws if the primary key is invalid. + * @param {Date} [date] - Use the given date for verification instead of the current time + * @param {Object} [userID] - User ID + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @throws {Error} If key verification failed + * @async + */ + async verifyPrimaryKey(date = new Date(), userID = {}, config$1 = config) { + const primaryKey = this.keyPacket; + // check for key revocation signatures + if (await this.isRevoked(null, null, date, config$1)) { + throw Error('Primary key is revoked'); + } + // check for valid, unrevoked, unexpired self signature + const { selfCertification } = await this.getPrimaryUser(date, userID, config$1); + // check for expiration time in binding signatures + if (isDataExpired(primaryKey, selfCertification, date)) { + throw Error('Primary key is expired'); + } + // check for expiration time in direct signatures + const directSignature = await getLatestValidSignature( + this.directSignatures, primaryKey, enums.signature.key, { key: primaryKey }, date, config$1 + ).catch(() => {}); // invalid signatures are discarded, to avoid breaking the key + + if (directSignature && isDataExpired(primaryKey, directSignature, date)) { + throw Error('Primary key is expired'); + } + } + + /** + * Returns the expiration date of the primary key, considering self-certifications and direct-key signatures. + * Returns `Infinity` if the key doesn't expire, or `null` if the key is revoked or invalid. + * @param {Object} [userID] - User ID to consider instead of the primary user + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} + * @async + */ + async getExpirationTime(userID, config$1 = config) { + let primaryKeyExpiry; + try { + const { selfCertification } = await this.getPrimaryUser(null, userID, config$1); + const selfSigKeyExpiry = getKeyExpirationTime(this.keyPacket, selfCertification); + const selfSigExpiry = selfCertification.getExpirationTime(); + const directSignature = await getLatestValidSignature( + this.directSignatures, this.keyPacket, enums.signature.key, { key: this.keyPacket }, null, config$1 + ).catch(() => {}); + if (directSignature) { + const directSigKeyExpiry = getKeyExpirationTime(this.keyPacket, directSignature); + // We do not support the edge case where the direct signature expires, since it would invalidate the corresponding key expiration, + // causing a discountinous validy period for the key + primaryKeyExpiry = Math.min(selfSigKeyExpiry, selfSigExpiry, directSigKeyExpiry); + } else { + primaryKeyExpiry = selfSigKeyExpiry < selfSigExpiry ? selfSigKeyExpiry : selfSigExpiry; + } + } catch (e) { + primaryKeyExpiry = null; + } + + return util.normalizeDate(primaryKeyExpiry); + } + + + /** + * Returns primary user and most significant (latest valid) self signature + * - if multiple primary users exist, returns the one with the latest self signature + * - otherwise, returns the user with the latest self signature + * @param {Date} [date] - Use the given date for verification instead of the current time + * @param {Object} [userID] - User ID to get instead of the primary user, if it exists + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise<{ + * user: User, + * selfCertification: SignaturePacket + * }>} The primary user and the self signature + * @async + */ + async getPrimaryUser(date = new Date(), userID = {}, config$1 = config) { + const primaryKey = this.keyPacket; + const users = []; + let exception; + for (let i = 0; i < this.users.length; i++) { + try { + const user = this.users[i]; + if (!user.userID) { + continue; + } + if ( + (userID.name !== undefined && user.userID.name !== userID.name) || + (userID.email !== undefined && user.userID.email !== userID.email) || + (userID.comment !== undefined && user.userID.comment !== userID.comment) + ) { + throw Error('Could not find user that matches that user ID'); + } + const dataToVerify = { userID: user.userID, key: primaryKey }; + const selfCertification = await getLatestValidSignature(user.selfCertifications, primaryKey, enums.signature.certGeneric, dataToVerify, date, config$1); + users.push({ index: i, user, selfCertification }); + } catch (e) { + exception = e; + } + } + if (!users.length) { + throw exception || Error('Could not find primary user'); + } + await Promise.all(users.map(async function (a) { + return a.selfCertification.revoked || a.user.isRevoked(a.selfCertification, null, date, config$1); + })); + // sort by primary user flag and signature creation time + const primaryUser = users.sort(function(a, b) { + const A = a.selfCertification; + const B = b.selfCertification; + return B.revoked - A.revoked || A.isPrimaryUserID - B.isPrimaryUserID || A.created - B.created; + }).pop(); + const { user, selfCertification: cert } = primaryUser; + if (cert.revoked || await user.isRevoked(cert, null, date, config$1)) { + throw Error('Primary user is revoked'); + } + return primaryUser; + } + + /** + * Update key with new components from specified key with same key ID: + * users, subkeys, certificates are merged into the destination key, + * duplicates and expired signatures are ignored. + * + * If the source key is a private key and the destination key is public, + * a private key is returned. + * @param {Key} sourceKey - Source key to merge + * @param {Date} [date] - Date to verify validity of signatures and keys + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} updated key + * @async + */ + async update(sourceKey, date = new Date(), config$1 = config) { + if (!this.hasSameFingerprintAs(sourceKey)) { + throw Error('Primary key fingerprints must be equal to update the key'); + } + if (!this.isPrivate() && sourceKey.isPrivate()) { + // check for equal subkey packets + const equal = (this.subkeys.length === sourceKey.subkeys.length) && + (this.subkeys.every(destSubkey => { + return sourceKey.subkeys.some(srcSubkey => { + return destSubkey.hasSameFingerprintAs(srcSubkey); + }); + })); + if (!equal) { + throw Error('Cannot update public key with private key if subkeys mismatch'); + } + + return sourceKey.update(this, config$1); + } + // from here on, either: + // - destination key is private, source key is public + // - the keys are of the same type + // hence we don't need to convert the destination key type + const updatedKey = this.clone(); + // revocation signatures + await mergeSignatures(sourceKey, updatedKey, 'revocationSignatures', date, srcRevSig => { + return isDataRevoked(updatedKey.keyPacket, enums.signature.keyRevocation, updatedKey, [srcRevSig], null, sourceKey.keyPacket, date, config$1); + }); + // direct signatures + await mergeSignatures(sourceKey, updatedKey, 'directSignatures', date); + // update users + await Promise.all(sourceKey.users.map(async srcUser => { + // multiple users with the same ID/attribute are not explicitly disallowed by the spec + // hence we support them, just in case + const usersToUpdate = updatedKey.users.filter(dstUser => ( + (srcUser.userID && srcUser.userID.equals(dstUser.userID)) || + (srcUser.userAttribute && srcUser.userAttribute.equals(dstUser.userAttribute)) + )); + if (usersToUpdate.length > 0) { + await Promise.all( + usersToUpdate.map(userToUpdate => userToUpdate.update(srcUser, date, config$1)) + ); + } else { + const newUser = srcUser.clone(); + newUser.mainKey = updatedKey; + updatedKey.users.push(newUser); + } + })); + // update subkeys + await Promise.all(sourceKey.subkeys.map(async srcSubkey => { + // multiple subkeys with same fingerprint might be preset + const subkeysToUpdate = updatedKey.subkeys.filter(dstSubkey => ( + dstSubkey.hasSameFingerprintAs(srcSubkey) + )); + if (subkeysToUpdate.length > 0) { + await Promise.all( + subkeysToUpdate.map(subkeyToUpdate => subkeyToUpdate.update(srcSubkey, date, config$1)) + ); + } else { + const newSubkey = srcSubkey.clone(); + newSubkey.mainKey = updatedKey; + updatedKey.subkeys.push(newSubkey); + } + })); + + return updatedKey; + } + + /** + * Get revocation certificate from a revoked key. + * (To get a revocation certificate for an unrevoked key, call revoke() first.) + * @param {Date} date - Use the given date instead of the current time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} Armored revocation certificate. + * @async + */ + async getRevocationCertificate(date = new Date(), config$1 = config) { + const dataToVerify = { key: this.keyPacket }; + const revocationSignature = await getLatestValidSignature(this.revocationSignatures, this.keyPacket, enums.signature.keyRevocation, dataToVerify, date, config$1); + const packetlist = new PacketList(); + packetlist.push(revocationSignature); + return armor(enums.armor.publicKey, packetlist.write(), null, null, 'This is a revocation certificate'); + } + + /** + * Applies a revocation certificate to a key + * This adds the first signature packet in the armored text to the key, + * if it is a valid revocation signature. + * @param {String} revocationCertificate - armored revocation certificate + * @param {Date} [date] - Date to verify the certificate + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} Revoked key. + * @async + */ + async applyRevocationCertificate(revocationCertificate, date = new Date(), config$1 = config) { + const input = await unarmor(revocationCertificate, config$1); + const packetlist = await PacketList.fromBinary(input.data, allowedRevocationPackets, config$1); + const revocationSignature = packetlist.findPacket(enums.packet.signature); + if (!revocationSignature || revocationSignature.signatureType !== enums.signature.keyRevocation) { + throw Error('Could not find revocation signature packet'); + } + if (!revocationSignature.issuerKeyID.equals(this.getKeyID())) { + throw Error('Revocation signature does not match key'); + } + try { + await revocationSignature.verify(this.keyPacket, enums.signature.keyRevocation, { key: this.keyPacket }, date, undefined, config$1); + } catch (e) { + throw util.wrapError('Could not verify revocation signature', e); + } + const key = this.clone(); + key.revocationSignatures.push(revocationSignature); + return key; + } + + /** + * Signs primary user of key + * @param {Array} privateKeys - decrypted private keys for signing + * @param {Date} [date] - Use the given date for verification instead of the current time + * @param {Object} [userID] - User ID to get instead of the primary user, if it exists + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} Key with new certificate signature. + * @async + */ + async signPrimaryUser(privateKeys, date, userID, config$1 = config) { + const { index, user } = await this.getPrimaryUser(date, userID, config$1); + const userSign = await user.certify(privateKeys, date, config$1); + const key = this.clone(); + key.users[index] = userSign; + return key; + } + + /** + * Signs all users of key + * @param {Array} privateKeys - decrypted private keys for signing + * @param {Date} [date] - Use the given date for signing, instead of the current time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} Key with new certificate signature. + * @async + */ + async signAllUsers(privateKeys, date = new Date(), config$1 = config) { + const key = this.clone(); + key.users = await Promise.all(this.users.map(function(user) { + return user.certify(privateKeys, date, config$1); + })); + return key; + } + + /** + * Verifies primary user of key + * - if no arguments are given, verifies the self certificates; + * - otherwise, verifies all certificates signed with given keys. + * @param {Array} [verificationKeys] - array of keys to verify certificate signatures, instead of the primary key + * @param {Date} [date] - Use the given date for verification instead of the current time + * @param {Object} [userID] - User ID to get instead of the primary user, if it exists + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise>} List of signer's keyID and validity of signature. + * Signature validity is null if the verification keys do not correspond to the certificate. + * @async + */ + async verifyPrimaryUser(verificationKeys, date = new Date(), userID, config$1 = config) { + const primaryKey = this.keyPacket; + const { user } = await this.getPrimaryUser(date, userID, config$1); + const results = verificationKeys ? + await user.verifyAllCertifications(verificationKeys, date, config$1) : + [{ keyID: primaryKey.getKeyID(), valid: await user.verify(date, config$1).catch(() => false) }]; + return results; + } + + /** + * Verifies all users of key + * - if no arguments are given, verifies the self certificates; + * - otherwise, verifies all certificates signed with given keys. + * @param {Array} [verificationKeys] - array of keys to verify certificate signatures + * @param {Date} [date] - Use the given date for verification instead of the current time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise>} List of userID, signer's keyID and validity of signature. + * Signature validity is null if the verification keys do not correspond to the certificate. + * @async + */ + async verifyAllUsers(verificationKeys, date = new Date(), config$1 = config) { + const primaryKey = this.keyPacket; + const results = []; + await Promise.all(this.users.map(async user => { + const signatures = verificationKeys ? + await user.verifyAllCertifications(verificationKeys, date, config$1) : + [{ keyID: primaryKey.getKeyID(), valid: await user.verify(date, config$1).catch(() => false) }]; + + results.push(...signatures.map( + signature => ({ + userID: user.userID ? user.userID.userID : null, + userAttribute: user.userAttribute, + keyID: signature.keyID, + valid: signature.valid + })) + ); + })); + return results; + } + } + + ['getKeyID', 'getFingerprint', 'getAlgorithmInfo', 'getCreationTime', 'hasSameFingerprintAs'].forEach(name => { + Key.prototype[name] = + Subkey.prototype[name]; + }); + + // This library is free software; you can redistribute it and/or + + /** + * Class that represents an OpenPGP Public Key + */ + class PublicKey extends Key { + /** + * @param {PacketList} packetlist - The packets that form this key + */ + constructor(packetlist) { + super(); + this.keyPacket = null; + this.revocationSignatures = []; + this.directSignatures = []; + this.users = []; + this.subkeys = []; + if (packetlist) { + this.packetListToStructure(packetlist, new Set([enums.packet.secretKey, enums.packet.secretSubkey])); + if (!this.keyPacket) { + throw Error('Invalid key: missing public-key packet'); + } + } + } + + /** + * Returns true if this is a private key + * @returns {false} + */ + isPrivate() { + return false; + } + + /** + * Returns key as public key (shallow copy) + * @returns {PublicKey} New public Key + */ + toPublic() { + return this; + } + + /** + * Returns ASCII armored text of key + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {ReadableStream} ASCII armor. + */ + armor(config$1 = config) { + return armor(enums.armor.publicKey, this.toPacketList().write(), undefined, undefined, undefined, config$1); + } + } + + /** + * Class that represents an OpenPGP Private key + */ + class PrivateKey extends PublicKey { + /** + * @param {PacketList} packetlist - The packets that form this key + */ + constructor(packetlist) { + super(); + this.packetListToStructure(packetlist, new Set([enums.packet.publicKey, enums.packet.publicSubkey])); + if (!this.keyPacket) { + throw Error('Invalid key: missing private-key packet'); + } + } + + /** + * Returns true if this is a private key + * @returns {Boolean} + */ + isPrivate() { + return true; + } + + /** + * Returns key as public key (shallow copy) + * @returns {PublicKey} New public Key + */ + toPublic() { + const packetlist = new PacketList(); + const keyPackets = this.toPacketList(); + for (const keyPacket of keyPackets) { + switch (keyPacket.constructor.tag) { + case enums.packet.secretKey: { + const pubKeyPacket = PublicKeyPacket.fromSecretKeyPacket(keyPacket); + packetlist.push(pubKeyPacket); + break; + } + case enums.packet.secretSubkey: { + const pubSubkeyPacket = PublicSubkeyPacket.fromSecretSubkeyPacket(keyPacket); + packetlist.push(pubSubkeyPacket); + break; + } + default: + packetlist.push(keyPacket); + } + } + return new PublicKey(packetlist); + } + + /** + * Returns ASCII armored text of key + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {ReadableStream} ASCII armor. + */ + armor(config$1 = config) { + return armor(enums.armor.privateKey, this.toPacketList().write(), undefined, undefined, undefined, config$1); + } + + /** + * Returns all keys that are available for decryption, matching the keyID when given + * This is useful to retrieve keys for session key decryption + * @param {module:type/keyid~KeyID} keyID, optional + * @param {Date} date, optional + * @param {String} userID, optional + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise>} Array of decryption keys. + * @async + */ + async getDecryptionKeys(keyID, date = new Date(), userID = {}, config$1 = config) { + const primaryKey = this.keyPacket; + const keys = []; + for (let i = 0; i < this.subkeys.length; i++) { + if (!keyID || this.subkeys[i].getKeyID().equals(keyID, true)) { + try { + const dataToVerify = { key: primaryKey, bind: this.subkeys[i].keyPacket }; + const bindingSignature = await getLatestValidSignature(this.subkeys[i].bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date, config$1); + if (isValidDecryptionKeyPacket(bindingSignature, config$1)) { + keys.push(this.subkeys[i]); + } + } catch (e) {} + } + } + + // evaluate primary key + const primaryUser = await this.getPrimaryUser(date, userID, config$1); + if ((!keyID || primaryKey.getKeyID().equals(keyID, true)) && + isValidDecryptionKeyPacket(primaryUser.selfCertification, config$1)) { + keys.push(this); + } + + return keys; + } + + /** + * Returns true if the primary key or any subkey is decrypted. + * A dummy key is considered encrypted. + */ + isDecrypted() { + return this.getKeys().some(({ keyPacket }) => keyPacket.isDecrypted()); + } + + /** + * Check whether the private and public primary key parameters correspond + * Together with verification of binding signatures, this guarantees key integrity + * In case of gnu-dummy primary key, it is enough to validate any signing subkeys + * otherwise all encryption subkeys are validated + * If only gnu-dummy keys are found, we cannot properly validate so we throw an error + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @throws {Error} if validation was not successful and the key cannot be trusted + * @async + */ + async validate(config$1 = config) { + if (!this.isPrivate()) { + throw Error('Cannot validate a public key'); + } + + let signingKeyPacket; + if (!this.keyPacket.isDummy()) { + signingKeyPacket = this.keyPacket; + } else { + /** + * It is enough to validate any signing keys + * since its binding signatures are also checked + */ + const signingKey = await this.getSigningKey(null, null, undefined, { ...config$1, rejectPublicKeyAlgorithms: new Set(), minRSABits: 0 }); + // This could again be a dummy key + if (signingKey && !signingKey.keyPacket.isDummy()) { + signingKeyPacket = signingKey.keyPacket; + } + } + + if (signingKeyPacket) { + return signingKeyPacket.validate(); + } else { + const keys = this.getKeys(); + const allDummies = keys.map(key => key.keyPacket.isDummy()).every(Boolean); + if (allDummies) { + throw Error('Cannot validate an all-gnu-dummy key'); + } + + return Promise.all(keys.map(async key => key.keyPacket.validate())); + } + } + + /** + * Clear private key parameters + */ + clearPrivateParams() { + this.getKeys().forEach(({ keyPacket }) => { + if (keyPacket.isDecrypted()) { + keyPacket.clearPrivateParams(); + } + }); + } + + /** + * Revokes the key + * @param {Object} reasonForRevocation - optional, object indicating the reason for revocation + * @param {module:enums.reasonForRevocation} reasonForRevocation.flag optional, flag indicating the reason for revocation + * @param {String} reasonForRevocation.string optional, string explaining the reason for revocation + * @param {Date} date - optional, override the creationtime of the revocation signature + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} New key with revocation signature. + * @async + */ + async revoke( + { + flag: reasonForRevocationFlag = enums.reasonForRevocation.noReason, + string: reasonForRevocationString = '' + } = {}, + date = new Date(), + config$1 = config + ) { + if (!this.isPrivate()) { + throw Error('Need private key for revoking'); + } + const dataToSign = { key: this.keyPacket }; + const key = this.clone(); + key.revocationSignatures.push(await createSignaturePacket(dataToSign, null, this.keyPacket, { + signatureType: enums.signature.keyRevocation, + reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), + reasonForRevocationString + }, date, undefined, undefined, undefined, config$1)); + return key; + } + + + /** + * Generates a new OpenPGP subkey, and returns a clone of the Key object with the new subkey added. + * Supports RSA and ECC keys. Defaults to the algorithm and bit size/curve of the primary key. DSA primary keys default to RSA subkeys. + * @param {ecc|rsa} options.type The subkey algorithm: ECC or RSA + * @param {String} options.curve (optional) Elliptic curve for ECC keys + * @param {Integer} options.rsaBits (optional) Number of bits for RSA subkeys + * @param {Number} options.keyExpirationTime (optional) Number of seconds from the key creation time after which the key expires + * @param {Date} options.date (optional) Override the creation date of the key and the key signatures + * @param {Boolean} options.sign (optional) Indicates whether the subkey should sign rather than encrypt. Defaults to false + * @param {Object} options.config (optional) custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} + * @async + */ + async addSubkey(options = {}) { + const config$1 = { ...config, ...options.config }; + if (options.passphrase) { + throw Error('Subkey could not be encrypted here, please encrypt whole key'); + } + if (options.rsaBits < config$1.minRSABits) { + throw Error(`rsaBits should be at least ${config$1.minRSABits}, got: ${options.rsaBits}`); + } + const secretKeyPacket = this.keyPacket; + if (secretKeyPacket.isDummy()) { + throw Error('Cannot add subkey to gnu-dummy primary key'); + } + if (!secretKeyPacket.isDecrypted()) { + throw Error('Key is not decrypted'); + } + const defaultOptions = secretKeyPacket.getAlgorithmInfo(); + defaultOptions.type = defaultOptions.curve ? 'ecc' : 'rsa'; // DSA keys default to RSA + defaultOptions.rsaBits = defaultOptions.bits || 4096; + defaultOptions.curve = defaultOptions.curve || 'curve25519'; + options = sanitizeKeyOptions(options, defaultOptions); + const keyPacket = await generateSecretSubkey(options); + checkKeyRequirements(keyPacket, config$1); + const bindingSignature = await createBindingSignature(keyPacket, secretKeyPacket, options, config$1); + const packetList = this.toPacketList(); + packetList.push(keyPacket, bindingSignature); + return new PrivateKey(packetList); + } + } + + // OpenPGP.js - An OpenPGP implementation in javascript + + // A Key can contain the following packets + const allowedKeyPackets = /*#__PURE__*/ util.constructAllowedPackets([ + PublicKeyPacket, + PublicSubkeyPacket, + SecretKeyPacket, + SecretSubkeyPacket, + UserIDPacket, + UserAttributePacket, + SignaturePacket + ]); + + /** + * Creates a PublicKey or PrivateKey depending on the packetlist in input + * @param {PacketList} - packets to parse + * @return {Key} parsed key + * @throws if no key packet was found + */ + function createKey(packetlist) { + for (const packet of packetlist) { + switch (packet.constructor.tag) { + case enums.packet.secretKey: + return new PrivateKey(packetlist); + case enums.packet.publicKey: + return new PublicKey(packetlist); + } + } + throw Error('No key packet found'); + } + + + /** + * Generates a new OpenPGP key. Supports RSA and ECC keys. + * By default, primary and subkeys will be of same type. + * @param {ecc|rsa} options.type The primary key algorithm type: ECC or RSA + * @param {String} options.curve Elliptic curve for ECC keys + * @param {Integer} options.rsaBits Number of bits for RSA keys + * @param {Array} options.userIDs User IDs as strings or objects: 'Jo Doe ' or { name:'Jo Doe', email:'info@jo.com' } + * @param {String} options.passphrase Passphrase used to encrypt the resulting private key + * @param {Number} options.keyExpirationTime (optional) Number of seconds from the key creation time after which the key expires + * @param {Date} options.date Creation date of the key and the key signatures + * @param {Object} config - Full configuration + * @param {Array} options.subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}] + * sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt + * @returns {Promise<{{ key: PrivateKey, revocationCertificate: String }}>} + * @async + * @static + * @private + */ + async function generate(options, config) { + options.sign = true; // primary key is always a signing key + options = sanitizeKeyOptions(options); + options.subkeys = options.subkeys.map((subkey, index) => sanitizeKeyOptions(subkey, options)); + let promises = [generateSecretKey(options, config)]; + promises = promises.concat(options.subkeys.map(options => generateSecretSubkey(options, config))); + const packets = await Promise.all(promises); + + const key = await wrapKeyObject(packets[0], packets.slice(1), options, config); + const revocationCertificate = await key.getRevocationCertificate(options.date, config); + key.revocationSignatures = []; + return { key, revocationCertificate }; + } + + /** + * Reformats and signs an OpenPGP key with a given User ID. Currently only supports RSA keys. + * @param {PrivateKey} options.privateKey The private key to reformat + * @param {Array} options.userIDs User IDs as strings or objects: 'Jo Doe ' or { name:'Jo Doe', email:'info@jo.com' } + * @param {String} options.passphrase Passphrase used to encrypt the resulting private key + * @param {Number} options.keyExpirationTime Number of seconds from the key creation time after which the key expires + * @param {Date} options.date Override the creation date of the key signatures + * @param {Array} options.subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}] + * @param {Object} config - Full configuration + * + * @returns {Promise<{{ key: PrivateKey, revocationCertificate: String }}>} + * @async + * @static + * @private + */ + async function reformat(options, config) { + options = sanitize(options); + const { privateKey } = options; + + if (!privateKey.isPrivate()) { + throw Error('Cannot reformat a public key'); + } + + if (privateKey.keyPacket.isDummy()) { + throw Error('Cannot reformat a gnu-dummy primary key'); + } + + const isDecrypted = privateKey.getKeys().every(({ keyPacket }) => keyPacket.isDecrypted()); + if (!isDecrypted) { + throw Error('Key is not decrypted'); + } + + const secretKeyPacket = privateKey.keyPacket; + + if (!options.subkeys) { + options.subkeys = await Promise.all(privateKey.subkeys.map(async subkey => { + const secretSubkeyPacket = subkey.keyPacket; + const dataToVerify = { key: secretKeyPacket, bind: secretSubkeyPacket }; + const bindingSignature = await ( + getLatestValidSignature(subkey.bindingSignatures, secretKeyPacket, enums.signature.subkeyBinding, dataToVerify, null, config) + ).catch(() => ({})); + return { + sign: bindingSignature.keyFlags && (bindingSignature.keyFlags[0] & enums.keyFlags.signData) + }; + })); + } + + const secretSubkeyPackets = privateKey.subkeys.map(subkey => subkey.keyPacket); + if (options.subkeys.length !== secretSubkeyPackets.length) { + throw Error('Number of subkey options does not match number of subkeys'); + } + + options.subkeys = options.subkeys.map(subkeyOptions => sanitize(subkeyOptions, options)); + + const key = await wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options, config); + const revocationCertificate = await key.getRevocationCertificate(options.date, config); + key.revocationSignatures = []; + return { key, revocationCertificate }; + + function sanitize(options, subkeyDefaults = {}) { + options.keyExpirationTime = options.keyExpirationTime || subkeyDefaults.keyExpirationTime; + options.passphrase = util.isString(options.passphrase) ? options.passphrase : subkeyDefaults.passphrase; + options.date = options.date || subkeyDefaults.date; + + return options; + } + } + + /** + * Construct PrivateKey object from the given key packets, add certification signatures and set passphrase protection + * The new key includes a revocation certificate that must be removed before returning the key, otherwise the key is considered revoked. + * @param {SecretKeyPacket} secretKeyPacket + * @param {SecretSubkeyPacket} secretSubkeyPackets + * @param {Object} options + * @param {Object} config - Full configuration + * @returns {PrivateKey} + */ + async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options, config) { + // set passphrase protection + if (options.passphrase) { + await secretKeyPacket.encrypt(options.passphrase, config); + } + + await Promise.all(secretSubkeyPackets.map(async function(secretSubkeyPacket, index) { + const subkeyPassphrase = options.subkeys[index].passphrase; + if (subkeyPassphrase) { + await secretSubkeyPacket.encrypt(subkeyPassphrase, config); + } + })); + + const packetlist = new PacketList(); + packetlist.push(secretKeyPacket); + + await Promise.all(options.userIDs.map(async function(userID, index) { + function createPreferredAlgos(algos, preferredAlgo) { + return [preferredAlgo, ...algos.filter(algo => algo !== preferredAlgo)]; + } + + const userIDPacket = UserIDPacket.fromObject(userID); + const dataToSign = {}; + dataToSign.userID = userIDPacket; + dataToSign.key = secretKeyPacket; + + const signatureProperties = {}; + signatureProperties.signatureType = enums.signature.certGeneric; + signatureProperties.keyFlags = [enums.keyFlags.certifyKeys | enums.keyFlags.signData]; + signatureProperties.preferredSymmetricAlgorithms = createPreferredAlgos([ + // prefer aes256, aes128, then aes192 (no WebCrypto support: https://www.chromium.org/blink/webcrypto#TOC-AES-support) + enums.symmetric.aes256, + enums.symmetric.aes128, + enums.symmetric.aes192 + ], config.preferredSymmetricAlgorithm); + if (config.aeadProtect) { + signatureProperties.preferredAEADAlgorithms = createPreferredAlgos([ + enums.aead.eax, + enums.aead.ocb + ], config.preferredAEADAlgorithm); + } + signatureProperties.preferredHashAlgorithms = createPreferredAlgos([ + // prefer fast asm.js implementations (SHA-256) + enums.hash.sha256, + enums.hash.sha512 + ], config.preferredHashAlgorithm); + signatureProperties.preferredCompressionAlgorithms = createPreferredAlgos([ + enums.compression.zlib, + enums.compression.zip, + enums.compression.uncompressed + ], config.preferredCompressionAlgorithm); + if (index === 0) { + signatureProperties.isPrimaryUserID = true; + } + // integrity protection always enabled + signatureProperties.features = [0]; + signatureProperties.features[0] |= enums.features.modificationDetection; + if (config.aeadProtect) { + signatureProperties.features[0] |= enums.features.aead; + } + if (config.v5Keys) { + signatureProperties.features[0] |= enums.features.v5Keys; + } + if (options.keyExpirationTime > 0) { + signatureProperties.keyExpirationTime = options.keyExpirationTime; + signatureProperties.keyNeverExpires = false; + } + + const signaturePacket = await createSignaturePacket(dataToSign, null, secretKeyPacket, signatureProperties, options.date, undefined, undefined, undefined, config); + + return { userIDPacket, signaturePacket }; + })).then(list => { + list.forEach(({ userIDPacket, signaturePacket }) => { + packetlist.push(userIDPacket); + packetlist.push(signaturePacket); + }); + }); + + await Promise.all(secretSubkeyPackets.map(async function(secretSubkeyPacket, index) { + const subkeyOptions = options.subkeys[index]; + const subkeySignaturePacket = await createBindingSignature(secretSubkeyPacket, secretKeyPacket, subkeyOptions, config); + return { secretSubkeyPacket, subkeySignaturePacket }; + })).then(packets => { + packets.forEach(({ secretSubkeyPacket, subkeySignaturePacket }) => { + packetlist.push(secretSubkeyPacket); + packetlist.push(subkeySignaturePacket); + }); + }); + + // Add revocation signature packet for creating a revocation certificate. + // This packet should be removed before returning the key. + const dataToSign = { key: secretKeyPacket }; + packetlist.push(await createSignaturePacket(dataToSign, null, secretKeyPacket, { + signatureType: enums.signature.keyRevocation, + reasonForRevocationFlag: enums.reasonForRevocation.noReason, + reasonForRevocationString: '' + }, options.date, undefined, undefined, undefined, config)); + + if (options.passphrase) { + secretKeyPacket.clearPrivateParams(); + } + + await Promise.all(secretSubkeyPackets.map(async function(secretSubkeyPacket, index) { + const subkeyPassphrase = options.subkeys[index].passphrase; + if (subkeyPassphrase) { + secretSubkeyPacket.clearPrivateParams(); + } + })); + + return new PrivateKey(packetlist); + } + + /** + * Reads an (optionally armored) OpenPGP key and returns a key object + * @param {Object} options + * @param {String} [options.armoredKey] - Armored key to be parsed + * @param {Uint8Array} [options.binaryKey] - Binary key to be parsed + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} Key object. + * @async + * @static + */ + async function readKey({ armoredKey, binaryKey, config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; + if (!armoredKey && !binaryKey) { + throw Error('readKey: must pass options object containing `armoredKey` or `binaryKey`'); + } + if (armoredKey && !util.isString(armoredKey)) { + throw Error('readKey: options.armoredKey must be a string'); + } + if (binaryKey && !util.isUint8Array(binaryKey)) { + throw Error('readKey: options.binaryKey must be a Uint8Array'); + } + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + let input; + if (armoredKey) { + const { type, data } = await unarmor(armoredKey, config$1); + if (!(type === enums.armor.publicKey || type === enums.armor.privateKey)) { + throw Error('Armored text not of type key'); + } + input = data; + } else { + input = binaryKey; + } + const packetlist = await PacketList.fromBinary(input, allowedKeyPackets, config$1); + return createKey(packetlist); + } + + /** + * Reads an (optionally armored) OpenPGP private key and returns a PrivateKey object + * @param {Object} options + * @param {String} [options.armoredKey] - Armored key to be parsed + * @param {Uint8Array} [options.binaryKey] - Binary key to be parsed + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} Key object. + * @async + * @static + */ + async function readPrivateKey({ armoredKey, binaryKey, config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; + if (!armoredKey && !binaryKey) { + throw Error('readPrivateKey: must pass options object containing `armoredKey` or `binaryKey`'); + } + if (armoredKey && !util.isString(armoredKey)) { + throw Error('readPrivateKey: options.armoredKey must be a string'); + } + if (binaryKey && !util.isUint8Array(binaryKey)) { + throw Error('readPrivateKey: options.binaryKey must be a Uint8Array'); + } + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + let input; + if (armoredKey) { + const { type, data } = await unarmor(armoredKey, config$1); + if (!(type === enums.armor.privateKey)) { + throw Error('Armored text not of type private key'); + } + input = data; + } else { + input = binaryKey; + } + const packetlist = await PacketList.fromBinary(input, allowedKeyPackets, config$1); + return new PrivateKey(packetlist); + } + + /** + * Reads an (optionally armored) OpenPGP key block and returns a list of key objects + * @param {Object} options + * @param {String} [options.armoredKeys] - Armored keys to be parsed + * @param {Uint8Array} [options.binaryKeys] - Binary keys to be parsed + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise>} Key objects. + * @async + * @static + */ + async function readKeys({ armoredKeys, binaryKeys, config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; + let input = armoredKeys || binaryKeys; + if (!input) { + throw Error('readKeys: must pass options object containing `armoredKeys` or `binaryKeys`'); + } + if (armoredKeys && !util.isString(armoredKeys)) { + throw Error('readKeys: options.armoredKeys must be a string'); + } + if (binaryKeys && !util.isUint8Array(binaryKeys)) { + throw Error('readKeys: options.binaryKeys must be a Uint8Array'); + } + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + if (armoredKeys) { + const { type, data } = await unarmor(armoredKeys, config$1); + if (type !== enums.armor.publicKey && type !== enums.armor.privateKey) { + throw Error('Armored text not of type key'); + } + input = data; + } + const keys = []; + const packetlist = await PacketList.fromBinary(input, allowedKeyPackets, config$1); + const keyIndex = packetlist.indexOfTag(enums.packet.publicKey, enums.packet.secretKey); + if (keyIndex.length === 0) { + throw Error('No key packet found'); + } + for (let i = 0; i < keyIndex.length; i++) { + const oneKeyList = packetlist.slice(keyIndex[i], keyIndex[i + 1]); + const newKey = createKey(oneKeyList); + keys.push(newKey); + } + return keys; + } + + /** + * Reads an (optionally armored) OpenPGP private key block and returns a list of PrivateKey objects + * @param {Object} options + * @param {String} [options.armoredKeys] - Armored keys to be parsed + * @param {Uint8Array} [options.binaryKeys] - Binary keys to be parsed + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise>} Key objects. + * @async + * @static + */ + async function readPrivateKeys({ armoredKeys, binaryKeys, config: config$1 }) { + config$1 = { ...config, ...config$1 }; + let input = armoredKeys || binaryKeys; + if (!input) { + throw Error('readPrivateKeys: must pass options object containing `armoredKeys` or `binaryKeys`'); + } + if (armoredKeys && !util.isString(armoredKeys)) { + throw Error('readPrivateKeys: options.armoredKeys must be a string'); + } + if (binaryKeys && !util.isUint8Array(binaryKeys)) { + throw Error('readPrivateKeys: options.binaryKeys must be a Uint8Array'); + } + if (armoredKeys) { + const { type, data } = await unarmor(armoredKeys, config$1); + if (type !== enums.armor.privateKey) { + throw Error('Armored text not of type private key'); + } + input = data; + } + const keys = []; + const packetlist = await PacketList.fromBinary(input, allowedKeyPackets, config$1); + const keyIndex = packetlist.indexOfTag(enums.packet.secretKey); + if (keyIndex.length === 0) { + throw Error('No secret key packet found'); + } + for (let i = 0; i < keyIndex.length; i++) { + const oneKeyList = packetlist.slice(keyIndex[i], keyIndex[i + 1]); + const newKey = new PrivateKey(oneKeyList); + keys.push(newKey); + } + return keys; + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + // A Message can contain the following packets + const allowedMessagePackets = /*#__PURE__*/ util.constructAllowedPackets([ + LiteralDataPacket, + CompressedDataPacket, + AEADEncryptedDataPacket, + SymEncryptedIntegrityProtectedDataPacket, + SymmetricallyEncryptedDataPacket, + PublicKeyEncryptedSessionKeyPacket, + SymEncryptedSessionKeyPacket, + OnePassSignaturePacket, + SignaturePacket + ]); + // A SKESK packet can contain the following packets + const allowedSymSessionKeyPackets = /*#__PURE__*/ util.constructAllowedPackets([SymEncryptedSessionKeyPacket]); + // A detached signature can contain the following packets + const allowedDetachedSignaturePackets = /*#__PURE__*/ util.constructAllowedPackets([SignaturePacket]); + + /** + * Class that represents an OpenPGP message. + * Can be an encrypted message, signed message, compressed message or literal message + * See {@link https://tools.ietf.org/html/rfc4880#section-11.3} + */ + class Message { + /** + * @param {PacketList} packetlist - The packets that form this message + */ + constructor(packetlist) { + this.packets = packetlist || new PacketList(); + } + + /** + * Returns the key IDs of the keys to which the session key is encrypted + * @returns {Array} Array of keyID objects. + */ + getEncryptionKeyIDs() { + const keyIDs = []; + const pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); + pkESKeyPacketlist.forEach(function(packet) { + keyIDs.push(packet.publicKeyID); + }); + return keyIDs; + } + + /** + * Returns the key IDs of the keys that signed the message + * @returns {Array} Array of keyID objects. + */ + getSigningKeyIDs() { + const msg = this.unwrapCompressed(); + // search for one pass signatures + const onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature); + if (onePassSigList.length > 0) { + return onePassSigList.map(packet => packet.issuerKeyID); + } + // if nothing found look for signature packets + const signatureList = msg.packets.filterByTag(enums.packet.signature); + return signatureList.map(packet => packet.issuerKeyID); + } + + /** + * Decrypt the message. Either a private key, a session key, or a password must be specified. + * @param {Array} [decryptionKeys] - Private keys with decrypted secret data + * @param {Array} [passwords] - Passwords used to decrypt + * @param {Array} [sessionKeys] - Session keys in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] } + * @param {Date} [date] - Use the given date for key verification instead of the current time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} New message with decrypted content. + * @async + */ + async decrypt(decryptionKeys, passwords, sessionKeys, date = new Date(), config$1 = config) { + const sessionKeyObjects = sessionKeys || await this.decryptSessionKeys(decryptionKeys, passwords, date, config$1); + + const symEncryptedPacketlist = this.packets.filterByTag( + enums.packet.symmetricallyEncryptedData, + enums.packet.symEncryptedIntegrityProtectedData, + enums.packet.aeadEncryptedData + ); + + if (symEncryptedPacketlist.length === 0) { + throw Error('No encrypted data found'); + } + + const symEncryptedPacket = symEncryptedPacketlist[0]; + let exception = null; + const decryptedPromise = Promise.all(sessionKeyObjects.map(async ({ algorithm: algorithmName, data }) => { + if (!util.isUint8Array(data) || !util.isString(algorithmName)) { + throw Error('Invalid session key for decryption.'); + } + + try { + const algo = enums.write(enums.symmetric, algorithmName); + await symEncryptedPacket.decrypt(algo, data, config$1); + } catch (e) { + console.error(e); + exception = e; + } + })); + // We don't await stream.cancel here because it only returns when the other copy is canceled too. + cancel(symEncryptedPacket.encrypted); // Don't keep copy of encrypted data in memory. + symEncryptedPacket.encrypted = null; + await decryptedPromise; + + if (!symEncryptedPacket.packets || !symEncryptedPacket.packets.length) { + throw exception || Error('Decryption failed.'); + } + + const resultMsg = new Message(symEncryptedPacket.packets); + symEncryptedPacket.packets = new PacketList(); // remove packets after decryption + + return resultMsg; + } + + /** + * Decrypt encrypted session keys either with private keys or passwords. + * @param {Array} [decryptionKeys] - Private keys with decrypted secret data + * @param {Array} [passwords] - Passwords used to decrypt + * @param {Date} [date] - Use the given date for key verification, instead of current time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise>} array of object with potential sessionKey, algorithm pairs + * @async + */ + async decryptSessionKeys(decryptionKeys, passwords, date = new Date(), config$1 = config) { + let decryptedSessionKeyPackets = []; + + let exception; + if (passwords) { + const skeskPackets = this.packets.filterByTag(enums.packet.symEncryptedSessionKey); + if (skeskPackets.length === 0) { + throw Error('No symmetrically encrypted session key packet found.'); + } + await Promise.all(passwords.map(async function(password, i) { + let packets; + if (i) { + packets = await PacketList.fromBinary(skeskPackets.write(), allowedSymSessionKeyPackets, config$1); + } else { + packets = skeskPackets; + } + await Promise.all(packets.map(async function(skeskPacket) { + try { + await skeskPacket.decrypt(password); + decryptedSessionKeyPackets.push(skeskPacket); + } catch (err) { + console.error(err); + } + })); + })); + } else if (decryptionKeys) { + const pkeskPackets = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); + if (pkeskPackets.length === 0) { + throw Error('No public key encrypted session key packet found.'); + } + await Promise.all(pkeskPackets.map(async function(pkeskPacket) { + await Promise.all(decryptionKeys.map(async function(decryptionKey) { + let algos = [ + enums.symmetric.aes256, // Old OpenPGP.js default fallback + enums.symmetric.aes128, // RFC4880bis fallback + enums.symmetric.tripledes, // RFC4880 fallback + enums.symmetric.cast5 // Golang OpenPGP fallback + ]; + try { + const primaryUser = await decryptionKey.getPrimaryUser(date, undefined, config$1); // TODO: Pass userID from somewhere. + if (primaryUser.selfCertification.preferredSymmetricAlgorithms) { + algos = algos.concat(primaryUser.selfCertification.preferredSymmetricAlgorithms); + } + } catch (e) {} + + // do not check key expiration to allow decryption of old messages + const decryptionKeyPackets = (await decryptionKey.getDecryptionKeys(pkeskPacket.publicKeyID, null, undefined, config$1)).map(key => key.keyPacket); + await Promise.all(decryptionKeyPackets.map(async function(decryptionKeyPacket) { + if (!decryptionKeyPacket || decryptionKeyPacket.isDummy()) { + return; + } + if (!decryptionKeyPacket.isDecrypted()) { + throw Error('Decryption key is not decrypted.'); + } + + // To hinder CCA attacks against PKCS1, we carry out a constant-time decryption flow if the `constantTimePKCS1Decryption` config option is set. + const doConstantTimeDecryption = config$1.constantTimePKCS1Decryption && ( + pkeskPacket.publicKeyAlgorithm === enums.publicKey.rsaEncrypt || + pkeskPacket.publicKeyAlgorithm === enums.publicKey.rsaEncryptSign || + pkeskPacket.publicKeyAlgorithm === enums.publicKey.rsaSign || + pkeskPacket.publicKeyAlgorithm === enums.publicKey.elgamal + ); + + if (doConstantTimeDecryption) { + // The goal is to not reveal whether PKESK decryption (specifically the PKCS1 decoding step) failed, hence, we always proceed to decrypt the message, + // either with the successfully decrypted session key, or with a randomly generated one. + // Since the SEIP/AEAD's symmetric algorithm and key size are stored in the encrypted portion of the PKESK, and the execution flow cannot depend on + // the decrypted payload, we always assume the message to be encrypted with one of the symmetric algorithms specified in `config.constantTimePKCS1DecryptionSupportedSymmetricAlgorithms`: + // - If the PKESK decryption succeeds, and the session key cipher is in the supported set, then we try to decrypt the data with the decrypted session key as well as with the + // randomly generated keys of the remaining key types. + // - If the PKESK decryptions fails, or if it succeeds but support for the cipher is not enabled, then we discard the session key and try to decrypt the data using only the randomly + // generated session keys. + // NB: as a result, if the data is encrypted with a non-suported cipher, decryption will always fail. + + const serialisedPKESK = pkeskPacket.write(); // make copies to be able to decrypt the PKESK packet multiple times + await Promise.all(Array.from(config$1.constantTimePKCS1DecryptionSupportedSymmetricAlgorithms).map(async sessionKeyAlgorithm => { + const pkeskPacketCopy = new PublicKeyEncryptedSessionKeyPacket(); + pkeskPacketCopy.read(serialisedPKESK); + const randomSessionKey = { + sessionKeyAlgorithm, + sessionKey: mod.generateSessionKey(sessionKeyAlgorithm) + }; + try { + await pkeskPacketCopy.decrypt(decryptionKeyPacket, randomSessionKey); + decryptedSessionKeyPackets.push(pkeskPacketCopy); + } catch (err) { + // `decrypt` can still throw some non-security-sensitive errors + console.error(err); + exception = err; + } + })); + + } else { + try { + await pkeskPacket.decrypt(decryptionKeyPacket); + if (!algos.includes(enums.write(enums.symmetric, pkeskPacket.sessionKeyAlgorithm))) { + throw Error('A non-preferred symmetric algorithm was used.'); + } + decryptedSessionKeyPackets.push(pkeskPacket); + } catch (err) { + console.error(err); + exception = err; + } + } + })); + })); + cancel(pkeskPacket.encrypted); // Don't keep copy of encrypted data in memory. + pkeskPacket.encrypted = null; + })); + } else { + throw Error('No key or password specified.'); + } + + if (decryptedSessionKeyPackets.length > 0) { + // Return only unique session keys + if (decryptedSessionKeyPackets.length > 1) { + const seen = new Set(); + decryptedSessionKeyPackets = decryptedSessionKeyPackets.filter(item => { + const k = item.sessionKeyAlgorithm + util.uint8ArrayToString(item.sessionKey); + if (seen.has(k)) { + return false; + } + seen.add(k); + return true; + }); + } + + return decryptedSessionKeyPackets.map(packet => ({ + data: packet.sessionKey, + algorithm: enums.read(enums.symmetric, packet.sessionKeyAlgorithm) + })); + } + throw exception || Error('Session key decryption failed.'); + } + + /** + * Get literal data that is the body of the message + * @returns {(Uint8Array|null)} Literal body of the message as Uint8Array. + */ + getLiteralData() { + const msg = this.unwrapCompressed(); + const literal = msg.packets.findPacket(enums.packet.literalData); + return (literal && literal.getBytes()) || null; + } + + /** + * Get filename from literal data packet + * @returns {(String|null)} Filename of literal data packet as string. + */ + getFilename() { + const msg = this.unwrapCompressed(); + const literal = msg.packets.findPacket(enums.packet.literalData); + return (literal && literal.getFilename()) || null; + } + + /** + * Get literal data as text + * @returns {(String|null)} Literal body of the message interpreted as text. + */ + getText() { + const msg = this.unwrapCompressed(); + const literal = msg.packets.findPacket(enums.packet.literalData); + if (literal) { + return literal.getText(); + } + return null; + } + + /** + * Generate a new session key object, taking the algorithm preferences of the passed encryption keys into account, if any. + * @param {Array} [encryptionKeys] - Public key(s) to select algorithm preferences for + * @param {Date} [date] - Date to select algorithm preferences at + * @param {Array} [userIDs] - User IDs to select algorithm preferences for + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise<{ data: Uint8Array, algorithm: String, aeadAlgorithm: undefined|String }>} Object with session key data and algorithms. + * @async + */ + static async generateSessionKey(encryptionKeys = [], date = new Date(), userIDs = [], config$1 = config) { + const algo = await getPreferredAlgo('symmetric', encryptionKeys, date, userIDs, config$1); + const algorithmName = enums.read(enums.symmetric, algo); + const aeadAlgorithmName = config$1.aeadProtect && await isAEADSupported(encryptionKeys, date, userIDs, config$1) ? + enums.read(enums.aead, await getPreferredAlgo('aead', encryptionKeys, date, userIDs, config$1)) : + undefined; + + await Promise.all(encryptionKeys.map(key => key.getEncryptionKey() + .catch(() => null) // ignore key strength requirements + .then(maybeKey => { + if (maybeKey && (maybeKey.keyPacket.algorithm === enums.publicKey.x25519) && !util.isAES(algo)) { + throw Error('Could not generate a session key compatible with the given `encryptionKeys`: X22519 keys can only be used to encrypt AES session keys; change `config.preferredSymmetricAlgorithm` accordingly.'); + } + }) + )); + + const sessionKeyData = mod.generateSessionKey(algo); + return { data: sessionKeyData, algorithm: algorithmName, aeadAlgorithm: aeadAlgorithmName }; + } + + /** + * Encrypt the message either with public keys, passwords, or both at once. + * @param {Array} [encryptionKeys] - Public key(s) for message encryption + * @param {Array} [passwords] - Password(s) for message encryption + * @param {Object} [sessionKey] - Session key in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] } + * @param {Boolean} [wildcard] - Use a key ID of 0 instead of the public key IDs + * @param {Array} [encryptionKeyIDs] - Array of key IDs to use for encryption. Each encryptionKeyIDs[i] corresponds to keys[i] + * @param {Date} [date] - Override the creation date of the literal package + * @param {Array} [userIDs] - User IDs to encrypt for, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }] + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} New message with encrypted content. + * @async + */ + async encrypt(encryptionKeys, passwords, sessionKey, wildcard = false, encryptionKeyIDs = [], date = new Date(), userIDs = [], config$1 = config) { + if (sessionKey) { + if (!util.isUint8Array(sessionKey.data) || !util.isString(sessionKey.algorithm)) { + throw Error('Invalid session key for encryption.'); + } + } else if (encryptionKeys && encryptionKeys.length) { + sessionKey = await Message.generateSessionKey(encryptionKeys, date, userIDs, config$1); + } else if (passwords && passwords.length) { + sessionKey = await Message.generateSessionKey(undefined, undefined, undefined, config$1); + } else { + throw Error('No keys, passwords, or session key provided.'); + } + + const { data: sessionKeyData, algorithm: algorithmName, aeadAlgorithm: aeadAlgorithmName } = sessionKey; + + const msg = await Message.encryptSessionKey(sessionKeyData, algorithmName, aeadAlgorithmName, encryptionKeys, passwords, wildcard, encryptionKeyIDs, date, userIDs, config$1); + + let symEncryptedPacket; + if (aeadAlgorithmName) { + symEncryptedPacket = new AEADEncryptedDataPacket(); + symEncryptedPacket.aeadAlgorithm = enums.write(enums.aead, aeadAlgorithmName); + } else { + symEncryptedPacket = new SymEncryptedIntegrityProtectedDataPacket(); + } + symEncryptedPacket.packets = this.packets; + + const algorithm = enums.write(enums.symmetric, algorithmName); + await symEncryptedPacket.encrypt(algorithm, sessionKeyData, config$1); + + msg.packets.push(symEncryptedPacket); + symEncryptedPacket.packets = new PacketList(); // remove packets after encryption + return msg; + } + + /** + * Encrypt a session key either with public keys, passwords, or both at once. + * @param {Uint8Array} sessionKey - session key for encryption + * @param {String} algorithmName - session key algorithm + * @param {String} [aeadAlgorithmName] - AEAD algorithm, e.g. 'eax' or 'ocb' + * @param {Array} [encryptionKeys] - Public key(s) for message encryption + * @param {Array} [passwords] - For message encryption + * @param {Boolean} [wildcard] - Use a key ID of 0 instead of the public key IDs + * @param {Array} [encryptionKeyIDs] - Array of key IDs to use for encryption. Each encryptionKeyIDs[i] corresponds to encryptionKeys[i] + * @param {Date} [date] - Override the date + * @param {Array} [userIDs] - User IDs to encrypt for, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }] + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} New message with encrypted content. + * @async + */ + static async encryptSessionKey(sessionKey, algorithmName, aeadAlgorithmName, encryptionKeys, passwords, wildcard = false, encryptionKeyIDs = [], date = new Date(), userIDs = [], config$1 = config) { + const packetlist = new PacketList(); + const algorithm = enums.write(enums.symmetric, algorithmName); + const aeadAlgorithm = aeadAlgorithmName && enums.write(enums.aead, aeadAlgorithmName); + + if (encryptionKeys) { + const results = await Promise.all(encryptionKeys.map(async function(primaryKey, i) { + const encryptionKey = await primaryKey.getEncryptionKey(encryptionKeyIDs[i], date, userIDs, config$1); + const pkESKeyPacket = new PublicKeyEncryptedSessionKeyPacket(); + pkESKeyPacket.publicKeyID = wildcard ? KeyID.wildcard() : encryptionKey.getKeyID(); + pkESKeyPacket.publicKeyAlgorithm = encryptionKey.keyPacket.algorithm; + pkESKeyPacket.sessionKey = sessionKey; + pkESKeyPacket.sessionKeyAlgorithm = algorithm; + await pkESKeyPacket.encrypt(encryptionKey.keyPacket); + delete pkESKeyPacket.sessionKey; // delete plaintext session key after encryption + return pkESKeyPacket; + })); + packetlist.push(...results); + } + if (passwords) { + const testDecrypt = async function(keyPacket, password) { + try { + await keyPacket.decrypt(password); + return 1; + } catch (e) { + return 0; + } + }; + + const sum = (accumulator, currentValue) => accumulator + currentValue; + + const encryptPassword = async function(sessionKey, algorithm, aeadAlgorithm, password) { + const symEncryptedSessionKeyPacket = new SymEncryptedSessionKeyPacket(config$1); + symEncryptedSessionKeyPacket.sessionKey = sessionKey; + symEncryptedSessionKeyPacket.sessionKeyAlgorithm = algorithm; + if (aeadAlgorithm) { + symEncryptedSessionKeyPacket.aeadAlgorithm = aeadAlgorithm; + } + await symEncryptedSessionKeyPacket.encrypt(password, config$1); + + if (config$1.passwordCollisionCheck) { + const results = await Promise.all(passwords.map(pwd => testDecrypt(symEncryptedSessionKeyPacket, pwd))); + if (results.reduce(sum) !== 1) { + return encryptPassword(sessionKey, algorithm, password); + } + } + + delete symEncryptedSessionKeyPacket.sessionKey; // delete plaintext session key after encryption + return symEncryptedSessionKeyPacket; + }; + + const results = await Promise.all(passwords.map(pwd => encryptPassword(sessionKey, algorithm, aeadAlgorithm, pwd))); + packetlist.push(...results); + } + + return new Message(packetlist); + } + + /** + * Sign the message (the literal data packet of the message) + * @param {Array} signingKeys - private keys with decrypted secret key data for signing + * @param {Signature} [signature] - Any existing detached signature to add to the message + * @param {Array} [signingKeyIDs] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to signingKeys[i] + * @param {Date} [date] - Override the creation time of the signature + * @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }] + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} New message with signed content. + * @async + */ + async sign(signingKeys = [], signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], config$1 = config) { + const packetlist = new PacketList(); + + const literalDataPacket = this.packets.findPacket(enums.packet.literalData); + if (!literalDataPacket) { + throw Error('No literal data packet to sign.'); + } + + let i; + let existingSigPacketlist; + // If data packet was created from Uint8Array, use binary, otherwise use text + const signatureType = literalDataPacket.text === null ? + enums.signature.binary : enums.signature.text; + + if (signature) { + existingSigPacketlist = signature.packets.filterByTag(enums.packet.signature); + for (i = existingSigPacketlist.length - 1; i >= 0; i--) { + const signaturePacket = existingSigPacketlist[i]; + const onePassSig = new OnePassSignaturePacket(); + onePassSig.signatureType = signaturePacket.signatureType; + onePassSig.hashAlgorithm = signaturePacket.hashAlgorithm; + onePassSig.publicKeyAlgorithm = signaturePacket.publicKeyAlgorithm; + onePassSig.issuerKeyID = signaturePacket.issuerKeyID; + if (!signingKeys.length && i === 0) { + onePassSig.flags = 1; + } + packetlist.push(onePassSig); + } + } + + await Promise.all(Array.from(signingKeys).reverse().map(async function (primaryKey, i) { + if (!primaryKey.isPrivate()) { + throw Error('Need private key for signing'); + } + const signingKeyID = signingKeyIDs[signingKeys.length - 1 - i]; + const signingKey = await primaryKey.getSigningKey(signingKeyID, date, userIDs, config$1); + const onePassSig = new OnePassSignaturePacket(); + onePassSig.signatureType = signatureType; + onePassSig.hashAlgorithm = await getPreferredHashAlgo(primaryKey, signingKey.keyPacket, date, userIDs, config$1); + onePassSig.publicKeyAlgorithm = signingKey.keyPacket.algorithm; + onePassSig.issuerKeyID = signingKey.getKeyID(); + if (i === signingKeys.length - 1) { + onePassSig.flags = 1; + } + return onePassSig; + })).then(onePassSignatureList => { + onePassSignatureList.forEach(onePassSig => packetlist.push(onePassSig)); + }); + + packetlist.push(literalDataPacket); + packetlist.push(...(await createSignaturePackets(literalDataPacket, signingKeys, signature, signingKeyIDs, date, userIDs, notations, false, config$1))); + + return new Message(packetlist); + } + + /** + * Compresses the message (the literal and -if signed- signature data packets of the message) + * @param {module:enums.compression} algo - compression algorithm + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Message} New message with compressed content. + */ + compress(algo, config$1 = config) { + if (algo === enums.compression.uncompressed) { + return this; + } + + const compressed = new CompressedDataPacket(config$1); + compressed.algorithm = algo; + compressed.packets = this.packets; + + const packetList = new PacketList(); + packetList.push(compressed); + + return new Message(packetList); + } + + /** + * Create a detached signature for the message (the literal data packet of the message) + * @param {Array} signingKeys - private keys with decrypted secret key data for signing + * @param {Signature} [signature] - Any existing detached signature + * @param {Array} [signingKeyIDs] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to signingKeys[i] + * @param {Date} [date] - Override the creation time of the signature + * @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }] + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} New detached signature of message content. + * @async + */ + async signDetached(signingKeys = [], signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], config$1 = config) { + const literalDataPacket = this.packets.findPacket(enums.packet.literalData); + if (!literalDataPacket) { + throw Error('No literal data packet to sign.'); + } + return new Signature$2(await createSignaturePackets(literalDataPacket, signingKeys, signature, signingKeyIDs, date, userIDs, notations, true, config$1)); + } + + /** + * Verify message signatures + * @param {Array} verificationKeys - Array of public keys to verify signatures + * @param {Date} [date] - Verify the signature against the given date, i.e. check signature creation time < date < expiration time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise, + * verified: Promise + * }>>} List of signer's keyID and validity of signatures. + * @async + */ + async verify(verificationKeys, date = new Date(), config$1 = config) { + const msg = this.unwrapCompressed(); + const literalDataList = msg.packets.filterByTag(enums.packet.literalData); + if (literalDataList.length !== 1) { + throw Error('Can only verify message with one literal data packet.'); + } + if (isArrayStream(msg.packets.stream)) { + msg.packets.push(...await readToEnd(msg.packets.stream, _ => _ || [])); + } + const onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature).reverse(); + const signatureList = msg.packets.filterByTag(enums.packet.signature); + if (onePassSigList.length && !signatureList.length && util.isStream(msg.packets.stream) && !isArrayStream(msg.packets.stream)) { + await Promise.all(onePassSigList.map(async onePassSig => { + onePassSig.correspondingSig = new Promise((resolve, reject) => { + onePassSig.correspondingSigResolve = resolve; + onePassSig.correspondingSigReject = reject; + }); + onePassSig.signatureData = fromAsync(async () => (await onePassSig.correspondingSig).signatureData); + onePassSig.hashed = readToEnd(await onePassSig.hash(onePassSig.signatureType, literalDataList[0], undefined, false)); + onePassSig.hashed.catch(() => {}); + })); + msg.packets.stream = transformPair(msg.packets.stream, async (readable, writable) => { + const reader = getReader(readable); + const writer = getWriter(writable); + try { + for (let i = 0; i < onePassSigList.length; i++) { + const { value: signature } = await reader.read(); + onePassSigList[i].correspondingSigResolve(signature); + } + await reader.readToEnd(); + await writer.ready; + await writer.close(); + } catch (e) { + onePassSigList.forEach(onePassSig => { + onePassSig.correspondingSigReject(e); + }); + await writer.abort(e); + } + }); + return createVerificationObjects(onePassSigList, literalDataList, verificationKeys, date, false, config$1); + } + return createVerificationObjects(signatureList, literalDataList, verificationKeys, date, false, config$1); + } + + /** + * Verify detached message signature + * @param {Array} verificationKeys - Array of public keys to verify signatures + * @param {Signature} signature + * @param {Date} date - Verify the signature against the given date, i.e. check signature creation time < date < expiration time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise, + * verified: Promise + * }>>} List of signer's keyID and validity of signature. + * @async + */ + verifyDetached(signature, verificationKeys, date = new Date(), config$1 = config) { + const msg = this.unwrapCompressed(); + const literalDataList = msg.packets.filterByTag(enums.packet.literalData); + if (literalDataList.length !== 1) { + throw Error('Can only verify message with one literal data packet.'); + } + const signatureList = signature.packets.filterByTag(enums.packet.signature); // drop UnparsablePackets + return createVerificationObjects(signatureList, literalDataList, verificationKeys, date, true, config$1); + } + + /** + * Unwrap compressed message + * @returns {Message} Message Content of compressed message. + */ + unwrapCompressed() { + const compressed = this.packets.filterByTag(enums.packet.compressedData); + if (compressed.length) { + return new Message(compressed[0].packets); + } + return this; + } + + /** + * Append signature to unencrypted message object + * @param {String|Uint8Array} detachedSignature - The detached ASCII-armored or Uint8Array PGP signature + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + async appendSignature(detachedSignature, config$1 = config) { + await this.packets.read( + util.isUint8Array(detachedSignature) ? detachedSignature : (await unarmor(detachedSignature)).data, + allowedDetachedSignaturePackets, + config$1 + ); + } + + /** + * Returns binary encoded message + * @returns {ReadableStream} Binary message. + */ + write() { + return this.packets.write(); + } + + /** + * Returns ASCII armored text of message + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {ReadableStream} ASCII armor. + */ + armor(config$1 = config) { + return armor(enums.armor.message, this.write(), null, null, null, config$1); + } + } + + /** + * Create signature packets for the message + * @param {LiteralDataPacket} literalDataPacket - the literal data packet to sign + * @param {Array} [signingKeys] - private keys with decrypted secret key data for signing + * @param {Signature} [signature] - Any existing detached signature to append + * @param {Array} [signingKeyIDs] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to signingKeys[i] + * @param {Date} [date] - Override the creationtime of the signature + * @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }] + * @param {Boolean} [detached] - Whether to create detached signature packets + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} List of signature packets. + * @async + * @private + */ + async function createSignaturePackets(literalDataPacket, signingKeys, signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], detached = false, config$1 = config) { + const packetlist = new PacketList(); + + // If data packet was created from Uint8Array, use binary, otherwise use text + const signatureType = literalDataPacket.text === null ? + enums.signature.binary : enums.signature.text; + + await Promise.all(signingKeys.map(async (primaryKey, i) => { + const userID = userIDs[i]; + if (!primaryKey.isPrivate()) { + throw Error('Need private key for signing'); + } + const signingKey = await primaryKey.getSigningKey(signingKeyIDs[i], date, userID, config$1); + return createSignaturePacket(literalDataPacket, primaryKey, signingKey.keyPacket, { signatureType }, date, userID, notations, detached, config$1); + })).then(signatureList => { + packetlist.push(...signatureList); + }); + + if (signature) { + const existingSigPacketlist = signature.packets.filterByTag(enums.packet.signature); + packetlist.push(...existingSigPacketlist); + } + return packetlist; + } + + /** + * Create object containing signer's keyID and validity of signature + * @param {SignaturePacket} signature - Signature packet + * @param {Array} literalDataList - Array of literal data packets + * @param {Array} verificationKeys - Array of public keys to verify signatures + * @param {Date} [date] - Check signature validity with respect to the given date + * @param {Boolean} [detached] - Whether to verify detached signature packets + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise<{ + * keyID: module:type/keyid~KeyID, + * signature: Promise, + * verified: Promise + * }>} signer's keyID and validity of signature + * @async + * @private + */ + async function createVerificationObject(signature, literalDataList, verificationKeys, date = new Date(), detached = false, config$1 = config) { + let primaryKey; + let unverifiedSigningKey; + + for (const key of verificationKeys) { + const issuerKeys = key.getKeys(signature.issuerKeyID); + if (issuerKeys.length > 0) { + primaryKey = key; + unverifiedSigningKey = issuerKeys[0]; + break; + } + } + + const isOnePassSignature = signature instanceof OnePassSignaturePacket; + const signaturePacketPromise = isOnePassSignature ? signature.correspondingSig : signature; + + const verifiedSig = { + keyID: signature.issuerKeyID, + verified: (async () => { + if (!unverifiedSigningKey) { + throw Error(`Could not find signing key with key ID ${signature.issuerKeyID.toHex()}`); + } + + await signature.verify(unverifiedSigningKey.keyPacket, signature.signatureType, literalDataList[0], date, detached, config$1); + const signaturePacket = await signaturePacketPromise; + if (unverifiedSigningKey.getCreationTime() > signaturePacket.created) { + throw Error('Key is newer than the signature'); + } + // We pass the signature creation time to check whether the key was expired at the time of signing. + // We check this after signature verification because for streamed one-pass signatures, the creation time is not available before + try { + await primaryKey.getSigningKey(unverifiedSigningKey.getKeyID(), signaturePacket.created, undefined, config$1); + } catch (e) { + // If a key was reformatted then the self-signatures of the signing key might be in the future compared to the message signature, + // making the key invalid at the time of signing. + // However, if the key is valid at the given `date`, we still allow using it provided the relevant `config` setting is enabled. + // Note: we do not support the edge case of a key that was reformatted and it has expired. + if (config$1.allowInsecureVerificationWithReformattedKeys && e.message.match(/Signature creation time is in the future/)) { + await primaryKey.getSigningKey(unverifiedSigningKey.getKeyID(), date, undefined, config$1); + } else { + throw e; + } + } + return true; + })(), + signature: (async () => { + const signaturePacket = await signaturePacketPromise; + const packetlist = new PacketList(); + signaturePacket && packetlist.push(signaturePacket); + return new Signature$2(packetlist); + })() + }; + + // Mark potential promise rejections as "handled". This is needed because in + // some cases, we reject them before the user has a reasonable chance to + // handle them (e.g. `await readToEnd(result.data); await result.verified` and + // the data stream errors). + verifiedSig.signature.catch(() => {}); + verifiedSig.verified.catch(() => {}); + + return verifiedSig; + } + + /** + * Create list of objects containing signer's keyID and validity of signature + * @param {Array} signatureList - Array of signature packets + * @param {Array} literalDataList - Array of literal data packets + * @param {Array} verificationKeys - Array of public keys to verify signatures + * @param {Date} date - Verify the signature against the given date, + * i.e. check signature creation time < date < expiration time + * @param {Boolean} [detached] - Whether to verify detached signature packets + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise, + * verified: Promise + * }>>} list of signer's keyID and validity of signatures (one entry per signature packet in input) + * @async + * @private + */ + async function createVerificationObjects(signatureList, literalDataList, verificationKeys, date = new Date(), detached = false, config$1 = config) { + return Promise.all(signatureList.filter(function(signature) { + return ['text', 'binary'].includes(enums.read(enums.signature, signature.signatureType)); + }).map(async function(signature) { + return createVerificationObject(signature, literalDataList, verificationKeys, date, detached, config$1); + })); + } + + /** + * Reads an (optionally armored) OpenPGP message and returns a Message object + * @param {Object} options + * @param {String | ReadableStream} [options.armoredMessage] - Armored message to be parsed + * @param {Uint8Array | ReadableStream} [options.binaryMessage] - Binary to be parsed + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} New message object. + * @async + * @static + */ + async function readMessage({ armoredMessage, binaryMessage, config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; + let input = armoredMessage || binaryMessage; + if (!input) { + throw Error('readMessage: must pass options object containing `armoredMessage` or `binaryMessage`'); + } + if (armoredMessage && !util.isString(armoredMessage) && !util.isStream(armoredMessage)) { + throw Error('readMessage: options.armoredMessage must be a string or stream'); + } + if (binaryMessage && !util.isUint8Array(binaryMessage) && !util.isStream(binaryMessage)) { + throw Error('readMessage: options.binaryMessage must be a Uint8Array or stream'); + } + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + const streamType = util.isStream(input); + if (streamType) { + await loadStreamsPonyfill(); + input = toStream(input); + } + if (armoredMessage) { + const { type, data } = await unarmor(input, config$1); + if (type !== enums.armor.message) { + throw Error('Armored text not of type message'); + } + input = data; + } + const packetlist = await PacketList.fromBinary(input, allowedMessagePackets, config$1); + const message = new Message(packetlist); + message.fromStream = streamType; + return message; + } + + /** + * Creates new message object from text or binary data. + * @param {Object} options + * @param {String | ReadableStream} [options.text] - The text message contents + * @param {Uint8Array | ReadableStream} [options.binary] - The binary message contents + * @param {String} [options.filename=""] - Name of the file (if any) + * @param {Date} [options.date=current date] - Date of the message, or modification date of the file + * @param {'utf8'|'binary'|'text'|'mime'} [options.format='utf8' if text is passed, 'binary' otherwise] - Data packet type + * @returns {Promise} New message object. + * @async + * @static + */ + async function createMessage({ text, binary, filename, date = new Date(), format = text !== undefined ? 'utf8' : 'binary', ...rest }) { + let input = text !== undefined ? text : binary; + if (input === undefined) { + throw Error('createMessage: must pass options object containing `text` or `binary`'); + } + if (text && !util.isString(text) && !util.isStream(text)) { + throw Error('createMessage: options.text must be a string or stream'); + } + if (binary && !util.isUint8Array(binary) && !util.isStream(binary)) { + throw Error('createMessage: options.binary must be a Uint8Array or stream'); + } + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + const streamType = util.isStream(input); + if (streamType) { + await loadStreamsPonyfill(); + input = toStream(input); + } + const literalDataPacket = new LiteralDataPacket(date); + if (text !== undefined) { + literalDataPacket.setText(input, enums.write(enums.literal, format)); + } else { + literalDataPacket.setBytes(input, enums.write(enums.literal, format)); + } + if (filename !== undefined) { + literalDataPacket.setFilename(filename); + } + const literalDataPacketlist = new PacketList(); + literalDataPacketlist.push(literalDataPacket); + const message = new Message(literalDataPacketlist); + message.fromStream = streamType; + return message; + } + + // GPG4Browsers - An OpenPGP implementation in javascript + + // A Cleartext message can contain the following packets + const allowedPackets = /*#__PURE__*/ util.constructAllowedPackets([SignaturePacket]); + + /** + * Class that represents an OpenPGP cleartext signed message. + * See {@link https://tools.ietf.org/html/rfc4880#section-7} + */ + class CleartextMessage { + /** + * @param {String} text - The cleartext of the signed message + * @param {Signature} signature - The detached signature or an empty signature for unsigned messages + */ + constructor(text, signature) { + // remove trailing whitespace and normalize EOL to canonical form + this.text = util.removeTrailingSpaces(text).replace(/\r?\n/g, '\r\n'); + if (signature && !(signature instanceof Signature$2)) { + throw Error('Invalid signature input'); + } + this.signature = signature || new Signature$2(new PacketList()); + } + + /** + * Returns the key IDs of the keys that signed the cleartext message + * @returns {Array} Array of keyID objects. + */ + getSigningKeyIDs() { + const keyIDs = []; + const signatureList = this.signature.packets; + signatureList.forEach(function(packet) { + keyIDs.push(packet.issuerKeyID); + }); + return keyIDs; + } + + /** + * Sign the cleartext message + * @param {Array} privateKeys - private keys with decrypted secret key data for signing + * @param {Signature} [signature] - Any existing detached signature + * @param {Array} [signingKeyIDs] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to privateKeys[i] + * @param {Date} [date] - The creation time of the signature that should be created + * @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }] + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise} New cleartext message with signed content. + * @async + */ + async sign(privateKeys, signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], config$1 = config) { + const literalDataPacket = new LiteralDataPacket(); + literalDataPacket.setText(this.text); + const newSignature = new Signature$2(await createSignaturePackets(literalDataPacket, privateKeys, signature, signingKeyIDs, date, userIDs, notations, true, config$1)); + return new CleartextMessage(this.text, newSignature); + } + + /** + * Verify signatures of cleartext signed message + * @param {Array} keys - Array of keys to verify signatures + * @param {Date} [date] - Verify the signature against the given date, i.e. check signature creation time < date < expiration time + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {Promise, + * verified: Promise + * }>>} List of signer's keyID and validity of signature. + * @async + */ + verify(keys, date = new Date(), config$1 = config) { + const signatureList = this.signature.packets.filterByTag(enums.packet.signature); // drop UnparsablePackets + const literalDataPacket = new LiteralDataPacket(); + // we assume that cleartext signature is generated based on UTF8 cleartext + literalDataPacket.setText(this.text); + return createVerificationObjects(signatureList, [literalDataPacket], keys, date, true, config$1); + } + + /** + * Get cleartext + * @returns {String} Cleartext of message. + */ + getText() { + // normalize end of line to \n + return this.text.replace(/\r\n/g, '\n'); + } + + /** + * Returns ASCII armored text of cleartext signed message + * @param {Object} [config] - Full configuration, defaults to openpgp.config + * @returns {String | ReadableStream} ASCII armor. + */ + armor(config$1 = config) { + let hashes = this.signature.packets.map(function(packet) { + return enums.read(enums.hash, packet.hashAlgorithm).toUpperCase(); + }); + hashes = hashes.filter(function(item, i, ar) { return ar.indexOf(item) === i; }); + const body = { + hash: hashes.join(), + text: this.text, + data: this.signature.packets.write() + }; + return armor(enums.armor.signed, body, undefined, undefined, undefined, config$1); + } + } + + /** + * Reads an OpenPGP cleartext signed message and returns a CleartextMessage object + * @param {Object} options + * @param {String} options.cleartextMessage - Text to be parsed + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} New cleartext message object. + * @async + * @static + */ + async function readCleartextMessage({ cleartextMessage, config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; + if (!cleartextMessage) { + throw Error('readCleartextMessage: must pass options object containing `cleartextMessage`'); + } + if (!util.isString(cleartextMessage)) { + throw Error('readCleartextMessage: options.cleartextMessage must be a string'); + } + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + const input = await unarmor(cleartextMessage); + if (input.type !== enums.armor.signed) { + throw Error('No cleartext signed message.'); + } + const packetlist = await PacketList.fromBinary(input.data, allowedPackets, config$1); + verifyHeaders(input.headers, packetlist); + const signature = new Signature$2(packetlist); + return new CleartextMessage(input.text, signature); + } + + /** + * Compare hash algorithm specified in the armor header with signatures + * @param {Array} headers - Armor headers + * @param {PacketList} packetlist - The packetlist with signature packets + * @private + */ + function verifyHeaders(headers, packetlist) { + const checkHashAlgos = function(hashAlgos) { + const check = packet => algo => packet.hashAlgorithm === algo; + + for (let i = 0; i < packetlist.length; i++) { + if (packetlist[i].constructor.tag === enums.packet.signature && !hashAlgos.some(check(packetlist[i]))) { + return false; + } + } + return true; + }; + + let oneHeader = null; + let hashAlgos = []; + headers.forEach(function(header) { + oneHeader = header.match(/^Hash: (.+)$/); // get header value + if (oneHeader) { + oneHeader = oneHeader[1].replace(/\s/g, ''); // remove whitespace + oneHeader = oneHeader.split(','); + oneHeader = oneHeader.map(function(hash) { + hash = hash.toLowerCase(); + try { + return enums.write(enums.hash, hash); + } catch (e) { + throw Error('Unknown hash algorithm in armor header: ' + hash); + } + }); + hashAlgos = hashAlgos.concat(oneHeader); + } else { + throw Error('Only "Hash" header allowed in cleartext signed message'); + } + }); + + if (!hashAlgos.length && !checkHashAlgos([enums.hash.md5])) { + throw Error('If no "Hash" header in cleartext signed message, then only MD5 signatures allowed'); + } else if (hashAlgos.length && !checkHashAlgos(hashAlgos)) { + throw Error('Hash algorithm mismatch in armor header and signature'); + } + } + + /** + * Creates a new CleartextMessage object from text + * @param {Object} options + * @param {String} options.text + * @static + * @async + */ + async function createCleartextMessage({ text, ...rest }) { + if (!text) { + throw Error('createCleartextMessage: must pass options object containing `text`'); + } + if (!util.isString(text)) { + throw Error('createCleartextMessage: options.text must be a string'); + } + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + return new CleartextMessage(text); + } + + // OpenPGP.js - An OpenPGP implementation in javascript + + + ////////////////////// + // // + // Key handling // + // // + ////////////////////// + + + /** + * Generates a new OpenPGP key pair. Supports RSA and ECC keys. By default, primary and subkeys will be of same type. + * The generated primary key will have signing capabilities. By default, one subkey with encryption capabilities is also generated. + * @param {Object} options + * @param {Object|Array} options.userIDs - User IDs as objects: `{ name: 'Jo Doe', email: 'info@jo.com' }` + * @param {'ecc'|'rsa'} [options.type='ecc'] - The primary key algorithm type: ECC (default) or RSA + * @param {String} [options.passphrase=(not protected)] - The passphrase used to encrypt the generated private key. If omitted or empty, the key won't be encrypted. + * @param {Number} [options.rsaBits=4096] - Number of bits for RSA keys + * @param {String} [options.curve='curve25519'] - Elliptic curve for ECC keys: + * curve25519 (default), p256, p384, p521, secp256k1, + * brainpoolP256r1, brainpoolP384r1, or brainpoolP512r1 + * @param {Date} [options.date=current date] - Override the creation date of the key and the key signatures + * @param {Number} [options.keyExpirationTime=0 (never expires)] - Number of seconds from the key creation time after which the key expires + * @param {Array} [options.subkeys=a single encryption subkey] - Options for each subkey e.g. `[{sign: true, passphrase: '123'}]` + * default to main key options, except for `sign` parameter that defaults to false, and indicates whether the subkey should sign rather than encrypt + * @param {'armored'|'binary'|'object'} [options.format='armored'] - format of the output keys + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} The generated key object in the form: + * { privateKey:PrivateKey|Uint8Array|String, publicKey:PublicKey|Uint8Array|String, revocationCertificate:String } + * @async + * @static + */ + async function generateKey({ userIDs = [], passphrase, type = 'ecc', rsaBits = 4096, curve = 'curve25519', keyExpirationTime = 0, date = new Date(), subkeys = [{}], format = 'armored', config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + userIDs = toArray(userIDs); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + if (userIDs.length === 0) { + throw Error('UserIDs are required for key generation'); + } + if (type === 'rsa' && rsaBits < config$1.minRSABits) { + throw Error(`rsaBits should be at least ${config$1.minRSABits}, got: ${rsaBits}`); + } + + const options = { userIDs, passphrase, type, rsaBits, curve, keyExpirationTime, date, subkeys }; + + try { + const { key, revocationCertificate } = await generate(options, config$1); + key.getKeys().forEach(({ keyPacket }) => checkKeyRequirements(keyPacket, config$1)); + + return { + privateKey: formatObject(key, format, config$1), + publicKey: formatObject(key.toPublic(), format, config$1), + revocationCertificate + }; + } catch (err) { + throw util.wrapError('Error generating keypair', err); + } + } + + /** + * Reformats signature packets for a key and rewraps key object. + * @param {Object} options + * @param {PrivateKey} options.privateKey - Private key to reformat + * @param {Object|Array} options.userIDs - User IDs as objects: `{ name: 'Jo Doe', email: 'info@jo.com' }` + * @param {String} [options.passphrase=(not protected)] - The passphrase used to encrypt the reformatted private key. If omitted or empty, the key won't be encrypted. + * @param {Number} [options.keyExpirationTime=0 (never expires)] - Number of seconds from the key creation time after which the key expires + * @param {Date} [options.date] - Override the creation date of the key signatures. If the key was previously used to sign messages, it is recommended + * to set the same date as the key creation time to ensure that old message signatures will still be verifiable using the reformatted key. + * @param {'armored'|'binary'|'object'} [options.format='armored'] - format of the output keys + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} The generated key object in the form: + * { privateKey:PrivateKey|Uint8Array|String, publicKey:PublicKey|Uint8Array|String, revocationCertificate:String } + * @async + * @static + */ + async function reformatKey({ privateKey, userIDs = [], passphrase, keyExpirationTime = 0, date, format = 'armored', config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + userIDs = toArray(userIDs); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + if (userIDs.length === 0) { + throw Error('UserIDs are required for key reformat'); + } + const options = { privateKey, userIDs, passphrase, keyExpirationTime, date }; + + try { + const { key: reformattedKey, revocationCertificate } = await reformat(options, config$1); + + return { + privateKey: formatObject(reformattedKey, format, config$1), + publicKey: formatObject(reformattedKey.toPublic(), format, config$1), + revocationCertificate + }; + } catch (err) { + throw util.wrapError('Error reformatting keypair', err); + } + } + + /** + * Revokes a key. Requires either a private key or a revocation certificate. + * If a revocation certificate is passed, the reasonForRevocation parameter will be ignored. + * @param {Object} options + * @param {Key} options.key - Public or private key to revoke + * @param {String} [options.revocationCertificate] - Revocation certificate to revoke the key with + * @param {Object} [options.reasonForRevocation] - Object indicating the reason for revocation + * @param {module:enums.reasonForRevocation} [options.reasonForRevocation.flag=[noReason]{@link module:enums.reasonForRevocation}] - Flag indicating the reason for revocation + * @param {String} [options.reasonForRevocation.string=""] - String explaining the reason for revocation + * @param {Date} [options.date] - Use the given date instead of the current time to verify validity of revocation certificate (if provided), or as creation time of the revocation signature + * @param {'armored'|'binary'|'object'} [options.format='armored'] - format of the output key(s) + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} The revoked key in the form: + * { privateKey:PrivateKey|Uint8Array|String, publicKey:PublicKey|Uint8Array|String } if private key is passed, or + * { privateKey: null, publicKey:PublicKey|Uint8Array|String } otherwise + * @async + * @static + */ + async function revokeKey({ key, revocationCertificate, reasonForRevocation, date = new Date(), format = 'armored', config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + try { + const revokedKey = revocationCertificate ? + await key.applyRevocationCertificate(revocationCertificate, date, config$1) : + await key.revoke(reasonForRevocation, date, config$1); + + return revokedKey.isPrivate() ? { + privateKey: formatObject(revokedKey, format, config$1), + publicKey: formatObject(revokedKey.toPublic(), format, config$1) + } : { + privateKey: null, + publicKey: formatObject(revokedKey, format, config$1) + }; + } catch (err) { + throw util.wrapError('Error revoking key', err); + } + } + + /** + * Unlock a private key with the given passphrase. + * This method does not change the original key. + * @param {Object} options + * @param {PrivateKey} options.privateKey - The private key to decrypt + * @param {String|Array} options.passphrase - The user's passphrase(s) + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} The unlocked key object. + * @async + */ + async function decryptKey({ privateKey, passphrase, config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + if (!privateKey.isPrivate()) { + throw Error('Cannot decrypt a public key'); + } + const clonedPrivateKey = privateKey.clone(true); + const passphrases = util.isArray(passphrase) ? passphrase : [passphrase]; + + try { + await Promise.all(clonedPrivateKey.getKeys().map(key => ( + // try to decrypt each key with any of the given passphrases + util.anyPromise(passphrases.map(passphrase => key.keyPacket.decrypt(passphrase))) + ))); + + await clonedPrivateKey.validate(config$1); + return clonedPrivateKey; + } catch (err) { + clonedPrivateKey.clearPrivateParams(); + throw util.wrapError('Error decrypting private key', err); + } + } + + /** + * Lock a private key with the given passphrase. + * This method does not change the original key. + * @param {Object} options + * @param {PrivateKey} options.privateKey - The private key to encrypt + * @param {String|Array} options.passphrase - If multiple passphrases, they should be in the same order as the packets each should encrypt + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} The locked key object. + * @async + */ + async function encryptKey({ privateKey, passphrase, config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + if (!privateKey.isPrivate()) { + throw Error('Cannot encrypt a public key'); + } + const clonedPrivateKey = privateKey.clone(true); + + const keys = clonedPrivateKey.getKeys(); + const passphrases = util.isArray(passphrase) ? passphrase : new Array(keys.length).fill(passphrase); + if (passphrases.length !== keys.length) { + throw Error('Invalid number of passphrases given for key encryption'); + } + + try { + await Promise.all(keys.map(async (key, i) => { + const { keyPacket } = key; + await keyPacket.encrypt(passphrases[i], config$1); + keyPacket.clearPrivateParams(); + })); + return clonedPrivateKey; + } catch (err) { + clonedPrivateKey.clearPrivateParams(); + throw util.wrapError('Error encrypting private key', err); + } + } + + + /////////////////////////////////////////// + // // + // Message encryption and decryption // + // // + /////////////////////////////////////////// + + + /** + * Encrypts a message using public keys, passwords or both at once. At least one of `encryptionKeys`, `passwords` or `sessionKeys` + * must be specified. If signing keys are specified, those will be used to sign the message. + * @param {Object} options + * @param {Message} options.message - Message to be encrypted as created by {@link createMessage} + * @param {PublicKey|PublicKey[]} [options.encryptionKeys] - Array of keys or single key, used to encrypt the message + * @param {PrivateKey|PrivateKey[]} [options.signingKeys] - Private keys for signing. If omitted message will not be signed + * @param {String|String[]} [options.passwords] - Array of passwords or a single password to encrypt the message + * @param {Object} [options.sessionKey] - Session key in the form: `{ data:Uint8Array, algorithm:String }` + * @param {'armored'|'binary'|'object'} [options.format='armored'] - Format of the returned message + * @param {Signature} [options.signature] - A detached signature to add to the encrypted message + * @param {Boolean} [options.wildcard=false] - Use a key ID of 0 instead of the public key IDs + * @param {KeyID|KeyID[]} [options.signingKeyIDs=latest-created valid signing (sub)keys] - Array of key IDs to use for signing. Each `signingKeyIDs[i]` corresponds to `signingKeys[i]` + * @param {KeyID|KeyID[]} [options.encryptionKeyIDs=latest-created valid encryption (sub)keys] - Array of key IDs to use for encryption. Each `encryptionKeyIDs[i]` corresponds to `encryptionKeys[i]` + * @param {Date} [options.date=current date] - Override the creation date of the message signature + * @param {Object|Object[]} [options.signingUserIDs=primary user IDs] - Array of user IDs to sign with, one per key in `signingKeys`, e.g. `[{ name: 'Steve Sender', email: 'steve@openpgp.org' }]` + * @param {Object|Object[]} [options.encryptionUserIDs=primary user IDs] - Array of user IDs to encrypt for, one per key in `encryptionKeys`, e.g. `[{ name: 'Robert Receiver', email: 'robert@openpgp.org' }]` + * @param {Object|Object[]} [options.signatureNotations=[]] - Array of notations to add to the signatures, e.g. `[{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }]` + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise|MaybeStream>} Encrypted message (string if `armor` was true, the default; Uint8Array if `armor` was false). + * @async + * @static + */ + async function encrypt({ message, encryptionKeys, signingKeys, passwords, sessionKey, format = 'armored', signature = null, wildcard = false, signingKeyIDs = [], encryptionKeyIDs = [], date = new Date(), signingUserIDs = [], encryptionUserIDs = [], signatureNotations = [], config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + checkMessage(message); checkOutputMessageFormat(format); + encryptionKeys = toArray(encryptionKeys); signingKeys = toArray(signingKeys); passwords = toArray(passwords); + signingKeyIDs = toArray(signingKeyIDs); encryptionKeyIDs = toArray(encryptionKeyIDs); signingUserIDs = toArray(signingUserIDs); encryptionUserIDs = toArray(encryptionUserIDs); signatureNotations = toArray(signatureNotations); + if (rest.detached) { + throw Error("The `detached` option has been removed from openpgp.encrypt, separately call openpgp.sign instead. Don't forget to remove the `privateKeys` option as well."); + } + if (rest.publicKeys) throw Error('The `publicKeys` option has been removed from openpgp.encrypt, pass `encryptionKeys` instead'); + if (rest.privateKeys) throw Error('The `privateKeys` option has been removed from openpgp.encrypt, pass `signingKeys` instead'); + if (rest.armor !== undefined) throw Error('The `armor` option has been removed from openpgp.encrypt, pass `format` instead.'); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + if (!signingKeys) { + signingKeys = []; + } + const streaming = message.fromStream; + try { + if (signingKeys.length || signature) { // sign the message only if signing keys or signature is specified + message = await message.sign(signingKeys, signature, signingKeyIDs, date, signingUserIDs, signatureNotations, config$1); + } + message = message.compress( + await getPreferredAlgo('compression', encryptionKeys, date, encryptionUserIDs, config$1), + config$1 + ); + message = await message.encrypt(encryptionKeys, passwords, sessionKey, wildcard, encryptionKeyIDs, date, encryptionUserIDs, config$1); + if (format === 'object') return message; + // serialize data + const armor = format === 'armored'; + const data = armor ? message.armor(config$1) : message.write(); + return convertStream(data, streaming, armor ? 'utf8' : 'binary'); + } catch (err) { + throw util.wrapError('Error encrypting message', err); + } + } + + /** + * Decrypts a message with the user's private key, a session key or a password. + * One of `decryptionKeys`, `sessionkeys` or `passwords` must be specified (passing a combination of these options is not supported). + * @param {Object} options + * @param {Message} options.message - The message object with the encrypted data + * @param {PrivateKey|PrivateKey[]} [options.decryptionKeys] - Private keys with decrypted secret key data or session key + * @param {String|String[]} [options.passwords] - Passwords to decrypt the message + * @param {Object|Object[]} [options.sessionKeys] - Session keys in the form: { data:Uint8Array, algorithm:String } + * @param {PublicKey|PublicKey[]} [options.verificationKeys] - Array of public keys or single key, to verify signatures + * @param {Boolean} [options.expectSigned=false] - If true, data decryption fails if the message is not signed with the provided publicKeys + * @param {'utf8'|'binary'} [options.format='utf8'] - Whether to return data as a string(Stream) or Uint8Array(Stream). If 'utf8' (the default), also normalize newlines. + * @param {Signature} [options.signature] - Detached signature for verification + * @param {Date} [options.date=current date] - Use the given date for verification instead of the current time + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} Object containing decrypted and verified message in the form: + * + * { + * data: MaybeStream, (if format was 'utf8', the default) + * data: MaybeStream, (if format was 'binary') + * filename: String, + * signatures: [ + * { + * keyID: module:type/keyid~KeyID, + * verified: Promise, + * signature: Promise + * }, ... + * ] + * } + * + * where `signatures` contains a separate entry for each signature packet found in the input message. + * @async + * @static + */ + async function decrypt({ message, decryptionKeys, passwords, sessionKeys, verificationKeys, expectSigned = false, format = 'utf8', signature = null, date = new Date(), config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + checkMessage(message); verificationKeys = toArray(verificationKeys); decryptionKeys = toArray(decryptionKeys); passwords = toArray(passwords); sessionKeys = toArray(sessionKeys); + if (rest.privateKeys) throw Error('The `privateKeys` option has been removed from openpgp.decrypt, pass `decryptionKeys` instead'); + if (rest.publicKeys) throw Error('The `publicKeys` option has been removed from openpgp.decrypt, pass `verificationKeys` instead'); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + try { + const decrypted = await message.decrypt(decryptionKeys, passwords, sessionKeys, date, config$1); + if (!verificationKeys) { + verificationKeys = []; + } + + const result = {}; + result.signatures = signature ? await decrypted.verifyDetached(signature, verificationKeys, date, config$1) : await decrypted.verify(verificationKeys, date, config$1); + result.data = format === 'binary' ? decrypted.getLiteralData() : decrypted.getText(); + result.filename = decrypted.getFilename(); + linkStreams(result, message); + if (expectSigned) { + if (verificationKeys.length === 0) { + throw Error('Verification keys are required to verify message signatures'); + } + if (result.signatures.length === 0) { + throw Error('Message is not signed'); + } + result.data = concat([ + result.data, + fromAsync(async () => { + await util.anyPromise(result.signatures.map(sig => sig.verified)); + }) + ]); + } + result.data = await convertStream(result.data, message.fromStream, format); + return result; + } catch (err) { + throw util.wrapError('Error decrypting message', err); + } + } + + + ////////////////////////////////////////// + // // + // Message signing and verification // + // // + ////////////////////////////////////////// + + + /** + * Signs a message. + * @param {Object} options + * @param {CleartextMessage|Message} options.message - (cleartext) message to be signed + * @param {PrivateKey|PrivateKey[]} options.signingKeys - Array of keys or single key with decrypted secret key data to sign cleartext + * @param {'armored'|'binary'|'object'} [options.format='armored'] - Format of the returned message + * @param {Boolean} [options.detached=false] - If the return value should contain a detached signature + * @param {KeyID|KeyID[]} [options.signingKeyIDs=latest-created valid signing (sub)keys] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to signingKeys[i] + * @param {Date} [options.date=current date] - Override the creation date of the signature + * @param {Object|Object[]} [options.signingUserIDs=primary user IDs] - Array of user IDs to sign with, one per key in `signingKeys`, e.g. `[{ name: 'Steve Sender', email: 'steve@openpgp.org' }]` + * @param {Object|Object[]} [options.signatureNotations=[]] - Array of notations to add to the signatures, e.g. `[{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }]` + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise>} Signed message (string if `armor` was true, the default; Uint8Array if `armor` was false). + * @async + * @static + */ + async function sign({ message, signingKeys, format = 'armored', detached = false, signingKeyIDs = [], date = new Date(), signingUserIDs = [], signatureNotations = [], config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + checkCleartextOrMessage(message); checkOutputMessageFormat(format); + signingKeys = toArray(signingKeys); signingKeyIDs = toArray(signingKeyIDs); signingUserIDs = toArray(signingUserIDs); signatureNotations = toArray(signatureNotations); + + if (rest.privateKeys) throw Error('The `privateKeys` option has been removed from openpgp.sign, pass `signingKeys` instead'); + if (rest.armor !== undefined) throw Error('The `armor` option has been removed from openpgp.sign, pass `format` instead.'); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + if (message instanceof CleartextMessage && format === 'binary') throw Error('Cannot return signed cleartext message in binary format'); + if (message instanceof CleartextMessage && detached) throw Error('Cannot detach-sign a cleartext message'); + + if (!signingKeys || signingKeys.length === 0) { + throw Error('No signing keys provided'); + } + + try { + let signature; + if (detached) { + signature = await message.signDetached(signingKeys, undefined, signingKeyIDs, date, signingUserIDs, signatureNotations, config$1); + } else { + signature = await message.sign(signingKeys, undefined, signingKeyIDs, date, signingUserIDs, signatureNotations, config$1); + } + if (format === 'object') return signature; + + const armor = format === 'armored'; + signature = armor ? signature.armor(config$1) : signature.write(); + if (detached) { + signature = transformPair(message.packets.write(), async (readable, writable) => { + await Promise.all([ + pipe(signature, writable), + readToEnd(readable).catch(() => {}) + ]); + }); + } + return convertStream(signature, message.fromStream, armor ? 'utf8' : 'binary'); + } catch (err) { + throw util.wrapError('Error signing message', err); + } + } + + /** + * Verifies signatures of cleartext signed message + * @param {Object} options + * @param {CleartextMessage|Message} options.message - (cleartext) message object with signatures + * @param {PublicKey|PublicKey[]} options.verificationKeys - Array of publicKeys or single key, to verify signatures + * @param {Boolean} [options.expectSigned=false] - If true, verification throws if the message is not signed with the provided publicKeys + * @param {'utf8'|'binary'} [options.format='utf8'] - Whether to return data as a string(Stream) or Uint8Array(Stream). If 'utf8' (the default), also normalize newlines. + * @param {Signature} [options.signature] - Detached signature for verification + * @param {Date} [options.date=current date] - Use the given date for verification instead of the current time + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} Object containing verified message in the form: + * + * { + * data: MaybeStream, (if `message` was a CleartextMessage) + * data: MaybeStream, (if `message` was a Message) + * signatures: [ + * { + * keyID: module:type/keyid~KeyID, + * verified: Promise, + * signature: Promise + * }, ... + * ] + * } + * + * where `signatures` contains a separate entry for each signature packet found in the input message. + * @async + * @static + */ + async function verify({ message, verificationKeys, expectSigned = false, format = 'utf8', signature = null, date = new Date(), config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + checkCleartextOrMessage(message); verificationKeys = toArray(verificationKeys); + if (rest.publicKeys) throw Error('The `publicKeys` option has been removed from openpgp.verify, pass `verificationKeys` instead'); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + if (message instanceof CleartextMessage && format === 'binary') throw Error("Can't return cleartext message data as binary"); + if (message instanceof CleartextMessage && signature) throw Error("Can't verify detached cleartext signature"); + + try { + const result = {}; + if (signature) { + result.signatures = await message.verifyDetached(signature, verificationKeys, date, config$1); + } else { + result.signatures = await message.verify(verificationKeys, date, config$1); + } + result.data = format === 'binary' ? message.getLiteralData() : message.getText(); + if (message.fromStream) linkStreams(result, message); + if (expectSigned) { + if (result.signatures.length === 0) { + throw Error('Message is not signed'); + } + result.data = concat([ + result.data, + fromAsync(async () => { + await util.anyPromise(result.signatures.map(sig => sig.verified)); + }) + ]); + } + result.data = await convertStream(result.data, message.fromStream, format); + return result; + } catch (err) { + throw util.wrapError('Error verifying signed message', err); + } + } + + + /////////////////////////////////////////////// + // // + // Session key encryption and decryption // + // // + /////////////////////////////////////////////// + + /** + * Generate a new session key object, taking the algorithm preferences of the passed public keys into account, if any. + * @param {Object} options + * @param {PublicKey|PublicKey[]} [options.encryptionKeys] - Array of public keys or single key used to select algorithm preferences for. If no keys are given, the algorithm will be [config.preferredSymmetricAlgorithm]{@link module:config.preferredSymmetricAlgorithm} + * @param {Date} [options.date=current date] - Date to select algorithm preferences at + * @param {Object|Object[]} [options.encryptionUserIDs=primary user IDs] - User IDs to select algorithm preferences for + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise<{ data: Uint8Array, algorithm: String }>} Object with session key data and algorithm. + * @async + * @static + */ + async function generateSessionKey({ encryptionKeys, date = new Date(), encryptionUserIDs = [], config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + encryptionKeys = toArray(encryptionKeys); encryptionUserIDs = toArray(encryptionUserIDs); + if (rest.publicKeys) throw Error('The `publicKeys` option has been removed from openpgp.generateSessionKey, pass `encryptionKeys` instead'); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + try { + const sessionKeys = await Message.generateSessionKey(encryptionKeys, date, encryptionUserIDs, config$1); + return sessionKeys; + } catch (err) { + throw util.wrapError('Error generating session key', err); + } + } + + /** + * Encrypt a symmetric session key with public keys, passwords, or both at once. + * At least one of `encryptionKeys` or `passwords` must be specified. + * @param {Object} options + * @param {Uint8Array} options.data - The session key to be encrypted e.g. 16 random bytes (for aes128) + * @param {String} options.algorithm - Algorithm of the symmetric session key e.g. 'aes128' or 'aes256' + * @param {String} [options.aeadAlgorithm] - AEAD algorithm, e.g. 'eax' or 'ocb' + * @param {PublicKey|PublicKey[]} [options.encryptionKeys] - Array of public keys or single key, used to encrypt the key + * @param {String|String[]} [options.passwords] - Passwords for the message + * @param {'armored'|'binary'} [options.format='armored'] - Format of the returned value + * @param {Boolean} [options.wildcard=false] - Use a key ID of 0 instead of the public key IDs + * @param {KeyID|KeyID[]} [options.encryptionKeyIDs=latest-created valid encryption (sub)keys] - Array of key IDs to use for encryption. Each encryptionKeyIDs[i] corresponds to encryptionKeys[i] + * @param {Date} [options.date=current date] - Override the date + * @param {Object|Object[]} [options.encryptionUserIDs=primary user IDs] - Array of user IDs to encrypt for, one per key in `encryptionKeys`, e.g. `[{ name: 'Phil Zimmermann', email: 'phil@openpgp.org' }]` + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} Encrypted session keys (string if `armor` was true, the default; Uint8Array if `armor` was false). + * @async + * @static + */ + async function encryptSessionKey({ data, algorithm, aeadAlgorithm, encryptionKeys, passwords, format = 'armored', wildcard = false, encryptionKeyIDs = [], date = new Date(), encryptionUserIDs = [], config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + checkBinary(data); checkString(algorithm, 'algorithm'); checkOutputMessageFormat(format); + encryptionKeys = toArray(encryptionKeys); passwords = toArray(passwords); encryptionKeyIDs = toArray(encryptionKeyIDs); encryptionUserIDs = toArray(encryptionUserIDs); + if (rest.publicKeys) throw Error('The `publicKeys` option has been removed from openpgp.encryptSessionKey, pass `encryptionKeys` instead'); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + if ((!encryptionKeys || encryptionKeys.length === 0) && (!passwords || passwords.length === 0)) { + throw Error('No encryption keys or passwords provided.'); + } + + try { + const message = await Message.encryptSessionKey(data, algorithm, aeadAlgorithm, encryptionKeys, passwords, wildcard, encryptionKeyIDs, date, encryptionUserIDs, config$1); + return formatObject(message, format, config$1); + } catch (err) { + throw util.wrapError('Error encrypting session key', err); + } + } + + /** + * Decrypt symmetric session keys using private keys or passwords (not both). + * One of `decryptionKeys` or `passwords` must be specified. + * @param {Object} options + * @param {Message} options.message - A message object containing the encrypted session key packets + * @param {PrivateKey|PrivateKey[]} [options.decryptionKeys] - Private keys with decrypted secret key data + * @param {String|String[]} [options.passwords] - Passwords to decrypt the session key + * @param {Date} [options.date] - Date to use for key verification instead of the current time + * @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config} + * @returns {Promise} Array of decrypted session key, algorithm pairs in the form: + * { data:Uint8Array, algorithm:String } + * @throws if no session key could be found or decrypted + * @async + * @static + */ + async function decryptSessionKeys({ message, decryptionKeys, passwords, date = new Date(), config: config$1, ...rest }) { + config$1 = { ...config, ...config$1 }; checkConfig(config$1); + checkMessage(message); decryptionKeys = toArray(decryptionKeys); passwords = toArray(passwords); + if (rest.privateKeys) throw Error('The `privateKeys` option has been removed from openpgp.decryptSessionKeys, pass `decryptionKeys` instead'); + const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw Error(`Unknown option: ${unknownOptions.join(', ')}`); + + try { + const sessionKeys = await message.decryptSessionKeys(decryptionKeys, passwords, date, config$1); + return sessionKeys; + } catch (err) { + throw util.wrapError('Error decrypting session keys', err); + } + } + + + ////////////////////////// + // // + // Helper functions // + // // + ////////////////////////// + + + /** + * Input validation + * @private + */ + function checkString(data, name) { + if (!util.isString(data)) { + throw Error('Parameter [' + (name || 'data') + '] must be of type String'); + } + } + function checkBinary(data, name) { + if (!util.isUint8Array(data)) { + throw Error('Parameter [' + (name || 'data') + '] must be of type Uint8Array'); + } + } + function checkMessage(message) { + if (!(message instanceof Message)) { + throw Error('Parameter [message] needs to be of type Message'); + } + } + function checkCleartextOrMessage(message) { + if (!(message instanceof CleartextMessage) && !(message instanceof Message)) { + throw Error('Parameter [message] needs to be of type Message or CleartextMessage'); + } + } + function checkOutputMessageFormat(format) { + if (format !== 'armored' && format !== 'binary' && format !== 'object') { + throw Error(`Unsupported format ${format}`); + } + } + const defaultConfigPropsCount = Object.keys(config).length; + function checkConfig(config$1) { + const inputConfigProps = Object.keys(config$1); + if (inputConfigProps.length !== defaultConfigPropsCount) { + for (const inputProp of inputConfigProps) { + if (config[inputProp] === undefined) { + throw Error(`Unknown config property: ${inputProp}`); + } + } + } + } + + /** + * Normalize parameter to an array if it is not undefined. + * @param {Object} param - the parameter to be normalized + * @returns {Array|undefined} The resulting array or undefined. + * @private + */ + function toArray(param) { + if (param && !util.isArray(param)) { + param = [param]; + } + return param; + } + + /** + * Convert data to or from Stream + * @param {Object} data - the data to convert + * @param {'web'|'ponyfill'|'node'|false} streaming - Whether to return a ReadableStream, and of what type + * @param {'utf8'|'binary'} [encoding] - How to return data in Node Readable streams + * @returns {Promise} The data in the respective format. + * @async + * @private + */ + async function convertStream(data, streaming, encoding = 'utf8') { + const streamType = util.isStream(data); + if (streamType === 'array') { + return readToEnd(data); + } + if (streaming === 'web' && streamType === 'ponyfill') { + return toNativeReadable(data); + } + return data; + } + + /** + * Link result.data to the message stream for cancellation. + * Also, forward errors in the message to result.data. + * @param {Object} result - the data to convert + * @param {Message} message - message object + * @returns {Object} + * @private + */ + function linkStreams(result, message) { + result.data = transformPair(message.packets.stream, async (readable, writable) => { + await pipe(result.data, writable, { + preventClose: true + }); + const writer = getWriter(writable); + try { + // Forward errors in the message stream to result.data. + await readToEnd(readable, _ => _); + await writer.close(); + } catch (e) { + await writer.abort(e); + } + }); + } + + /** + * Convert the object to the given format + * @param {Key|Message} object + * @param {'armored'|'binary'|'object'} format + * @param {Object} config - Full configuration + * @returns {String|Uint8Array|Object} + */ + function formatObject(object, format, config) { + switch (format) { + case 'object': + return object; + case 'armored': + return object.armor(config); + case 'binary': + return object.write(); + default: + throw Error(`Unsupported format ${format}`); + } + } + + /** + * web-streams-polyfill v3.0.3 + */ + /// + const SymbolPolyfill = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' ? + Symbol : + description => `Symbol(${description})`; + + /// + function noop$1() { + return undefined; + } + function getGlobals() { + if (typeof self !== 'undefined') { + return self; + } + else if (typeof window !== 'undefined') { + return window; + } + else if (typeof global !== 'undefined') { + return global; + } + return undefined; + } + const globals = getGlobals(); + + function typeIsObject$1(x) { + return (typeof x === 'object' && x !== null) || typeof x === 'function'; + } + const rethrowAssertionErrorRejection = noop$1; + + const originalPromise = Promise; + const originalPromiseThen = Promise.prototype.then; + const originalPromiseResolve = Promise.resolve.bind(originalPromise); + const originalPromiseReject = Promise.reject.bind(originalPromise); + function newPromise(executor) { + return new originalPromise(executor); + } + function promiseResolvedWith(value) { + return originalPromiseResolve(value); + } + function promiseRejectedWith(reason) { + return originalPromiseReject(reason); + } + function PerformPromiseThen(promise, onFulfilled, onRejected) { + // There doesn't appear to be any way to correctly emulate the behaviour from JavaScript, so this is just an + // approximation. + return originalPromiseThen.call(promise, onFulfilled, onRejected); + } + function uponPromise(promise, onFulfilled, onRejected) { + PerformPromiseThen(PerformPromiseThen(promise, onFulfilled, onRejected), undefined, rethrowAssertionErrorRejection); + } + function uponFulfillment(promise, onFulfilled) { + uponPromise(promise, onFulfilled); + } + function uponRejection(promise, onRejected) { + uponPromise(promise, undefined, onRejected); + } + function transformPromiseWith(promise, fulfillmentHandler, rejectionHandler) { + return PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler); + } + function setPromiseIsHandledToTrue(promise) { + PerformPromiseThen(promise, undefined, rethrowAssertionErrorRejection); + } + const queueMicrotask = (() => { + const globalQueueMicrotask = globals && globals.queueMicrotask; + if (typeof globalQueueMicrotask === 'function') { + return globalQueueMicrotask; + } + const resolvedPromise = promiseResolvedWith(undefined); + return (fn) => PerformPromiseThen(resolvedPromise, fn); + })(); + function reflectCall(F, V, args) { + if (typeof F !== 'function') { + throw new TypeError('Argument is not a function'); + } + return Function.prototype.apply.call(F, V, args); + } + function promiseCall(F, V, args) { + try { + return promiseResolvedWith(reflectCall(F, V, args)); + } + catch (value) { + return promiseRejectedWith(value); + } + } + + // Original from Chromium + // https://chromium.googlesource.com/chromium/src/+/0aee4434a4dba42a42abaea9bfbc0cd196a63bc1/third_party/blink/renderer/core/streams/SimpleQueue.js + const QUEUE_MAX_ARRAY_SIZE = 16384; + /** + * Simple queue structure. + * + * Avoids scalability issues with using a packed array directly by using + * multiple arrays in a linked list and keeping the array size bounded. + */ + class SimpleQueue { + constructor() { + this._cursor = 0; + this._size = 0; + // _front and _back are always defined. + this._front = { + _elements: [], + _next: undefined + }; + this._back = this._front; + // The cursor is used to avoid calling Array.shift(). + // It contains the index of the front element of the array inside the + // front-most node. It is always in the range [0, QUEUE_MAX_ARRAY_SIZE). + this._cursor = 0; + // When there is only one node, size === elements.length - cursor. + this._size = 0; + } + get length() { + return this._size; + } + // For exception safety, this method is structured in order: + // 1. Read state + // 2. Calculate required state mutations + // 3. Perform state mutations + push(element) { + const oldBack = this._back; + let newBack = oldBack; + if (oldBack._elements.length === QUEUE_MAX_ARRAY_SIZE - 1) { + newBack = { + _elements: [], + _next: undefined + }; + } + // push() is the mutation most likely to throw an exception, so it + // goes first. + oldBack._elements.push(element); + if (newBack !== oldBack) { + this._back = newBack; + oldBack._next = newBack; + } + ++this._size; + } + // Like push(), shift() follows the read -> calculate -> mutate pattern for + // exception safety. + shift() { // must not be called on an empty queue + const oldFront = this._front; + let newFront = oldFront; + const oldCursor = this._cursor; + let newCursor = oldCursor + 1; + const elements = oldFront._elements; + const element = elements[oldCursor]; + if (newCursor === QUEUE_MAX_ARRAY_SIZE) { + newFront = oldFront._next; + newCursor = 0; + } + // No mutations before this point. + --this._size; + this._cursor = newCursor; + if (oldFront !== newFront) { + this._front = newFront; + } + // Permit shifted element to be garbage collected. + elements[oldCursor] = undefined; + return element; + } + // The tricky thing about forEach() is that it can be called + // re-entrantly. The queue may be mutated inside the callback. It is easy to + // see that push() within the callback has no negative effects since the end + // of the queue is checked for on every iteration. If shift() is called + // repeatedly within the callback then the next iteration may return an + // element that has been removed. In this case the callback will be called + // with undefined values until we either "catch up" with elements that still + // exist or reach the back of the queue. + forEach(callback) { + let i = this._cursor; + let node = this._front; + let elements = node._elements; + while (i !== elements.length || node._next !== undefined) { + if (i === elements.length) { + node = node._next; + elements = node._elements; + i = 0; + if (elements.length === 0) { + break; + } + } + callback(elements[i]); + ++i; + } + } + // Return the element that would be returned if shift() was called now, + // without modifying the queue. + peek() { // must not be called on an empty queue + const front = this._front; + const cursor = this._cursor; + return front._elements[cursor]; + } + } + + function ReadableStreamReaderGenericInitialize(reader, stream) { + reader._ownerReadableStream = stream; + stream._reader = reader; + if (stream._state === 'readable') { + defaultReaderClosedPromiseInitialize(reader); + } + else if (stream._state === 'closed') { + defaultReaderClosedPromiseInitializeAsResolved(reader); + } + else { + defaultReaderClosedPromiseInitializeAsRejected(reader, stream._storedError); + } + } + // A client of ReadableStreamDefaultReader and ReadableStreamBYOBReader may use these functions directly to bypass state + // check. + function ReadableStreamReaderGenericCancel(reader, reason) { + const stream = reader._ownerReadableStream; + return ReadableStreamCancel(stream, reason); + } + function ReadableStreamReaderGenericRelease(reader) { + if (reader._ownerReadableStream._state === 'readable') { + defaultReaderClosedPromiseReject(reader, new TypeError(`Reader was released and can no longer be used to monitor the stream's closedness`)); + } + else { + defaultReaderClosedPromiseResetToRejected(reader, new TypeError(`Reader was released and can no longer be used to monitor the stream's closedness`)); + } + reader._ownerReadableStream._reader = undefined; + reader._ownerReadableStream = undefined; + } + // Helper functions for the readers. + function readerLockException(name) { + return new TypeError('Cannot ' + name + ' a stream using a released reader'); + } + // Helper functions for the ReadableStreamDefaultReader. + function defaultReaderClosedPromiseInitialize(reader) { + reader._closedPromise = newPromise((resolve, reject) => { + reader._closedPromise_resolve = resolve; + reader._closedPromise_reject = reject; + }); + } + function defaultReaderClosedPromiseInitializeAsRejected(reader, reason) { + defaultReaderClosedPromiseInitialize(reader); + defaultReaderClosedPromiseReject(reader, reason); + } + function defaultReaderClosedPromiseInitializeAsResolved(reader) { + defaultReaderClosedPromiseInitialize(reader); + defaultReaderClosedPromiseResolve(reader); + } + function defaultReaderClosedPromiseReject(reader, reason) { + if (reader._closedPromise_reject === undefined) { + return; + } + setPromiseIsHandledToTrue(reader._closedPromise); + reader._closedPromise_reject(reason); + reader._closedPromise_resolve = undefined; + reader._closedPromise_reject = undefined; + } + function defaultReaderClosedPromiseResetToRejected(reader, reason) { + defaultReaderClosedPromiseInitializeAsRejected(reader, reason); + } + function defaultReaderClosedPromiseResolve(reader) { + if (reader._closedPromise_resolve === undefined) { + return; + } + reader._closedPromise_resolve(undefined); + reader._closedPromise_resolve = undefined; + reader._closedPromise_reject = undefined; + } + + const AbortSteps = SymbolPolyfill('[[AbortSteps]]'); + const ErrorSteps = SymbolPolyfill('[[ErrorSteps]]'); + const CancelSteps = SymbolPolyfill('[[CancelSteps]]'); + const PullSteps = SymbolPolyfill('[[PullSteps]]'); + + /// + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite#Polyfill + const NumberIsFinite = Number.isFinite || function (x) { + return typeof x === 'number' && isFinite(x); + }; + + /// + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc#Polyfill + const MathTrunc = Math.trunc || function (v) { + return v < 0 ? Math.ceil(v) : Math.floor(v); + }; + + // https://heycam.github.io/webidl/#idl-dictionaries + function isDictionary(x) { + return typeof x === 'object' || typeof x === 'function'; + } + function assertDictionary(obj, context) { + if (obj !== undefined && !isDictionary(obj)) { + throw new TypeError(`${context} is not an object.`); + } + } + // https://heycam.github.io/webidl/#idl-callback-functions + function assertFunction(x, context) { + if (typeof x !== 'function') { + throw new TypeError(`${context} is not a function.`); + } + } + // https://heycam.github.io/webidl/#idl-object + function isObject(x) { + return (typeof x === 'object' && x !== null) || typeof x === 'function'; + } + function assertObject(x, context) { + if (!isObject(x)) { + throw new TypeError(`${context} is not an object.`); + } + } + function assertRequiredArgument(x, position, context) { + if (x === undefined) { + throw new TypeError(`Parameter ${position} is required in '${context}'.`); + } + } + function assertRequiredField(x, field, context) { + if (x === undefined) { + throw new TypeError(`${field} is required in '${context}'.`); + } + } + // https://heycam.github.io/webidl/#idl-unrestricted-double + function convertUnrestrictedDouble(value) { + return Number(value); + } + function censorNegativeZero(x) { + return x === 0 ? 0 : x; + } + function integerPart(x) { + return censorNegativeZero(MathTrunc(x)); + } + // https://heycam.github.io/webidl/#idl-unsigned-long-long + function convertUnsignedLongLongWithEnforceRange(value, context) { + const lowerBound = 0; + const upperBound = Number.MAX_SAFE_INTEGER; + let x = Number(value); + x = censorNegativeZero(x); + if (!NumberIsFinite(x)) { + throw new TypeError(`${context} is not a finite number`); + } + x = integerPart(x); + if (x < lowerBound || x > upperBound) { + throw new TypeError(`${context} is outside the accepted range of ${lowerBound} to ${upperBound}, inclusive`); + } + if (!NumberIsFinite(x) || x === 0) { + return 0; + } + // TODO Use BigInt if supported? + // let xBigInt = BigInt(integerPart(x)); + // xBigInt = BigInt.asUintN(64, xBigInt); + // return Number(xBigInt); + return x; + } + + function assertReadableStream(x, context) { + if (!IsReadableStream(x)) { + throw new TypeError(`${context} is not a ReadableStream.`); + } + } + + // Abstract operations for the ReadableStream. + function AcquireReadableStreamDefaultReader(stream) { + return new ReadableStreamDefaultReader(stream); + } + // ReadableStream API exposed for controllers. + function ReadableStreamAddReadRequest(stream, readRequest) { + stream._reader._readRequests.push(readRequest); + } + function ReadableStreamFulfillReadRequest(stream, chunk, done) { + const reader = stream._reader; + const readRequest = reader._readRequests.shift(); + if (done) { + readRequest._closeSteps(); + } + else { + readRequest._chunkSteps(chunk); + } + } + function ReadableStreamGetNumReadRequests(stream) { + return stream._reader._readRequests.length; + } + function ReadableStreamHasDefaultReader(stream) { + const reader = stream._reader; + if (reader === undefined) { + return false; + } + if (!IsReadableStreamDefaultReader(reader)) { + return false; + } + return true; + } + /** + * A default reader vended by a {@link ReadableStream}. + * + * @public + */ + class ReadableStreamDefaultReader { + constructor(stream) { + assertRequiredArgument(stream, 1, 'ReadableStreamDefaultReader'); + assertReadableStream(stream, 'First parameter'); + if (IsReadableStreamLocked(stream)) { + throw new TypeError('This stream has already been locked for exclusive reading by another reader'); + } + ReadableStreamReaderGenericInitialize(this, stream); + this._readRequests = new SimpleQueue(); + } + /** + * Returns a promise that will be fulfilled when the stream becomes closed, + * or rejected if the stream ever errors or the reader's lock is released before the stream finishes closing. + */ + get closed() { + if (!IsReadableStreamDefaultReader(this)) { + return promiseRejectedWith(defaultReaderBrandCheckException('closed')); + } + return this._closedPromise; + } + /** + * If the reader is active, behaves the same as {@link ReadableStream.cancel | stream.cancel(reason)}. + */ + cancel(reason = undefined) { + if (!IsReadableStreamDefaultReader(this)) { + return promiseRejectedWith(defaultReaderBrandCheckException('cancel')); + } + if (this._ownerReadableStream === undefined) { + return promiseRejectedWith(readerLockException('cancel')); + } + return ReadableStreamReaderGenericCancel(this, reason); + } + /** + * Returns a promise that allows access to the next chunk from the stream's internal queue, if available. + * + * If reading a chunk causes the queue to become empty, more data will be pulled from the underlying source. + */ + read() { + if (!IsReadableStreamDefaultReader(this)) { + return promiseRejectedWith(defaultReaderBrandCheckException('read')); + } + if (this._ownerReadableStream === undefined) { + return promiseRejectedWith(readerLockException('read from')); + } + let resolvePromise; + let rejectPromise; + const promise = newPromise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + const readRequest = { + _chunkSteps: chunk => resolvePromise({ value: chunk, done: false }), + _closeSteps: () => resolvePromise({ value: undefined, done: true }), + _errorSteps: e => rejectPromise(e) + }; + ReadableStreamDefaultReaderRead(this, readRequest); + return promise; + } + /** + * Releases the reader's lock on the corresponding stream. After the lock is released, the reader is no longer active. + * If the associated stream is errored when the lock is released, the reader will appear errored in the same way + * from now on; otherwise, the reader will appear closed. + * + * A reader's lock cannot be released while it still has a pending read request, i.e., if a promise returned by + * the reader's {@link ReadableStreamDefaultReader.read | read()} method has not yet been settled. Attempting to + * do so will throw a `TypeError` and leave the reader locked to the stream. + */ + releaseLock() { + if (!IsReadableStreamDefaultReader(this)) { + throw defaultReaderBrandCheckException('releaseLock'); + } + if (this._ownerReadableStream === undefined) { + return; + } + if (this._readRequests.length > 0) { + throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled'); + } + ReadableStreamReaderGenericRelease(this); + } + } + Object.defineProperties(ReadableStreamDefaultReader.prototype, { + cancel: { enumerable: true }, + read: { enumerable: true }, + releaseLock: { enumerable: true }, + closed: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(ReadableStreamDefaultReader.prototype, SymbolPolyfill.toStringTag, { + value: 'ReadableStreamDefaultReader', + configurable: true + }); + } + // Abstract operations for the readers. + function IsReadableStreamDefaultReader(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_readRequests')) { + return false; + } + return true; + } + function ReadableStreamDefaultReaderRead(reader, readRequest) { + const stream = reader._ownerReadableStream; + stream._disturbed = true; + if (stream._state === 'closed') { + readRequest._closeSteps(); + } + else if (stream._state === 'errored') { + readRequest._errorSteps(stream._storedError); + } + else { + stream._readableStreamController[PullSteps](readRequest); + } + } + // Helper functions for the ReadableStreamDefaultReader. + function defaultReaderBrandCheckException(name) { + return new TypeError(`ReadableStreamDefaultReader.prototype.${name} can only be used on a ReadableStreamDefaultReader`); + } + + /// + let AsyncIteratorPrototype; + if (typeof SymbolPolyfill.asyncIterator === 'symbol') { + // We're running inside a ES2018+ environment, but we're compiling to an older syntax. + // We cannot access %AsyncIteratorPrototype% without non-ES2018 syntax, but we can re-create it. + AsyncIteratorPrototype = { + // 25.1.3.1 %AsyncIteratorPrototype% [ @@asyncIterator ] ( ) + // https://tc39.github.io/ecma262/#sec-asynciteratorprototype-asynciterator + [SymbolPolyfill.asyncIterator]() { + return this; + } + }; + Object.defineProperty(AsyncIteratorPrototype, SymbolPolyfill.asyncIterator, { enumerable: false }); + } + + /// + class ReadableStreamAsyncIteratorImpl { + constructor(reader, preventCancel) { + this._ongoingPromise = undefined; + this._isFinished = false; + this._reader = reader; + this._preventCancel = preventCancel; + } + next() { + const nextSteps = () => this._nextSteps(); + this._ongoingPromise = this._ongoingPromise ? + transformPromiseWith(this._ongoingPromise, nextSteps, nextSteps) : + nextSteps(); + return this._ongoingPromise; + } + return(value) { + const returnSteps = () => this._returnSteps(value); + return this._ongoingPromise ? + transformPromiseWith(this._ongoingPromise, returnSteps, returnSteps) : + returnSteps(); + } + _nextSteps() { + if (this._isFinished) { + return Promise.resolve({ value: undefined, done: true }); + } + const reader = this._reader; + if (reader._ownerReadableStream === undefined) { + return promiseRejectedWith(readerLockException('iterate')); + } + let resolvePromise; + let rejectPromise; + const promise = newPromise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + const readRequest = { + _chunkSteps: chunk => { + this._ongoingPromise = undefined; + // This needs to be delayed by one microtask, otherwise we stop pulling too early which breaks a test. + // FIXME Is this a bug in the specification, or in the test? + queueMicrotask(() => resolvePromise({ value: chunk, done: false })); + }, + _closeSteps: () => { + this._ongoingPromise = undefined; + this._isFinished = true; + ReadableStreamReaderGenericRelease(reader); + resolvePromise({ value: undefined, done: true }); + }, + _errorSteps: reason => { + this._ongoingPromise = undefined; + this._isFinished = true; + ReadableStreamReaderGenericRelease(reader); + rejectPromise(reason); + } + }; + ReadableStreamDefaultReaderRead(reader, readRequest); + return promise; + } + _returnSteps(value) { + if (this._isFinished) { + return Promise.resolve({ value, done: true }); + } + this._isFinished = true; + const reader = this._reader; + if (reader._ownerReadableStream === undefined) { + return promiseRejectedWith(readerLockException('finish iterating')); + } + if (!this._preventCancel) { + const result = ReadableStreamReaderGenericCancel(reader, value); + ReadableStreamReaderGenericRelease(reader); + return transformPromiseWith(result, () => ({ value, done: true })); + } + ReadableStreamReaderGenericRelease(reader); + return promiseResolvedWith({ value, done: true }); + } + } + const ReadableStreamAsyncIteratorPrototype = { + next() { + if (!IsReadableStreamAsyncIterator(this)) { + return promiseRejectedWith(streamAsyncIteratorBrandCheckException('next')); + } + return this._asyncIteratorImpl.next(); + }, + return(value) { + if (!IsReadableStreamAsyncIterator(this)) { + return promiseRejectedWith(streamAsyncIteratorBrandCheckException('return')); + } + return this._asyncIteratorImpl.return(value); + } + }; + if (AsyncIteratorPrototype !== undefined) { + Object.setPrototypeOf(ReadableStreamAsyncIteratorPrototype, AsyncIteratorPrototype); + } + // Abstract operations for the ReadableStream. + function AcquireReadableStreamAsyncIterator(stream, preventCancel) { + const reader = AcquireReadableStreamDefaultReader(stream); + const impl = new ReadableStreamAsyncIteratorImpl(reader, preventCancel); + const iterator = Object.create(ReadableStreamAsyncIteratorPrototype); + iterator._asyncIteratorImpl = impl; + return iterator; + } + function IsReadableStreamAsyncIterator(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_asyncIteratorImpl')) { + return false; + } + return true; + } + // Helper functions for the ReadableStream. + function streamAsyncIteratorBrandCheckException(name) { + return new TypeError(`ReadableStreamAsyncIterator.${name} can only be used on a ReadableSteamAsyncIterator`); + } + + /// + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN#Polyfill + const NumberIsNaN = Number.isNaN || function (x) { + // eslint-disable-next-line no-self-compare + return x !== x; + }; + + function IsFiniteNonNegativeNumber(v) { + if (!IsNonNegativeNumber(v)) { + return false; + } + if (v === Infinity) { + return false; + } + return true; + } + function IsNonNegativeNumber(v) { + if (typeof v !== 'number') { + return false; + } + if (NumberIsNaN(v)) { + return false; + } + if (v < 0) { + return false; + } + return true; + } + + function DequeueValue(container) { + const pair = container._queue.shift(); + container._queueTotalSize -= pair.size; + if (container._queueTotalSize < 0) { + container._queueTotalSize = 0; + } + return pair.value; + } + function EnqueueValueWithSize(container, value, size) { + size = Number(size); + if (!IsFiniteNonNegativeNumber(size)) { + throw new RangeError('Size must be a finite, non-NaN, non-negative number.'); + } + container._queue.push({ value, size }); + container._queueTotalSize += size; + } + function PeekQueueValue(container) { + const pair = container._queue.peek(); + return pair.value; + } + function ResetQueue(container) { + container._queue = new SimpleQueue(); + container._queueTotalSize = 0; + } + + function CreateArrayFromList(elements) { + // We use arrays to represent lists, so this is basically a no-op. + // Do a slice though just in case we happen to depend on the unique-ness. + return elements.slice(); + } + function CopyDataBlockBytes(dest, destOffset, src, srcOffset, n) { + new Uint8Array(dest).set(new Uint8Array(src, srcOffset, n), destOffset); + } + // Not implemented correctly + function TransferArrayBuffer(O) { + return O; + } + // Not implemented correctly + function IsDetachedBuffer(O) { + return false; + } + + /** + * A pull-into request in a {@link ReadableByteStreamController}. + * + * @public + */ + class ReadableStreamBYOBRequest { + constructor() { + throw new TypeError('Illegal constructor'); + } + /** + * Returns the view for writing in to, or `null` if the BYOB request has already been responded to. + */ + get view() { + if (!IsReadableStreamBYOBRequest(this)) { + throw byobRequestBrandCheckException('view'); + } + return this._view; + } + respond(bytesWritten) { + if (!IsReadableStreamBYOBRequest(this)) { + throw byobRequestBrandCheckException('respond'); + } + assertRequiredArgument(bytesWritten, 1, 'respond'); + bytesWritten = convertUnsignedLongLongWithEnforceRange(bytesWritten, 'First parameter'); + if (this._associatedReadableByteStreamController === undefined) { + throw new TypeError('This BYOB request has been invalidated'); + } + if (IsDetachedBuffer(this._view.buffer)) ; + ReadableByteStreamControllerRespond(this._associatedReadableByteStreamController, bytesWritten); + } + respondWithNewView(view) { + if (!IsReadableStreamBYOBRequest(this)) { + throw byobRequestBrandCheckException('respondWithNewView'); + } + assertRequiredArgument(view, 1, 'respondWithNewView'); + if (!ArrayBuffer.isView(view)) { + throw new TypeError('You can only respond with array buffer views'); + } + if (view.byteLength === 0) { + throw new TypeError('chunk must have non-zero byteLength'); + } + if (view.buffer.byteLength === 0) { + throw new TypeError(`chunk's buffer must have non-zero byteLength`); + } + if (this._associatedReadableByteStreamController === undefined) { + throw new TypeError('This BYOB request has been invalidated'); + } + ReadableByteStreamControllerRespondWithNewView(this._associatedReadableByteStreamController, view); + } + } + Object.defineProperties(ReadableStreamBYOBRequest.prototype, { + respond: { enumerable: true }, + respondWithNewView: { enumerable: true }, + view: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(ReadableStreamBYOBRequest.prototype, SymbolPolyfill.toStringTag, { + value: 'ReadableStreamBYOBRequest', + configurable: true + }); + } + /** + * Allows control of a {@link ReadableStream | readable byte stream}'s state and internal queue. + * + * @public + */ + class ReadableByteStreamController { + constructor() { + throw new TypeError('Illegal constructor'); + } + /** + * Returns the current BYOB pull request, or `null` if there isn't one. + */ + get byobRequest() { + if (!IsReadableByteStreamController(this)) { + throw byteStreamControllerBrandCheckException('byobRequest'); + } + if (this._byobRequest === null && this._pendingPullIntos.length > 0) { + const firstDescriptor = this._pendingPullIntos.peek(); + const view = new Uint8Array(firstDescriptor.buffer, firstDescriptor.byteOffset + firstDescriptor.bytesFilled, firstDescriptor.byteLength - firstDescriptor.bytesFilled); + const byobRequest = Object.create(ReadableStreamBYOBRequest.prototype); + SetUpReadableStreamBYOBRequest(byobRequest, this, view); + this._byobRequest = byobRequest; + } + return this._byobRequest; + } + /** + * Returns the desired size to fill the controlled stream's internal queue. It can be negative, if the queue is + * over-full. An underlying byte source ought to use this information to determine when and how to apply backpressure. + */ + get desiredSize() { + if (!IsReadableByteStreamController(this)) { + throw byteStreamControllerBrandCheckException('desiredSize'); + } + return ReadableByteStreamControllerGetDesiredSize(this); + } + /** + * Closes the controlled readable stream. Consumers will still be able to read any previously-enqueued chunks from + * the stream, but once those are read, the stream will become closed. + */ + close() { + if (!IsReadableByteStreamController(this)) { + throw byteStreamControllerBrandCheckException('close'); + } + if (this._closeRequested) { + throw new TypeError('The stream has already been closed; do not close it again!'); + } + const state = this._controlledReadableByteStream._state; + if (state !== 'readable') { + throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be closed`); + } + ReadableByteStreamControllerClose(this); + } + enqueue(chunk) { + if (!IsReadableByteStreamController(this)) { + throw byteStreamControllerBrandCheckException('enqueue'); + } + assertRequiredArgument(chunk, 1, 'enqueue'); + if (!ArrayBuffer.isView(chunk)) { + throw new TypeError('chunk must be an array buffer view'); + } + if (chunk.byteLength === 0) { + throw new TypeError('chunk must have non-zero byteLength'); + } + if (chunk.buffer.byteLength === 0) { + throw new TypeError(`chunk's buffer must have non-zero byteLength`); + } + if (this._closeRequested) { + throw new TypeError('stream is closed or draining'); + } + const state = this._controlledReadableByteStream._state; + if (state !== 'readable') { + throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be enqueued to`); + } + ReadableByteStreamControllerEnqueue(this, chunk); + } + /** + * Errors the controlled readable stream, making all future interactions with it fail with the given error `e`. + */ + error(e = undefined) { + if (!IsReadableByteStreamController(this)) { + throw byteStreamControllerBrandCheckException('error'); + } + ReadableByteStreamControllerError(this, e); + } + /** @internal */ + [CancelSteps](reason) { + if (this._pendingPullIntos.length > 0) { + const firstDescriptor = this._pendingPullIntos.peek(); + firstDescriptor.bytesFilled = 0; + } + ResetQueue(this); + const result = this._cancelAlgorithm(reason); + ReadableByteStreamControllerClearAlgorithms(this); + return result; + } + /** @internal */ + [PullSteps](readRequest) { + const stream = this._controlledReadableByteStream; + if (this._queueTotalSize > 0) { + const entry = this._queue.shift(); + this._queueTotalSize -= entry.byteLength; + ReadableByteStreamControllerHandleQueueDrain(this); + const view = new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength); + readRequest._chunkSteps(view); + return; + } + const autoAllocateChunkSize = this._autoAllocateChunkSize; + if (autoAllocateChunkSize !== undefined) { + let buffer; + try { + buffer = new ArrayBuffer(autoAllocateChunkSize); + } + catch (bufferE) { + readRequest._errorSteps(bufferE); + return; + } + const pullIntoDescriptor = { + buffer, + byteOffset: 0, + byteLength: autoAllocateChunkSize, + bytesFilled: 0, + elementSize: 1, + viewConstructor: Uint8Array, + readerType: 'default' + }; + this._pendingPullIntos.push(pullIntoDescriptor); + } + ReadableStreamAddReadRequest(stream, readRequest); + ReadableByteStreamControllerCallPullIfNeeded(this); + } + } + Object.defineProperties(ReadableByteStreamController.prototype, { + close: { enumerable: true }, + enqueue: { enumerable: true }, + error: { enumerable: true }, + byobRequest: { enumerable: true }, + desiredSize: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(ReadableByteStreamController.prototype, SymbolPolyfill.toStringTag, { + value: 'ReadableByteStreamController', + configurable: true + }); + } + // Abstract operations for the ReadableByteStreamController. + function IsReadableByteStreamController(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_controlledReadableByteStream')) { + return false; + } + return true; + } + function IsReadableStreamBYOBRequest(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_associatedReadableByteStreamController')) { + return false; + } + return true; + } + function ReadableByteStreamControllerCallPullIfNeeded(controller) { + const shouldPull = ReadableByteStreamControllerShouldCallPull(controller); + if (!shouldPull) { + return; + } + if (controller._pulling) { + controller._pullAgain = true; + return; + } + controller._pulling = true; + // TODO: Test controller argument + const pullPromise = controller._pullAlgorithm(); + uponPromise(pullPromise, () => { + controller._pulling = false; + if (controller._pullAgain) { + controller._pullAgain = false; + ReadableByteStreamControllerCallPullIfNeeded(controller); + } + }, e => { + ReadableByteStreamControllerError(controller, e); + }); + } + function ReadableByteStreamControllerClearPendingPullIntos(controller) { + ReadableByteStreamControllerInvalidateBYOBRequest(controller); + controller._pendingPullIntos = new SimpleQueue(); + } + function ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor) { + let done = false; + if (stream._state === 'closed') { + done = true; + } + const filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor); + if (pullIntoDescriptor.readerType === 'default') { + ReadableStreamFulfillReadRequest(stream, filledView, done); + } + else { + ReadableStreamFulfillReadIntoRequest(stream, filledView, done); + } + } + function ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor) { + const bytesFilled = pullIntoDescriptor.bytesFilled; + const elementSize = pullIntoDescriptor.elementSize; + return new pullIntoDescriptor.viewConstructor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, bytesFilled / elementSize); + } + function ReadableByteStreamControllerEnqueueChunkToQueue(controller, buffer, byteOffset, byteLength) { + controller._queue.push({ buffer, byteOffset, byteLength }); + controller._queueTotalSize += byteLength; + } + function ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) { + const elementSize = pullIntoDescriptor.elementSize; + const currentAlignedBytes = pullIntoDescriptor.bytesFilled - pullIntoDescriptor.bytesFilled % elementSize; + const maxBytesToCopy = Math.min(controller._queueTotalSize, pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled); + const maxBytesFilled = pullIntoDescriptor.bytesFilled + maxBytesToCopy; + const maxAlignedBytes = maxBytesFilled - maxBytesFilled % elementSize; + let totalBytesToCopyRemaining = maxBytesToCopy; + let ready = false; + if (maxAlignedBytes > currentAlignedBytes) { + totalBytesToCopyRemaining = maxAlignedBytes - pullIntoDescriptor.bytesFilled; + ready = true; + } + const queue = controller._queue; + while (totalBytesToCopyRemaining > 0) { + const headOfQueue = queue.peek(); + const bytesToCopy = Math.min(totalBytesToCopyRemaining, headOfQueue.byteLength); + const destStart = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled; + CopyDataBlockBytes(pullIntoDescriptor.buffer, destStart, headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy); + if (headOfQueue.byteLength === bytesToCopy) { + queue.shift(); + } + else { + headOfQueue.byteOffset += bytesToCopy; + headOfQueue.byteLength -= bytesToCopy; + } + controller._queueTotalSize -= bytesToCopy; + ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesToCopy, pullIntoDescriptor); + totalBytesToCopyRemaining -= bytesToCopy; + } + return ready; + } + function ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, size, pullIntoDescriptor) { + ReadableByteStreamControllerInvalidateBYOBRequest(controller); + pullIntoDescriptor.bytesFilled += size; + } + function ReadableByteStreamControllerHandleQueueDrain(controller) { + if (controller._queueTotalSize === 0 && controller._closeRequested) { + ReadableByteStreamControllerClearAlgorithms(controller); + ReadableStreamClose(controller._controlledReadableByteStream); + } + else { + ReadableByteStreamControllerCallPullIfNeeded(controller); + } + } + function ReadableByteStreamControllerInvalidateBYOBRequest(controller) { + if (controller._byobRequest === null) { + return; + } + controller._byobRequest._associatedReadableByteStreamController = undefined; + controller._byobRequest._view = null; + controller._byobRequest = null; + } + function ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller) { + while (controller._pendingPullIntos.length > 0) { + if (controller._queueTotalSize === 0) { + return; + } + const pullIntoDescriptor = controller._pendingPullIntos.peek(); + if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor)) { + ReadableByteStreamControllerShiftPendingPullInto(controller); + ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableByteStream, pullIntoDescriptor); + } + } + } + function ReadableByteStreamControllerPullInto(controller, view, readIntoRequest) { + const stream = controller._controlledReadableByteStream; + let elementSize = 1; + if (view.constructor !== DataView) { + elementSize = view.constructor.BYTES_PER_ELEMENT; + } + const ctor = view.constructor; + const buffer = TransferArrayBuffer(view.buffer); + const pullIntoDescriptor = { + buffer, + byteOffset: view.byteOffset, + byteLength: view.byteLength, + bytesFilled: 0, + elementSize, + viewConstructor: ctor, + readerType: 'byob' + }; + if (controller._pendingPullIntos.length > 0) { + controller._pendingPullIntos.push(pullIntoDescriptor); + // No ReadableByteStreamControllerCallPullIfNeeded() call since: + // - No change happens on desiredSize + // - The source has already been notified of that there's at least 1 pending read(view) + ReadableStreamAddReadIntoRequest(stream, readIntoRequest); + return; + } + if (stream._state === 'closed') { + const emptyView = new ctor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, 0); + readIntoRequest._closeSteps(emptyView); + return; + } + if (controller._queueTotalSize > 0) { + if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor)) { + const filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor); + ReadableByteStreamControllerHandleQueueDrain(controller); + readIntoRequest._chunkSteps(filledView); + return; + } + if (controller._closeRequested) { + const e = new TypeError('Insufficient bytes to fill elements in the given buffer'); + ReadableByteStreamControllerError(controller, e); + readIntoRequest._errorSteps(e); + return; + } + } + controller._pendingPullIntos.push(pullIntoDescriptor); + ReadableStreamAddReadIntoRequest(stream, readIntoRequest); + ReadableByteStreamControllerCallPullIfNeeded(controller); + } + function ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor) { + firstDescriptor.buffer = TransferArrayBuffer(firstDescriptor.buffer); + const stream = controller._controlledReadableByteStream; + if (ReadableStreamHasBYOBReader(stream)) { + while (ReadableStreamGetNumReadIntoRequests(stream) > 0) { + const pullIntoDescriptor = ReadableByteStreamControllerShiftPendingPullInto(controller); + ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor); + } + } + } + function ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, pullIntoDescriptor) { + if (pullIntoDescriptor.bytesFilled + bytesWritten > pullIntoDescriptor.byteLength) { + throw new RangeError('bytesWritten out of range'); + } + ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesWritten, pullIntoDescriptor); + if (pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize) { + // TODO: Figure out whether we should detach the buffer or not here. + return; + } + ReadableByteStreamControllerShiftPendingPullInto(controller); + const remainderSize = pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize; + if (remainderSize > 0) { + const end = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled; + const remainder = pullIntoDescriptor.buffer.slice(end - remainderSize, end); + ReadableByteStreamControllerEnqueueChunkToQueue(controller, remainder, 0, remainder.byteLength); + } + pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer); + pullIntoDescriptor.bytesFilled -= remainderSize; + ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableByteStream, pullIntoDescriptor); + ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); + } + function ReadableByteStreamControllerRespondInternal(controller, bytesWritten) { + const firstDescriptor = controller._pendingPullIntos.peek(); + const state = controller._controlledReadableByteStream._state; + if (state === 'closed') { + if (bytesWritten !== 0) { + throw new TypeError('bytesWritten must be 0 when calling respond() on a closed stream'); + } + ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor); + } + else { + ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor); + } + ReadableByteStreamControllerCallPullIfNeeded(controller); + } + function ReadableByteStreamControllerShiftPendingPullInto(controller) { + const descriptor = controller._pendingPullIntos.shift(); + ReadableByteStreamControllerInvalidateBYOBRequest(controller); + return descriptor; + } + function ReadableByteStreamControllerShouldCallPull(controller) { + const stream = controller._controlledReadableByteStream; + if (stream._state !== 'readable') { + return false; + } + if (controller._closeRequested) { + return false; + } + if (!controller._started) { + return false; + } + if (ReadableStreamHasDefaultReader(stream) && ReadableStreamGetNumReadRequests(stream) > 0) { + return true; + } + if (ReadableStreamHasBYOBReader(stream) && ReadableStreamGetNumReadIntoRequests(stream) > 0) { + return true; + } + const desiredSize = ReadableByteStreamControllerGetDesiredSize(controller); + if (desiredSize > 0) { + return true; + } + return false; + } + function ReadableByteStreamControllerClearAlgorithms(controller) { + controller._pullAlgorithm = undefined; + controller._cancelAlgorithm = undefined; + } + // A client of ReadableByteStreamController may use these functions directly to bypass state check. + function ReadableByteStreamControllerClose(controller) { + const stream = controller._controlledReadableByteStream; + if (controller._closeRequested || stream._state !== 'readable') { + return; + } + if (controller._queueTotalSize > 0) { + controller._closeRequested = true; + return; + } + if (controller._pendingPullIntos.length > 0) { + const firstPendingPullInto = controller._pendingPullIntos.peek(); + if (firstPendingPullInto.bytesFilled > 0) { + const e = new TypeError('Insufficient bytes to fill elements in the given buffer'); + ReadableByteStreamControllerError(controller, e); + throw e; + } + } + ReadableByteStreamControllerClearAlgorithms(controller); + ReadableStreamClose(stream); + } + function ReadableByteStreamControllerEnqueue(controller, chunk) { + const stream = controller._controlledReadableByteStream; + if (controller._closeRequested || stream._state !== 'readable') { + return; + } + const buffer = chunk.buffer; + const byteOffset = chunk.byteOffset; + const byteLength = chunk.byteLength; + const transferredBuffer = TransferArrayBuffer(buffer); + if (ReadableStreamHasDefaultReader(stream)) { + if (ReadableStreamGetNumReadRequests(stream) === 0) { + ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); + } + else { + const transferredView = new Uint8Array(transferredBuffer, byteOffset, byteLength); + ReadableStreamFulfillReadRequest(stream, transferredView, false); + } + } + else if (ReadableStreamHasBYOBReader(stream)) { + // TODO: Ideally in this branch detaching should happen only if the buffer is not consumed fully. + ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); + ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); + } + else { + ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); + } + ReadableByteStreamControllerCallPullIfNeeded(controller); + } + function ReadableByteStreamControllerError(controller, e) { + const stream = controller._controlledReadableByteStream; + if (stream._state !== 'readable') { + return; + } + ReadableByteStreamControllerClearPendingPullIntos(controller); + ResetQueue(controller); + ReadableByteStreamControllerClearAlgorithms(controller); + ReadableStreamError(stream, e); + } + function ReadableByteStreamControllerGetDesiredSize(controller) { + const state = controller._controlledReadableByteStream._state; + if (state === 'errored') { + return null; + } + if (state === 'closed') { + return 0; + } + return controller._strategyHWM - controller._queueTotalSize; + } + function ReadableByteStreamControllerRespond(controller, bytesWritten) { + bytesWritten = Number(bytesWritten); + if (!IsFiniteNonNegativeNumber(bytesWritten)) { + throw new RangeError('bytesWritten must be a finite'); + } + ReadableByteStreamControllerRespondInternal(controller, bytesWritten); + } + function ReadableByteStreamControllerRespondWithNewView(controller, view) { + const firstDescriptor = controller._pendingPullIntos.peek(); + if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset) { + throw new RangeError('The region specified by view does not match byobRequest'); + } + if (firstDescriptor.byteLength !== view.byteLength) { + throw new RangeError('The buffer of view has different capacity than byobRequest'); + } + firstDescriptor.buffer = view.buffer; + ReadableByteStreamControllerRespondInternal(controller, view.byteLength); + } + function SetUpReadableByteStreamController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, autoAllocateChunkSize) { + controller._controlledReadableByteStream = stream; + controller._pullAgain = false; + controller._pulling = false; + controller._byobRequest = null; + // Need to set the slots so that the assert doesn't fire. In the spec the slots already exist implicitly. + controller._queue = controller._queueTotalSize = undefined; + ResetQueue(controller); + controller._closeRequested = false; + controller._started = false; + controller._strategyHWM = highWaterMark; + controller._pullAlgorithm = pullAlgorithm; + controller._cancelAlgorithm = cancelAlgorithm; + controller._autoAllocateChunkSize = autoAllocateChunkSize; + controller._pendingPullIntos = new SimpleQueue(); + stream._readableStreamController = controller; + const startResult = startAlgorithm(); + uponPromise(promiseResolvedWith(startResult), () => { + controller._started = true; + ReadableByteStreamControllerCallPullIfNeeded(controller); + }, r => { + ReadableByteStreamControllerError(controller, r); + }); + } + function SetUpReadableByteStreamControllerFromUnderlyingSource(stream, underlyingByteSource, highWaterMark) { + const controller = Object.create(ReadableByteStreamController.prototype); + let startAlgorithm = () => undefined; + let pullAlgorithm = () => promiseResolvedWith(undefined); + let cancelAlgorithm = () => promiseResolvedWith(undefined); + if (underlyingByteSource.start !== undefined) { + startAlgorithm = () => underlyingByteSource.start(controller); + } + if (underlyingByteSource.pull !== undefined) { + pullAlgorithm = () => underlyingByteSource.pull(controller); + } + if (underlyingByteSource.cancel !== undefined) { + cancelAlgorithm = reason => underlyingByteSource.cancel(reason); + } + const autoAllocateChunkSize = underlyingByteSource.autoAllocateChunkSize; + if (autoAllocateChunkSize === 0) { + throw new TypeError('autoAllocateChunkSize must be greater than 0'); + } + SetUpReadableByteStreamController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, autoAllocateChunkSize); + } + function SetUpReadableStreamBYOBRequest(request, controller, view) { + request._associatedReadableByteStreamController = controller; + request._view = view; + } + // Helper functions for the ReadableStreamBYOBRequest. + function byobRequestBrandCheckException(name) { + return new TypeError(`ReadableStreamBYOBRequest.prototype.${name} can only be used on a ReadableStreamBYOBRequest`); + } + // Helper functions for the ReadableByteStreamController. + function byteStreamControllerBrandCheckException(name) { + return new TypeError(`ReadableByteStreamController.prototype.${name} can only be used on a ReadableByteStreamController`); + } + + // Abstract operations for the ReadableStream. + function AcquireReadableStreamBYOBReader(stream) { + return new ReadableStreamBYOBReader(stream); + } + // ReadableStream API exposed for controllers. + function ReadableStreamAddReadIntoRequest(stream, readIntoRequest) { + stream._reader._readIntoRequests.push(readIntoRequest); + } + function ReadableStreamFulfillReadIntoRequest(stream, chunk, done) { + const reader = stream._reader; + const readIntoRequest = reader._readIntoRequests.shift(); + if (done) { + readIntoRequest._closeSteps(chunk); + } + else { + readIntoRequest._chunkSteps(chunk); + } + } + function ReadableStreamGetNumReadIntoRequests(stream) { + return stream._reader._readIntoRequests.length; + } + function ReadableStreamHasBYOBReader(stream) { + const reader = stream._reader; + if (reader === undefined) { + return false; + } + if (!IsReadableStreamBYOBReader(reader)) { + return false; + } + return true; + } + /** + * A BYOB reader vended by a {@link ReadableStream}. + * + * @public + */ + class ReadableStreamBYOBReader { + constructor(stream) { + assertRequiredArgument(stream, 1, 'ReadableStreamBYOBReader'); + assertReadableStream(stream, 'First parameter'); + if (IsReadableStreamLocked(stream)) { + throw new TypeError('This stream has already been locked for exclusive reading by another reader'); + } + if (!IsReadableByteStreamController(stream._readableStreamController)) { + throw new TypeError('Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte ' + + 'source'); + } + ReadableStreamReaderGenericInitialize(this, stream); + this._readIntoRequests = new SimpleQueue(); + } + /** + * Returns a promise that will be fulfilled when the stream becomes closed, or rejected if the stream ever errors or + * the reader's lock is released before the stream finishes closing. + */ + get closed() { + if (!IsReadableStreamBYOBReader(this)) { + return promiseRejectedWith(byobReaderBrandCheckException('closed')); + } + return this._closedPromise; + } + /** + * If the reader is active, behaves the same as {@link ReadableStream.cancel | stream.cancel(reason)}. + */ + cancel(reason = undefined) { + if (!IsReadableStreamBYOBReader(this)) { + return promiseRejectedWith(byobReaderBrandCheckException('cancel')); + } + if (this._ownerReadableStream === undefined) { + return promiseRejectedWith(readerLockException('cancel')); + } + return ReadableStreamReaderGenericCancel(this, reason); + } + /** + * Attempts to reads bytes into view, and returns a promise resolved with the result. + * + * If reading a chunk causes the queue to become empty, more data will be pulled from the underlying source. + */ + read(view) { + if (!IsReadableStreamBYOBReader(this)) { + return promiseRejectedWith(byobReaderBrandCheckException('read')); + } + if (!ArrayBuffer.isView(view)) { + return promiseRejectedWith(new TypeError('view must be an array buffer view')); + } + if (view.byteLength === 0) { + return promiseRejectedWith(new TypeError('view must have non-zero byteLength')); + } + if (view.buffer.byteLength === 0) { + return promiseRejectedWith(new TypeError(`view's buffer must have non-zero byteLength`)); + } + if (this._ownerReadableStream === undefined) { + return promiseRejectedWith(readerLockException('read from')); + } + let resolvePromise; + let rejectPromise; + const promise = newPromise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + const readIntoRequest = { + _chunkSteps: chunk => resolvePromise({ value: chunk, done: false }), + _closeSteps: chunk => resolvePromise({ value: chunk, done: true }), + _errorSteps: e => rejectPromise(e) + }; + ReadableStreamBYOBReaderRead(this, view, readIntoRequest); + return promise; + } + /** + * Releases the reader's lock on the corresponding stream. After the lock is released, the reader is no longer active. + * If the associated stream is errored when the lock is released, the reader will appear errored in the same way + * from now on; otherwise, the reader will appear closed. + * + * A reader's lock cannot be released while it still has a pending read request, i.e., if a promise returned by + * the reader's {@link ReadableStreamBYOBReader.read | read()} method has not yet been settled. Attempting to + * do so will throw a `TypeError` and leave the reader locked to the stream. + */ + releaseLock() { + if (!IsReadableStreamBYOBReader(this)) { + throw byobReaderBrandCheckException('releaseLock'); + } + if (this._ownerReadableStream === undefined) { + return; + } + if (this._readIntoRequests.length > 0) { + throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled'); + } + ReadableStreamReaderGenericRelease(this); + } + } + Object.defineProperties(ReadableStreamBYOBReader.prototype, { + cancel: { enumerable: true }, + read: { enumerable: true }, + releaseLock: { enumerable: true }, + closed: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(ReadableStreamBYOBReader.prototype, SymbolPolyfill.toStringTag, { + value: 'ReadableStreamBYOBReader', + configurable: true + }); + } + // Abstract operations for the readers. + function IsReadableStreamBYOBReader(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_readIntoRequests')) { + return false; + } + return true; + } + function ReadableStreamBYOBReaderRead(reader, view, readIntoRequest) { + const stream = reader._ownerReadableStream; + stream._disturbed = true; + if (stream._state === 'errored') { + readIntoRequest._errorSteps(stream._storedError); + } + else { + ReadableByteStreamControllerPullInto(stream._readableStreamController, view, readIntoRequest); + } + } + // Helper functions for the ReadableStreamBYOBReader. + function byobReaderBrandCheckException(name) { + return new TypeError(`ReadableStreamBYOBReader.prototype.${name} can only be used on a ReadableStreamBYOBReader`); + } + + function ExtractHighWaterMark(strategy, defaultHWM) { + const { highWaterMark } = strategy; + if (highWaterMark === undefined) { + return defaultHWM; + } + if (NumberIsNaN(highWaterMark) || highWaterMark < 0) { + throw new RangeError('Invalid highWaterMark'); + } + return highWaterMark; + } + function ExtractSizeAlgorithm(strategy) { + const { size } = strategy; + if (!size) { + return () => 1; + } + return size; + } + + function convertQueuingStrategy(init, context) { + assertDictionary(init, context); + const highWaterMark = init === null || init === void 0 ? void 0 : init.highWaterMark; + const size = init === null || init === void 0 ? void 0 : init.size; + return { + highWaterMark: highWaterMark === undefined ? undefined : convertUnrestrictedDouble(highWaterMark), + size: size === undefined ? undefined : convertQueuingStrategySize(size, `${context} has member 'size' that`) + }; + } + function convertQueuingStrategySize(fn, context) { + assertFunction(fn, context); + return chunk => convertUnrestrictedDouble(fn(chunk)); + } + + function convertUnderlyingSink(original, context) { + assertDictionary(original, context); + const abort = original === null || original === void 0 ? void 0 : original.abort; + const close = original === null || original === void 0 ? void 0 : original.close; + const start = original === null || original === void 0 ? void 0 : original.start; + const type = original === null || original === void 0 ? void 0 : original.type; + const write = original === null || original === void 0 ? void 0 : original.write; + return { + abort: abort === undefined ? + undefined : + convertUnderlyingSinkAbortCallback(abort, original, `${context} has member 'abort' that`), + close: close === undefined ? + undefined : + convertUnderlyingSinkCloseCallback(close, original, `${context} has member 'close' that`), + start: start === undefined ? + undefined : + convertUnderlyingSinkStartCallback(start, original, `${context} has member 'start' that`), + write: write === undefined ? + undefined : + convertUnderlyingSinkWriteCallback(write, original, `${context} has member 'write' that`), + type + }; + } + function convertUnderlyingSinkAbortCallback(fn, original, context) { + assertFunction(fn, context); + return (reason) => promiseCall(fn, original, [reason]); + } + function convertUnderlyingSinkCloseCallback(fn, original, context) { + assertFunction(fn, context); + return () => promiseCall(fn, original, []); + } + function convertUnderlyingSinkStartCallback(fn, original, context) { + assertFunction(fn, context); + return (controller) => reflectCall(fn, original, [controller]); + } + function convertUnderlyingSinkWriteCallback(fn, original, context) { + assertFunction(fn, context); + return (chunk, controller) => promiseCall(fn, original, [chunk, controller]); + } + + function assertWritableStream(x, context) { + if (!IsWritableStream(x)) { + throw new TypeError(`${context} is not a WritableStream.`); + } + } + + /** + * A writable stream represents a destination for data, into which you can write. + * + * @public + */ + class WritableStream { + constructor(rawUnderlyingSink = {}, rawStrategy = {}) { + if (rawUnderlyingSink === undefined) { + rawUnderlyingSink = null; + } + else { + assertObject(rawUnderlyingSink, 'First parameter'); + } + const strategy = convertQueuingStrategy(rawStrategy, 'Second parameter'); + const underlyingSink = convertUnderlyingSink(rawUnderlyingSink, 'First parameter'); + InitializeWritableStream(this); + const type = underlyingSink.type; + if (type !== undefined) { + throw new RangeError('Invalid type is specified'); + } + const sizeAlgorithm = ExtractSizeAlgorithm(strategy); + const highWaterMark = ExtractHighWaterMark(strategy, 1); + SetUpWritableStreamDefaultControllerFromUnderlyingSink(this, underlyingSink, highWaterMark, sizeAlgorithm); + } + /** + * Returns whether or not the writable stream is locked to a writer. + */ + get locked() { + if (!IsWritableStream(this)) { + throw streamBrandCheckException$2('locked'); + } + return IsWritableStreamLocked(this); + } + /** + * Aborts the stream, signaling that the producer can no longer successfully write to the stream and it is to be + * immediately moved to an errored state, with any queued-up writes discarded. This will also execute any abort + * mechanism of the underlying sink. + * + * The returned promise will fulfill if the stream shuts down successfully, or reject if the underlying sink signaled + * that there was an error doing so. Additionally, it will reject with a `TypeError` (without attempting to cancel + * the stream) if the stream is currently locked. + */ + abort(reason = undefined) { + if (!IsWritableStream(this)) { + return promiseRejectedWith(streamBrandCheckException$2('abort')); + } + if (IsWritableStreamLocked(this)) { + return promiseRejectedWith(new TypeError('Cannot abort a stream that already has a writer')); + } + return WritableStreamAbort(this, reason); + } + /** + * Closes the stream. The underlying sink will finish processing any previously-written chunks, before invoking its + * close behavior. During this time any further attempts to write will fail (without erroring the stream). + * + * The method returns a promise that will fulfill if all remaining chunks are successfully written and the stream + * successfully closes, or rejects if an error is encountered during this process. Additionally, it will reject with + * a `TypeError` (without attempting to cancel the stream) if the stream is currently locked. + */ + close() { + if (!IsWritableStream(this)) { + return promiseRejectedWith(streamBrandCheckException$2('close')); + } + if (IsWritableStreamLocked(this)) { + return promiseRejectedWith(new TypeError('Cannot close a stream that already has a writer')); + } + if (WritableStreamCloseQueuedOrInFlight(this)) { + return promiseRejectedWith(new TypeError('Cannot close an already-closing stream')); + } + return WritableStreamClose(this); + } + /** + * Creates a {@link WritableStreamDefaultWriter | writer} and locks the stream to the new writer. While the stream + * is locked, no other writer can be acquired until this one is released. + * + * This functionality is especially useful for creating abstractions that desire the ability to write to a stream + * without interruption or interleaving. By getting a writer for the stream, you can ensure nobody else can write at + * the same time, which would cause the resulting written data to be unpredictable and probably useless. + */ + getWriter() { + if (!IsWritableStream(this)) { + throw streamBrandCheckException$2('getWriter'); + } + return AcquireWritableStreamDefaultWriter(this); + } + } + Object.defineProperties(WritableStream.prototype, { + abort: { enumerable: true }, + close: { enumerable: true }, + getWriter: { enumerable: true }, + locked: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(WritableStream.prototype, SymbolPolyfill.toStringTag, { + value: 'WritableStream', + configurable: true + }); + } + // Abstract operations for the WritableStream. + function AcquireWritableStreamDefaultWriter(stream) { + return new WritableStreamDefaultWriter(stream); + } + // Throws if and only if startAlgorithm throws. + function CreateWritableStream(startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark = 1, sizeAlgorithm = () => 1) { + const stream = Object.create(WritableStream.prototype); + InitializeWritableStream(stream); + const controller = Object.create(WritableStreamDefaultController.prototype); + SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm); + return stream; + } + function InitializeWritableStream(stream) { + stream._state = 'writable'; + // The error that will be reported by new method calls once the state becomes errored. Only set when [[state]] is + // 'erroring' or 'errored'. May be set to an undefined value. + stream._storedError = undefined; + stream._writer = undefined; + // Initialize to undefined first because the constructor of the controller checks this + // variable to validate the caller. + stream._writableStreamController = undefined; + // This queue is placed here instead of the writer class in order to allow for passing a writer to the next data + // producer without waiting for the queued writes to finish. + stream._writeRequests = new SimpleQueue(); + // Write requests are removed from _writeRequests when write() is called on the underlying sink. This prevents + // them from being erroneously rejected on error. If a write() call is in-flight, the request is stored here. + stream._inFlightWriteRequest = undefined; + // The promise that was returned from writer.close(). Stored here because it may be fulfilled after the writer + // has been detached. + stream._closeRequest = undefined; + // Close request is removed from _closeRequest when close() is called on the underlying sink. This prevents it + // from being erroneously rejected on error. If a close() call is in-flight, the request is stored here. + stream._inFlightCloseRequest = undefined; + // The promise that was returned from writer.abort(). This may also be fulfilled after the writer has detached. + stream._pendingAbortRequest = undefined; + // The backpressure signal set by the controller. + stream._backpressure = false; + } + function IsWritableStream(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_writableStreamController')) { + return false; + } + return true; + } + function IsWritableStreamLocked(stream) { + if (stream._writer === undefined) { + return false; + } + return true; + } + function WritableStreamAbort(stream, reason) { + const state = stream._state; + if (state === 'closed' || state === 'errored') { + return promiseResolvedWith(undefined); + } + if (stream._pendingAbortRequest !== undefined) { + return stream._pendingAbortRequest._promise; + } + let wasAlreadyErroring = false; + if (state === 'erroring') { + wasAlreadyErroring = true; + // reason will not be used, so don't keep a reference to it. + reason = undefined; + } + const promise = newPromise((resolve, reject) => { + stream._pendingAbortRequest = { + _promise: undefined, + _resolve: resolve, + _reject: reject, + _reason: reason, + _wasAlreadyErroring: wasAlreadyErroring + }; + }); + stream._pendingAbortRequest._promise = promise; + if (!wasAlreadyErroring) { + WritableStreamStartErroring(stream, reason); + } + return promise; + } + function WritableStreamClose(stream) { + const state = stream._state; + if (state === 'closed' || state === 'errored') { + return promiseRejectedWith(new TypeError(`The stream (in ${state} state) is not in the writable state and cannot be closed`)); + } + const promise = newPromise((resolve, reject) => { + const closeRequest = { + _resolve: resolve, + _reject: reject + }; + stream._closeRequest = closeRequest; + }); + const writer = stream._writer; + if (writer !== undefined && stream._backpressure && state === 'writable') { + defaultWriterReadyPromiseResolve(writer); + } + WritableStreamDefaultControllerClose(stream._writableStreamController); + return promise; + } + // WritableStream API exposed for controllers. + function WritableStreamAddWriteRequest(stream) { + const promise = newPromise((resolve, reject) => { + const writeRequest = { + _resolve: resolve, + _reject: reject + }; + stream._writeRequests.push(writeRequest); + }); + return promise; + } + function WritableStreamDealWithRejection(stream, error) { + const state = stream._state; + if (state === 'writable') { + WritableStreamStartErroring(stream, error); + return; + } + WritableStreamFinishErroring(stream); + } + function WritableStreamStartErroring(stream, reason) { + const controller = stream._writableStreamController; + stream._state = 'erroring'; + stream._storedError = reason; + const writer = stream._writer; + if (writer !== undefined) { + WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason); + } + if (!WritableStreamHasOperationMarkedInFlight(stream) && controller._started) { + WritableStreamFinishErroring(stream); + } + } + function WritableStreamFinishErroring(stream) { + stream._state = 'errored'; + stream._writableStreamController[ErrorSteps](); + const storedError = stream._storedError; + stream._writeRequests.forEach(writeRequest => { + writeRequest._reject(storedError); + }); + stream._writeRequests = new SimpleQueue(); + if (stream._pendingAbortRequest === undefined) { + WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return; + } + const abortRequest = stream._pendingAbortRequest; + stream._pendingAbortRequest = undefined; + if (abortRequest._wasAlreadyErroring) { + abortRequest._reject(storedError); + WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return; + } + const promise = stream._writableStreamController[AbortSteps](abortRequest._reason); + uponPromise(promise, () => { + abortRequest._resolve(); + WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + }, (reason) => { + abortRequest._reject(reason); + WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + }); + } + function WritableStreamFinishInFlightWrite(stream) { + stream._inFlightWriteRequest._resolve(undefined); + stream._inFlightWriteRequest = undefined; + } + function WritableStreamFinishInFlightWriteWithError(stream, error) { + stream._inFlightWriteRequest._reject(error); + stream._inFlightWriteRequest = undefined; + WritableStreamDealWithRejection(stream, error); + } + function WritableStreamFinishInFlightClose(stream) { + stream._inFlightCloseRequest._resolve(undefined); + stream._inFlightCloseRequest = undefined; + const state = stream._state; + if (state === 'erroring') { + // The error was too late to do anything, so it is ignored. + stream._storedError = undefined; + if (stream._pendingAbortRequest !== undefined) { + stream._pendingAbortRequest._resolve(); + stream._pendingAbortRequest = undefined; + } + } + stream._state = 'closed'; + const writer = stream._writer; + if (writer !== undefined) { + defaultWriterClosedPromiseResolve(writer); + } + } + function WritableStreamFinishInFlightCloseWithError(stream, error) { + stream._inFlightCloseRequest._reject(error); + stream._inFlightCloseRequest = undefined; + // Never execute sink abort() after sink close(). + if (stream._pendingAbortRequest !== undefined) { + stream._pendingAbortRequest._reject(error); + stream._pendingAbortRequest = undefined; + } + WritableStreamDealWithRejection(stream, error); + } + // TODO(ricea): Fix alphabetical order. + function WritableStreamCloseQueuedOrInFlight(stream) { + if (stream._closeRequest === undefined && stream._inFlightCloseRequest === undefined) { + return false; + } + return true; + } + function WritableStreamHasOperationMarkedInFlight(stream) { + if (stream._inFlightWriteRequest === undefined && stream._inFlightCloseRequest === undefined) { + return false; + } + return true; + } + function WritableStreamMarkCloseRequestInFlight(stream) { + stream._inFlightCloseRequest = stream._closeRequest; + stream._closeRequest = undefined; + } + function WritableStreamMarkFirstWriteRequestInFlight(stream) { + stream._inFlightWriteRequest = stream._writeRequests.shift(); + } + function WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream) { + if (stream._closeRequest !== undefined) { + stream._closeRequest._reject(stream._storedError); + stream._closeRequest = undefined; + } + const writer = stream._writer; + if (writer !== undefined) { + defaultWriterClosedPromiseReject(writer, stream._storedError); + } + } + function WritableStreamUpdateBackpressure(stream, backpressure) { + const writer = stream._writer; + if (writer !== undefined && backpressure !== stream._backpressure) { + if (backpressure) { + defaultWriterReadyPromiseReset(writer); + } + else { + defaultWriterReadyPromiseResolve(writer); + } + } + stream._backpressure = backpressure; + } + /** + * A default writer vended by a {@link WritableStream}. + * + * @public + */ + class WritableStreamDefaultWriter { + constructor(stream) { + assertRequiredArgument(stream, 1, 'WritableStreamDefaultWriter'); + assertWritableStream(stream, 'First parameter'); + if (IsWritableStreamLocked(stream)) { + throw new TypeError('This stream has already been locked for exclusive writing by another writer'); + } + this._ownerWritableStream = stream; + stream._writer = this; + const state = stream._state; + if (state === 'writable') { + if (!WritableStreamCloseQueuedOrInFlight(stream) && stream._backpressure) { + defaultWriterReadyPromiseInitialize(this); + } + else { + defaultWriterReadyPromiseInitializeAsResolved(this); + } + defaultWriterClosedPromiseInitialize(this); + } + else if (state === 'erroring') { + defaultWriterReadyPromiseInitializeAsRejected(this, stream._storedError); + defaultWriterClosedPromiseInitialize(this); + } + else if (state === 'closed') { + defaultWriterReadyPromiseInitializeAsResolved(this); + defaultWriterClosedPromiseInitializeAsResolved(this); + } + else { + const storedError = stream._storedError; + defaultWriterReadyPromiseInitializeAsRejected(this, storedError); + defaultWriterClosedPromiseInitializeAsRejected(this, storedError); + } + } + /** + * Returns a promise that will be fulfilled when the stream becomes closed, or rejected if the stream ever errors or + * the writer’s lock is released before the stream finishes closing. + */ + get closed() { + if (!IsWritableStreamDefaultWriter(this)) { + return promiseRejectedWith(defaultWriterBrandCheckException('closed')); + } + return this._closedPromise; + } + /** + * Returns the desired size to fill the stream’s internal queue. It can be negative, if the queue is over-full. + * A producer can use this information to determine the right amount of data to write. + * + * It will be `null` if the stream cannot be successfully written to (due to either being errored, or having an abort + * queued up). It will return zero if the stream is closed. And the getter will throw an exception if invoked when + * the writer’s lock is released. + */ + get desiredSize() { + if (!IsWritableStreamDefaultWriter(this)) { + throw defaultWriterBrandCheckException('desiredSize'); + } + if (this._ownerWritableStream === undefined) { + throw defaultWriterLockException('desiredSize'); + } + return WritableStreamDefaultWriterGetDesiredSize(this); + } + /** + * Returns a promise that will be fulfilled when the desired size to fill the stream’s internal queue transitions + * from non-positive to positive, signaling that it is no longer applying backpressure. Once the desired size dips + * back to zero or below, the getter will return a new promise that stays pending until the next transition. + * + * If the stream becomes errored or aborted, or the writer’s lock is released, the returned promise will become + * rejected. + */ + get ready() { + if (!IsWritableStreamDefaultWriter(this)) { + return promiseRejectedWith(defaultWriterBrandCheckException('ready')); + } + return this._readyPromise; + } + /** + * If the reader is active, behaves the same as {@link WritableStream.abort | stream.abort(reason)}. + */ + abort(reason = undefined) { + if (!IsWritableStreamDefaultWriter(this)) { + return promiseRejectedWith(defaultWriterBrandCheckException('abort')); + } + if (this._ownerWritableStream === undefined) { + return promiseRejectedWith(defaultWriterLockException('abort')); + } + return WritableStreamDefaultWriterAbort(this, reason); + } + /** + * If the reader is active, behaves the same as {@link WritableStream.close | stream.close()}. + */ + close() { + if (!IsWritableStreamDefaultWriter(this)) { + return promiseRejectedWith(defaultWriterBrandCheckException('close')); + } + const stream = this._ownerWritableStream; + if (stream === undefined) { + return promiseRejectedWith(defaultWriterLockException('close')); + } + if (WritableStreamCloseQueuedOrInFlight(stream)) { + return promiseRejectedWith(new TypeError('Cannot close an already-closing stream')); + } + return WritableStreamDefaultWriterClose(this); + } + /** + * Releases the writer’s lock on the corresponding stream. After the lock is released, the writer is no longer active. + * If the associated stream is errored when the lock is released, the writer will appear errored in the same way from + * now on; otherwise, the writer will appear closed. + * + * Note that the lock can still be released even if some ongoing writes have not yet finished (i.e. even if the + * promises returned from previous calls to {@link WritableStreamDefaultWriter.write | write()} have not yet settled). + * It’s not necessary to hold the lock on the writer for the duration of the write; the lock instead simply prevents + * other producers from writing in an interleaved manner. + */ + releaseLock() { + if (!IsWritableStreamDefaultWriter(this)) { + throw defaultWriterBrandCheckException('releaseLock'); + } + const stream = this._ownerWritableStream; + if (stream === undefined) { + return; + } + WritableStreamDefaultWriterRelease(this); + } + write(chunk = undefined) { + if (!IsWritableStreamDefaultWriter(this)) { + return promiseRejectedWith(defaultWriterBrandCheckException('write')); + } + if (this._ownerWritableStream === undefined) { + return promiseRejectedWith(defaultWriterLockException('write to')); + } + return WritableStreamDefaultWriterWrite(this, chunk); + } + } + Object.defineProperties(WritableStreamDefaultWriter.prototype, { + abort: { enumerable: true }, + close: { enumerable: true }, + releaseLock: { enumerable: true }, + write: { enumerable: true }, + closed: { enumerable: true }, + desiredSize: { enumerable: true }, + ready: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(WritableStreamDefaultWriter.prototype, SymbolPolyfill.toStringTag, { + value: 'WritableStreamDefaultWriter', + configurable: true + }); + } + // Abstract operations for the WritableStreamDefaultWriter. + function IsWritableStreamDefaultWriter(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_ownerWritableStream')) { + return false; + } + return true; + } + // A client of WritableStreamDefaultWriter may use these functions directly to bypass state check. + function WritableStreamDefaultWriterAbort(writer, reason) { + const stream = writer._ownerWritableStream; + return WritableStreamAbort(stream, reason); + } + function WritableStreamDefaultWriterClose(writer) { + const stream = writer._ownerWritableStream; + return WritableStreamClose(stream); + } + function WritableStreamDefaultWriterCloseWithErrorPropagation(writer) { + const stream = writer._ownerWritableStream; + const state = stream._state; + if (WritableStreamCloseQueuedOrInFlight(stream) || state === 'closed') { + return promiseResolvedWith(undefined); + } + if (state === 'errored') { + return promiseRejectedWith(stream._storedError); + } + return WritableStreamDefaultWriterClose(writer); + } + function WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, error) { + if (writer._closedPromiseState === 'pending') { + defaultWriterClosedPromiseReject(writer, error); + } + else { + defaultWriterClosedPromiseResetToRejected(writer, error); + } + } + function WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, error) { + if (writer._readyPromiseState === 'pending') { + defaultWriterReadyPromiseReject(writer, error); + } + else { + defaultWriterReadyPromiseResetToRejected(writer, error); + } + } + function WritableStreamDefaultWriterGetDesiredSize(writer) { + const stream = writer._ownerWritableStream; + const state = stream._state; + if (state === 'errored' || state === 'erroring') { + return null; + } + if (state === 'closed') { + return 0; + } + return WritableStreamDefaultControllerGetDesiredSize(stream._writableStreamController); + } + function WritableStreamDefaultWriterRelease(writer) { + const stream = writer._ownerWritableStream; + const releasedError = new TypeError(`Writer was released and can no longer be used to monitor the stream's closedness`); + WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, releasedError); + // The state transitions to "errored" before the sink abort() method runs, but the writer.closed promise is not + // rejected until afterwards. This means that simply testing state will not work. + WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, releasedError); + stream._writer = undefined; + writer._ownerWritableStream = undefined; + } + function WritableStreamDefaultWriterWrite(writer, chunk) { + const stream = writer._ownerWritableStream; + const controller = stream._writableStreamController; + const chunkSize = WritableStreamDefaultControllerGetChunkSize(controller, chunk); + if (stream !== writer._ownerWritableStream) { + return promiseRejectedWith(defaultWriterLockException('write to')); + } + const state = stream._state; + if (state === 'errored') { + return promiseRejectedWith(stream._storedError); + } + if (WritableStreamCloseQueuedOrInFlight(stream) || state === 'closed') { + return promiseRejectedWith(new TypeError('The stream is closing or closed and cannot be written to')); + } + if (state === 'erroring') { + return promiseRejectedWith(stream._storedError); + } + const promise = WritableStreamAddWriteRequest(stream); + WritableStreamDefaultControllerWrite(controller, chunk, chunkSize); + return promise; + } + const closeSentinel = {}; + /** + * Allows control of a {@link WritableStream | writable stream}'s state and internal queue. + * + * @public + */ + class WritableStreamDefaultController { + constructor() { + throw new TypeError('Illegal constructor'); + } + /** + * Closes the controlled writable stream, making all future interactions with it fail with the given error `e`. + * + * This method is rarely used, since usually it suffices to return a rejected promise from one of the underlying + * sink's methods. However, it can be useful for suddenly shutting down a stream in response to an event outside the + * normal lifecycle of interactions with the underlying sink. + */ + error(e = undefined) { + if (!IsWritableStreamDefaultController(this)) { + throw new TypeError('WritableStreamDefaultController.prototype.error can only be used on a WritableStreamDefaultController'); + } + const state = this._controlledWritableStream._state; + if (state !== 'writable') { + // The stream is closed, errored or will be soon. The sink can't do anything useful if it gets an error here, so + // just treat it as a no-op. + return; + } + WritableStreamDefaultControllerError(this, e); + } + /** @internal */ + [AbortSteps](reason) { + const result = this._abortAlgorithm(reason); + WritableStreamDefaultControllerClearAlgorithms(this); + return result; + } + /** @internal */ + [ErrorSteps]() { + ResetQueue(this); + } + } + Object.defineProperties(WritableStreamDefaultController.prototype, { + error: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(WritableStreamDefaultController.prototype, SymbolPolyfill.toStringTag, { + value: 'WritableStreamDefaultController', + configurable: true + }); + } + // Abstract operations implementing interface required by the WritableStream. + function IsWritableStreamDefaultController(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_controlledWritableStream')) { + return false; + } + return true; + } + function SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm) { + controller._controlledWritableStream = stream; + stream._writableStreamController = controller; + // Need to set the slots so that the assert doesn't fire. In the spec the slots already exist implicitly. + controller._queue = undefined; + controller._queueTotalSize = undefined; + ResetQueue(controller); + controller._started = false; + controller._strategySizeAlgorithm = sizeAlgorithm; + controller._strategyHWM = highWaterMark; + controller._writeAlgorithm = writeAlgorithm; + controller._closeAlgorithm = closeAlgorithm; + controller._abortAlgorithm = abortAlgorithm; + const backpressure = WritableStreamDefaultControllerGetBackpressure(controller); + WritableStreamUpdateBackpressure(stream, backpressure); + const startResult = startAlgorithm(); + const startPromise = promiseResolvedWith(startResult); + uponPromise(startPromise, () => { + controller._started = true; + WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + }, r => { + controller._started = true; + WritableStreamDealWithRejection(stream, r); + }); + } + function SetUpWritableStreamDefaultControllerFromUnderlyingSink(stream, underlyingSink, highWaterMark, sizeAlgorithm) { + const controller = Object.create(WritableStreamDefaultController.prototype); + let startAlgorithm = () => undefined; + let writeAlgorithm = () => promiseResolvedWith(undefined); + let closeAlgorithm = () => promiseResolvedWith(undefined); + let abortAlgorithm = () => promiseResolvedWith(undefined); + if (underlyingSink.start !== undefined) { + startAlgorithm = () => underlyingSink.start(controller); + } + if (underlyingSink.write !== undefined) { + writeAlgorithm = chunk => underlyingSink.write(chunk, controller); + } + if (underlyingSink.close !== undefined) { + closeAlgorithm = () => underlyingSink.close(); + } + if (underlyingSink.abort !== undefined) { + abortAlgorithm = reason => underlyingSink.abort(reason); + } + SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm); + } + // ClearAlgorithms may be called twice. Erroring the same stream in multiple ways will often result in redundant calls. + function WritableStreamDefaultControllerClearAlgorithms(controller) { + controller._writeAlgorithm = undefined; + controller._closeAlgorithm = undefined; + controller._abortAlgorithm = undefined; + controller._strategySizeAlgorithm = undefined; + } + function WritableStreamDefaultControllerClose(controller) { + EnqueueValueWithSize(controller, closeSentinel, 0); + WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + } + function WritableStreamDefaultControllerGetChunkSize(controller, chunk) { + try { + return controller._strategySizeAlgorithm(chunk); + } + catch (chunkSizeE) { + WritableStreamDefaultControllerErrorIfNeeded(controller, chunkSizeE); + return 1; + } + } + function WritableStreamDefaultControllerGetDesiredSize(controller) { + return controller._strategyHWM - controller._queueTotalSize; + } + function WritableStreamDefaultControllerWrite(controller, chunk, chunkSize) { + try { + EnqueueValueWithSize(controller, chunk, chunkSize); + } + catch (enqueueE) { + WritableStreamDefaultControllerErrorIfNeeded(controller, enqueueE); + return; + } + const stream = controller._controlledWritableStream; + if (!WritableStreamCloseQueuedOrInFlight(stream) && stream._state === 'writable') { + const backpressure = WritableStreamDefaultControllerGetBackpressure(controller); + WritableStreamUpdateBackpressure(stream, backpressure); + } + WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + } + // Abstract operations for the WritableStreamDefaultController. + function WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller) { + const stream = controller._controlledWritableStream; + if (!controller._started) { + return; + } + if (stream._inFlightWriteRequest !== undefined) { + return; + } + const state = stream._state; + if (state === 'erroring') { + WritableStreamFinishErroring(stream); + return; + } + if (controller._queue.length === 0) { + return; + } + const value = PeekQueueValue(controller); + if (value === closeSentinel) { + WritableStreamDefaultControllerProcessClose(controller); + } + else { + WritableStreamDefaultControllerProcessWrite(controller, value); + } + } + function WritableStreamDefaultControllerErrorIfNeeded(controller, error) { + if (controller._controlledWritableStream._state === 'writable') { + WritableStreamDefaultControllerError(controller, error); + } + } + function WritableStreamDefaultControllerProcessClose(controller) { + const stream = controller._controlledWritableStream; + WritableStreamMarkCloseRequestInFlight(stream); + DequeueValue(controller); + const sinkClosePromise = controller._closeAlgorithm(); + WritableStreamDefaultControllerClearAlgorithms(controller); + uponPromise(sinkClosePromise, () => { + WritableStreamFinishInFlightClose(stream); + }, reason => { + WritableStreamFinishInFlightCloseWithError(stream, reason); + }); + } + function WritableStreamDefaultControllerProcessWrite(controller, chunk) { + const stream = controller._controlledWritableStream; + WritableStreamMarkFirstWriteRequestInFlight(stream); + const sinkWritePromise = controller._writeAlgorithm(chunk); + uponPromise(sinkWritePromise, () => { + WritableStreamFinishInFlightWrite(stream); + const state = stream._state; + DequeueValue(controller); + if (!WritableStreamCloseQueuedOrInFlight(stream) && state === 'writable') { + const backpressure = WritableStreamDefaultControllerGetBackpressure(controller); + WritableStreamUpdateBackpressure(stream, backpressure); + } + WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + }, reason => { + if (stream._state === 'writable') { + WritableStreamDefaultControllerClearAlgorithms(controller); + } + WritableStreamFinishInFlightWriteWithError(stream, reason); + }); + } + function WritableStreamDefaultControllerGetBackpressure(controller) { + const desiredSize = WritableStreamDefaultControllerGetDesiredSize(controller); + return desiredSize <= 0; + } + // A client of WritableStreamDefaultController may use these functions directly to bypass state check. + function WritableStreamDefaultControllerError(controller, error) { + const stream = controller._controlledWritableStream; + WritableStreamDefaultControllerClearAlgorithms(controller); + WritableStreamStartErroring(stream, error); + } + // Helper functions for the WritableStream. + function streamBrandCheckException$2(name) { + return new TypeError(`WritableStream.prototype.${name} can only be used on a WritableStream`); + } + // Helper functions for the WritableStreamDefaultWriter. + function defaultWriterBrandCheckException(name) { + return new TypeError(`WritableStreamDefaultWriter.prototype.${name} can only be used on a WritableStreamDefaultWriter`); + } + function defaultWriterLockException(name) { + return new TypeError('Cannot ' + name + ' a stream using a released writer'); + } + function defaultWriterClosedPromiseInitialize(writer) { + writer._closedPromise = newPromise((resolve, reject) => { + writer._closedPromise_resolve = resolve; + writer._closedPromise_reject = reject; + writer._closedPromiseState = 'pending'; + }); + } + function defaultWriterClosedPromiseInitializeAsRejected(writer, reason) { + defaultWriterClosedPromiseInitialize(writer); + defaultWriterClosedPromiseReject(writer, reason); + } + function defaultWriterClosedPromiseInitializeAsResolved(writer) { + defaultWriterClosedPromiseInitialize(writer); + defaultWriterClosedPromiseResolve(writer); + } + function defaultWriterClosedPromiseReject(writer, reason) { + if (writer._closedPromise_reject === undefined) { + return; + } + setPromiseIsHandledToTrue(writer._closedPromise); + writer._closedPromise_reject(reason); + writer._closedPromise_resolve = undefined; + writer._closedPromise_reject = undefined; + writer._closedPromiseState = 'rejected'; + } + function defaultWriterClosedPromiseResetToRejected(writer, reason) { + defaultWriterClosedPromiseInitializeAsRejected(writer, reason); + } + function defaultWriterClosedPromiseResolve(writer) { + if (writer._closedPromise_resolve === undefined) { + return; + } + writer._closedPromise_resolve(undefined); + writer._closedPromise_resolve = undefined; + writer._closedPromise_reject = undefined; + writer._closedPromiseState = 'resolved'; + } + function defaultWriterReadyPromiseInitialize(writer) { + writer._readyPromise = newPromise((resolve, reject) => { + writer._readyPromise_resolve = resolve; + writer._readyPromise_reject = reject; + }); + writer._readyPromiseState = 'pending'; + } + function defaultWriterReadyPromiseInitializeAsRejected(writer, reason) { + defaultWriterReadyPromiseInitialize(writer); + defaultWriterReadyPromiseReject(writer, reason); + } + function defaultWriterReadyPromiseInitializeAsResolved(writer) { + defaultWriterReadyPromiseInitialize(writer); + defaultWriterReadyPromiseResolve(writer); + } + function defaultWriterReadyPromiseReject(writer, reason) { + if (writer._readyPromise_reject === undefined) { + return; + } + setPromiseIsHandledToTrue(writer._readyPromise); + writer._readyPromise_reject(reason); + writer._readyPromise_resolve = undefined; + writer._readyPromise_reject = undefined; + writer._readyPromiseState = 'rejected'; + } + function defaultWriterReadyPromiseReset(writer) { + defaultWriterReadyPromiseInitialize(writer); + } + function defaultWriterReadyPromiseResetToRejected(writer, reason) { + defaultWriterReadyPromiseInitializeAsRejected(writer, reason); + } + function defaultWriterReadyPromiseResolve(writer) { + if (writer._readyPromise_resolve === undefined) { + return; + } + writer._readyPromise_resolve(undefined); + writer._readyPromise_resolve = undefined; + writer._readyPromise_reject = undefined; + writer._readyPromiseState = 'fulfilled'; + } + + function isAbortSignal(value) { + if (typeof value !== 'object' || value === null) { + return false; + } + try { + return typeof value.aborted === 'boolean'; + } + catch (_a) { + // AbortSignal.prototype.aborted throws if its brand check fails + return false; + } + } + + /// + const NativeDOMException = typeof DOMException !== 'undefined' ? DOMException : undefined; + + /// + function isDOMExceptionConstructor(ctor) { + if (!(typeof ctor === 'function' || typeof ctor === 'object')) { + return false; + } + try { + new ctor(); + return true; + } + catch (_a) { + return false; + } + } + function createDOMExceptionPolyfill() { + // eslint-disable-next-line no-shadow + const ctor = function DOMException(message, name) { + this.message = message || ''; + this.name = name || 'Error'; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + }; + ctor.prototype = Object.create(Error.prototype); + Object.defineProperty(ctor.prototype, 'constructor', { value: ctor, writable: true, configurable: true }); + return ctor; + } + // eslint-disable-next-line no-redeclare + const DOMException$1 = isDOMExceptionConstructor(NativeDOMException) ? NativeDOMException : createDOMExceptionPolyfill(); + + function ReadableStreamPipeTo(source, dest, preventClose, preventAbort, preventCancel, signal) { + const reader = AcquireReadableStreamDefaultReader(source); + const writer = AcquireWritableStreamDefaultWriter(dest); + source._disturbed = true; + let shuttingDown = false; + // This is used to keep track of the spec's requirement that we wait for ongoing writes during shutdown. + let currentWrite = promiseResolvedWith(undefined); + return newPromise((resolve, reject) => { + let abortAlgorithm; + if (signal !== undefined) { + abortAlgorithm = () => { + const error = new DOMException$1('Aborted', 'AbortError'); + const actions = []; + if (!preventAbort) { + actions.push(() => { + if (dest._state === 'writable') { + return WritableStreamAbort(dest, error); + } + return promiseResolvedWith(undefined); + }); + } + if (!preventCancel) { + actions.push(() => { + if (source._state === 'readable') { + return ReadableStreamCancel(source, error); + } + return promiseResolvedWith(undefined); + }); + } + shutdownWithAction(() => Promise.all(actions.map(action => action())), true, error); + }; + if (signal.aborted) { + abortAlgorithm(); + return; + } + signal.addEventListener('abort', abortAlgorithm); + } + // Using reader and writer, read all chunks from this and write them to dest + // - Backpressure must be enforced + // - Shutdown must stop all activity + function pipeLoop() { + return newPromise((resolveLoop, rejectLoop) => { + function next(done) { + if (done) { + resolveLoop(); + } + else { + // Use `PerformPromiseThen` instead of `uponPromise` to avoid + // adding unnecessary `.catch(rethrowAssertionErrorRejection)` handlers + PerformPromiseThen(pipeStep(), next, rejectLoop); + } + } + next(false); + }); + } + function pipeStep() { + if (shuttingDown) { + return promiseResolvedWith(true); + } + return PerformPromiseThen(writer._readyPromise, () => { + return newPromise((resolveRead, rejectRead) => { + ReadableStreamDefaultReaderRead(reader, { + _chunkSteps: chunk => { + currentWrite = PerformPromiseThen(WritableStreamDefaultWriterWrite(writer, chunk), undefined, noop$1); + resolveRead(false); + }, + _closeSteps: () => resolveRead(true), + _errorSteps: rejectRead + }); + }); + }); + } + // Errors must be propagated forward + isOrBecomesErrored(source, reader._closedPromise, storedError => { + if (!preventAbort) { + shutdownWithAction(() => WritableStreamAbort(dest, storedError), true, storedError); + } + else { + shutdown(true, storedError); + } + }); + // Errors must be propagated backward + isOrBecomesErrored(dest, writer._closedPromise, storedError => { + if (!preventCancel) { + shutdownWithAction(() => ReadableStreamCancel(source, storedError), true, storedError); + } + else { + shutdown(true, storedError); + } + }); + // Closing must be propagated forward + isOrBecomesClosed(source, reader._closedPromise, () => { + if (!preventClose) { + shutdownWithAction(() => WritableStreamDefaultWriterCloseWithErrorPropagation(writer)); + } + else { + shutdown(); + } + }); + // Closing must be propagated backward + if (WritableStreamCloseQueuedOrInFlight(dest) || dest._state === 'closed') { + const destClosed = new TypeError('the destination writable stream closed before all data could be piped to it'); + if (!preventCancel) { + shutdownWithAction(() => ReadableStreamCancel(source, destClosed), true, destClosed); + } + else { + shutdown(true, destClosed); + } + } + setPromiseIsHandledToTrue(pipeLoop()); + function waitForWritesToFinish() { + // Another write may have started while we were waiting on this currentWrite, so we have to be sure to wait + // for that too. + const oldCurrentWrite = currentWrite; + return PerformPromiseThen(currentWrite, () => oldCurrentWrite !== currentWrite ? waitForWritesToFinish() : undefined); + } + function isOrBecomesErrored(stream, promise, action) { + if (stream._state === 'errored') { + action(stream._storedError); + } + else { + uponRejection(promise, action); + } + } + function isOrBecomesClosed(stream, promise, action) { + if (stream._state === 'closed') { + action(); + } + else { + uponFulfillment(promise, action); + } + } + function shutdownWithAction(action, originalIsError, originalError) { + if (shuttingDown) { + return; + } + shuttingDown = true; + if (dest._state === 'writable' && !WritableStreamCloseQueuedOrInFlight(dest)) { + uponFulfillment(waitForWritesToFinish(), doTheRest); + } + else { + doTheRest(); + } + function doTheRest() { + uponPromise(action(), () => finalize(originalIsError, originalError), newError => finalize(true, newError)); + } + } + function shutdown(isError, error) { + if (shuttingDown) { + return; + } + shuttingDown = true; + if (dest._state === 'writable' && !WritableStreamCloseQueuedOrInFlight(dest)) { + uponFulfillment(waitForWritesToFinish(), () => finalize(isError, error)); + } + else { + finalize(isError, error); + } + } + function finalize(isError, error) { + WritableStreamDefaultWriterRelease(writer); + ReadableStreamReaderGenericRelease(reader); + if (signal !== undefined) { + signal.removeEventListener('abort', abortAlgorithm); + } + if (isError) { + reject(error); + } + else { + resolve(undefined); + } + } + }); + } + + /** + * Allows control of a {@link ReadableStream | readable stream}'s state and internal queue. + * + * @public + */ + class ReadableStreamDefaultController { + constructor() { + throw new TypeError('Illegal constructor'); + } + /** + * Returns the desired size to fill the controlled stream's internal queue. It can be negative, if the queue is + * over-full. An underlying source ought to use this information to determine when and how to apply backpressure. + */ + get desiredSize() { + if (!IsReadableStreamDefaultController(this)) { + throw defaultControllerBrandCheckException$1('desiredSize'); + } + return ReadableStreamDefaultControllerGetDesiredSize(this); + } + /** + * Closes the controlled readable stream. Consumers will still be able to read any previously-enqueued chunks from + * the stream, but once those are read, the stream will become closed. + */ + close() { + if (!IsReadableStreamDefaultController(this)) { + throw defaultControllerBrandCheckException$1('close'); + } + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(this)) { + throw new TypeError('The stream is not in a state that permits close'); + } + ReadableStreamDefaultControllerClose(this); + } + enqueue(chunk = undefined) { + if (!IsReadableStreamDefaultController(this)) { + throw defaultControllerBrandCheckException$1('enqueue'); + } + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(this)) { + throw new TypeError('The stream is not in a state that permits enqueue'); + } + return ReadableStreamDefaultControllerEnqueue(this, chunk); + } + /** + * Errors the controlled readable stream, making all future interactions with it fail with the given error `e`. + */ + error(e = undefined) { + if (!IsReadableStreamDefaultController(this)) { + throw defaultControllerBrandCheckException$1('error'); + } + ReadableStreamDefaultControllerError(this, e); + } + /** @internal */ + [CancelSteps](reason) { + ResetQueue(this); + const result = this._cancelAlgorithm(reason); + ReadableStreamDefaultControllerClearAlgorithms(this); + return result; + } + /** @internal */ + [PullSteps](readRequest) { + const stream = this._controlledReadableStream; + if (this._queue.length > 0) { + const chunk = DequeueValue(this); + if (this._closeRequested && this._queue.length === 0) { + ReadableStreamDefaultControllerClearAlgorithms(this); + ReadableStreamClose(stream); + } + else { + ReadableStreamDefaultControllerCallPullIfNeeded(this); + } + readRequest._chunkSteps(chunk); + } + else { + ReadableStreamAddReadRequest(stream, readRequest); + ReadableStreamDefaultControllerCallPullIfNeeded(this); + } + } + } + Object.defineProperties(ReadableStreamDefaultController.prototype, { + close: { enumerable: true }, + enqueue: { enumerable: true }, + error: { enumerable: true }, + desiredSize: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(ReadableStreamDefaultController.prototype, SymbolPolyfill.toStringTag, { + value: 'ReadableStreamDefaultController', + configurable: true + }); + } + // Abstract operations for the ReadableStreamDefaultController. + function IsReadableStreamDefaultController(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_controlledReadableStream')) { + return false; + } + return true; + } + function ReadableStreamDefaultControllerCallPullIfNeeded(controller) { + const shouldPull = ReadableStreamDefaultControllerShouldCallPull(controller); + if (!shouldPull) { + return; + } + if (controller._pulling) { + controller._pullAgain = true; + return; + } + controller._pulling = true; + const pullPromise = controller._pullAlgorithm(); + uponPromise(pullPromise, () => { + controller._pulling = false; + if (controller._pullAgain) { + controller._pullAgain = false; + ReadableStreamDefaultControllerCallPullIfNeeded(controller); + } + }, e => { + ReadableStreamDefaultControllerError(controller, e); + }); + } + function ReadableStreamDefaultControllerShouldCallPull(controller) { + const stream = controller._controlledReadableStream; + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(controller)) { + return false; + } + if (!controller._started) { + return false; + } + if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0) { + return true; + } + const desiredSize = ReadableStreamDefaultControllerGetDesiredSize(controller); + if (desiredSize > 0) { + return true; + } + return false; + } + function ReadableStreamDefaultControllerClearAlgorithms(controller) { + controller._pullAlgorithm = undefined; + controller._cancelAlgorithm = undefined; + controller._strategySizeAlgorithm = undefined; + } + // A client of ReadableStreamDefaultController may use these functions directly to bypass state check. + function ReadableStreamDefaultControllerClose(controller) { + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(controller)) { + return; + } + const stream = controller._controlledReadableStream; + controller._closeRequested = true; + if (controller._queue.length === 0) { + ReadableStreamDefaultControllerClearAlgorithms(controller); + ReadableStreamClose(stream); + } + } + function ReadableStreamDefaultControllerEnqueue(controller, chunk) { + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(controller)) { + return; + } + const stream = controller._controlledReadableStream; + if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0) { + ReadableStreamFulfillReadRequest(stream, chunk, false); + } + else { + let chunkSize; + try { + chunkSize = controller._strategySizeAlgorithm(chunk); + } + catch (chunkSizeE) { + ReadableStreamDefaultControllerError(controller, chunkSizeE); + throw chunkSizeE; + } + try { + EnqueueValueWithSize(controller, chunk, chunkSize); + } + catch (enqueueE) { + ReadableStreamDefaultControllerError(controller, enqueueE); + throw enqueueE; + } + } + ReadableStreamDefaultControllerCallPullIfNeeded(controller); + } + function ReadableStreamDefaultControllerError(controller, e) { + const stream = controller._controlledReadableStream; + if (stream._state !== 'readable') { + return; + } + ResetQueue(controller); + ReadableStreamDefaultControllerClearAlgorithms(controller); + ReadableStreamError(stream, e); + } + function ReadableStreamDefaultControllerGetDesiredSize(controller) { + const state = controller._controlledReadableStream._state; + if (state === 'errored') { + return null; + } + if (state === 'closed') { + return 0; + } + return controller._strategyHWM - controller._queueTotalSize; + } + // This is used in the implementation of TransformStream. + function ReadableStreamDefaultControllerHasBackpressure(controller) { + if (ReadableStreamDefaultControllerShouldCallPull(controller)) { + return false; + } + return true; + } + function ReadableStreamDefaultControllerCanCloseOrEnqueue(controller) { + const state = controller._controlledReadableStream._state; + if (!controller._closeRequested && state === 'readable') { + return true; + } + return false; + } + function SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm) { + controller._controlledReadableStream = stream; + controller._queue = undefined; + controller._queueTotalSize = undefined; + ResetQueue(controller); + controller._started = false; + controller._closeRequested = false; + controller._pullAgain = false; + controller._pulling = false; + controller._strategySizeAlgorithm = sizeAlgorithm; + controller._strategyHWM = highWaterMark; + controller._pullAlgorithm = pullAlgorithm; + controller._cancelAlgorithm = cancelAlgorithm; + stream._readableStreamController = controller; + const startResult = startAlgorithm(); + uponPromise(promiseResolvedWith(startResult), () => { + controller._started = true; + ReadableStreamDefaultControllerCallPullIfNeeded(controller); + }, r => { + ReadableStreamDefaultControllerError(controller, r); + }); + } + function SetUpReadableStreamDefaultControllerFromUnderlyingSource(stream, underlyingSource, highWaterMark, sizeAlgorithm) { + const controller = Object.create(ReadableStreamDefaultController.prototype); + let startAlgorithm = () => undefined; + let pullAlgorithm = () => promiseResolvedWith(undefined); + let cancelAlgorithm = () => promiseResolvedWith(undefined); + if (underlyingSource.start !== undefined) { + startAlgorithm = () => underlyingSource.start(controller); + } + if (underlyingSource.pull !== undefined) { + pullAlgorithm = () => underlyingSource.pull(controller); + } + if (underlyingSource.cancel !== undefined) { + cancelAlgorithm = reason => underlyingSource.cancel(reason); + } + SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm); + } + // Helper functions for the ReadableStreamDefaultController. + function defaultControllerBrandCheckException$1(name) { + return new TypeError(`ReadableStreamDefaultController.prototype.${name} can only be used on a ReadableStreamDefaultController`); + } + + function ReadableStreamTee(stream, cloneForBranch2) { + const reader = AcquireReadableStreamDefaultReader(stream); + let reading = false; + let canceled1 = false; + let canceled2 = false; + let reason1; + let reason2; + let branch1; + let branch2; + let resolveCancelPromise; + const cancelPromise = newPromise(resolve => { + resolveCancelPromise = resolve; + }); + function pullAlgorithm() { + if (reading) { + return promiseResolvedWith(undefined); + } + reading = true; + const readRequest = { + _chunkSteps: value => { + // This needs to be delayed a microtask because it takes at least a microtask to detect errors (using + // reader._closedPromise below), and we want errors in stream to error both branches immediately. We cannot let + // successful synchronously-available reads get ahead of asynchronously-available errors. + queueMicrotask(() => { + reading = false; + const value1 = value; + const value2 = value; + // There is no way to access the cloning code right now in the reference implementation. + // If we add one then we'll need an implementation for serializable objects. + // if (!canceled2 && cloneForBranch2) { + // value2 = StructuredDeserialize(StructuredSerialize(value2)); + // } + if (!canceled1) { + ReadableStreamDefaultControllerEnqueue(branch1._readableStreamController, value1); + } + if (!canceled2) { + ReadableStreamDefaultControllerEnqueue(branch2._readableStreamController, value2); + } + }); + }, + _closeSteps: () => { + reading = false; + if (!canceled1) { + ReadableStreamDefaultControllerClose(branch1._readableStreamController); + } + if (!canceled2) { + ReadableStreamDefaultControllerClose(branch2._readableStreamController); + } + if (!canceled1 || !canceled2) { + resolveCancelPromise(undefined); + } + }, + _errorSteps: () => { + reading = false; + } + }; + ReadableStreamDefaultReaderRead(reader, readRequest); + return promiseResolvedWith(undefined); + } + function cancel1Algorithm(reason) { + canceled1 = true; + reason1 = reason; + if (canceled2) { + const compositeReason = CreateArrayFromList([reason1, reason2]); + const cancelResult = ReadableStreamCancel(stream, compositeReason); + resolveCancelPromise(cancelResult); + } + return cancelPromise; + } + function cancel2Algorithm(reason) { + canceled2 = true; + reason2 = reason; + if (canceled1) { + const compositeReason = CreateArrayFromList([reason1, reason2]); + const cancelResult = ReadableStreamCancel(stream, compositeReason); + resolveCancelPromise(cancelResult); + } + return cancelPromise; + } + function startAlgorithm() { + // do nothing + } + branch1 = CreateReadableStream(startAlgorithm, pullAlgorithm, cancel1Algorithm); + branch2 = CreateReadableStream(startAlgorithm, pullAlgorithm, cancel2Algorithm); + uponRejection(reader._closedPromise, (r) => { + ReadableStreamDefaultControllerError(branch1._readableStreamController, r); + ReadableStreamDefaultControllerError(branch2._readableStreamController, r); + if (!canceled1 || !canceled2) { + resolveCancelPromise(undefined); + } + }); + return [branch1, branch2]; + } + + function convertUnderlyingDefaultOrByteSource(source, context) { + assertDictionary(source, context); + const original = source; + const autoAllocateChunkSize = original === null || original === void 0 ? void 0 : original.autoAllocateChunkSize; + const cancel = original === null || original === void 0 ? void 0 : original.cancel; + const pull = original === null || original === void 0 ? void 0 : original.pull; + const start = original === null || original === void 0 ? void 0 : original.start; + const type = original === null || original === void 0 ? void 0 : original.type; + return { + autoAllocateChunkSize: autoAllocateChunkSize === undefined ? + undefined : + convertUnsignedLongLongWithEnforceRange(autoAllocateChunkSize, `${context} has member 'autoAllocateChunkSize' that`), + cancel: cancel === undefined ? + undefined : + convertUnderlyingSourceCancelCallback(cancel, original, `${context} has member 'cancel' that`), + pull: pull === undefined ? + undefined : + convertUnderlyingSourcePullCallback(pull, original, `${context} has member 'pull' that`), + start: start === undefined ? + undefined : + convertUnderlyingSourceStartCallback(start, original, `${context} has member 'start' that`), + type: type === undefined ? undefined : convertReadableStreamType(type, `${context} has member 'type' that`) + }; + } + function convertUnderlyingSourceCancelCallback(fn, original, context) { + assertFunction(fn, context); + return (reason) => promiseCall(fn, original, [reason]); + } + function convertUnderlyingSourcePullCallback(fn, original, context) { + assertFunction(fn, context); + return (controller) => promiseCall(fn, original, [controller]); + } + function convertUnderlyingSourceStartCallback(fn, original, context) { + assertFunction(fn, context); + return (controller) => reflectCall(fn, original, [controller]); + } + function convertReadableStreamType(type, context) { + type = `${type}`; + if (type !== 'bytes') { + throw new TypeError(`${context} '${type}' is not a valid enumeration value for ReadableStreamType`); + } + return type; + } + + function convertReaderOptions(options, context) { + assertDictionary(options, context); + const mode = options === null || options === void 0 ? void 0 : options.mode; + return { + mode: mode === undefined ? undefined : convertReadableStreamReaderMode(mode, `${context} has member 'mode' that`) + }; + } + function convertReadableStreamReaderMode(mode, context) { + mode = `${mode}`; + if (mode !== 'byob') { + throw new TypeError(`${context} '${mode}' is not a valid enumeration value for ReadableStreamReaderMode`); + } + return mode; + } + + function convertIteratorOptions(options, context) { + assertDictionary(options, context); + const preventCancel = options === null || options === void 0 ? void 0 : options.preventCancel; + return { preventCancel: Boolean(preventCancel) }; + } + + function convertPipeOptions(options, context) { + assertDictionary(options, context); + const preventAbort = options === null || options === void 0 ? void 0 : options.preventAbort; + const preventCancel = options === null || options === void 0 ? void 0 : options.preventCancel; + const preventClose = options === null || options === void 0 ? void 0 : options.preventClose; + const signal = options === null || options === void 0 ? void 0 : options.signal; + if (signal !== undefined) { + assertAbortSignal(signal, `${context} has member 'signal' that`); + } + return { + preventAbort: Boolean(preventAbort), + preventCancel: Boolean(preventCancel), + preventClose: Boolean(preventClose), + signal + }; + } + function assertAbortSignal(signal, context) { + if (!isAbortSignal(signal)) { + throw new TypeError(`${context} is not an AbortSignal.`); + } + } + + function convertReadableWritablePair(pair, context) { + assertDictionary(pair, context); + const readable = pair === null || pair === void 0 ? void 0 : pair.readable; + assertRequiredField(readable, 'readable', 'ReadableWritablePair'); + assertReadableStream(readable, `${context} has member 'readable' that`); + const writable = pair === null || pair === void 0 ? void 0 : pair.writable; + assertRequiredField(writable, 'writable', 'ReadableWritablePair'); + assertWritableStream(writable, `${context} has member 'writable' that`); + return { readable, writable }; + } + + /** + * A readable stream represents a source of data, from which you can read. + * + * @public + */ + class ReadableStream { + constructor(rawUnderlyingSource = {}, rawStrategy = {}) { + if (rawUnderlyingSource === undefined) { + rawUnderlyingSource = null; + } + else { + assertObject(rawUnderlyingSource, 'First parameter'); + } + const strategy = convertQueuingStrategy(rawStrategy, 'Second parameter'); + const underlyingSource = convertUnderlyingDefaultOrByteSource(rawUnderlyingSource, 'First parameter'); + InitializeReadableStream(this); + if (underlyingSource.type === 'bytes') { + if (strategy.size !== undefined) { + throw new RangeError('The strategy for a byte stream cannot have a size function'); + } + const highWaterMark = ExtractHighWaterMark(strategy, 0); + SetUpReadableByteStreamControllerFromUnderlyingSource(this, underlyingSource, highWaterMark); + } + else { + const sizeAlgorithm = ExtractSizeAlgorithm(strategy); + const highWaterMark = ExtractHighWaterMark(strategy, 1); + SetUpReadableStreamDefaultControllerFromUnderlyingSource(this, underlyingSource, highWaterMark, sizeAlgorithm); + } + } + /** + * Whether or not the readable stream is locked to a {@link ReadableStreamDefaultReader | reader}. + */ + get locked() { + if (!IsReadableStream(this)) { + throw streamBrandCheckException$1('locked'); + } + return IsReadableStreamLocked(this); + } + /** + * Cancels the stream, signaling a loss of interest in the stream by a consumer. + * + * The supplied `reason` argument will be given to the underlying source's {@link UnderlyingSource.cancel | cancel()} + * method, which might or might not use it. + */ + cancel(reason = undefined) { + if (!IsReadableStream(this)) { + return promiseRejectedWith(streamBrandCheckException$1('cancel')); + } + if (IsReadableStreamLocked(this)) { + return promiseRejectedWith(new TypeError('Cannot cancel a stream that already has a reader')); + } + return ReadableStreamCancel(this, reason); + } + getReader(rawOptions = undefined) { + if (!IsReadableStream(this)) { + throw streamBrandCheckException$1('getReader'); + } + const options = convertReaderOptions(rawOptions, 'First parameter'); + if (options.mode === undefined) { + return AcquireReadableStreamDefaultReader(this); + } + return AcquireReadableStreamBYOBReader(this); + } + pipeThrough(rawTransform, rawOptions = {}) { + if (!IsReadableStream(this)) { + throw streamBrandCheckException$1('pipeThrough'); + } + assertRequiredArgument(rawTransform, 1, 'pipeThrough'); + const transform = convertReadableWritablePair(rawTransform, 'First parameter'); + const options = convertPipeOptions(rawOptions, 'Second parameter'); + if (IsReadableStreamLocked(this)) { + throw new TypeError('ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream'); + } + if (IsWritableStreamLocked(transform.writable)) { + throw new TypeError('ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream'); + } + const promise = ReadableStreamPipeTo(this, transform.writable, options.preventClose, options.preventAbort, options.preventCancel, options.signal); + setPromiseIsHandledToTrue(promise); + return transform.readable; + } + pipeTo(destination, rawOptions = {}) { + if (!IsReadableStream(this)) { + return promiseRejectedWith(streamBrandCheckException$1('pipeTo')); + } + if (destination === undefined) { + return promiseRejectedWith(`Parameter 1 is required in 'pipeTo'.`); + } + if (!IsWritableStream(destination)) { + return promiseRejectedWith(new TypeError(`ReadableStream.prototype.pipeTo's first argument must be a WritableStream`)); + } + let options; + try { + options = convertPipeOptions(rawOptions, 'Second parameter'); + } + catch (e) { + return promiseRejectedWith(e); + } + if (IsReadableStreamLocked(this)) { + return promiseRejectedWith(new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream')); + } + if (IsWritableStreamLocked(destination)) { + return promiseRejectedWith(new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream')); + } + return ReadableStreamPipeTo(this, destination, options.preventClose, options.preventAbort, options.preventCancel, options.signal); + } + /** + * Tees this readable stream, returning a two-element array containing the two resulting branches as + * new {@link ReadableStream} instances. + * + * Teeing a stream will lock it, preventing any other consumer from acquiring a reader. + * To cancel the stream, cancel both of the resulting branches; a composite cancellation reason will then be + * propagated to the stream's underlying source. + * + * Note that the chunks seen in each branch will be the same object. If the chunks are not immutable, + * this could allow interference between the two branches. + */ + tee() { + if (!IsReadableStream(this)) { + throw streamBrandCheckException$1('tee'); + } + const branches = ReadableStreamTee(this); + return CreateArrayFromList(branches); + } + values(rawOptions = undefined) { + if (!IsReadableStream(this)) { + throw streamBrandCheckException$1('values'); + } + const options = convertIteratorOptions(rawOptions, 'First parameter'); + return AcquireReadableStreamAsyncIterator(this, options.preventCancel); + } + } + Object.defineProperties(ReadableStream.prototype, { + cancel: { enumerable: true }, + getReader: { enumerable: true }, + pipeThrough: { enumerable: true }, + pipeTo: { enumerable: true }, + tee: { enumerable: true }, + values: { enumerable: true }, + locked: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(ReadableStream.prototype, SymbolPolyfill.toStringTag, { + value: 'ReadableStream', + configurable: true + }); + } + if (typeof SymbolPolyfill.asyncIterator === 'symbol') { + Object.defineProperty(ReadableStream.prototype, SymbolPolyfill.asyncIterator, { + value: ReadableStream.prototype.values, + writable: true, + configurable: true + }); + } + // Abstract operations for the ReadableStream. + // Throws if and only if startAlgorithm throws. + function CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark = 1, sizeAlgorithm = () => 1) { + const stream = Object.create(ReadableStream.prototype); + InitializeReadableStream(stream); + const controller = Object.create(ReadableStreamDefaultController.prototype); + SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm); + return stream; + } + function InitializeReadableStream(stream) { + stream._state = 'readable'; + stream._reader = undefined; + stream._storedError = undefined; + stream._disturbed = false; + } + function IsReadableStream(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_readableStreamController')) { + return false; + } + return true; + } + function IsReadableStreamLocked(stream) { + if (stream._reader === undefined) { + return false; + } + return true; + } + // ReadableStream API exposed for controllers. + function ReadableStreamCancel(stream, reason) { + stream._disturbed = true; + if (stream._state === 'closed') { + return promiseResolvedWith(undefined); + } + if (stream._state === 'errored') { + return promiseRejectedWith(stream._storedError); + } + ReadableStreamClose(stream); + const sourceCancelPromise = stream._readableStreamController[CancelSteps](reason); + return transformPromiseWith(sourceCancelPromise, noop$1); + } + function ReadableStreamClose(stream) { + stream._state = 'closed'; + const reader = stream._reader; + if (reader === undefined) { + return; + } + defaultReaderClosedPromiseResolve(reader); + if (IsReadableStreamDefaultReader(reader)) { + reader._readRequests.forEach(readRequest => { + readRequest._closeSteps(); + }); + reader._readRequests = new SimpleQueue(); + } + } + function ReadableStreamError(stream, e) { + stream._state = 'errored'; + stream._storedError = e; + const reader = stream._reader; + if (reader === undefined) { + return; + } + defaultReaderClosedPromiseReject(reader, e); + if (IsReadableStreamDefaultReader(reader)) { + reader._readRequests.forEach(readRequest => { + readRequest._errorSteps(e); + }); + reader._readRequests = new SimpleQueue(); + } + else { + reader._readIntoRequests.forEach(readIntoRequest => { + readIntoRequest._errorSteps(e); + }); + reader._readIntoRequests = new SimpleQueue(); + } + } + // Helper functions for the ReadableStream. + function streamBrandCheckException$1(name) { + return new TypeError(`ReadableStream.prototype.${name} can only be used on a ReadableStream`); + } + + function convertQueuingStrategyInit(init, context) { + assertDictionary(init, context); + const highWaterMark = init === null || init === void 0 ? void 0 : init.highWaterMark; + assertRequiredField(highWaterMark, 'highWaterMark', 'QueuingStrategyInit'); + return { + highWaterMark: convertUnrestrictedDouble(highWaterMark) + }; + } + + const byteLengthSizeFunction = function size(chunk) { + return chunk.byteLength; + }; + /** + * A queuing strategy that counts the number of bytes in each chunk. + * + * @public + */ + class ByteLengthQueuingStrategy { + constructor(options) { + assertRequiredArgument(options, 1, 'ByteLengthQueuingStrategy'); + options = convertQueuingStrategyInit(options, 'First parameter'); + this._byteLengthQueuingStrategyHighWaterMark = options.highWaterMark; + } + /** + * Returns the high water mark provided to the constructor. + */ + get highWaterMark() { + if (!IsByteLengthQueuingStrategy(this)) { + throw byteLengthBrandCheckException('highWaterMark'); + } + return this._byteLengthQueuingStrategyHighWaterMark; + } + /** + * Measures the size of `chunk` by returning the value of its `byteLength` property. + */ + get size() { + if (!IsByteLengthQueuingStrategy(this)) { + throw byteLengthBrandCheckException('size'); + } + return byteLengthSizeFunction; + } + } + Object.defineProperties(ByteLengthQueuingStrategy.prototype, { + highWaterMark: { enumerable: true }, + size: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(ByteLengthQueuingStrategy.prototype, SymbolPolyfill.toStringTag, { + value: 'ByteLengthQueuingStrategy', + configurable: true + }); + } + // Helper functions for the ByteLengthQueuingStrategy. + function byteLengthBrandCheckException(name) { + return new TypeError(`ByteLengthQueuingStrategy.prototype.${name} can only be used on a ByteLengthQueuingStrategy`); + } + function IsByteLengthQueuingStrategy(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_byteLengthQueuingStrategyHighWaterMark')) { + return false; + } + return true; + } + + const countSizeFunction = function size() { + return 1; + }; + /** + * A queuing strategy that counts the number of chunks. + * + * @public + */ + class CountQueuingStrategy { + constructor(options) { + assertRequiredArgument(options, 1, 'CountQueuingStrategy'); + options = convertQueuingStrategyInit(options, 'First parameter'); + this._countQueuingStrategyHighWaterMark = options.highWaterMark; + } + /** + * Returns the high water mark provided to the constructor. + */ + get highWaterMark() { + if (!IsCountQueuingStrategy(this)) { + throw countBrandCheckException('highWaterMark'); + } + return this._countQueuingStrategyHighWaterMark; + } + /** + * Measures the size of `chunk` by always returning 1. + * This ensures that the total queue size is a count of the number of chunks in the queue. + */ + get size() { + if (!IsCountQueuingStrategy(this)) { + throw countBrandCheckException('size'); + } + return countSizeFunction; + } + } + Object.defineProperties(CountQueuingStrategy.prototype, { + highWaterMark: { enumerable: true }, + size: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(CountQueuingStrategy.prototype, SymbolPolyfill.toStringTag, { + value: 'CountQueuingStrategy', + configurable: true + }); + } + // Helper functions for the CountQueuingStrategy. + function countBrandCheckException(name) { + return new TypeError(`CountQueuingStrategy.prototype.${name} can only be used on a CountQueuingStrategy`); + } + function IsCountQueuingStrategy(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_countQueuingStrategyHighWaterMark')) { + return false; + } + return true; + } + + function convertTransformer(original, context) { + assertDictionary(original, context); + const flush = original === null || original === void 0 ? void 0 : original.flush; + const readableType = original === null || original === void 0 ? void 0 : original.readableType; + const start = original === null || original === void 0 ? void 0 : original.start; + const transform = original === null || original === void 0 ? void 0 : original.transform; + const writableType = original === null || original === void 0 ? void 0 : original.writableType; + return { + flush: flush === undefined ? + undefined : + convertTransformerFlushCallback(flush, original, `${context} has member 'flush' that`), + readableType, + start: start === undefined ? + undefined : + convertTransformerStartCallback(start, original, `${context} has member 'start' that`), + transform: transform === undefined ? + undefined : + convertTransformerTransformCallback(transform, original, `${context} has member 'transform' that`), + writableType + }; + } + function convertTransformerFlushCallback(fn, original, context) { + assertFunction(fn, context); + return (controller) => promiseCall(fn, original, [controller]); + } + function convertTransformerStartCallback(fn, original, context) { + assertFunction(fn, context); + return (controller) => reflectCall(fn, original, [controller]); + } + function convertTransformerTransformCallback(fn, original, context) { + assertFunction(fn, context); + return (chunk, controller) => promiseCall(fn, original, [chunk, controller]); + } + + // Class TransformStream + /** + * A transform stream consists of a pair of streams: a {@link WritableStream | writable stream}, + * known as its writable side, and a {@link ReadableStream | readable stream}, known as its readable side. + * In a manner specific to the transform stream in question, writes to the writable side result in new data being + * made available for reading from the readable side. + * + * @public + */ + class TransformStream { + constructor(rawTransformer = {}, rawWritableStrategy = {}, rawReadableStrategy = {}) { + if (rawTransformer === undefined) { + rawTransformer = null; + } + const writableStrategy = convertQueuingStrategy(rawWritableStrategy, 'Second parameter'); + const readableStrategy = convertQueuingStrategy(rawReadableStrategy, 'Third parameter'); + const transformer = convertTransformer(rawTransformer, 'First parameter'); + if (transformer.readableType !== undefined) { + throw new RangeError('Invalid readableType specified'); + } + if (transformer.writableType !== undefined) { + throw new RangeError('Invalid writableType specified'); + } + const readableHighWaterMark = ExtractHighWaterMark(readableStrategy, 0); + const readableSizeAlgorithm = ExtractSizeAlgorithm(readableStrategy); + const writableHighWaterMark = ExtractHighWaterMark(writableStrategy, 1); + const writableSizeAlgorithm = ExtractSizeAlgorithm(writableStrategy); + let startPromise_resolve; + const startPromise = newPromise(resolve => { + startPromise_resolve = resolve; + }); + InitializeTransformStream(this, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm); + SetUpTransformStreamDefaultControllerFromTransformer(this, transformer); + if (transformer.start !== undefined) { + startPromise_resolve(transformer.start(this._transformStreamController)); + } + else { + startPromise_resolve(undefined); + } + } + /** + * The readable side of the transform stream. + */ + get readable() { + if (!IsTransformStream(this)) { + throw streamBrandCheckException('readable'); + } + return this._readable; + } + /** + * The writable side of the transform stream. + */ + get writable() { + if (!IsTransformStream(this)) { + throw streamBrandCheckException('writable'); + } + return this._writable; + } + } + Object.defineProperties(TransformStream.prototype, { + readable: { enumerable: true }, + writable: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(TransformStream.prototype, SymbolPolyfill.toStringTag, { + value: 'TransformStream', + configurable: true + }); + } + function InitializeTransformStream(stream, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm) { + function startAlgorithm() { + return startPromise; + } + function writeAlgorithm(chunk) { + return TransformStreamDefaultSinkWriteAlgorithm(stream, chunk); + } + function abortAlgorithm(reason) { + return TransformStreamDefaultSinkAbortAlgorithm(stream, reason); + } + function closeAlgorithm() { + return TransformStreamDefaultSinkCloseAlgorithm(stream); + } + stream._writable = CreateWritableStream(startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, writableHighWaterMark, writableSizeAlgorithm); + function pullAlgorithm() { + return TransformStreamDefaultSourcePullAlgorithm(stream); + } + function cancelAlgorithm(reason) { + TransformStreamErrorWritableAndUnblockWrite(stream, reason); + return promiseResolvedWith(undefined); + } + stream._readable = CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, readableHighWaterMark, readableSizeAlgorithm); + // The [[backpressure]] slot is set to undefined so that it can be initialised by TransformStreamSetBackpressure. + stream._backpressure = undefined; + stream._backpressureChangePromise = undefined; + stream._backpressureChangePromise_resolve = undefined; + TransformStreamSetBackpressure(stream, true); + stream._transformStreamController = undefined; + } + function IsTransformStream(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_transformStreamController')) { + return false; + } + return true; + } + // This is a no-op if both sides are already errored. + function TransformStreamError(stream, e) { + ReadableStreamDefaultControllerError(stream._readable._readableStreamController, e); + TransformStreamErrorWritableAndUnblockWrite(stream, e); + } + function TransformStreamErrorWritableAndUnblockWrite(stream, e) { + TransformStreamDefaultControllerClearAlgorithms(stream._transformStreamController); + WritableStreamDefaultControllerErrorIfNeeded(stream._writable._writableStreamController, e); + if (stream._backpressure) { + // Pretend that pull() was called to permit any pending write() calls to complete. TransformStreamSetBackpressure() + // cannot be called from enqueue() or pull() once the ReadableStream is errored, so this will will be the final time + // _backpressure is set. + TransformStreamSetBackpressure(stream, false); + } + } + function TransformStreamSetBackpressure(stream, backpressure) { + // Passes also when called during construction. + if (stream._backpressureChangePromise !== undefined) { + stream._backpressureChangePromise_resolve(); + } + stream._backpressureChangePromise = newPromise(resolve => { + stream._backpressureChangePromise_resolve = resolve; + }); + stream._backpressure = backpressure; + } + // Class TransformStreamDefaultController + /** + * Allows control of the {@link ReadableStream} and {@link WritableStream} of the associated {@link TransformStream}. + * + * @public + */ + class TransformStreamDefaultController { + constructor() { + throw new TypeError('Illegal constructor'); + } + /** + * Returns the desired size to fill the readable side’s internal queue. It can be negative, if the queue is over-full. + */ + get desiredSize() { + if (!IsTransformStreamDefaultController(this)) { + throw defaultControllerBrandCheckException('desiredSize'); + } + const readableController = this._controlledTransformStream._readable._readableStreamController; + return ReadableStreamDefaultControllerGetDesiredSize(readableController); + } + enqueue(chunk = undefined) { + if (!IsTransformStreamDefaultController(this)) { + throw defaultControllerBrandCheckException('enqueue'); + } + TransformStreamDefaultControllerEnqueue(this, chunk); + } + /** + * Errors both the readable side and the writable side of the controlled transform stream, making all future + * interactions with it fail with the given error `e`. Any chunks queued for transformation will be discarded. + */ + error(reason = undefined) { + if (!IsTransformStreamDefaultController(this)) { + throw defaultControllerBrandCheckException('error'); + } + TransformStreamDefaultControllerError(this, reason); + } + /** + * Closes the readable side and errors the writable side of the controlled transform stream. This is useful when the + * transformer only needs to consume a portion of the chunks written to the writable side. + */ + terminate() { + if (!IsTransformStreamDefaultController(this)) { + throw defaultControllerBrandCheckException('terminate'); + } + TransformStreamDefaultControllerTerminate(this); + } + } + Object.defineProperties(TransformStreamDefaultController.prototype, { + enqueue: { enumerable: true }, + error: { enumerable: true }, + terminate: { enumerable: true }, + desiredSize: { enumerable: true } + }); + if (typeof SymbolPolyfill.toStringTag === 'symbol') { + Object.defineProperty(TransformStreamDefaultController.prototype, SymbolPolyfill.toStringTag, { + value: 'TransformStreamDefaultController', + configurable: true + }); + } + // Transform Stream Default Controller Abstract Operations + function IsTransformStreamDefaultController(x) { + if (!typeIsObject$1(x)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x, '_controlledTransformStream')) { + return false; + } + return true; + } + function SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm) { + controller._controlledTransformStream = stream; + stream._transformStreamController = controller; + controller._transformAlgorithm = transformAlgorithm; + controller._flushAlgorithm = flushAlgorithm; + } + function SetUpTransformStreamDefaultControllerFromTransformer(stream, transformer) { + const controller = Object.create(TransformStreamDefaultController.prototype); + let transformAlgorithm = (chunk) => { + try { + TransformStreamDefaultControllerEnqueue(controller, chunk); + return promiseResolvedWith(undefined); + } + catch (transformResultE) { + return promiseRejectedWith(transformResultE); + } + }; + let flushAlgorithm = () => promiseResolvedWith(undefined); + if (transformer.transform !== undefined) { + transformAlgorithm = chunk => transformer.transform(chunk, controller); + } + if (transformer.flush !== undefined) { + flushAlgorithm = () => transformer.flush(controller); + } + SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm); + } + function TransformStreamDefaultControllerClearAlgorithms(controller) { + controller._transformAlgorithm = undefined; + controller._flushAlgorithm = undefined; + } + function TransformStreamDefaultControllerEnqueue(controller, chunk) { + const stream = controller._controlledTransformStream; + const readableController = stream._readable._readableStreamController; + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(readableController)) { + throw new TypeError('Readable side is not in a state that permits enqueue'); + } + // We throttle transform invocations based on the backpressure of the ReadableStream, but we still + // accept TransformStreamDefaultControllerEnqueue() calls. + try { + ReadableStreamDefaultControllerEnqueue(readableController, chunk); + } + catch (e) { + // This happens when readableStrategy.size() throws. + TransformStreamErrorWritableAndUnblockWrite(stream, e); + throw stream._readable._storedError; + } + const backpressure = ReadableStreamDefaultControllerHasBackpressure(readableController); + if (backpressure !== stream._backpressure) { + TransformStreamSetBackpressure(stream, true); + } + } + function TransformStreamDefaultControllerError(controller, e) { + TransformStreamError(controller._controlledTransformStream, e); + } + function TransformStreamDefaultControllerPerformTransform(controller, chunk) { + const transformPromise = controller._transformAlgorithm(chunk); + return transformPromiseWith(transformPromise, undefined, r => { + TransformStreamError(controller._controlledTransformStream, r); + throw r; + }); + } + function TransformStreamDefaultControllerTerminate(controller) { + const stream = controller._controlledTransformStream; + const readableController = stream._readable._readableStreamController; + ReadableStreamDefaultControllerClose(readableController); + const error = new TypeError('TransformStream terminated'); + TransformStreamErrorWritableAndUnblockWrite(stream, error); + } + // TransformStreamDefaultSink Algorithms + function TransformStreamDefaultSinkWriteAlgorithm(stream, chunk) { + const controller = stream._transformStreamController; + if (stream._backpressure) { + const backpressureChangePromise = stream._backpressureChangePromise; + return transformPromiseWith(backpressureChangePromise, () => { + const writable = stream._writable; + const state = writable._state; + if (state === 'erroring') { + throw writable._storedError; + } + return TransformStreamDefaultControllerPerformTransform(controller, chunk); + }); + } + return TransformStreamDefaultControllerPerformTransform(controller, chunk); + } + function TransformStreamDefaultSinkAbortAlgorithm(stream, reason) { + // abort() is not called synchronously, so it is possible for abort() to be called when the stream is already + // errored. + TransformStreamError(stream, reason); + return promiseResolvedWith(undefined); + } + function TransformStreamDefaultSinkCloseAlgorithm(stream) { + // stream._readable cannot change after construction, so caching it across a call to user code is safe. + const readable = stream._readable; + const controller = stream._transformStreamController; + const flushPromise = controller._flushAlgorithm(); + TransformStreamDefaultControllerClearAlgorithms(controller); + // Return a promise that is fulfilled with undefined on success. + return transformPromiseWith(flushPromise, () => { + if (readable._state === 'errored') { + throw readable._storedError; + } + ReadableStreamDefaultControllerClose(readable._readableStreamController); + }, r => { + TransformStreamError(stream, r); + throw readable._storedError; + }); + } + // TransformStreamDefaultSource Algorithms + function TransformStreamDefaultSourcePullAlgorithm(stream) { + // Invariant. Enforced by the promises returned by start() and pull(). + TransformStreamSetBackpressure(stream, false); + // Prevent the next pull() call until there is backpressure. + return stream._backpressureChangePromise; + } + // Helper functions for the TransformStreamDefaultController. + function defaultControllerBrandCheckException(name) { + return new TypeError(`TransformStreamDefaultController.prototype.${name} can only be used on a TransformStreamDefaultController`); + } + // Helper functions for the TransformStream. + function streamBrandCheckException(name) { + return new TypeError(`TransformStream.prototype.${name} can only be used on a TransformStream`); + } + + var ponyfill_es6 = /*#__PURE__*/Object.freeze({ + __proto__: null, + ByteLengthQueuingStrategy: ByteLengthQueuingStrategy, + CountQueuingStrategy: CountQueuingStrategy, + ReadableByteStreamController: ReadableByteStreamController, + ReadableStream: ReadableStream, + ReadableStreamBYOBReader: ReadableStreamBYOBReader, + ReadableStreamBYOBRequest: ReadableStreamBYOBRequest, + ReadableStreamDefaultController: ReadableStreamDefaultController, + ReadableStreamDefaultReader: ReadableStreamDefaultReader, + TransformStream: TransformStream, + TransformStreamDefaultController: TransformStreamDefaultController, + WritableStream: WritableStream, + WritableStreamDefaultController: WritableStreamDefaultController, + WritableStreamDefaultWriter: WritableStreamDefaultWriter + }); + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + /* global Reflect, Promise */ + + var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + + function __extends(d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + } + + function assert$9(test) { + if (!test) { + throw new TypeError('Assertion failed'); + } + } + + function noop() { + return; + } + function typeIsObject(x) { + return (typeof x === 'object' && x !== null) || typeof x === 'function'; + } + + function isStreamConstructor(ctor) { + if (typeof ctor !== 'function') { + return false; + } + var startCalled = false; + try { + new ctor({ + start: function () { + startCalled = true; + } + }); + } + catch (e) { + // ignore + } + return startCalled; + } + function isReadableStream(readable) { + if (!typeIsObject(readable)) { + return false; + } + if (typeof readable.getReader !== 'function') { + return false; + } + return true; + } + function isReadableStreamConstructor(ctor) { + if (!isStreamConstructor(ctor)) { + return false; + } + if (!isReadableStream(new ctor())) { + return false; + } + return true; + } + function isWritableStream(writable) { + if (!typeIsObject(writable)) { + return false; + } + if (typeof writable.getWriter !== 'function') { + return false; + } + return true; + } + function isWritableStreamConstructor(ctor) { + if (!isStreamConstructor(ctor)) { + return false; + } + if (!isWritableStream(new ctor())) { + return false; + } + return true; + } + function isTransformStream(transform) { + if (!typeIsObject(transform)) { + return false; + } + if (!isReadableStream(transform.readable)) { + return false; + } + if (!isWritableStream(transform.writable)) { + return false; + } + return true; + } + function isTransformStreamConstructor(ctor) { + if (!isStreamConstructor(ctor)) { + return false; + } + if (!isTransformStream(new ctor())) { + return false; + } + return true; + } + function supportsByobReader(readable) { + try { + var reader = readable.getReader({ mode: 'byob' }); + reader.releaseLock(); + return true; + } + catch (_a) { + return false; + } + } + function supportsByteSource(ctor) { + try { + new ctor({ type: 'bytes' }); + return true; + } + catch (_a) { + return false; + } + } + + function createReadableStreamWrapper(ctor) { + assert$9(isReadableStreamConstructor(ctor)); + var byteSourceSupported = supportsByteSource(ctor); + return function (readable, _a) { + var _b = _a === void 0 ? {} : _a, type = _b.type; + type = parseReadableType(type); + if (type === 'bytes' && !byteSourceSupported) { + type = undefined; + } + if (readable.constructor === ctor) { + if (type !== 'bytes' || supportsByobReader(readable)) { + return readable; + } + } + if (type === 'bytes') { + var source = createWrappingReadableSource(readable, { type: type }); + return new ctor(source); + } + else { + var source = createWrappingReadableSource(readable); + return new ctor(source); + } + }; + } + function createWrappingReadableSource(readable, _a) { + var _b = _a === void 0 ? {} : _a, type = _b.type; + assert$9(isReadableStream(readable)); + assert$9(readable.locked === false); + type = parseReadableType(type); + var source; + if (type === 'bytes') { + source = new WrappingReadableByteStreamSource(readable); + } + else { + source = new WrappingReadableStreamDefaultSource(readable); + } + return source; + } + function parseReadableType(type) { + var typeString = String(type); + if (typeString === 'bytes') { + return typeString; + } + else if (type === undefined) { + return type; + } + else { + throw new RangeError('Invalid type is specified'); + } + } + var AbstractWrappingReadableStreamSource = /** @class */ (function () { + function AbstractWrappingReadableStreamSource(underlyingStream) { + this._underlyingReader = undefined; + this._readerMode = undefined; + this._readableStreamController = undefined; + this._pendingRead = undefined; + this._underlyingStream = underlyingStream; + // always keep a reader attached to detect close/error + this._attachDefaultReader(); + } + AbstractWrappingReadableStreamSource.prototype.start = function (controller) { + this._readableStreamController = controller; + }; + AbstractWrappingReadableStreamSource.prototype.cancel = function (reason) { + assert$9(this._underlyingReader !== undefined); + return this._underlyingReader.cancel(reason); + }; + AbstractWrappingReadableStreamSource.prototype._attachDefaultReader = function () { + if (this._readerMode === "default" /* DEFAULT */) { + return; + } + this._detachReader(); + var reader = this._underlyingStream.getReader(); + this._readerMode = "default" /* DEFAULT */; + this._attachReader(reader); + }; + AbstractWrappingReadableStreamSource.prototype._attachReader = function (reader) { + var _this = this; + assert$9(this._underlyingReader === undefined); + this._underlyingReader = reader; + var closed = this._underlyingReader.closed; + if (!closed) { + return; + } + closed + .then(function () { return _this._finishPendingRead(); }) + .then(function () { + if (reader === _this._underlyingReader) { + _this._readableStreamController.close(); + } + }, function (reason) { + if (reader === _this._underlyingReader) { + _this._readableStreamController.error(reason); + } + }) + .catch(noop); + }; + AbstractWrappingReadableStreamSource.prototype._detachReader = function () { + if (this._underlyingReader === undefined) { + return; + } + this._underlyingReader.releaseLock(); + this._underlyingReader = undefined; + this._readerMode = undefined; + }; + AbstractWrappingReadableStreamSource.prototype._pullWithDefaultReader = function () { + var _this = this; + this._attachDefaultReader(); + // TODO Backpressure? + var read = this._underlyingReader.read() + .then(function (result) { + var controller = _this._readableStreamController; + if (result.done) { + _this._tryClose(); + } + else { + controller.enqueue(result.value); + } + }); + this._setPendingRead(read); + return read; + }; + AbstractWrappingReadableStreamSource.prototype._tryClose = function () { + try { + this._readableStreamController.close(); + } + catch (_a) { + // already errored or closed + } + }; + AbstractWrappingReadableStreamSource.prototype._setPendingRead = function (readPromise) { + var _this = this; + var pendingRead; + var finishRead = function () { + if (_this._pendingRead === pendingRead) { + _this._pendingRead = undefined; + } + }; + this._pendingRead = pendingRead = readPromise.then(finishRead, finishRead); + }; + AbstractWrappingReadableStreamSource.prototype._finishPendingRead = function () { + var _this = this; + if (!this._pendingRead) { + return undefined; + } + var afterRead = function () { return _this._finishPendingRead(); }; + return this._pendingRead.then(afterRead, afterRead); + }; + return AbstractWrappingReadableStreamSource; + }()); + var WrappingReadableStreamDefaultSource = /** @class */ (function (_super) { + __extends(WrappingReadableStreamDefaultSource, _super); + function WrappingReadableStreamDefaultSource() { + return _super !== null && _super.apply(this, arguments) || this; + } + WrappingReadableStreamDefaultSource.prototype.pull = function () { + return this._pullWithDefaultReader(); + }; + return WrappingReadableStreamDefaultSource; + }(AbstractWrappingReadableStreamSource)); + function toUint8Array(view) { + return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); + } + function copyArrayBufferView(from, to) { + var fromArray = toUint8Array(from); + var toArray = toUint8Array(to); + toArray.set(fromArray, 0); + } + var WrappingReadableByteStreamSource = /** @class */ (function (_super) { + __extends(WrappingReadableByteStreamSource, _super); + function WrappingReadableByteStreamSource(underlyingStream) { + var _this = this; + var supportsByob = supportsByobReader(underlyingStream); + _this = _super.call(this, underlyingStream) || this; + _this._supportsByob = supportsByob; + return _this; + } + Object.defineProperty(WrappingReadableByteStreamSource.prototype, "type", { + get: function () { + return 'bytes'; + }, + enumerable: false, + configurable: true + }); + WrappingReadableByteStreamSource.prototype._attachByobReader = function () { + if (this._readerMode === "byob" /* BYOB */) { + return; + } + assert$9(this._supportsByob); + this._detachReader(); + var reader = this._underlyingStream.getReader({ mode: 'byob' }); + this._readerMode = "byob" /* BYOB */; + this._attachReader(reader); + }; + WrappingReadableByteStreamSource.prototype.pull = function () { + if (this._supportsByob) { + var byobRequest = this._readableStreamController.byobRequest; + if (byobRequest) { + return this._pullWithByobRequest(byobRequest); + } + } + return this._pullWithDefaultReader(); + }; + WrappingReadableByteStreamSource.prototype._pullWithByobRequest = function (byobRequest) { + var _this = this; + this._attachByobReader(); + // reader.read(view) detaches the input view, therefore we cannot pass byobRequest.view directly + // create a separate buffer to read into, then copy that to byobRequest.view + var buffer = new Uint8Array(byobRequest.view.byteLength); + // TODO Backpressure? + var read = this._underlyingReader.read(buffer) + .then(function (result) { + _this._readableStreamController; + if (result.done) { + _this._tryClose(); + byobRequest.respond(0); + } + else { + copyArrayBufferView(result.value, byobRequest.view); + byobRequest.respond(result.value.byteLength); + } + }); + this._setPendingRead(read); + return read; + }; + return WrappingReadableByteStreamSource; + }(AbstractWrappingReadableStreamSource)); + + function createWritableStreamWrapper(ctor) { + assert$9(isWritableStreamConstructor(ctor)); + return function (writable) { + if (writable.constructor === ctor) { + return writable; + } + var sink = createWrappingWritableSink(writable); + return new ctor(sink); + }; + } + function createWrappingWritableSink(writable) { + assert$9(isWritableStream(writable)); + assert$9(writable.locked === false); + var writer = writable.getWriter(); + return new WrappingWritableStreamSink(writer); + } + var WrappingWritableStreamSink = /** @class */ (function () { + function WrappingWritableStreamSink(underlyingWriter) { + var _this = this; + this._writableStreamController = undefined; + this._pendingWrite = undefined; + this._state = "writable" /* WRITABLE */; + this._storedError = undefined; + this._underlyingWriter = underlyingWriter; + this._errorPromise = new Promise(function (resolve, reject) { + _this._errorPromiseReject = reject; + }); + this._errorPromise.catch(noop); + } + WrappingWritableStreamSink.prototype.start = function (controller) { + var _this = this; + this._writableStreamController = controller; + this._underlyingWriter.closed + .then(function () { + _this._state = "closed" /* CLOSED */; + }) + .catch(function (reason) { return _this._finishErroring(reason); }); + }; + WrappingWritableStreamSink.prototype.write = function (chunk) { + var _this = this; + var writer = this._underlyingWriter; + // Detect past errors + if (writer.desiredSize === null) { + return writer.ready; + } + var writeRequest = writer.write(chunk); + // Detect future errors + writeRequest.catch(function (reason) { return _this._finishErroring(reason); }); + writer.ready.catch(function (reason) { return _this._startErroring(reason); }); + // Reject write when errored + var write = Promise.race([writeRequest, this._errorPromise]); + this._setPendingWrite(write); + return write; + }; + WrappingWritableStreamSink.prototype.close = function () { + var _this = this; + if (this._pendingWrite === undefined) { + return this._underlyingWriter.close(); + } + return this._finishPendingWrite().then(function () { return _this.close(); }); + }; + WrappingWritableStreamSink.prototype.abort = function (reason) { + if (this._state === "errored" /* ERRORED */) { + return undefined; + } + var writer = this._underlyingWriter; + return writer.abort(reason); + }; + WrappingWritableStreamSink.prototype._setPendingWrite = function (writePromise) { + var _this = this; + var pendingWrite; + var finishWrite = function () { + if (_this._pendingWrite === pendingWrite) { + _this._pendingWrite = undefined; + } + }; + this._pendingWrite = pendingWrite = writePromise.then(finishWrite, finishWrite); + }; + WrappingWritableStreamSink.prototype._finishPendingWrite = function () { + var _this = this; + if (this._pendingWrite === undefined) { + return Promise.resolve(); + } + var afterWrite = function () { return _this._finishPendingWrite(); }; + return this._pendingWrite.then(afterWrite, afterWrite); + }; + WrappingWritableStreamSink.prototype._startErroring = function (reason) { + var _this = this; + if (this._state === "writable" /* WRITABLE */) { + this._state = "erroring" /* ERRORING */; + this._storedError = reason; + var afterWrite = function () { return _this._finishErroring(reason); }; + if (this._pendingWrite === undefined) { + afterWrite(); + } + else { + this._finishPendingWrite().then(afterWrite, afterWrite); + } + this._writableStreamController.error(reason); + } + }; + WrappingWritableStreamSink.prototype._finishErroring = function (reason) { + if (this._state === "writable" /* WRITABLE */) { + this._startErroring(reason); + } + if (this._state === "erroring" /* ERRORING */) { + this._state = "errored" /* ERRORED */; + this._errorPromiseReject(this._storedError); + } + }; + return WrappingWritableStreamSink; + }()); + + function createTransformStreamWrapper(ctor) { + assert$9(isTransformStreamConstructor(ctor)); + return function (transform) { + if (transform.constructor === ctor) { + return transform; + } + var transformer = createWrappingTransformer(transform); + return new ctor(transformer); + }; + } + function createWrappingTransformer(transform) { + assert$9(isTransformStream(transform)); + var readable = transform.readable, writable = transform.writable; + assert$9(readable.locked === false); + assert$9(writable.locked === false); + var reader = readable.getReader(); + var writer; + try { + writer = writable.getWriter(); + } + catch (e) { + reader.releaseLock(); // do not leak reader + throw e; + } + return new WrappingTransformStreamTransformer(reader, writer); + } + var WrappingTransformStreamTransformer = /** @class */ (function () { + function WrappingTransformStreamTransformer(reader, writer) { + var _this = this; + this._transformStreamController = undefined; + this._onRead = function (result) { + if (result.done) { + return; + } + _this._transformStreamController.enqueue(result.value); + return _this._reader.read().then(_this._onRead); + }; + this._onError = function (reason) { + _this._flushReject(reason); + _this._transformStreamController.error(reason); + _this._reader.cancel(reason).catch(noop); + _this._writer.abort(reason).catch(noop); + }; + this._onTerminate = function () { + _this._flushResolve(); + _this._transformStreamController.terminate(); + var error = new TypeError('TransformStream terminated'); + _this._writer.abort(error).catch(noop); + }; + this._reader = reader; + this._writer = writer; + this._flushPromise = new Promise(function (resolve, reject) { + _this._flushResolve = resolve; + _this._flushReject = reject; + }); + } + WrappingTransformStreamTransformer.prototype.start = function (controller) { + this._transformStreamController = controller; + this._reader.read() + .then(this._onRead) + .then(this._onTerminate, this._onError); + var readerClosed = this._reader.closed; + if (readerClosed) { + readerClosed + .then(this._onTerminate, this._onError); + } + }; + WrappingTransformStreamTransformer.prototype.transform = function (chunk) { + return this._writer.write(chunk); + }; + WrappingTransformStreamTransformer.prototype.flush = function () { + var _this = this; + return this._writer.close() + .then(function () { return _this._flushPromise; }); + }; + return WrappingTransformStreamTransformer; + }()); + + var webStreamsAdapter = /*#__PURE__*/Object.freeze({ + __proto__: null, + createReadableStreamWrapper: createReadableStreamWrapper, + createTransformStreamWrapper: createTransformStreamWrapper, + createWrappingReadableSource: createWrappingReadableSource, + createWrappingTransformer: createWrappingTransformer, + createWrappingWritableSink: createWrappingWritableSink, + createWritableStreamWrapper: createWritableStreamWrapper + }); + + var bn = createCommonjsModule(function (module) { + (function (module, exports) { + + // Utils + function assert (val, msg) { + if (!val) throw Error(msg || 'Assertion failed'); + } + + // Could use `inherits` module, but don't want to move from single file + // architecture yet. + function inherits (ctor, superCtor) { + ctor.super_ = superCtor; + var TempCtor = function () {}; + TempCtor.prototype = superCtor.prototype; + ctor.prototype = new TempCtor(); + ctor.prototype.constructor = ctor; + } + + // BN + + function BN (number, base, endian) { + if (BN.isBN(number)) { + return number; + } + + this.negative = 0; + this.words = null; + this.length = 0; + + // Reduction context + this.red = null; + + if (number !== null) { + if (base === 'le' || base === 'be') { + endian = base; + base = 10; + } + + this._init(number || 0, base || 10, endian || 'be'); + } + } + if (typeof module === 'object') { + module.exports = BN; + } else { + exports.BN = BN; + } + + BN.BN = BN; + BN.wordSize = 26; + + var Buffer; + try { + if (typeof window !== 'undefined' && typeof window.Buffer !== 'undefined') { + Buffer = window.Buffer; + } else { + Buffer = void('buffer').Buffer; + } + } catch (e) { + } + + BN.isBN = function isBN (num) { + if (num instanceof BN) { + return true; + } + + return num !== null && typeof num === 'object' && + num.constructor.wordSize === BN.wordSize && Array.isArray(num.words); + }; + + BN.max = function max (left, right) { + if (left.cmp(right) > 0) return left; + return right; + }; + + BN.min = function min (left, right) { + if (left.cmp(right) < 0) return left; + return right; + }; + + BN.prototype._init = function init (number, base, endian) { + if (typeof number === 'number') { + return this._initNumber(number, base, endian); + } + + if (typeof number === 'object') { + return this._initArray(number, base, endian); + } + + if (base === 'hex') { + base = 16; + } + assert(base === (base | 0) && base >= 2 && base <= 36); + + number = number.toString().replace(/\s+/g, ''); + var start = 0; + if (number[0] === '-') { + start++; + this.negative = 1; + } + + if (start < number.length) { + if (base === 16) { + this._parseHex(number, start, endian); + } else { + this._parseBase(number, base, start); + if (endian === 'le') { + this._initArray(this.toArray(), base, endian); + } + } + } + }; + + BN.prototype._initNumber = function _initNumber (number, base, endian) { + if (number < 0) { + this.negative = 1; + number = -number; + } + if (number < 0x4000000) { + this.words = [ number & 0x3ffffff ]; + this.length = 1; + } else if (number < 0x10000000000000) { + this.words = [ + number & 0x3ffffff, + (number / 0x4000000) & 0x3ffffff + ]; + this.length = 2; + } else { + assert(number < 0x20000000000000); // 2 ^ 53 (unsafe) + this.words = [ + number & 0x3ffffff, + (number / 0x4000000) & 0x3ffffff, + 1 + ]; + this.length = 3; + } + + if (endian !== 'le') return; + + // Reverse the bytes + this._initArray(this.toArray(), base, endian); + }; + + BN.prototype._initArray = function _initArray (number, base, endian) { + // Perhaps a Uint8Array + assert(typeof number.length === 'number'); + if (number.length <= 0) { + this.words = [ 0 ]; + this.length = 1; + return this; + } + + this.length = Math.ceil(number.length / 3); + this.words = new Array(this.length); + for (var i = 0; i < this.length; i++) { + this.words[i] = 0; + } + + var j, w; + var off = 0; + if (endian === 'be') { + for (i = number.length - 1, j = 0; i >= 0; i -= 3) { + w = number[i] | (number[i - 1] << 8) | (number[i - 2] << 16); + this.words[j] |= (w << off) & 0x3ffffff; + this.words[j + 1] = (w >>> (26 - off)) & 0x3ffffff; + off += 24; + if (off >= 26) { + off -= 26; + j++; + } + } + } else if (endian === 'le') { + for (i = 0, j = 0; i < number.length; i += 3) { + w = number[i] | (number[i + 1] << 8) | (number[i + 2] << 16); + this.words[j] |= (w << off) & 0x3ffffff; + this.words[j + 1] = (w >>> (26 - off)) & 0x3ffffff; + off += 24; + if (off >= 26) { + off -= 26; + j++; + } + } + } + return this.strip(); + }; + + function parseHex4Bits (string, index) { + var c = string.charCodeAt(index); + // 'A' - 'F' + if (c >= 65 && c <= 70) { + return c - 55; + // 'a' - 'f' + } else if (c >= 97 && c <= 102) { + return c - 87; + // '0' - '9' + } else { + return (c - 48) & 0xf; + } + } + + function parseHexByte (string, lowerBound, index) { + var r = parseHex4Bits(string, index); + if (index - 1 >= lowerBound) { + r |= parseHex4Bits(string, index - 1) << 4; + } + return r; + } + + BN.prototype._parseHex = function _parseHex (number, start, endian) { + // Create possibly bigger array to ensure that it fits the number + this.length = Math.ceil((number.length - start) / 6); + this.words = new Array(this.length); + for (var i = 0; i < this.length; i++) { + this.words[i] = 0; + } + + // 24-bits chunks + var off = 0; + var j = 0; + + var w; + if (endian === 'be') { + for (i = number.length - 1; i >= start; i -= 2) { + w = parseHexByte(number, start, i) << off; + this.words[j] |= w & 0x3ffffff; + if (off >= 18) { + off -= 18; + j += 1; + this.words[j] |= w >>> 26; + } else { + off += 8; + } + } + } else { + var parseLength = number.length - start; + for (i = parseLength % 2 === 0 ? start + 1 : start; i < number.length; i += 2) { + w = parseHexByte(number, start, i) << off; + this.words[j] |= w & 0x3ffffff; + if (off >= 18) { + off -= 18; + j += 1; + this.words[j] |= w >>> 26; + } else { + off += 8; + } + } + } + + this.strip(); + }; + + function parseBase (str, start, end, mul) { + var r = 0; + var len = Math.min(str.length, end); + for (var i = start; i < len; i++) { + var c = str.charCodeAt(i) - 48; + + r *= mul; + + // 'a' + if (c >= 49) { + r += c - 49 + 0xa; + + // 'A' + } else if (c >= 17) { + r += c - 17 + 0xa; + + // '0' - '9' + } else { + r += c; + } + } + return r; + } + + BN.prototype._parseBase = function _parseBase (number, base, start) { + // Initialize as zero + this.words = [ 0 ]; + this.length = 1; + + // Find length of limb in base + for (var limbLen = 0, limbPow = 1; limbPow <= 0x3ffffff; limbPow *= base) { + limbLen++; + } + limbLen--; + limbPow = (limbPow / base) | 0; + + var total = number.length - start; + var mod = total % limbLen; + var end = Math.min(total, total - mod) + start; + + var word = 0; + for (var i = start; i < end; i += limbLen) { + word = parseBase(number, i, i + limbLen, base); + + this.imuln(limbPow); + if (this.words[0] + word < 0x4000000) { + this.words[0] += word; + } else { + this._iaddn(word); + } + } + + if (mod !== 0) { + var pow = 1; + word = parseBase(number, i, number.length, base); + + for (i = 0; i < mod; i++) { + pow *= base; + } + + this.imuln(pow); + if (this.words[0] + word < 0x4000000) { + this.words[0] += word; + } else { + this._iaddn(word); + } + } + + this.strip(); + }; + + BN.prototype.copy = function copy (dest) { + dest.words = new Array(this.length); + for (var i = 0; i < this.length; i++) { + dest.words[i] = this.words[i]; + } + dest.length = this.length; + dest.negative = this.negative; + dest.red = this.red; + }; + + BN.prototype.clone = function clone () { + var r = new BN(null); + this.copy(r); + return r; + }; + + BN.prototype._expand = function _expand (size) { + while (this.length < size) { + this.words[this.length++] = 0; + } + return this; + }; + + // Remove leading `0` from `this` + BN.prototype.strip = function strip () { + while (this.length > 1 && this.words[this.length - 1] === 0) { + this.length--; + } + return this._normSign(); + }; + + BN.prototype._normSign = function _normSign () { + // -0 = 0 + if (this.length === 1 && this.words[0] === 0) { + this.negative = 0; + } + return this; + }; + + BN.prototype.inspect = function inspect () { + return (this.red ? ''; + }; + + /* + + var zeros = []; + var groupSizes = []; + var groupBases = []; + + var s = ''; + var i = -1; + while (++i < BN.wordSize) { + zeros[i] = s; + s += '0'; + } + groupSizes[0] = 0; + groupSizes[1] = 0; + groupBases[0] = 0; + groupBases[1] = 0; + var base = 2 - 1; + while (++base < 36 + 1) { + var groupSize = 0; + var groupBase = 1; + while (groupBase < (1 << BN.wordSize) / base) { + groupBase *= base; + groupSize += 1; + } + groupSizes[base] = groupSize; + groupBases[base] = groupBase; + } + + */ + + var zeros = [ + '', + '0', + '00', + '000', + '0000', + '00000', + '000000', + '0000000', + '00000000', + '000000000', + '0000000000', + '00000000000', + '000000000000', + '0000000000000', + '00000000000000', + '000000000000000', + '0000000000000000', + '00000000000000000', + '000000000000000000', + '0000000000000000000', + '00000000000000000000', + '000000000000000000000', + '0000000000000000000000', + '00000000000000000000000', + '000000000000000000000000', + '0000000000000000000000000' + ]; + + var groupSizes = [ + 0, 0, + 25, 16, 12, 11, 10, 9, 8, + 8, 7, 7, 7, 7, 6, 6, + 6, 6, 6, 6, 6, 5, 5, + 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5 + ]; + + var groupBases = [ + 0, 0, + 33554432, 43046721, 16777216, 48828125, 60466176, 40353607, 16777216, + 43046721, 10000000, 19487171, 35831808, 62748517, 7529536, 11390625, + 16777216, 24137569, 34012224, 47045881, 64000000, 4084101, 5153632, + 6436343, 7962624, 9765625, 11881376, 14348907, 17210368, 20511149, + 24300000, 28629151, 33554432, 39135393, 45435424, 52521875, 60466176 + ]; + + BN.prototype.toString = function toString (base, padding) { + base = base || 10; + padding = padding | 0 || 1; + + var out; + if (base === 16 || base === 'hex') { + out = ''; + var off = 0; + var carry = 0; + for (var i = 0; i < this.length; i++) { + var w = this.words[i]; + var word = (((w << off) | carry) & 0xffffff).toString(16); + carry = (w >>> (24 - off)) & 0xffffff; + if (carry !== 0 || i !== this.length - 1) { + out = zeros[6 - word.length] + word + out; + } else { + out = word + out; + } + off += 2; + if (off >= 26) { + off -= 26; + i--; + } + } + if (carry !== 0) { + out = carry.toString(16) + out; + } + while (out.length % padding !== 0) { + out = '0' + out; + } + if (this.negative !== 0) { + out = '-' + out; + } + return out; + } + + if (base === (base | 0) && base >= 2 && base <= 36) { + // var groupSize = Math.floor(BN.wordSize * Math.LN2 / Math.log(base)); + var groupSize = groupSizes[base]; + // var groupBase = Math.pow(base, groupSize); + var groupBase = groupBases[base]; + out = ''; + var c = this.clone(); + c.negative = 0; + while (!c.isZero()) { + var r = c.modn(groupBase).toString(base); + c = c.idivn(groupBase); + + if (!c.isZero()) { + out = zeros[groupSize - r.length] + r + out; + } else { + out = r + out; + } + } + if (this.isZero()) { + out = '0' + out; + } + while (out.length % padding !== 0) { + out = '0' + out; + } + if (this.negative !== 0) { + out = '-' + out; + } + return out; + } + + assert(false, 'Base should be between 2 and 36'); + }; + + BN.prototype.toNumber = function toNumber () { + var ret = this.words[0]; + if (this.length === 2) { + ret += this.words[1] * 0x4000000; + } else if (this.length === 3 && this.words[2] === 0x01) { + // NOTE: at this stage it is known that the top bit is set + ret += 0x10000000000000 + (this.words[1] * 0x4000000); + } else if (this.length > 2) { + assert(false, 'Number can only safely store up to 53 bits'); + } + return (this.negative !== 0) ? -ret : ret; + }; + + BN.prototype.toJSON = function toJSON () { + return this.toString(16); + }; + + BN.prototype.toBuffer = function toBuffer (endian, length) { + assert(typeof Buffer !== 'undefined'); + return this.toArrayLike(Buffer, endian, length); + }; + + BN.prototype.toArray = function toArray (endian, length) { + return this.toArrayLike(Array, endian, length); + }; + + BN.prototype.toArrayLike = function toArrayLike (ArrayType, endian, length) { + var byteLength = this.byteLength(); + var reqLength = length || Math.max(1, byteLength); + assert(byteLength <= reqLength, 'byte array longer than desired length'); + assert(reqLength > 0, 'Requested array length <= 0'); + + this.strip(); + var littleEndian = endian === 'le'; + var res = new ArrayType(reqLength); + + var b, i; + var q = this.clone(); + if (!littleEndian) { + // Assume big-endian + for (i = 0; i < reqLength - byteLength; i++) { + res[i] = 0; + } + + for (i = 0; !q.isZero(); i++) { + b = q.andln(0xff); + q.iushrn(8); + + res[reqLength - i - 1] = b; + } + } else { + for (i = 0; !q.isZero(); i++) { + b = q.andln(0xff); + q.iushrn(8); + + res[i] = b; + } + + for (; i < reqLength; i++) { + res[i] = 0; + } + } + + return res; + }; + + if (Math.clz32) { + BN.prototype._countBits = function _countBits (w) { + return 32 - Math.clz32(w); + }; + } else { + BN.prototype._countBits = function _countBits (w) { + var t = w; + var r = 0; + if (t >= 0x1000) { + r += 13; + t >>>= 13; + } + if (t >= 0x40) { + r += 7; + t >>>= 7; + } + if (t >= 0x8) { + r += 4; + t >>>= 4; + } + if (t >= 0x02) { + r += 2; + t >>>= 2; + } + return r + t; + }; + } + + BN.prototype._zeroBits = function _zeroBits (w) { + // Short-cut + if (w === 0) return 26; + + var t = w; + var r = 0; + if ((t & 0x1fff) === 0) { + r += 13; + t >>>= 13; + } + if ((t & 0x7f) === 0) { + r += 7; + t >>>= 7; + } + if ((t & 0xf) === 0) { + r += 4; + t >>>= 4; + } + if ((t & 0x3) === 0) { + r += 2; + t >>>= 2; + } + if ((t & 0x1) === 0) { + r++; + } + return r; + }; + + // Return number of used bits in a BN + BN.prototype.bitLength = function bitLength () { + var w = this.words[this.length - 1]; + var hi = this._countBits(w); + return (this.length - 1) * 26 + hi; + }; + + function toBitArray (num) { + var w = new Array(num.bitLength()); + + for (var bit = 0; bit < w.length; bit++) { + var off = (bit / 26) | 0; + var wbit = bit % 26; + + w[bit] = (num.words[off] & (1 << wbit)) >>> wbit; + } + + return w; + } + + // Number of trailing zero bits + BN.prototype.zeroBits = function zeroBits () { + if (this.isZero()) return 0; + + var r = 0; + for (var i = 0; i < this.length; i++) { + var b = this._zeroBits(this.words[i]); + r += b; + if (b !== 26) break; + } + return r; + }; + + BN.prototype.byteLength = function byteLength () { + return Math.ceil(this.bitLength() / 8); + }; + + BN.prototype.toTwos = function toTwos (width) { + if (this.negative !== 0) { + return this.abs().inotn(width).iaddn(1); + } + return this.clone(); + }; + + BN.prototype.fromTwos = function fromTwos (width) { + if (this.testn(width - 1)) { + return this.notn(width).iaddn(1).ineg(); + } + return this.clone(); + }; + + BN.prototype.isNeg = function isNeg () { + return this.negative !== 0; + }; + + // Return negative clone of `this` + BN.prototype.neg = function neg () { + return this.clone().ineg(); + }; + + BN.prototype.ineg = function ineg () { + if (!this.isZero()) { + this.negative ^= 1; + } + + return this; + }; + + // Or `num` with `this` in-place + BN.prototype.iuor = function iuor (num) { + while (this.length < num.length) { + this.words[this.length++] = 0; + } + + for (var i = 0; i < num.length; i++) { + this.words[i] = this.words[i] | num.words[i]; + } + + return this.strip(); + }; + + BN.prototype.ior = function ior (num) { + assert((this.negative | num.negative) === 0); + return this.iuor(num); + }; + + // Or `num` with `this` + BN.prototype.or = function or (num) { + if (this.length > num.length) return this.clone().ior(num); + return num.clone().ior(this); + }; + + BN.prototype.uor = function uor (num) { + if (this.length > num.length) return this.clone().iuor(num); + return num.clone().iuor(this); + }; + + // And `num` with `this` in-place + BN.prototype.iuand = function iuand (num) { + // b = min-length(num, this) + var b; + if (this.length > num.length) { + b = num; + } else { + b = this; + } + + for (var i = 0; i < b.length; i++) { + this.words[i] = this.words[i] & num.words[i]; + } + + this.length = b.length; + + return this.strip(); + }; + + BN.prototype.iand = function iand (num) { + assert((this.negative | num.negative) === 0); + return this.iuand(num); + }; + + // And `num` with `this` + BN.prototype.and = function and (num) { + if (this.length > num.length) return this.clone().iand(num); + return num.clone().iand(this); + }; + + BN.prototype.uand = function uand (num) { + if (this.length > num.length) return this.clone().iuand(num); + return num.clone().iuand(this); + }; + + // Xor `num` with `this` in-place + BN.prototype.iuxor = function iuxor (num) { + // a.length > b.length + var a; + var b; + if (this.length > num.length) { + a = this; + b = num; + } else { + a = num; + b = this; + } + + for (var i = 0; i < b.length; i++) { + this.words[i] = a.words[i] ^ b.words[i]; + } + + if (this !== a) { + for (; i < a.length; i++) { + this.words[i] = a.words[i]; + } + } + + this.length = a.length; + + return this.strip(); + }; + + BN.prototype.ixor = function ixor (num) { + assert((this.negative | num.negative) === 0); + return this.iuxor(num); + }; + + // Xor `num` with `this` + BN.prototype.xor = function xor (num) { + if (this.length > num.length) return this.clone().ixor(num); + return num.clone().ixor(this); + }; + + BN.prototype.uxor = function uxor (num) { + if (this.length > num.length) return this.clone().iuxor(num); + return num.clone().iuxor(this); + }; + + // Not ``this`` with ``width`` bitwidth + BN.prototype.inotn = function inotn (width) { + assert(typeof width === 'number' && width >= 0); + + var bytesNeeded = Math.ceil(width / 26) | 0; + var bitsLeft = width % 26; + + // Extend the buffer with leading zeroes + this._expand(bytesNeeded); + + if (bitsLeft > 0) { + bytesNeeded--; + } + + // Handle complete words + for (var i = 0; i < bytesNeeded; i++) { + this.words[i] = ~this.words[i] & 0x3ffffff; + } + + // Handle the residue + if (bitsLeft > 0) { + this.words[i] = ~this.words[i] & (0x3ffffff >> (26 - bitsLeft)); + } + + // And remove leading zeroes + return this.strip(); + }; + + BN.prototype.notn = function notn (width) { + return this.clone().inotn(width); + }; + + // Set `bit` of `this` + BN.prototype.setn = function setn (bit, val) { + assert(typeof bit === 'number' && bit >= 0); + + var off = (bit / 26) | 0; + var wbit = bit % 26; + + this._expand(off + 1); + + if (val) { + this.words[off] = this.words[off] | (1 << wbit); + } else { + this.words[off] = this.words[off] & ~(1 << wbit); + } + + return this.strip(); + }; + + // Add `num` to `this` in-place + BN.prototype.iadd = function iadd (num) { + var r; + + // negative + positive + if (this.negative !== 0 && num.negative === 0) { + this.negative = 0; + r = this.isub(num); + this.negative ^= 1; + return this._normSign(); + + // positive + negative + } else if (this.negative === 0 && num.negative !== 0) { + num.negative = 0; + r = this.isub(num); + num.negative = 1; + return r._normSign(); + } + + // a.length > b.length + var a, b; + if (this.length > num.length) { + a = this; + b = num; + } else { + a = num; + b = this; + } + + var carry = 0; + for (var i = 0; i < b.length; i++) { + r = (a.words[i] | 0) + (b.words[i] | 0) + carry; + this.words[i] = r & 0x3ffffff; + carry = r >>> 26; + } + for (; carry !== 0 && i < a.length; i++) { + r = (a.words[i] | 0) + carry; + this.words[i] = r & 0x3ffffff; + carry = r >>> 26; + } + + this.length = a.length; + if (carry !== 0) { + this.words[this.length] = carry; + this.length++; + // Copy the rest of the words + } else if (a !== this) { + for (; i < a.length; i++) { + this.words[i] = a.words[i]; + } + } + + return this; + }; + + // Add `num` to `this` + BN.prototype.add = function add (num) { + var res; + if (num.negative !== 0 && this.negative === 0) { + num.negative = 0; + res = this.sub(num); + num.negative ^= 1; + return res; + } else if (num.negative === 0 && this.negative !== 0) { + this.negative = 0; + res = num.sub(this); + this.negative = 1; + return res; + } + + if (this.length > num.length) return this.clone().iadd(num); + + return num.clone().iadd(this); + }; + + // Subtract `num` from `this` in-place + BN.prototype.isub = function isub (num) { + // this - (-num) = this + num + if (num.negative !== 0) { + num.negative = 0; + var r = this.iadd(num); + num.negative = 1; + return r._normSign(); + + // -this - num = -(this + num) + } else if (this.negative !== 0) { + this.negative = 0; + this.iadd(num); + this.negative = 1; + return this._normSign(); + } + + // At this point both numbers are positive + var cmp = this.cmp(num); + + // Optimization - zeroify + if (cmp === 0) { + this.negative = 0; + this.length = 1; + this.words[0] = 0; + return this; + } + + // a > b + var a, b; + if (cmp > 0) { + a = this; + b = num; + } else { + a = num; + b = this; + } + + var carry = 0; + for (var i = 0; i < b.length; i++) { + r = (a.words[i] | 0) - (b.words[i] | 0) + carry; + carry = r >> 26; + this.words[i] = r & 0x3ffffff; + } + for (; carry !== 0 && i < a.length; i++) { + r = (a.words[i] | 0) + carry; + carry = r >> 26; + this.words[i] = r & 0x3ffffff; + } + + // Copy rest of the words + if (carry === 0 && i < a.length && a !== this) { + for (; i < a.length; i++) { + this.words[i] = a.words[i]; + } + } + + this.length = Math.max(this.length, i); + + if (a !== this) { + this.negative = 1; + } + + return this.strip(); + }; + + // Subtract `num` from `this` + BN.prototype.sub = function sub (num) { + return this.clone().isub(num); + }; + + function smallMulTo (self, num, out) { + out.negative = num.negative ^ self.negative; + var len = (self.length + num.length) | 0; + out.length = len; + len = (len - 1) | 0; + + // Peel one iteration (compiler can't do it, because of code complexity) + var a = self.words[0] | 0; + var b = num.words[0] | 0; + var r = a * b; + + var lo = r & 0x3ffffff; + var carry = (r / 0x4000000) | 0; + out.words[0] = lo; + + for (var k = 1; k < len; k++) { + // Sum all words with the same `i + j = k` and accumulate `ncarry`, + // note that ncarry could be >= 0x3ffffff + var ncarry = carry >>> 26; + var rword = carry & 0x3ffffff; + var maxJ = Math.min(k, num.length - 1); + for (var j = Math.max(0, k - self.length + 1); j <= maxJ; j++) { + var i = (k - j) | 0; + a = self.words[i] | 0; + b = num.words[j] | 0; + r = a * b + rword; + ncarry += (r / 0x4000000) | 0; + rword = r & 0x3ffffff; + } + out.words[k] = rword | 0; + carry = ncarry | 0; + } + if (carry !== 0) { + out.words[k] = carry | 0; + } else { + out.length--; + } + + return out.strip(); + } + + // TODO(indutny): it may be reasonable to omit it for users who don't need + // to work with 256-bit numbers, otherwise it gives 20% improvement for 256-bit + // multiplication (like elliptic secp256k1). + var comb10MulTo = function comb10MulTo (self, num, out) { + var a = self.words; + var b = num.words; + var o = out.words; + var c = 0; + var lo; + var mid; + var hi; + var a0 = a[0] | 0; + var al0 = a0 & 0x1fff; + var ah0 = a0 >>> 13; + var a1 = a[1] | 0; + var al1 = a1 & 0x1fff; + var ah1 = a1 >>> 13; + var a2 = a[2] | 0; + var al2 = a2 & 0x1fff; + var ah2 = a2 >>> 13; + var a3 = a[3] | 0; + var al3 = a3 & 0x1fff; + var ah3 = a3 >>> 13; + var a4 = a[4] | 0; + var al4 = a4 & 0x1fff; + var ah4 = a4 >>> 13; + var a5 = a[5] | 0; + var al5 = a5 & 0x1fff; + var ah5 = a5 >>> 13; + var a6 = a[6] | 0; + var al6 = a6 & 0x1fff; + var ah6 = a6 >>> 13; + var a7 = a[7] | 0; + var al7 = a7 & 0x1fff; + var ah7 = a7 >>> 13; + var a8 = a[8] | 0; + var al8 = a8 & 0x1fff; + var ah8 = a8 >>> 13; + var a9 = a[9] | 0; + var al9 = a9 & 0x1fff; + var ah9 = a9 >>> 13; + var b0 = b[0] | 0; + var bl0 = b0 & 0x1fff; + var bh0 = b0 >>> 13; + var b1 = b[1] | 0; + var bl1 = b1 & 0x1fff; + var bh1 = b1 >>> 13; + var b2 = b[2] | 0; + var bl2 = b2 & 0x1fff; + var bh2 = b2 >>> 13; + var b3 = b[3] | 0; + var bl3 = b3 & 0x1fff; + var bh3 = b3 >>> 13; + var b4 = b[4] | 0; + var bl4 = b4 & 0x1fff; + var bh4 = b4 >>> 13; + var b5 = b[5] | 0; + var bl5 = b5 & 0x1fff; + var bh5 = b5 >>> 13; + var b6 = b[6] | 0; + var bl6 = b6 & 0x1fff; + var bh6 = b6 >>> 13; + var b7 = b[7] | 0; + var bl7 = b7 & 0x1fff; + var bh7 = b7 >>> 13; + var b8 = b[8] | 0; + var bl8 = b8 & 0x1fff; + var bh8 = b8 >>> 13; + var b9 = b[9] | 0; + var bl9 = b9 & 0x1fff; + var bh9 = b9 >>> 13; + + out.negative = self.negative ^ num.negative; + out.length = 19; + /* k = 0 */ + lo = Math.imul(al0, bl0); + mid = Math.imul(al0, bh0); + mid = (mid + Math.imul(ah0, bl0)) | 0; + hi = Math.imul(ah0, bh0); + var w0 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w0 >>> 26)) | 0; + w0 &= 0x3ffffff; + /* k = 1 */ + lo = Math.imul(al1, bl0); + mid = Math.imul(al1, bh0); + mid = (mid + Math.imul(ah1, bl0)) | 0; + hi = Math.imul(ah1, bh0); + lo = (lo + Math.imul(al0, bl1)) | 0; + mid = (mid + Math.imul(al0, bh1)) | 0; + mid = (mid + Math.imul(ah0, bl1)) | 0; + hi = (hi + Math.imul(ah0, bh1)) | 0; + var w1 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w1 >>> 26)) | 0; + w1 &= 0x3ffffff; + /* k = 2 */ + lo = Math.imul(al2, bl0); + mid = Math.imul(al2, bh0); + mid = (mid + Math.imul(ah2, bl0)) | 0; + hi = Math.imul(ah2, bh0); + lo = (lo + Math.imul(al1, bl1)) | 0; + mid = (mid + Math.imul(al1, bh1)) | 0; + mid = (mid + Math.imul(ah1, bl1)) | 0; + hi = (hi + Math.imul(ah1, bh1)) | 0; + lo = (lo + Math.imul(al0, bl2)) | 0; + mid = (mid + Math.imul(al0, bh2)) | 0; + mid = (mid + Math.imul(ah0, bl2)) | 0; + hi = (hi + Math.imul(ah0, bh2)) | 0; + var w2 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w2 >>> 26)) | 0; + w2 &= 0x3ffffff; + /* k = 3 */ + lo = Math.imul(al3, bl0); + mid = Math.imul(al3, bh0); + mid = (mid + Math.imul(ah3, bl0)) | 0; + hi = Math.imul(ah3, bh0); + lo = (lo + Math.imul(al2, bl1)) | 0; + mid = (mid + Math.imul(al2, bh1)) | 0; + mid = (mid + Math.imul(ah2, bl1)) | 0; + hi = (hi + Math.imul(ah2, bh1)) | 0; + lo = (lo + Math.imul(al1, bl2)) | 0; + mid = (mid + Math.imul(al1, bh2)) | 0; + mid = (mid + Math.imul(ah1, bl2)) | 0; + hi = (hi + Math.imul(ah1, bh2)) | 0; + lo = (lo + Math.imul(al0, bl3)) | 0; + mid = (mid + Math.imul(al0, bh3)) | 0; + mid = (mid + Math.imul(ah0, bl3)) | 0; + hi = (hi + Math.imul(ah0, bh3)) | 0; + var w3 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w3 >>> 26)) | 0; + w3 &= 0x3ffffff; + /* k = 4 */ + lo = Math.imul(al4, bl0); + mid = Math.imul(al4, bh0); + mid = (mid + Math.imul(ah4, bl0)) | 0; + hi = Math.imul(ah4, bh0); + lo = (lo + Math.imul(al3, bl1)) | 0; + mid = (mid + Math.imul(al3, bh1)) | 0; + mid = (mid + Math.imul(ah3, bl1)) | 0; + hi = (hi + Math.imul(ah3, bh1)) | 0; + lo = (lo + Math.imul(al2, bl2)) | 0; + mid = (mid + Math.imul(al2, bh2)) | 0; + mid = (mid + Math.imul(ah2, bl2)) | 0; + hi = (hi + Math.imul(ah2, bh2)) | 0; + lo = (lo + Math.imul(al1, bl3)) | 0; + mid = (mid + Math.imul(al1, bh3)) | 0; + mid = (mid + Math.imul(ah1, bl3)) | 0; + hi = (hi + Math.imul(ah1, bh3)) | 0; + lo = (lo + Math.imul(al0, bl4)) | 0; + mid = (mid + Math.imul(al0, bh4)) | 0; + mid = (mid + Math.imul(ah0, bl4)) | 0; + hi = (hi + Math.imul(ah0, bh4)) | 0; + var w4 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w4 >>> 26)) | 0; + w4 &= 0x3ffffff; + /* k = 5 */ + lo = Math.imul(al5, bl0); + mid = Math.imul(al5, bh0); + mid = (mid + Math.imul(ah5, bl0)) | 0; + hi = Math.imul(ah5, bh0); + lo = (lo + Math.imul(al4, bl1)) | 0; + mid = (mid + Math.imul(al4, bh1)) | 0; + mid = (mid + Math.imul(ah4, bl1)) | 0; + hi = (hi + Math.imul(ah4, bh1)) | 0; + lo = (lo + Math.imul(al3, bl2)) | 0; + mid = (mid + Math.imul(al3, bh2)) | 0; + mid = (mid + Math.imul(ah3, bl2)) | 0; + hi = (hi + Math.imul(ah3, bh2)) | 0; + lo = (lo + Math.imul(al2, bl3)) | 0; + mid = (mid + Math.imul(al2, bh3)) | 0; + mid = (mid + Math.imul(ah2, bl3)) | 0; + hi = (hi + Math.imul(ah2, bh3)) | 0; + lo = (lo + Math.imul(al1, bl4)) | 0; + mid = (mid + Math.imul(al1, bh4)) | 0; + mid = (mid + Math.imul(ah1, bl4)) | 0; + hi = (hi + Math.imul(ah1, bh4)) | 0; + lo = (lo + Math.imul(al0, bl5)) | 0; + mid = (mid + Math.imul(al0, bh5)) | 0; + mid = (mid + Math.imul(ah0, bl5)) | 0; + hi = (hi + Math.imul(ah0, bh5)) | 0; + var w5 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w5 >>> 26)) | 0; + w5 &= 0x3ffffff; + /* k = 6 */ + lo = Math.imul(al6, bl0); + mid = Math.imul(al6, bh0); + mid = (mid + Math.imul(ah6, bl0)) | 0; + hi = Math.imul(ah6, bh0); + lo = (lo + Math.imul(al5, bl1)) | 0; + mid = (mid + Math.imul(al5, bh1)) | 0; + mid = (mid + Math.imul(ah5, bl1)) | 0; + hi = (hi + Math.imul(ah5, bh1)) | 0; + lo = (lo + Math.imul(al4, bl2)) | 0; + mid = (mid + Math.imul(al4, bh2)) | 0; + mid = (mid + Math.imul(ah4, bl2)) | 0; + hi = (hi + Math.imul(ah4, bh2)) | 0; + lo = (lo + Math.imul(al3, bl3)) | 0; + mid = (mid + Math.imul(al3, bh3)) | 0; + mid = (mid + Math.imul(ah3, bl3)) | 0; + hi = (hi + Math.imul(ah3, bh3)) | 0; + lo = (lo + Math.imul(al2, bl4)) | 0; + mid = (mid + Math.imul(al2, bh4)) | 0; + mid = (mid + Math.imul(ah2, bl4)) | 0; + hi = (hi + Math.imul(ah2, bh4)) | 0; + lo = (lo + Math.imul(al1, bl5)) | 0; + mid = (mid + Math.imul(al1, bh5)) | 0; + mid = (mid + Math.imul(ah1, bl5)) | 0; + hi = (hi + Math.imul(ah1, bh5)) | 0; + lo = (lo + Math.imul(al0, bl6)) | 0; + mid = (mid + Math.imul(al0, bh6)) | 0; + mid = (mid + Math.imul(ah0, bl6)) | 0; + hi = (hi + Math.imul(ah0, bh6)) | 0; + var w6 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w6 >>> 26)) | 0; + w6 &= 0x3ffffff; + /* k = 7 */ + lo = Math.imul(al7, bl0); + mid = Math.imul(al7, bh0); + mid = (mid + Math.imul(ah7, bl0)) | 0; + hi = Math.imul(ah7, bh0); + lo = (lo + Math.imul(al6, bl1)) | 0; + mid = (mid + Math.imul(al6, bh1)) | 0; + mid = (mid + Math.imul(ah6, bl1)) | 0; + hi = (hi + Math.imul(ah6, bh1)) | 0; + lo = (lo + Math.imul(al5, bl2)) | 0; + mid = (mid + Math.imul(al5, bh2)) | 0; + mid = (mid + Math.imul(ah5, bl2)) | 0; + hi = (hi + Math.imul(ah5, bh2)) | 0; + lo = (lo + Math.imul(al4, bl3)) | 0; + mid = (mid + Math.imul(al4, bh3)) | 0; + mid = (mid + Math.imul(ah4, bl3)) | 0; + hi = (hi + Math.imul(ah4, bh3)) | 0; + lo = (lo + Math.imul(al3, bl4)) | 0; + mid = (mid + Math.imul(al3, bh4)) | 0; + mid = (mid + Math.imul(ah3, bl4)) | 0; + hi = (hi + Math.imul(ah3, bh4)) | 0; + lo = (lo + Math.imul(al2, bl5)) | 0; + mid = (mid + Math.imul(al2, bh5)) | 0; + mid = (mid + Math.imul(ah2, bl5)) | 0; + hi = (hi + Math.imul(ah2, bh5)) | 0; + lo = (lo + Math.imul(al1, bl6)) | 0; + mid = (mid + Math.imul(al1, bh6)) | 0; + mid = (mid + Math.imul(ah1, bl6)) | 0; + hi = (hi + Math.imul(ah1, bh6)) | 0; + lo = (lo + Math.imul(al0, bl7)) | 0; + mid = (mid + Math.imul(al0, bh7)) | 0; + mid = (mid + Math.imul(ah0, bl7)) | 0; + hi = (hi + Math.imul(ah0, bh7)) | 0; + var w7 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w7 >>> 26)) | 0; + w7 &= 0x3ffffff; + /* k = 8 */ + lo = Math.imul(al8, bl0); + mid = Math.imul(al8, bh0); + mid = (mid + Math.imul(ah8, bl0)) | 0; + hi = Math.imul(ah8, bh0); + lo = (lo + Math.imul(al7, bl1)) | 0; + mid = (mid + Math.imul(al7, bh1)) | 0; + mid = (mid + Math.imul(ah7, bl1)) | 0; + hi = (hi + Math.imul(ah7, bh1)) | 0; + lo = (lo + Math.imul(al6, bl2)) | 0; + mid = (mid + Math.imul(al6, bh2)) | 0; + mid = (mid + Math.imul(ah6, bl2)) | 0; + hi = (hi + Math.imul(ah6, bh2)) | 0; + lo = (lo + Math.imul(al5, bl3)) | 0; + mid = (mid + Math.imul(al5, bh3)) | 0; + mid = (mid + Math.imul(ah5, bl3)) | 0; + hi = (hi + Math.imul(ah5, bh3)) | 0; + lo = (lo + Math.imul(al4, bl4)) | 0; + mid = (mid + Math.imul(al4, bh4)) | 0; + mid = (mid + Math.imul(ah4, bl4)) | 0; + hi = (hi + Math.imul(ah4, bh4)) | 0; + lo = (lo + Math.imul(al3, bl5)) | 0; + mid = (mid + Math.imul(al3, bh5)) | 0; + mid = (mid + Math.imul(ah3, bl5)) | 0; + hi = (hi + Math.imul(ah3, bh5)) | 0; + lo = (lo + Math.imul(al2, bl6)) | 0; + mid = (mid + Math.imul(al2, bh6)) | 0; + mid = (mid + Math.imul(ah2, bl6)) | 0; + hi = (hi + Math.imul(ah2, bh6)) | 0; + lo = (lo + Math.imul(al1, bl7)) | 0; + mid = (mid + Math.imul(al1, bh7)) | 0; + mid = (mid + Math.imul(ah1, bl7)) | 0; + hi = (hi + Math.imul(ah1, bh7)) | 0; + lo = (lo + Math.imul(al0, bl8)) | 0; + mid = (mid + Math.imul(al0, bh8)) | 0; + mid = (mid + Math.imul(ah0, bl8)) | 0; + hi = (hi + Math.imul(ah0, bh8)) | 0; + var w8 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w8 >>> 26)) | 0; + w8 &= 0x3ffffff; + /* k = 9 */ + lo = Math.imul(al9, bl0); + mid = Math.imul(al9, bh0); + mid = (mid + Math.imul(ah9, bl0)) | 0; + hi = Math.imul(ah9, bh0); + lo = (lo + Math.imul(al8, bl1)) | 0; + mid = (mid + Math.imul(al8, bh1)) | 0; + mid = (mid + Math.imul(ah8, bl1)) | 0; + hi = (hi + Math.imul(ah8, bh1)) | 0; + lo = (lo + Math.imul(al7, bl2)) | 0; + mid = (mid + Math.imul(al7, bh2)) | 0; + mid = (mid + Math.imul(ah7, bl2)) | 0; + hi = (hi + Math.imul(ah7, bh2)) | 0; + lo = (lo + Math.imul(al6, bl3)) | 0; + mid = (mid + Math.imul(al6, bh3)) | 0; + mid = (mid + Math.imul(ah6, bl3)) | 0; + hi = (hi + Math.imul(ah6, bh3)) | 0; + lo = (lo + Math.imul(al5, bl4)) | 0; + mid = (mid + Math.imul(al5, bh4)) | 0; + mid = (mid + Math.imul(ah5, bl4)) | 0; + hi = (hi + Math.imul(ah5, bh4)) | 0; + lo = (lo + Math.imul(al4, bl5)) | 0; + mid = (mid + Math.imul(al4, bh5)) | 0; + mid = (mid + Math.imul(ah4, bl5)) | 0; + hi = (hi + Math.imul(ah4, bh5)) | 0; + lo = (lo + Math.imul(al3, bl6)) | 0; + mid = (mid + Math.imul(al3, bh6)) | 0; + mid = (mid + Math.imul(ah3, bl6)) | 0; + hi = (hi + Math.imul(ah3, bh6)) | 0; + lo = (lo + Math.imul(al2, bl7)) | 0; + mid = (mid + Math.imul(al2, bh7)) | 0; + mid = (mid + Math.imul(ah2, bl7)) | 0; + hi = (hi + Math.imul(ah2, bh7)) | 0; + lo = (lo + Math.imul(al1, bl8)) | 0; + mid = (mid + Math.imul(al1, bh8)) | 0; + mid = (mid + Math.imul(ah1, bl8)) | 0; + hi = (hi + Math.imul(ah1, bh8)) | 0; + lo = (lo + Math.imul(al0, bl9)) | 0; + mid = (mid + Math.imul(al0, bh9)) | 0; + mid = (mid + Math.imul(ah0, bl9)) | 0; + hi = (hi + Math.imul(ah0, bh9)) | 0; + var w9 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w9 >>> 26)) | 0; + w9 &= 0x3ffffff; + /* k = 10 */ + lo = Math.imul(al9, bl1); + mid = Math.imul(al9, bh1); + mid = (mid + Math.imul(ah9, bl1)) | 0; + hi = Math.imul(ah9, bh1); + lo = (lo + Math.imul(al8, bl2)) | 0; + mid = (mid + Math.imul(al8, bh2)) | 0; + mid = (mid + Math.imul(ah8, bl2)) | 0; + hi = (hi + Math.imul(ah8, bh2)) | 0; + lo = (lo + Math.imul(al7, bl3)) | 0; + mid = (mid + Math.imul(al7, bh3)) | 0; + mid = (mid + Math.imul(ah7, bl3)) | 0; + hi = (hi + Math.imul(ah7, bh3)) | 0; + lo = (lo + Math.imul(al6, bl4)) | 0; + mid = (mid + Math.imul(al6, bh4)) | 0; + mid = (mid + Math.imul(ah6, bl4)) | 0; + hi = (hi + Math.imul(ah6, bh4)) | 0; + lo = (lo + Math.imul(al5, bl5)) | 0; + mid = (mid + Math.imul(al5, bh5)) | 0; + mid = (mid + Math.imul(ah5, bl5)) | 0; + hi = (hi + Math.imul(ah5, bh5)) | 0; + lo = (lo + Math.imul(al4, bl6)) | 0; + mid = (mid + Math.imul(al4, bh6)) | 0; + mid = (mid + Math.imul(ah4, bl6)) | 0; + hi = (hi + Math.imul(ah4, bh6)) | 0; + lo = (lo + Math.imul(al3, bl7)) | 0; + mid = (mid + Math.imul(al3, bh7)) | 0; + mid = (mid + Math.imul(ah3, bl7)) | 0; + hi = (hi + Math.imul(ah3, bh7)) | 0; + lo = (lo + Math.imul(al2, bl8)) | 0; + mid = (mid + Math.imul(al2, bh8)) | 0; + mid = (mid + Math.imul(ah2, bl8)) | 0; + hi = (hi + Math.imul(ah2, bh8)) | 0; + lo = (lo + Math.imul(al1, bl9)) | 0; + mid = (mid + Math.imul(al1, bh9)) | 0; + mid = (mid + Math.imul(ah1, bl9)) | 0; + hi = (hi + Math.imul(ah1, bh9)) | 0; + var w10 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w10 >>> 26)) | 0; + w10 &= 0x3ffffff; + /* k = 11 */ + lo = Math.imul(al9, bl2); + mid = Math.imul(al9, bh2); + mid = (mid + Math.imul(ah9, bl2)) | 0; + hi = Math.imul(ah9, bh2); + lo = (lo + Math.imul(al8, bl3)) | 0; + mid = (mid + Math.imul(al8, bh3)) | 0; + mid = (mid + Math.imul(ah8, bl3)) | 0; + hi = (hi + Math.imul(ah8, bh3)) | 0; + lo = (lo + Math.imul(al7, bl4)) | 0; + mid = (mid + Math.imul(al7, bh4)) | 0; + mid = (mid + Math.imul(ah7, bl4)) | 0; + hi = (hi + Math.imul(ah7, bh4)) | 0; + lo = (lo + Math.imul(al6, bl5)) | 0; + mid = (mid + Math.imul(al6, bh5)) | 0; + mid = (mid + Math.imul(ah6, bl5)) | 0; + hi = (hi + Math.imul(ah6, bh5)) | 0; + lo = (lo + Math.imul(al5, bl6)) | 0; + mid = (mid + Math.imul(al5, bh6)) | 0; + mid = (mid + Math.imul(ah5, bl6)) | 0; + hi = (hi + Math.imul(ah5, bh6)) | 0; + lo = (lo + Math.imul(al4, bl7)) | 0; + mid = (mid + Math.imul(al4, bh7)) | 0; + mid = (mid + Math.imul(ah4, bl7)) | 0; + hi = (hi + Math.imul(ah4, bh7)) | 0; + lo = (lo + Math.imul(al3, bl8)) | 0; + mid = (mid + Math.imul(al3, bh8)) | 0; + mid = (mid + Math.imul(ah3, bl8)) | 0; + hi = (hi + Math.imul(ah3, bh8)) | 0; + lo = (lo + Math.imul(al2, bl9)) | 0; + mid = (mid + Math.imul(al2, bh9)) | 0; + mid = (mid + Math.imul(ah2, bl9)) | 0; + hi = (hi + Math.imul(ah2, bh9)) | 0; + var w11 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w11 >>> 26)) | 0; + w11 &= 0x3ffffff; + /* k = 12 */ + lo = Math.imul(al9, bl3); + mid = Math.imul(al9, bh3); + mid = (mid + Math.imul(ah9, bl3)) | 0; + hi = Math.imul(ah9, bh3); + lo = (lo + Math.imul(al8, bl4)) | 0; + mid = (mid + Math.imul(al8, bh4)) | 0; + mid = (mid + Math.imul(ah8, bl4)) | 0; + hi = (hi + Math.imul(ah8, bh4)) | 0; + lo = (lo + Math.imul(al7, bl5)) | 0; + mid = (mid + Math.imul(al7, bh5)) | 0; + mid = (mid + Math.imul(ah7, bl5)) | 0; + hi = (hi + Math.imul(ah7, bh5)) | 0; + lo = (lo + Math.imul(al6, bl6)) | 0; + mid = (mid + Math.imul(al6, bh6)) | 0; + mid = (mid + Math.imul(ah6, bl6)) | 0; + hi = (hi + Math.imul(ah6, bh6)) | 0; + lo = (lo + Math.imul(al5, bl7)) | 0; + mid = (mid + Math.imul(al5, bh7)) | 0; + mid = (mid + Math.imul(ah5, bl7)) | 0; + hi = (hi + Math.imul(ah5, bh7)) | 0; + lo = (lo + Math.imul(al4, bl8)) | 0; + mid = (mid + Math.imul(al4, bh8)) | 0; + mid = (mid + Math.imul(ah4, bl8)) | 0; + hi = (hi + Math.imul(ah4, bh8)) | 0; + lo = (lo + Math.imul(al3, bl9)) | 0; + mid = (mid + Math.imul(al3, bh9)) | 0; + mid = (mid + Math.imul(ah3, bl9)) | 0; + hi = (hi + Math.imul(ah3, bh9)) | 0; + var w12 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w12 >>> 26)) | 0; + w12 &= 0x3ffffff; + /* k = 13 */ + lo = Math.imul(al9, bl4); + mid = Math.imul(al9, bh4); + mid = (mid + Math.imul(ah9, bl4)) | 0; + hi = Math.imul(ah9, bh4); + lo = (lo + Math.imul(al8, bl5)) | 0; + mid = (mid + Math.imul(al8, bh5)) | 0; + mid = (mid + Math.imul(ah8, bl5)) | 0; + hi = (hi + Math.imul(ah8, bh5)) | 0; + lo = (lo + Math.imul(al7, bl6)) | 0; + mid = (mid + Math.imul(al7, bh6)) | 0; + mid = (mid + Math.imul(ah7, bl6)) | 0; + hi = (hi + Math.imul(ah7, bh6)) | 0; + lo = (lo + Math.imul(al6, bl7)) | 0; + mid = (mid + Math.imul(al6, bh7)) | 0; + mid = (mid + Math.imul(ah6, bl7)) | 0; + hi = (hi + Math.imul(ah6, bh7)) | 0; + lo = (lo + Math.imul(al5, bl8)) | 0; + mid = (mid + Math.imul(al5, bh8)) | 0; + mid = (mid + Math.imul(ah5, bl8)) | 0; + hi = (hi + Math.imul(ah5, bh8)) | 0; + lo = (lo + Math.imul(al4, bl9)) | 0; + mid = (mid + Math.imul(al4, bh9)) | 0; + mid = (mid + Math.imul(ah4, bl9)) | 0; + hi = (hi + Math.imul(ah4, bh9)) | 0; + var w13 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w13 >>> 26)) | 0; + w13 &= 0x3ffffff; + /* k = 14 */ + lo = Math.imul(al9, bl5); + mid = Math.imul(al9, bh5); + mid = (mid + Math.imul(ah9, bl5)) | 0; + hi = Math.imul(ah9, bh5); + lo = (lo + Math.imul(al8, bl6)) | 0; + mid = (mid + Math.imul(al8, bh6)) | 0; + mid = (mid + Math.imul(ah8, bl6)) | 0; + hi = (hi + Math.imul(ah8, bh6)) | 0; + lo = (lo + Math.imul(al7, bl7)) | 0; + mid = (mid + Math.imul(al7, bh7)) | 0; + mid = (mid + Math.imul(ah7, bl7)) | 0; + hi = (hi + Math.imul(ah7, bh7)) | 0; + lo = (lo + Math.imul(al6, bl8)) | 0; + mid = (mid + Math.imul(al6, bh8)) | 0; + mid = (mid + Math.imul(ah6, bl8)) | 0; + hi = (hi + Math.imul(ah6, bh8)) | 0; + lo = (lo + Math.imul(al5, bl9)) | 0; + mid = (mid + Math.imul(al5, bh9)) | 0; + mid = (mid + Math.imul(ah5, bl9)) | 0; + hi = (hi + Math.imul(ah5, bh9)) | 0; + var w14 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w14 >>> 26)) | 0; + w14 &= 0x3ffffff; + /* k = 15 */ + lo = Math.imul(al9, bl6); + mid = Math.imul(al9, bh6); + mid = (mid + Math.imul(ah9, bl6)) | 0; + hi = Math.imul(ah9, bh6); + lo = (lo + Math.imul(al8, bl7)) | 0; + mid = (mid + Math.imul(al8, bh7)) | 0; + mid = (mid + Math.imul(ah8, bl7)) | 0; + hi = (hi + Math.imul(ah8, bh7)) | 0; + lo = (lo + Math.imul(al7, bl8)) | 0; + mid = (mid + Math.imul(al7, bh8)) | 0; + mid = (mid + Math.imul(ah7, bl8)) | 0; + hi = (hi + Math.imul(ah7, bh8)) | 0; + lo = (lo + Math.imul(al6, bl9)) | 0; + mid = (mid + Math.imul(al6, bh9)) | 0; + mid = (mid + Math.imul(ah6, bl9)) | 0; + hi = (hi + Math.imul(ah6, bh9)) | 0; + var w15 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w15 >>> 26)) | 0; + w15 &= 0x3ffffff; + /* k = 16 */ + lo = Math.imul(al9, bl7); + mid = Math.imul(al9, bh7); + mid = (mid + Math.imul(ah9, bl7)) | 0; + hi = Math.imul(ah9, bh7); + lo = (lo + Math.imul(al8, bl8)) | 0; + mid = (mid + Math.imul(al8, bh8)) | 0; + mid = (mid + Math.imul(ah8, bl8)) | 0; + hi = (hi + Math.imul(ah8, bh8)) | 0; + lo = (lo + Math.imul(al7, bl9)) | 0; + mid = (mid + Math.imul(al7, bh9)) | 0; + mid = (mid + Math.imul(ah7, bl9)) | 0; + hi = (hi + Math.imul(ah7, bh9)) | 0; + var w16 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w16 >>> 26)) | 0; + w16 &= 0x3ffffff; + /* k = 17 */ + lo = Math.imul(al9, bl8); + mid = Math.imul(al9, bh8); + mid = (mid + Math.imul(ah9, bl8)) | 0; + hi = Math.imul(ah9, bh8); + lo = (lo + Math.imul(al8, bl9)) | 0; + mid = (mid + Math.imul(al8, bh9)) | 0; + mid = (mid + Math.imul(ah8, bl9)) | 0; + hi = (hi + Math.imul(ah8, bh9)) | 0; + var w17 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w17 >>> 26)) | 0; + w17 &= 0x3ffffff; + /* k = 18 */ + lo = Math.imul(al9, bl9); + mid = Math.imul(al9, bh9); + mid = (mid + Math.imul(ah9, bl9)) | 0; + hi = Math.imul(ah9, bh9); + var w18 = (((c + lo) | 0) + ((mid & 0x1fff) << 13)) | 0; + c = (((hi + (mid >>> 13)) | 0) + (w18 >>> 26)) | 0; + w18 &= 0x3ffffff; + o[0] = w0; + o[1] = w1; + o[2] = w2; + o[3] = w3; + o[4] = w4; + o[5] = w5; + o[6] = w6; + o[7] = w7; + o[8] = w8; + o[9] = w9; + o[10] = w10; + o[11] = w11; + o[12] = w12; + o[13] = w13; + o[14] = w14; + o[15] = w15; + o[16] = w16; + o[17] = w17; + o[18] = w18; + if (c !== 0) { + o[19] = c; + out.length++; + } + return out; + }; + + // Polyfill comb + if (!Math.imul) { + comb10MulTo = smallMulTo; + } + + function bigMulTo (self, num, out) { + out.negative = num.negative ^ self.negative; + out.length = self.length + num.length; + + var carry = 0; + var hncarry = 0; + for (var k = 0; k < out.length - 1; k++) { + // Sum all words with the same `i + j = k` and accumulate `ncarry`, + // note that ncarry could be >= 0x3ffffff + var ncarry = hncarry; + hncarry = 0; + var rword = carry & 0x3ffffff; + var maxJ = Math.min(k, num.length - 1); + for (var j = Math.max(0, k - self.length + 1); j <= maxJ; j++) { + var i = k - j; + var a = self.words[i] | 0; + var b = num.words[j] | 0; + var r = a * b; + + var lo = r & 0x3ffffff; + ncarry = (ncarry + ((r / 0x4000000) | 0)) | 0; + lo = (lo + rword) | 0; + rword = lo & 0x3ffffff; + ncarry = (ncarry + (lo >>> 26)) | 0; + + hncarry += ncarry >>> 26; + ncarry &= 0x3ffffff; + } + out.words[k] = rword; + carry = ncarry; + ncarry = hncarry; + } + if (carry !== 0) { + out.words[k] = carry; + } else { + out.length--; + } + + return out.strip(); + } + + function jumboMulTo (self, num, out) { + var fftm = new FFTM(); + return fftm.mulp(self, num, out); + } + + BN.prototype.mulTo = function mulTo (num, out) { + var res; + var len = this.length + num.length; + if (this.length === 10 && num.length === 10) { + res = comb10MulTo(this, num, out); + } else if (len < 63) { + res = smallMulTo(this, num, out); + } else if (len < 1024) { + res = bigMulTo(this, num, out); + } else { + res = jumboMulTo(this, num, out); + } + + return res; + }; + + // Cooley-Tukey algorithm for FFT + // slightly revisited to rely on looping instead of recursion + + function FFTM (x, y) { + this.x = x; + this.y = y; + } + + FFTM.prototype.makeRBT = function makeRBT (N) { + var t = new Array(N); + var l = BN.prototype._countBits(N) - 1; + for (var i = 0; i < N; i++) { + t[i] = this.revBin(i, l, N); + } + + return t; + }; + + // Returns binary-reversed representation of `x` + FFTM.prototype.revBin = function revBin (x, l, N) { + if (x === 0 || x === N - 1) return x; + + var rb = 0; + for (var i = 0; i < l; i++) { + rb |= (x & 1) << (l - i - 1); + x >>= 1; + } + + return rb; + }; + + // Performs "tweedling" phase, therefore 'emulating' + // behaviour of the recursive algorithm + FFTM.prototype.permute = function permute (rbt, rws, iws, rtws, itws, N) { + for (var i = 0; i < N; i++) { + rtws[i] = rws[rbt[i]]; + itws[i] = iws[rbt[i]]; + } + }; + + FFTM.prototype.transform = function transform (rws, iws, rtws, itws, N, rbt) { + this.permute(rbt, rws, iws, rtws, itws, N); + + for (var s = 1; s < N; s <<= 1) { + var l = s << 1; + + var rtwdf = Math.cos(2 * Math.PI / l); + var itwdf = Math.sin(2 * Math.PI / l); + + for (var p = 0; p < N; p += l) { + var rtwdf_ = rtwdf; + var itwdf_ = itwdf; + + for (var j = 0; j < s; j++) { + var re = rtws[p + j]; + var ie = itws[p + j]; + + var ro = rtws[p + j + s]; + var io = itws[p + j + s]; + + var rx = rtwdf_ * ro - itwdf_ * io; + + io = rtwdf_ * io + itwdf_ * ro; + ro = rx; + + rtws[p + j] = re + ro; + itws[p + j] = ie + io; + + rtws[p + j + s] = re - ro; + itws[p + j + s] = ie - io; + + /* jshint maxdepth : false */ + if (j !== l) { + rx = rtwdf * rtwdf_ - itwdf * itwdf_; + + itwdf_ = rtwdf * itwdf_ + itwdf * rtwdf_; + rtwdf_ = rx; + } + } + } + } + }; + + FFTM.prototype.guessLen13b = function guessLen13b (n, m) { + var N = Math.max(m, n) | 1; + var odd = N & 1; + var i = 0; + for (N = N / 2 | 0; N; N = N >>> 1) { + i++; + } + + return 1 << i + 1 + odd; + }; + + FFTM.prototype.conjugate = function conjugate (rws, iws, N) { + if (N <= 1) return; + + for (var i = 0; i < N / 2; i++) { + var t = rws[i]; + + rws[i] = rws[N - i - 1]; + rws[N - i - 1] = t; + + t = iws[i]; + + iws[i] = -iws[N - i - 1]; + iws[N - i - 1] = -t; + } + }; + + FFTM.prototype.normalize13b = function normalize13b (ws, N) { + var carry = 0; + for (var i = 0; i < N / 2; i++) { + var w = Math.round(ws[2 * i + 1] / N) * 0x2000 + + Math.round(ws[2 * i] / N) + + carry; + + ws[i] = w & 0x3ffffff; + + if (w < 0x4000000) { + carry = 0; + } else { + carry = w / 0x4000000 | 0; + } + } + + return ws; + }; + + FFTM.prototype.convert13b = function convert13b (ws, len, rws, N) { + var carry = 0; + for (var i = 0; i < len; i++) { + carry = carry + (ws[i] | 0); + + rws[2 * i] = carry & 0x1fff; carry = carry >>> 13; + rws[2 * i + 1] = carry & 0x1fff; carry = carry >>> 13; + } + + // Pad with zeroes + for (i = 2 * len; i < N; ++i) { + rws[i] = 0; + } + + assert(carry === 0); + assert((carry & ~0x1fff) === 0); + }; + + FFTM.prototype.stub = function stub (N) { + var ph = new Array(N); + for (var i = 0; i < N; i++) { + ph[i] = 0; + } + + return ph; + }; + + FFTM.prototype.mulp = function mulp (x, y, out) { + var N = 2 * this.guessLen13b(x.length, y.length); + + var rbt = this.makeRBT(N); + + var _ = this.stub(N); + + var rws = new Array(N); + var rwst = new Array(N); + var iwst = new Array(N); + + var nrws = new Array(N); + var nrwst = new Array(N); + var niwst = new Array(N); + + var rmws = out.words; + rmws.length = N; + + this.convert13b(x.words, x.length, rws, N); + this.convert13b(y.words, y.length, nrws, N); + + this.transform(rws, _, rwst, iwst, N, rbt); + this.transform(nrws, _, nrwst, niwst, N, rbt); + + for (var i = 0; i < N; i++) { + var rx = rwst[i] * nrwst[i] - iwst[i] * niwst[i]; + iwst[i] = rwst[i] * niwst[i] + iwst[i] * nrwst[i]; + rwst[i] = rx; + } + + this.conjugate(rwst, iwst, N); + this.transform(rwst, iwst, rmws, _, N, rbt); + this.conjugate(rmws, _, N); + this.normalize13b(rmws, N); + + out.negative = x.negative ^ y.negative; + out.length = x.length + y.length; + return out.strip(); + }; + + // Multiply `this` by `num` + BN.prototype.mul = function mul (num) { + var out = new BN(null); + out.words = new Array(this.length + num.length); + return this.mulTo(num, out); + }; + + // Multiply employing FFT + BN.prototype.mulf = function mulf (num) { + var out = new BN(null); + out.words = new Array(this.length + num.length); + return jumboMulTo(this, num, out); + }; + + // In-place Multiplication + BN.prototype.imul = function imul (num) { + return this.clone().mulTo(num, this); + }; + + BN.prototype.imuln = function imuln (num) { + assert(typeof num === 'number'); + assert(num < 0x4000000); + + // Carry + var carry = 0; + for (var i = 0; i < this.length; i++) { + var w = (this.words[i] | 0) * num; + var lo = (w & 0x3ffffff) + (carry & 0x3ffffff); + carry >>= 26; + carry += (w / 0x4000000) | 0; + // NOTE: lo is 27bit maximum + carry += lo >>> 26; + this.words[i] = lo & 0x3ffffff; + } + + if (carry !== 0) { + this.words[i] = carry; + this.length++; + } + + return this; + }; + + BN.prototype.muln = function muln (num) { + return this.clone().imuln(num); + }; + + // `this` * `this` + BN.prototype.sqr = function sqr () { + return this.mul(this); + }; + + // `this` * `this` in-place + BN.prototype.isqr = function isqr () { + return this.imul(this.clone()); + }; + + // Math.pow(`this`, `num`) + BN.prototype.pow = function pow (num) { + var w = toBitArray(num); + if (w.length === 0) return new BN(1); + + // Skip leading zeroes + var res = this; + for (var i = 0; i < w.length; i++, res = res.sqr()) { + if (w[i] !== 0) break; + } + + if (++i < w.length) { + for (var q = res.sqr(); i < w.length; i++, q = q.sqr()) { + if (w[i] === 0) continue; + + res = res.mul(q); + } + } + + return res; + }; + + // Shift-left in-place + BN.prototype.iushln = function iushln (bits) { + assert(typeof bits === 'number' && bits >= 0); + var r = bits % 26; + var s = (bits - r) / 26; + var carryMask = (0x3ffffff >>> (26 - r)) << (26 - r); + var i; + + if (r !== 0) { + var carry = 0; + + for (i = 0; i < this.length; i++) { + var newCarry = this.words[i] & carryMask; + var c = ((this.words[i] | 0) - newCarry) << r; + this.words[i] = c | carry; + carry = newCarry >>> (26 - r); + } + + if (carry) { + this.words[i] = carry; + this.length++; + } + } + + if (s !== 0) { + for (i = this.length - 1; i >= 0; i--) { + this.words[i + s] = this.words[i]; + } + + for (i = 0; i < s; i++) { + this.words[i] = 0; + } + + this.length += s; + } + + return this.strip(); + }; + + BN.prototype.ishln = function ishln (bits) { + // TODO(indutny): implement me + assert(this.negative === 0); + return this.iushln(bits); + }; + + // Shift-right in-place + // NOTE: `hint` is a lowest bit before trailing zeroes + // NOTE: if `extended` is present - it will be filled with destroyed bits + BN.prototype.iushrn = function iushrn (bits, hint, extended) { + assert(typeof bits === 'number' && bits >= 0); + var h; + if (hint) { + h = (hint - (hint % 26)) / 26; + } else { + h = 0; + } + + var r = bits % 26; + var s = Math.min((bits - r) / 26, this.length); + var mask = 0x3ffffff ^ ((0x3ffffff >>> r) << r); + var maskedWords = extended; + + h -= s; + h = Math.max(0, h); + + // Extended mode, copy masked part + if (maskedWords) { + for (var i = 0; i < s; i++) { + maskedWords.words[i] = this.words[i]; + } + maskedWords.length = s; + } + + if (s === 0) ; else if (this.length > s) { + this.length -= s; + for (i = 0; i < this.length; i++) { + this.words[i] = this.words[i + s]; + } + } else { + this.words[0] = 0; + this.length = 1; + } + + var carry = 0; + for (i = this.length - 1; i >= 0 && (carry !== 0 || i >= h); i--) { + var word = this.words[i] | 0; + this.words[i] = (carry << (26 - r)) | (word >>> r); + carry = word & mask; + } + + // Push carried bits as a mask + if (maskedWords && carry !== 0) { + maskedWords.words[maskedWords.length++] = carry; + } + + if (this.length === 0) { + this.words[0] = 0; + this.length = 1; + } + + return this.strip(); + }; + + BN.prototype.ishrn = function ishrn (bits, hint, extended) { + // TODO(indutny): implement me + assert(this.negative === 0); + return this.iushrn(bits, hint, extended); + }; + + // Shift-left + BN.prototype.shln = function shln (bits) { + return this.clone().ishln(bits); + }; + + BN.prototype.ushln = function ushln (bits) { + return this.clone().iushln(bits); + }; + + // Shift-right + BN.prototype.shrn = function shrn (bits) { + return this.clone().ishrn(bits); + }; + + BN.prototype.ushrn = function ushrn (bits) { + return this.clone().iushrn(bits); + }; + + // Test if n bit is set + BN.prototype.testn = function testn (bit) { + assert(typeof bit === 'number' && bit >= 0); + var r = bit % 26; + var s = (bit - r) / 26; + var q = 1 << r; + + // Fast case: bit is much higher than all existing words + if (this.length <= s) return false; + + // Check bit and return + var w = this.words[s]; + + return !!(w & q); + }; + + // Return only lowers bits of number (in-place) + BN.prototype.imaskn = function imaskn (bits) { + assert(typeof bits === 'number' && bits >= 0); + var r = bits % 26; + var s = (bits - r) / 26; + + assert(this.negative === 0, 'imaskn works only with positive numbers'); + + if (this.length <= s) { + return this; + } + + if (r !== 0) { + s++; + } + this.length = Math.min(s, this.length); + + if (r !== 0) { + var mask = 0x3ffffff ^ ((0x3ffffff >>> r) << r); + this.words[this.length - 1] &= mask; + } + + return this.strip(); + }; + + // Return only lowers bits of number + BN.prototype.maskn = function maskn (bits) { + return this.clone().imaskn(bits); + }; + + // Add plain number `num` to `this` + BN.prototype.iaddn = function iaddn (num) { + assert(typeof num === 'number'); + assert(num < 0x4000000); + if (num < 0) return this.isubn(-num); + + // Possible sign change + if (this.negative !== 0) { + if (this.length === 1 && (this.words[0] | 0) < num) { + this.words[0] = num - (this.words[0] | 0); + this.negative = 0; + return this; + } + + this.negative = 0; + this.isubn(num); + this.negative = 1; + return this; + } + + // Add without checks + return this._iaddn(num); + }; + + BN.prototype._iaddn = function _iaddn (num) { + this.words[0] += num; + + // Carry + for (var i = 0; i < this.length && this.words[i] >= 0x4000000; i++) { + this.words[i] -= 0x4000000; + if (i === this.length - 1) { + this.words[i + 1] = 1; + } else { + this.words[i + 1]++; + } + } + this.length = Math.max(this.length, i + 1); + + return this; + }; + + // Subtract plain number `num` from `this` + BN.prototype.isubn = function isubn (num) { + assert(typeof num === 'number'); + assert(num < 0x4000000); + if (num < 0) return this.iaddn(-num); + + if (this.negative !== 0) { + this.negative = 0; + this.iaddn(num); + this.negative = 1; + return this; + } + + this.words[0] -= num; + + if (this.length === 1 && this.words[0] < 0) { + this.words[0] = -this.words[0]; + this.negative = 1; + } else { + // Carry + for (var i = 0; i < this.length && this.words[i] < 0; i++) { + this.words[i] += 0x4000000; + this.words[i + 1] -= 1; + } + } + + return this.strip(); + }; + + BN.prototype.addn = function addn (num) { + return this.clone().iaddn(num); + }; + + BN.prototype.subn = function subn (num) { + return this.clone().isubn(num); + }; + + BN.prototype.iabs = function iabs () { + this.negative = 0; + + return this; + }; + + BN.prototype.abs = function abs () { + return this.clone().iabs(); + }; + + BN.prototype._ishlnsubmul = function _ishlnsubmul (num, mul, shift) { + var len = num.length + shift; + var i; + + this._expand(len); + + var w; + var carry = 0; + for (i = 0; i < num.length; i++) { + w = (this.words[i + shift] | 0) + carry; + var right = (num.words[i] | 0) * mul; + w -= right & 0x3ffffff; + carry = (w >> 26) - ((right / 0x4000000) | 0); + this.words[i + shift] = w & 0x3ffffff; + } + for (; i < this.length - shift; i++) { + w = (this.words[i + shift] | 0) + carry; + carry = w >> 26; + this.words[i + shift] = w & 0x3ffffff; + } + + if (carry === 0) return this.strip(); + + // Subtraction overflow + assert(carry === -1); + carry = 0; + for (i = 0; i < this.length; i++) { + w = -(this.words[i] | 0) + carry; + carry = w >> 26; + this.words[i] = w & 0x3ffffff; + } + this.negative = 1; + + return this.strip(); + }; + + BN.prototype._wordDiv = function _wordDiv (num, mode) { + var shift = this.length - num.length; + + var a = this.clone(); + var b = num; + + // Normalize + var bhi = b.words[b.length - 1] | 0; + var bhiBits = this._countBits(bhi); + shift = 26 - bhiBits; + if (shift !== 0) { + b = b.ushln(shift); + a.iushln(shift); + bhi = b.words[b.length - 1] | 0; + } + + // Initialize quotient + var m = a.length - b.length; + var q; + + if (mode !== 'mod') { + q = new BN(null); + q.length = m + 1; + q.words = new Array(q.length); + for (var i = 0; i < q.length; i++) { + q.words[i] = 0; + } + } + + var diff = a.clone()._ishlnsubmul(b, 1, m); + if (diff.negative === 0) { + a = diff; + if (q) { + q.words[m] = 1; + } + } + + for (var j = m - 1; j >= 0; j--) { + var qj = (a.words[b.length + j] | 0) * 0x4000000 + + (a.words[b.length + j - 1] | 0); + + // NOTE: (qj / bhi) is (0x3ffffff * 0x4000000 + 0x3ffffff) / 0x2000000 max + // (0x7ffffff) + qj = Math.min((qj / bhi) | 0, 0x3ffffff); + + a._ishlnsubmul(b, qj, j); + while (a.negative !== 0) { + qj--; + a.negative = 0; + a._ishlnsubmul(b, 1, j); + if (!a.isZero()) { + a.negative ^= 1; + } + } + if (q) { + q.words[j] = qj; + } + } + if (q) { + q.strip(); + } + a.strip(); + + // Denormalize + if (mode !== 'div' && shift !== 0) { + a.iushrn(shift); + } + + return { + div: q || null, + mod: a + }; + }; + + // NOTE: 1) `mode` can be set to `mod` to request mod only, + // to `div` to request div only, or be absent to + // request both div & mod + // 2) `positive` is true if unsigned mod is requested + BN.prototype.divmod = function divmod (num, mode, positive) { + assert(!num.isZero()); + + if (this.isZero()) { + return { + div: new BN(0), + mod: new BN(0) + }; + } + + var div, mod, res; + if (this.negative !== 0 && num.negative === 0) { + res = this.neg().divmod(num, mode); + + if (mode !== 'mod') { + div = res.div.neg(); + } + + if (mode !== 'div') { + mod = res.mod.neg(); + if (positive && mod.negative !== 0) { + mod.iadd(num); + } + } + + return { + div: div, + mod: mod + }; + } + + if (this.negative === 0 && num.negative !== 0) { + res = this.divmod(num.neg(), mode); + + if (mode !== 'mod') { + div = res.div.neg(); + } + + return { + div: div, + mod: res.mod + }; + } + + if ((this.negative & num.negative) !== 0) { + res = this.neg().divmod(num.neg(), mode); + + if (mode !== 'div') { + mod = res.mod.neg(); + if (positive && mod.negative !== 0) { + mod.isub(num); + } + } + + return { + div: res.div, + mod: mod + }; + } + + // Both numbers are positive at this point + + // Strip both numbers to approximate shift value + if (num.length > this.length || this.cmp(num) < 0) { + return { + div: new BN(0), + mod: this + }; + } + + // Very short reduction + if (num.length === 1) { + if (mode === 'div') { + return { + div: this.divn(num.words[0]), + mod: null + }; + } + + if (mode === 'mod') { + return { + div: null, + mod: new BN(this.modn(num.words[0])) + }; + } + + return { + div: this.divn(num.words[0]), + mod: new BN(this.modn(num.words[0])) + }; + } + + return this._wordDiv(num, mode); + }; + + // Find `this` / `num` + BN.prototype.div = function div (num) { + return this.divmod(num, 'div', false).div; + }; + + // Find `this` % `num` + BN.prototype.mod = function mod (num) { + return this.divmod(num, 'mod', false).mod; + }; + + BN.prototype.umod = function umod (num) { + return this.divmod(num, 'mod', true).mod; + }; + + // Find Round(`this` / `num`) + BN.prototype.divRound = function divRound (num) { + var dm = this.divmod(num); + + // Fast case - exact division + if (dm.mod.isZero()) return dm.div; + + var mod = dm.div.negative !== 0 ? dm.mod.isub(num) : dm.mod; + + var half = num.ushrn(1); + var r2 = num.andln(1); + var cmp = mod.cmp(half); + + // Round down + if (cmp < 0 || r2 === 1 && cmp === 0) return dm.div; + + // Round up + return dm.div.negative !== 0 ? dm.div.isubn(1) : dm.div.iaddn(1); + }; + + BN.prototype.modn = function modn (num) { + assert(num <= 0x3ffffff); + var p = (1 << 26) % num; + + var acc = 0; + for (var i = this.length - 1; i >= 0; i--) { + acc = (p * acc + (this.words[i] | 0)) % num; + } + + return acc; + }; + + // In-place division by number + BN.prototype.idivn = function idivn (num) { + assert(num <= 0x3ffffff); + + var carry = 0; + for (var i = this.length - 1; i >= 0; i--) { + var w = (this.words[i] | 0) + carry * 0x4000000; + this.words[i] = (w / num) | 0; + carry = w % num; + } + + return this.strip(); + }; + + BN.prototype.divn = function divn (num) { + return this.clone().idivn(num); + }; + + BN.prototype.egcd = function egcd (p) { + assert(p.negative === 0); + assert(!p.isZero()); + + var x = this; + var y = p.clone(); + + if (x.negative !== 0) { + x = x.umod(p); + } else { + x = x.clone(); + } + + // A * x + B * y = x + var A = new BN(1); + var B = new BN(0); + + // C * x + D * y = y + var C = new BN(0); + var D = new BN(1); + + var g = 0; + + while (x.isEven() && y.isEven()) { + x.iushrn(1); + y.iushrn(1); + ++g; + } + + var yp = y.clone(); + var xp = x.clone(); + + while (!x.isZero()) { + for (var i = 0, im = 1; (x.words[0] & im) === 0 && i < 26; ++i, im <<= 1); + if (i > 0) { + x.iushrn(i); + while (i-- > 0) { + if (A.isOdd() || B.isOdd()) { + A.iadd(yp); + B.isub(xp); + } + + A.iushrn(1); + B.iushrn(1); + } + } + + for (var j = 0, jm = 1; (y.words[0] & jm) === 0 && j < 26; ++j, jm <<= 1); + if (j > 0) { + y.iushrn(j); + while (j-- > 0) { + if (C.isOdd() || D.isOdd()) { + C.iadd(yp); + D.isub(xp); + } + + C.iushrn(1); + D.iushrn(1); + } + } + + if (x.cmp(y) >= 0) { + x.isub(y); + A.isub(C); + B.isub(D); + } else { + y.isub(x); + C.isub(A); + D.isub(B); + } + } + + return { + a: C, + b: D, + gcd: y.iushln(g) + }; + }; + + // This is reduced incarnation of the binary EEA + // above, designated to invert members of the + // _prime_ fields F(p) at a maximal speed + BN.prototype._invmp = function _invmp (p) { + assert(p.negative === 0); + assert(!p.isZero()); + + var a = this; + var b = p.clone(); + + if (a.negative !== 0) { + a = a.umod(p); + } else { + a = a.clone(); + } + + var x1 = new BN(1); + var x2 = new BN(0); + + var delta = b.clone(); + + while (a.cmpn(1) > 0 && b.cmpn(1) > 0) { + for (var i = 0, im = 1; (a.words[0] & im) === 0 && i < 26; ++i, im <<= 1); + if (i > 0) { + a.iushrn(i); + while (i-- > 0) { + if (x1.isOdd()) { + x1.iadd(delta); + } + + x1.iushrn(1); + } + } + + for (var j = 0, jm = 1; (b.words[0] & jm) === 0 && j < 26; ++j, jm <<= 1); + if (j > 0) { + b.iushrn(j); + while (j-- > 0) { + if (x2.isOdd()) { + x2.iadd(delta); + } + + x2.iushrn(1); + } + } + + if (a.cmp(b) >= 0) { + a.isub(b); + x1.isub(x2); + } else { + b.isub(a); + x2.isub(x1); + } + } + + var res; + if (a.cmpn(1) === 0) { + res = x1; + } else { + res = x2; + } + + if (res.cmpn(0) < 0) { + res.iadd(p); + } + + return res; + }; + + BN.prototype.gcd = function gcd (num) { + if (this.isZero()) return num.abs(); + if (num.isZero()) return this.abs(); + + var a = this.clone(); + var b = num.clone(); + a.negative = 0; + b.negative = 0; + + // Remove common factor of two + for (var shift = 0; a.isEven() && b.isEven(); shift++) { + a.iushrn(1); + b.iushrn(1); + } + + do { + while (a.isEven()) { + a.iushrn(1); + } + while (b.isEven()) { + b.iushrn(1); + } + + var r = a.cmp(b); + if (r < 0) { + // Swap `a` and `b` to make `a` always bigger than `b` + var t = a; + a = b; + b = t; + } else if (r === 0 || b.cmpn(1) === 0) { + break; + } + + a.isub(b); + } while (true); + + return b.iushln(shift); + }; + + // Invert number in the field F(num) + BN.prototype.invm = function invm (num) { + return this.egcd(num).a.umod(num); + }; + + BN.prototype.isEven = function isEven () { + return (this.words[0] & 1) === 0; + }; + + BN.prototype.isOdd = function isOdd () { + return (this.words[0] & 1) === 1; + }; + + // And first word and num + BN.prototype.andln = function andln (num) { + return this.words[0] & num; + }; + + // Increment at the bit position in-line + BN.prototype.bincn = function bincn (bit) { + assert(typeof bit === 'number'); + var r = bit % 26; + var s = (bit - r) / 26; + var q = 1 << r; + + // Fast case: bit is much higher than all existing words + if (this.length <= s) { + this._expand(s + 1); + this.words[s] |= q; + return this; + } + + // Add bit and propagate, if needed + var carry = q; + for (var i = s; carry !== 0 && i < this.length; i++) { + var w = this.words[i] | 0; + w += carry; + carry = w >>> 26; + w &= 0x3ffffff; + this.words[i] = w; + } + if (carry !== 0) { + this.words[i] = carry; + this.length++; + } + return this; + }; + + BN.prototype.isZero = function isZero () { + return this.length === 1 && this.words[0] === 0; + }; + + BN.prototype.cmpn = function cmpn (num) { + var negative = num < 0; + + if (this.negative !== 0 && !negative) return -1; + if (this.negative === 0 && negative) return 1; + + this.strip(); + + var res; + if (this.length > 1) { + res = 1; + } else { + if (negative) { + num = -num; + } + + assert(num <= 0x3ffffff, 'Number is too big'); + + var w = this.words[0] | 0; + res = w === num ? 0 : w < num ? -1 : 1; + } + if (this.negative !== 0) return -res | 0; + return res; + }; + + // Compare two numbers and return: + // 1 - if `this` > `num` + // 0 - if `this` == `num` + // -1 - if `this` < `num` + BN.prototype.cmp = function cmp (num) { + if (this.negative !== 0 && num.negative === 0) return -1; + if (this.negative === 0 && num.negative !== 0) return 1; + + var res = this.ucmp(num); + if (this.negative !== 0) return -res | 0; + return res; + }; + + // Unsigned comparison + BN.prototype.ucmp = function ucmp (num) { + // At this point both numbers have the same sign + if (this.length > num.length) return 1; + if (this.length < num.length) return -1; + + var res = 0; + for (var i = this.length - 1; i >= 0; i--) { + var a = this.words[i] | 0; + var b = num.words[i] | 0; + + if (a === b) continue; + if (a < b) { + res = -1; + } else if (a > b) { + res = 1; + } + break; + } + return res; + }; + + BN.prototype.gtn = function gtn (num) { + return this.cmpn(num) === 1; + }; + + BN.prototype.gt = function gt (num) { + return this.cmp(num) === 1; + }; + + BN.prototype.gten = function gten (num) { + return this.cmpn(num) >= 0; + }; + + BN.prototype.gte = function gte (num) { + return this.cmp(num) >= 0; + }; + + BN.prototype.ltn = function ltn (num) { + return this.cmpn(num) === -1; + }; + + BN.prototype.lt = function lt (num) { + return this.cmp(num) === -1; + }; + + BN.prototype.lten = function lten (num) { + return this.cmpn(num) <= 0; + }; + + BN.prototype.lte = function lte (num) { + return this.cmp(num) <= 0; + }; + + BN.prototype.eqn = function eqn (num) { + return this.cmpn(num) === 0; + }; + + BN.prototype.eq = function eq (num) { + return this.cmp(num) === 0; + }; + + // + // A reduce context, could be using montgomery or something better, depending + // on the `m` itself. + // + BN.red = function red (num) { + return new Red(num); + }; + + BN.prototype.toRed = function toRed (ctx) { + assert(!this.red, 'Already a number in reduction context'); + assert(this.negative === 0, 'red works only with positives'); + return ctx.convertTo(this)._forceRed(ctx); + }; + + BN.prototype.fromRed = function fromRed () { + assert(this.red, 'fromRed works only with numbers in reduction context'); + return this.red.convertFrom(this); + }; + + BN.prototype._forceRed = function _forceRed (ctx) { + this.red = ctx; + return this; + }; + + BN.prototype.forceRed = function forceRed (ctx) { + assert(!this.red, 'Already a number in reduction context'); + return this._forceRed(ctx); + }; + + BN.prototype.redAdd = function redAdd (num) { + assert(this.red, 'redAdd works only with red numbers'); + return this.red.add(this, num); + }; + + BN.prototype.redIAdd = function redIAdd (num) { + assert(this.red, 'redIAdd works only with red numbers'); + return this.red.iadd(this, num); + }; + + BN.prototype.redSub = function redSub (num) { + assert(this.red, 'redSub works only with red numbers'); + return this.red.sub(this, num); + }; + + BN.prototype.redISub = function redISub (num) { + assert(this.red, 'redISub works only with red numbers'); + return this.red.isub(this, num); + }; + + BN.prototype.redShl = function redShl (num) { + assert(this.red, 'redShl works only with red numbers'); + return this.red.shl(this, num); + }; + + BN.prototype.redMul = function redMul (num) { + assert(this.red, 'redMul works only with red numbers'); + this.red._verify2(this, num); + return this.red.mul(this, num); + }; + + BN.prototype.redIMul = function redIMul (num) { + assert(this.red, 'redMul works only with red numbers'); + this.red._verify2(this, num); + return this.red.imul(this, num); + }; + + BN.prototype.redSqr = function redSqr () { + assert(this.red, 'redSqr works only with red numbers'); + this.red._verify1(this); + return this.red.sqr(this); + }; + + BN.prototype.redISqr = function redISqr () { + assert(this.red, 'redISqr works only with red numbers'); + this.red._verify1(this); + return this.red.isqr(this); + }; + + // Square root over p + BN.prototype.redSqrt = function redSqrt () { + assert(this.red, 'redSqrt works only with red numbers'); + this.red._verify1(this); + return this.red.sqrt(this); + }; + + BN.prototype.redInvm = function redInvm () { + assert(this.red, 'redInvm works only with red numbers'); + this.red._verify1(this); + return this.red.invm(this); + }; + + // Return negative clone of `this` % `red modulo` + BN.prototype.redNeg = function redNeg () { + assert(this.red, 'redNeg works only with red numbers'); + this.red._verify1(this); + return this.red.neg(this); + }; + + BN.prototype.redPow = function redPow (num) { + assert(this.red && !num.red, 'redPow(normalNum)'); + this.red._verify1(this); + return this.red.pow(this, num); + }; + + // Prime numbers with efficient reduction + var primes = { + k256: null, + p224: null, + p192: null, + p25519: null + }; + + // Pseudo-Mersenne prime + function MPrime (name, p) { + // P = 2 ^ N - K + this.name = name; + this.p = new BN(p, 16); + this.n = this.p.bitLength(); + this.k = new BN(1).iushln(this.n).isub(this.p); + + this.tmp = this._tmp(); + } + + MPrime.prototype._tmp = function _tmp () { + var tmp = new BN(null); + tmp.words = new Array(Math.ceil(this.n / 13)); + return tmp; + }; + + MPrime.prototype.ireduce = function ireduce (num) { + // Assumes that `num` is less than `P^2` + // num = HI * (2 ^ N - K) + HI * K + LO = HI * K + LO (mod P) + var r = num; + var rlen; + + do { + this.split(r, this.tmp); + r = this.imulK(r); + r = r.iadd(this.tmp); + rlen = r.bitLength(); + } while (rlen > this.n); + + var cmp = rlen < this.n ? -1 : r.ucmp(this.p); + if (cmp === 0) { + r.words[0] = 0; + r.length = 1; + } else if (cmp > 0) { + r.isub(this.p); + } else { + if (r.strip !== undefined) { + // r is BN v4 instance + r.strip(); + } else { + // r is BN v5 instance + r._strip(); + } + } + + return r; + }; + + MPrime.prototype.split = function split (input, out) { + input.iushrn(this.n, 0, out); + }; + + MPrime.prototype.imulK = function imulK (num) { + return num.imul(this.k); + }; + + function K256 () { + MPrime.call( + this, + 'k256', + 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe fffffc2f'); + } + inherits(K256, MPrime); + + K256.prototype.split = function split (input, output) { + // 256 = 9 * 26 + 22 + var mask = 0x3fffff; + + var outLen = Math.min(input.length, 9); + for (var i = 0; i < outLen; i++) { + output.words[i] = input.words[i]; + } + output.length = outLen; + + if (input.length <= 9) { + input.words[0] = 0; + input.length = 1; + return; + } + + // Shift by 9 limbs + var prev = input.words[9]; + output.words[output.length++] = prev & mask; + + for (i = 10; i < input.length; i++) { + var next = input.words[i] | 0; + input.words[i - 10] = ((next & mask) << 4) | (prev >>> 22); + prev = next; + } + prev >>>= 22; + input.words[i - 10] = prev; + if (prev === 0 && input.length > 10) { + input.length -= 10; + } else { + input.length -= 9; + } + }; + + K256.prototype.imulK = function imulK (num) { + // K = 0x1000003d1 = [ 0x40, 0x3d1 ] + num.words[num.length] = 0; + num.words[num.length + 1] = 0; + num.length += 2; + + // bounded at: 0x40 * 0x3ffffff + 0x3d0 = 0x100000390 + var lo = 0; + for (var i = 0; i < num.length; i++) { + var w = num.words[i] | 0; + lo += w * 0x3d1; + num.words[i] = lo & 0x3ffffff; + lo = w * 0x40 + ((lo / 0x4000000) | 0); + } + + // Fast length reduction + if (num.words[num.length - 1] === 0) { + num.length--; + if (num.words[num.length - 1] === 0) { + num.length--; + } + } + return num; + }; + + function P224 () { + MPrime.call( + this, + 'p224', + 'ffffffff ffffffff ffffffff ffffffff 00000000 00000000 00000001'); + } + inherits(P224, MPrime); + + function P192 () { + MPrime.call( + this, + 'p192', + 'ffffffff ffffffff ffffffff fffffffe ffffffff ffffffff'); + } + inherits(P192, MPrime); + + function P25519 () { + // 2 ^ 255 - 19 + MPrime.call( + this, + '25519', + '7fffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffed'); + } + inherits(P25519, MPrime); + + P25519.prototype.imulK = function imulK (num) { + // K = 0x13 + var carry = 0; + for (var i = 0; i < num.length; i++) { + var hi = (num.words[i] | 0) * 0x13 + carry; + var lo = hi & 0x3ffffff; + hi >>>= 26; + + num.words[i] = lo; + carry = hi; + } + if (carry !== 0) { + num.words[num.length++] = carry; + } + return num; + }; + + // Exported mostly for testing purposes, use plain name instead + BN._prime = function prime (name) { + // Cached version of prime + if (primes[name]) return primes[name]; + + var prime; + if (name === 'k256') { + prime = new K256(); + } else if (name === 'p224') { + prime = new P224(); + } else if (name === 'p192') { + prime = new P192(); + } else if (name === 'p25519') { + prime = new P25519(); + } else { + throw Error('Unknown prime ' + name); + } + primes[name] = prime; + + return prime; + }; + + // + // Base reduction engine + // + function Red (m) { + if (typeof m === 'string') { + var prime = BN._prime(m); + this.m = prime.p; + this.prime = prime; + } else { + assert(m.gtn(1), 'modulus must be greater than 1'); + this.m = m; + this.prime = null; + } + } + + Red.prototype._verify1 = function _verify1 (a) { + assert(a.negative === 0, 'red works only with positives'); + assert(a.red, 'red works only with red numbers'); + }; + + Red.prototype._verify2 = function _verify2 (a, b) { + assert((a.negative | b.negative) === 0, 'red works only with positives'); + assert(a.red && a.red === b.red, + 'red works only with red numbers'); + }; + + Red.prototype.imod = function imod (a) { + if (this.prime) return this.prime.ireduce(a)._forceRed(this); + return a.umod(this.m)._forceRed(this); + }; + + Red.prototype.neg = function neg (a) { + if (a.isZero()) { + return a.clone(); + } + + return this.m.sub(a)._forceRed(this); + }; + + Red.prototype.add = function add (a, b) { + this._verify2(a, b); + + var res = a.add(b); + if (res.cmp(this.m) >= 0) { + res.isub(this.m); + } + return res._forceRed(this); + }; + + Red.prototype.iadd = function iadd (a, b) { + this._verify2(a, b); + + var res = a.iadd(b); + if (res.cmp(this.m) >= 0) { + res.isub(this.m); + } + return res; + }; + + Red.prototype.sub = function sub (a, b) { + this._verify2(a, b); + + var res = a.sub(b); + if (res.cmpn(0) < 0) { + res.iadd(this.m); + } + return res._forceRed(this); + }; + + Red.prototype.isub = function isub (a, b) { + this._verify2(a, b); + + var res = a.isub(b); + if (res.cmpn(0) < 0) { + res.iadd(this.m); + } + return res; + }; + + Red.prototype.shl = function shl (a, num) { + this._verify1(a); + return this.imod(a.ushln(num)); + }; + + Red.prototype.imul = function imul (a, b) { + this._verify2(a, b); + return this.imod(a.imul(b)); + }; + + Red.prototype.mul = function mul (a, b) { + this._verify2(a, b); + return this.imod(a.mul(b)); + }; + + Red.prototype.isqr = function isqr (a) { + return this.imul(a, a.clone()); + }; + + Red.prototype.sqr = function sqr (a) { + return this.mul(a, a); + }; + + Red.prototype.sqrt = function sqrt (a) { + if (a.isZero()) return a.clone(); + + var mod3 = this.m.andln(3); + assert(mod3 % 2 === 1); + + // Fast case + if (mod3 === 3) { + var pow = this.m.add(new BN(1)).iushrn(2); + return this.pow(a, pow); + } + + // Tonelli-Shanks algorithm (Totally unoptimized and slow) + // + // Find Q and S, that Q * 2 ^ S = (P - 1) + var q = this.m.subn(1); + var s = 0; + while (!q.isZero() && q.andln(1) === 0) { + s++; + q.iushrn(1); + } + assert(!q.isZero()); + + var one = new BN(1).toRed(this); + var nOne = one.redNeg(); + + // Find quadratic non-residue + // NOTE: Max is such because of generalized Riemann hypothesis. + var lpow = this.m.subn(1).iushrn(1); + var z = this.m.bitLength(); + z = new BN(2 * z * z).toRed(this); + + while (this.pow(z, lpow).cmp(nOne) !== 0) { + z.redIAdd(nOne); + } + + var c = this.pow(z, q); + var r = this.pow(a, q.addn(1).iushrn(1)); + var t = this.pow(a, q); + var m = s; + while (t.cmp(one) !== 0) { + var tmp = t; + for (var i = 0; tmp.cmp(one) !== 0; i++) { + tmp = tmp.redSqr(); + } + assert(i < m); + var b = this.pow(c, new BN(1).iushln(m - i - 1)); + + r = r.redMul(b); + c = b.redSqr(); + t = t.redMul(c); + m = i; + } + + return r; + }; + + Red.prototype.invm = function invm (a) { + var inv = a._invmp(this.m); + if (inv.negative !== 0) { + inv.negative = 0; + return this.imod(inv).redNeg(); + } else { + return this.imod(inv); + } + }; + + Red.prototype.pow = function pow (a, num) { + if (num.isZero()) return new BN(1).toRed(this); + if (num.cmpn(1) === 0) return a.clone(); + + var windowSize = 4; + var wnd = new Array(1 << windowSize); + wnd[0] = new BN(1).toRed(this); + wnd[1] = a; + for (var i = 2; i < wnd.length; i++) { + wnd[i] = this.mul(wnd[i - 1], a); + } + + var res = wnd[0]; + var current = 0; + var currentLen = 0; + var start = num.bitLength() % 26; + if (start === 0) { + start = 26; + } + + for (i = num.length - 1; i >= 0; i--) { + var word = num.words[i]; + for (var j = start - 1; j >= 0; j--) { + var bit = (word >> j) & 1; + if (res !== wnd[0]) { + res = this.sqr(res); + } + + if (bit === 0 && current === 0) { + currentLen = 0; + continue; + } + + current <<= 1; + current |= bit; + currentLen++; + if (currentLen !== windowSize && (i !== 0 || j !== 0)) continue; + + res = this.mul(res, wnd[current]); + currentLen = 0; + current = 0; + } + start = 26; + } + + return res; + }; + + Red.prototype.convertTo = function convertTo (num) { + var r = num.umod(this.m); + + return r === num ? r.clone() : r; + }; + + Red.prototype.convertFrom = function convertFrom (num) { + var res = num.clone(); + res.red = null; + return res; + }; + + // + // Montgomery method engine + // + + BN.mont = function mont (num) { + return new Mont(num); + }; + + function Mont (m) { + Red.call(this, m); + + this.shift = this.m.bitLength(); + if (this.shift % 26 !== 0) { + this.shift += 26 - (this.shift % 26); + } + + this.r = new BN(1).iushln(this.shift); + this.r2 = this.imod(this.r.sqr()); + this.rinv = this.r._invmp(this.m); + + this.minv = this.rinv.mul(this.r).isubn(1).div(this.m); + this.minv = this.minv.umod(this.r); + this.minv = this.r.sub(this.minv); + } + inherits(Mont, Red); + + Mont.prototype.convertTo = function convertTo (num) { + return this.imod(num.ushln(this.shift)); + }; + + Mont.prototype.convertFrom = function convertFrom (num) { + var r = this.imod(num.mul(this.rinv)); + r.red = null; + return r; + }; + + Mont.prototype.imul = function imul (a, b) { + if (a.isZero() || b.isZero()) { + a.words[0] = 0; + a.length = 1; + return a; + } + + var t = a.imul(b); + var c = t.maskn(this.shift).mul(this.minv).imaskn(this.shift).mul(this.m); + var u = t.isub(c).iushrn(this.shift); + var res = u; + + if (u.cmp(this.m) >= 0) { + res = u.isub(this.m); + } else if (u.cmpn(0) < 0) { + res = u.iadd(this.m); + } + + return res._forceRed(this); + }; + + Mont.prototype.mul = function mul (a, b) { + if (a.isZero() || b.isZero()) return new BN(0)._forceRed(this); + + var t = a.mul(b); + var c = t.maskn(this.shift).mul(this.minv).imaskn(this.shift).mul(this.m); + var u = t.isub(c).iushrn(this.shift); + var res = u; + if (u.cmp(this.m) >= 0) { + res = u.isub(this.m); + } else if (u.cmpn(0) < 0) { + res = u.iadd(this.m); + } + + return res._forceRed(this); + }; + + Mont.prototype.invm = function invm (a) { + // (AR)^-1 * R^2 = (A^-1 * R^-1) * R^2 = A^-1 * R + var res = this.imod(a._invmp(this.m).mul(this.r2)); + return res._forceRed(this); + }; + })(module, commonjsGlobal); + }); + + var BN = bn; + + /** + * @fileoverview + * BigInteger implementation of basic operations + * Wrapper of bn.js library (wwww.github.com/indutny/bn.js) + * @module biginteger/bn + * @private + */ + + /** + * @private + */ + class BigInteger { + /** + * Get a BigInteger (input must be big endian for strings and arrays) + * @param {Number|String|Uint8Array} n - Value to convert + * @throws {Error} on undefined input + */ + constructor(n) { + if (n === undefined) { + throw Error('Invalid BigInteger input'); + } + + this.value = new BN(n); + } + + clone() { + const clone = new BigInteger(null); + this.value.copy(clone.value); + return clone; + } + + /** + * BigInteger increment in place + */ + iinc() { + this.value.iadd(new BN(1)); + return this; + } + + /** + * BigInteger increment + * @returns {BigInteger} this + 1. + */ + inc() { + return this.clone().iinc(); + } + + /** + * BigInteger decrement in place + */ + idec() { + this.value.isub(new BN(1)); + return this; + } + + /** + * BigInteger decrement + * @returns {BigInteger} this - 1. + */ + dec() { + return this.clone().idec(); + } + + + /** + * BigInteger addition in place + * @param {BigInteger} x - Value to add + */ + iadd(x) { + this.value.iadd(x.value); + return this; + } + + /** + * BigInteger addition + * @param {BigInteger} x - Value to add + * @returns {BigInteger} this + x. + */ + add(x) { + return this.clone().iadd(x); + } + + /** + * BigInteger subtraction in place + * @param {BigInteger} x - Value to subtract + */ + isub(x) { + this.value.isub(x.value); + return this; + } + + /** + * BigInteger subtraction + * @param {BigInteger} x - Value to subtract + * @returns {BigInteger} this - x. + */ + sub(x) { + return this.clone().isub(x); + } + + /** + * BigInteger multiplication in place + * @param {BigInteger} x - Value to multiply + */ + imul(x) { + this.value.imul(x.value); + return this; + } + + /** + * BigInteger multiplication + * @param {BigInteger} x - Value to multiply + * @returns {BigInteger} this * x. + */ + mul(x) { + return this.clone().imul(x); + } + + /** + * Compute value modulo m, in place + * @param {BigInteger} m - Modulo + */ + imod(m) { + this.value = this.value.umod(m.value); + return this; + } + + /** + * Compute value modulo m + * @param {BigInteger} m - Modulo + * @returns {BigInteger} this mod m. + */ + mod(m) { + return this.clone().imod(m); + } + + /** + * Compute modular exponentiation + * Much faster than this.exp(e).mod(n) + * @param {BigInteger} e - Exponent + * @param {BigInteger} n - Modulo + * @returns {BigInteger} this ** e mod n. + */ + modExp(e, n) { + // We use either Montgomery or normal reduction context + // Montgomery requires coprime n and R (montogmery multiplier) + // bn.js picks R as power of 2, so n must be odd + const nred = n.isEven() ? BN.red(n.value) : BN.mont(n.value); + const x = this.clone(); + x.value = x.value.toRed(nred).redPow(e.value).fromRed(); + return x; + } + + /** + * Compute the inverse of this value modulo n + * Note: this and and n must be relatively prime + * @param {BigInteger} n - Modulo + * @returns {BigInteger} x such that this*x = 1 mod n + * @throws {Error} if the inverse does not exist + */ + modInv(n) { + // invm returns a wrong result if the inverse does not exist + if (!this.gcd(n).isOne()) { + throw Error('Inverse does not exist'); + } + return new BigInteger(this.value.invm(n.value)); + } + + /** + * Compute greatest common divisor between this and n + * @param {BigInteger} n - Operand + * @returns {BigInteger} gcd + */ + gcd(n) { + return new BigInteger(this.value.gcd(n.value)); + } + + /** + * Shift this to the left by x, in place + * @param {BigInteger} x - Shift value + */ + ileftShift(x) { + this.value.ishln(x.value.toNumber()); + return this; + } + + /** + * Shift this to the left by x + * @param {BigInteger} x - Shift value + * @returns {BigInteger} this << x. + */ + leftShift(x) { + return this.clone().ileftShift(x); + } + + /** + * Shift this to the right by x, in place + * @param {BigInteger} x - Shift value + */ + irightShift(x) { + this.value.ishrn(x.value.toNumber()); + return this; + } + + /** + * Shift this to the right by x + * @param {BigInteger} x - Shift value + * @returns {BigInteger} this >> x. + */ + rightShift(x) { + return this.clone().irightShift(x); + } + + /** + * Whether this value is equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + equal(x) { + return this.value.eq(x.value); + } + + /** + * Whether this value is less than x + * @param {BigInteger} x + * @returns {Boolean} + */ + lt(x) { + return this.value.lt(x.value); + } + + /** + * Whether this value is less than or equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + lte(x) { + return this.value.lte(x.value); + } + + /** + * Whether this value is greater than x + * @param {BigInteger} x + * @returns {Boolean} + */ + gt(x) { + return this.value.gt(x.value); + } + + /** + * Whether this value is greater than or equal to x + * @param {BigInteger} x + * @returns {Boolean} + */ + gte(x) { + return this.value.gte(x.value); + } + + isZero() { + return this.value.isZero(); + } + + isOne() { + return this.value.eq(new BN(1)); + } + + isNegative() { + return this.value.isNeg(); + } + + isEven() { + return this.value.isEven(); + } + + abs() { + const res = this.clone(); + res.value = res.value.abs(); + return res; + } + + /** + * Get this value as a string + * @returns {String} this value. + */ + toString() { + return this.value.toString(); + } + + /** + * Get this value as an exact Number (max 53 bits) + * Fails if this value is too large + * @returns {Number} + */ + toNumber() { + return this.value.toNumber(); + } + + /** + * Get value of i-th bit + * @param {Number} i - Bit index + * @returns {Number} Bit value. + */ + getBit(i) { + return this.value.testn(i) ? 1 : 0; + } + + /** + * Compute bit length + * @returns {Number} Bit length. + */ + bitLength() { + return this.value.bitLength(); + } + + /** + * Compute byte length + * @returns {Number} Byte length. + */ + byteLength() { + return this.value.byteLength(); + } + + /** + * Get Uint8Array representation of this number + * @param {String} endian - Endianess of output array (defaults to 'be') + * @param {Number} length - Of output array + * @returns {Uint8Array} + */ + toUint8Array(endian = 'be', length) { + return this.value.toArrayLike(Uint8Array, endian, length); + } + } + + var bn_interface = /*#__PURE__*/Object.freeze({ + __proto__: null, + 'default': BigInteger + }); + + var utils_1$1 = createCommonjsModule(function (module, exports) { + + var utils = exports; + + function toArray(msg, enc) { + if (Array.isArray(msg)) + return msg.slice(); + if (!msg) + return []; + var res = []; + if (typeof msg !== 'string') { + for (var i = 0; i < msg.length; i++) + res[i] = msg[i] | 0; + return res; + } + if (enc === 'hex') { + msg = msg.replace(/[^a-z0-9]+/ig, ''); + if (msg.length % 2 !== 0) + msg = '0' + msg; + for (var i = 0; i < msg.length; i += 2) + res.push(parseInt(msg[i] + msg[i + 1], 16)); + } else { + for (var i = 0; i < msg.length; i++) { + var c = msg.charCodeAt(i); + var hi = c >> 8; + var lo = c & 0xff; + if (hi) + res.push(hi, lo); + else + res.push(lo); + } + } + return res; + } + utils.toArray = toArray; + + function zero2(word) { + if (word.length === 1) + return '0' + word; + else + return word; + } + utils.zero2 = zero2; + + function toHex(msg) { + var res = ''; + for (var i = 0; i < msg.length; i++) + res += zero2(msg[i].toString(16)); + return res; + } + utils.toHex = toHex; + + utils.encode = function encode(arr, enc) { + if (enc === 'hex') + return toHex(arr); + else + return arr; + }; + }); + + var utils_1 = createCommonjsModule(function (module, exports) { + + var utils = exports; + + + + + utils.assert = minimalisticAssert; + utils.toArray = utils_1$1.toArray; + utils.zero2 = utils_1$1.zero2; + utils.toHex = utils_1$1.toHex; + utils.encode = utils_1$1.encode; + + // Represent num in a w-NAF form + function getNAF(num, w) { + var naf = []; + var ws = 1 << (w + 1); + var k = num.clone(); + while (k.cmpn(1) >= 0) { + var z; + if (k.isOdd()) { + var mod = k.andln(ws - 1); + if (mod > (ws >> 1) - 1) + z = (ws >> 1) - mod; + else + z = mod; + k.isubn(z); + } else { + z = 0; + } + naf.push(z); + + // Optimization, shift by word if possible + var shift = (k.cmpn(0) !== 0 && k.andln(ws - 1) === 0) ? (w + 1) : 1; + for (var i = 1; i < shift; i++) + naf.push(0); + k.iushrn(shift); + } + + return naf; + } + utils.getNAF = getNAF; + + // Represent k1, k2 in a Joint Sparse Form + function getJSF(k1, k2) { + var jsf = [ + [], + [] + ]; + + k1 = k1.clone(); + k2 = k2.clone(); + var d1 = 0; + var d2 = 0; + while (k1.cmpn(-d1) > 0 || k2.cmpn(-d2) > 0) { + + // First phase + var m14 = (k1.andln(3) + d1) & 3; + var m24 = (k2.andln(3) + d2) & 3; + if (m14 === 3) + m14 = -1; + if (m24 === 3) + m24 = -1; + var u1; + if ((m14 & 1) === 0) { + u1 = 0; + } else { + var m8 = (k1.andln(7) + d1) & 7; + if ((m8 === 3 || m8 === 5) && m24 === 2) + u1 = -m14; + else + u1 = m14; + } + jsf[0].push(u1); + + var u2; + if ((m24 & 1) === 0) { + u2 = 0; + } else { + var m8 = (k2.andln(7) + d2) & 7; + if ((m8 === 3 || m8 === 5) && m14 === 2) + u2 = -m24; + else + u2 = m24; + } + jsf[1].push(u2); + + // Second phase + if (2 * d1 === u1 + 1) + d1 = 1 - d1; + if (2 * d2 === u2 + 1) + d2 = 1 - d2; + k1.iushrn(1); + k2.iushrn(1); + } + + return jsf; + } + utils.getJSF = getJSF; + + function cachedProperty(obj, name, computer) { + var key = '_' + name; + obj.prototype[name] = function cachedProperty() { + return this[key] !== undefined ? this[key] : + this[key] = computer.call(this); + }; + } + utils.cachedProperty = cachedProperty; + + function parseBytes(bytes) { + return typeof bytes === 'string' ? utils.toArray(bytes, 'hex') : + bytes; + } + utils.parseBytes = parseBytes; + + function intFromLE(bytes) { + return new bn(bytes, 'hex', 'le'); + } + utils.intFromLE = intFromLE; + }); + + var r; + + var brorand = function rand(len) { + if (!r) + r = new Rand(null); + + return r.generate(len); + }; + + function Rand(rand) { + this.rand = rand; + } + var Rand_1 = Rand; + + Rand.prototype.generate = function generate(len) { + return this._rand(len); + }; + + // Emulate crypto API using randy + Rand.prototype._rand = function _rand(n) { + if (this.rand.getBytes) + return this.rand.getBytes(n); + + var res = new Uint8Array(n); + for (var i = 0; i < res.length; i++) + res[i] = this.rand.getByte(); + return res; + }; + + if (typeof self === 'object') { + if (self.crypto && self.crypto.getRandomValues) { + // Modern browsers + Rand.prototype._rand = function _rand(n) { + var arr = new Uint8Array(n); + self.crypto.getRandomValues(arr); + return arr; + }; + } else if (self.msCrypto && self.msCrypto.getRandomValues) { + // IE + Rand.prototype._rand = function _rand(n) { + var arr = new Uint8Array(n); + self.msCrypto.getRandomValues(arr); + return arr; + }; + + // Safari's WebWorkers do not have `crypto` + } else if (typeof window === 'object') { + // Old junk + Rand.prototype._rand = function() { + throw Error('Not implemented yet'); + }; + } + } else { + // Node.js or Web worker with no crypto support + try { + var crypto$1 = void('crypto'); + if (typeof crypto$1.randomBytes !== 'function') + throw Error('Not supported'); + + Rand.prototype._rand = function _rand(n) { + return crypto$1.randomBytes(n); + }; + } catch (e) { + } + } + brorand.Rand = Rand_1; + + var getNAF = utils_1.getNAF; + var getJSF = utils_1.getJSF; + var assert$8 = utils_1.assert; + + function BaseCurve(type, conf) { + this.type = type; + this.p = new bn(conf.p, 16); + + // Use Montgomery, when there is no fast reduction for the prime + this.red = conf.prime ? bn.red(conf.prime) : bn.mont(this.p); + + // Useful for many curves + this.zero = new bn(0).toRed(this.red); + this.one = new bn(1).toRed(this.red); + this.two = new bn(2).toRed(this.red); + + // Curve configuration, optional + this.n = conf.n && new bn(conf.n, 16); + this.g = conf.g && this.pointFromJSON(conf.g, conf.gRed); + + // Temporary arrays + this._wnafT1 = new Array(4); + this._wnafT2 = new Array(4); + this._wnafT3 = new Array(4); + this._wnafT4 = new Array(4); + + // Generalized Greg Maxwell's trick + var adjustCount = this.n && this.p.div(this.n); + if (!adjustCount || adjustCount.cmpn(100) > 0) { + this.redN = null; + } else { + this._maxwellTrick = true; + this.redN = this.n.toRed(this.red); + } + } + var base = BaseCurve; + + BaseCurve.prototype.point = function point() { + throw Error('Not implemented'); + }; + + BaseCurve.prototype.validate = function validate() { + throw Error('Not implemented'); + }; + + BaseCurve.prototype._fixedNafMul = function _fixedNafMul(p, k) { + assert$8(p.precomputed); + var doubles = p._getDoubles(); + + var naf = getNAF(k, 1); + var I = (1 << (doubles.step + 1)) - (doubles.step % 2 === 0 ? 2 : 1); + I /= 3; + + // Translate into more windowed form + var repr = []; + for (var j = 0; j < naf.length; j += doubles.step) { + var nafW = 0; + for (var k = j + doubles.step - 1; k >= j; k--) + nafW = (nafW << 1) + naf[k]; + repr.push(nafW); + } + + var a = this.jpoint(null, null, null); + var b = this.jpoint(null, null, null); + for (var i = I; i > 0; i--) { + for (var j = 0; j < repr.length; j++) { + var nafW = repr[j]; + if (nafW === i) + b = b.mixedAdd(doubles.points[j]); + else if (nafW === -i) + b = b.mixedAdd(doubles.points[j].neg()); + } + a = a.add(b); + } + return a.toP(); + }; + + BaseCurve.prototype._wnafMul = function _wnafMul(p, k) { + var w = 4; + + // Precompute window + var nafPoints = p._getNAFPoints(w); + w = nafPoints.wnd; + var wnd = nafPoints.points; + + // Get NAF form + var naf = getNAF(k, w); + + // Add `this`*(N+1) for every w-NAF index + var acc = this.jpoint(null, null, null); + for (var i = naf.length - 1; i >= 0; i--) { + // Count zeroes + for (var k = 0; i >= 0 && naf[i] === 0; i--) + k++; + if (i >= 0) + k++; + acc = acc.dblp(k); + + if (i < 0) + break; + var z = naf[i]; + assert$8(z !== 0); + if (p.type === 'affine') { + // J +- P + if (z > 0) + acc = acc.mixedAdd(wnd[(z - 1) >> 1]); + else + acc = acc.mixedAdd(wnd[(-z - 1) >> 1].neg()); + } else { + // J +- J + if (z > 0) + acc = acc.add(wnd[(z - 1) >> 1]); + else + acc = acc.add(wnd[(-z - 1) >> 1].neg()); + } + } + return p.type === 'affine' ? acc.toP() : acc; + }; + + BaseCurve.prototype._wnafMulAdd = function _wnafMulAdd(defW, + points, + coeffs, + len, + jacobianResult) { + var wndWidth = this._wnafT1; + var wnd = this._wnafT2; + var naf = this._wnafT3; + + // Fill all arrays + var max = 0; + for (var i = 0; i < len; i++) { + var p = points[i]; + var nafPoints = p._getNAFPoints(defW); + wndWidth[i] = nafPoints.wnd; + wnd[i] = nafPoints.points; + } + + // Comb small window NAFs + for (var i = len - 1; i >= 1; i -= 2) { + var a = i - 1; + var b = i; + if (wndWidth[a] !== 1 || wndWidth[b] !== 1) { + naf[a] = getNAF(coeffs[a], wndWidth[a]); + naf[b] = getNAF(coeffs[b], wndWidth[b]); + max = Math.max(naf[a].length, max); + max = Math.max(naf[b].length, max); + continue; + } + + var comb = [ + points[a], /* 1 */ + null, /* 3 */ + null, /* 5 */ + points[b] /* 7 */ + ]; + + // Try to avoid Projective points, if possible + if (points[a].y.cmp(points[b].y) === 0) { + comb[1] = points[a].add(points[b]); + comb[2] = points[a].toJ().mixedAdd(points[b].neg()); + } else if (points[a].y.cmp(points[b].y.redNeg()) === 0) { + comb[1] = points[a].toJ().mixedAdd(points[b]); + comb[2] = points[a].add(points[b].neg()); + } else { + comb[1] = points[a].toJ().mixedAdd(points[b]); + comb[2] = points[a].toJ().mixedAdd(points[b].neg()); + } + + var index = [ + -3, /* -1 -1 */ + -1, /* -1 0 */ + -5, /* -1 1 */ + -7, /* 0 -1 */ + 0, /* 0 0 */ + 7, /* 0 1 */ + 5, /* 1 -1 */ + 1, /* 1 0 */ + 3 /* 1 1 */ + ]; + + var jsf = getJSF(coeffs[a], coeffs[b]); + max = Math.max(jsf[0].length, max); + naf[a] = new Array(max); + naf[b] = new Array(max); + for (var j = 0; j < max; j++) { + var ja = jsf[0][j] | 0; + var jb = jsf[1][j] | 0; + + naf[a][j] = index[(ja + 1) * 3 + (jb + 1)]; + naf[b][j] = 0; + wnd[a] = comb; + } + } + + var acc = this.jpoint(null, null, null); + var tmp = this._wnafT4; + for (var i = max; i >= 0; i--) { + var k = 0; + + while (i >= 0) { + var zero = true; + for (var j = 0; j < len; j++) { + tmp[j] = naf[j][i] | 0; + if (tmp[j] !== 0) + zero = false; + } + if (!zero) + break; + k++; + i--; + } + if (i >= 0) + k++; + acc = acc.dblp(k); + if (i < 0) + break; + + for (var j = 0; j < len; j++) { + var z = tmp[j]; + var p; + if (z === 0) + continue; + else if (z > 0) + p = wnd[j][(z - 1) >> 1]; + else if (z < 0) + p = wnd[j][(-z - 1) >> 1].neg(); + + if (p.type === 'affine') + acc = acc.mixedAdd(p); + else + acc = acc.add(p); + } + } + // Zeroify references + for (var i = 0; i < len; i++) + wnd[i] = null; + + if (jacobianResult) + return acc; + else + return acc.toP(); + }; + + function BasePoint(curve, type) { + this.curve = curve; + this.type = type; + this.precomputed = null; + } + BaseCurve.BasePoint = BasePoint; + + BasePoint.prototype.eq = function eq(/*other*/) { + throw Error('Not implemented'); + }; + + BasePoint.prototype.validate = function validate() { + return this.curve.validate(this); + }; + + BaseCurve.prototype.decodePoint = function decodePoint(bytes, enc) { + bytes = utils_1.toArray(bytes, enc); + + var len = this.p.byteLength(); + + // uncompressed, hybrid-odd, hybrid-even + if ((bytes[0] === 0x04 || bytes[0] === 0x06 || bytes[0] === 0x07) && + bytes.length - 1 === 2 * len) { + if (bytes[0] === 0x06) + assert$8(bytes[bytes.length - 1] % 2 === 0); + else if (bytes[0] === 0x07) + assert$8(bytes[bytes.length - 1] % 2 === 1); + + var res = this.point(bytes.slice(1, 1 + len), + bytes.slice(1 + len, 1 + 2 * len)); + + return res; + } else if ((bytes[0] === 0x02 || bytes[0] === 0x03) && + bytes.length - 1 === len) { + return this.pointFromX(bytes.slice(1, 1 + len), bytes[0] === 0x03); + } + throw Error('Unknown point format'); + }; + + BasePoint.prototype.encodeCompressed = function encodeCompressed(enc) { + return this.encode(enc, true); + }; + + BasePoint.prototype._encode = function _encode(compact) { + var len = this.curve.p.byteLength(); + var x = this.getX().toArray('be', len); + + if (compact) + return [ this.getY().isEven() ? 0x02 : 0x03 ].concat(x); + + return [ 0x04 ].concat(x, this.getY().toArray('be', len)) ; + }; + + BasePoint.prototype.encode = function encode(enc, compact) { + return utils_1.encode(this._encode(compact), enc); + }; + + BasePoint.prototype.precompute = function precompute(power) { + if (this.precomputed) + return this; + + var precomputed = { + doubles: null, + naf: null, + beta: null + }; + precomputed.naf = this._getNAFPoints(8); + precomputed.doubles = this._getDoubles(4, power); + precomputed.beta = this._getBeta(); + this.precomputed = precomputed; + + return this; + }; + + BasePoint.prototype._hasDoubles = function _hasDoubles(k) { + if (!this.precomputed) + return false; + + var doubles = this.precomputed.doubles; + if (!doubles) + return false; + + return doubles.points.length >= Math.ceil((k.bitLength() + 1) / doubles.step); + }; + + BasePoint.prototype._getDoubles = function _getDoubles(step, power) { + if (this.precomputed && this.precomputed.doubles) + return this.precomputed.doubles; + + var doubles = [ this ]; + var acc = this; + for (var i = 0; i < power; i += step) { + for (var j = 0; j < step; j++) + acc = acc.dbl(); + doubles.push(acc); + } + return { + step: step, + points: doubles + }; + }; + + BasePoint.prototype._getNAFPoints = function _getNAFPoints(wnd) { + if (this.precomputed && this.precomputed.naf) + return this.precomputed.naf; + + var res = [ this ]; + var max = (1 << wnd) - 1; + var dbl = max === 1 ? null : this.dbl(); + for (var i = 1; i < max; i++) + res[i] = res[i - 1].add(dbl); + return { + wnd: wnd, + points: res + }; + }; + + BasePoint.prototype._getBeta = function _getBeta() { + return null; + }; + + BasePoint.prototype.dblp = function dblp(k) { + var r = this; + for (var i = 0; i < k; i++) + r = r.dbl(); + return r; + }; + + var assert$7 = utils_1.assert; + + function ShortCurve(conf) { + base.call(this, 'short', conf); + + this.a = new bn(conf.a, 16).toRed(this.red); + this.b = new bn(conf.b, 16).toRed(this.red); + this.tinv = this.two.redInvm(); + + this.zeroA = this.a.fromRed().cmpn(0) === 0; + this.threeA = this.a.fromRed().sub(this.p).cmpn(-3) === 0; + + // If the curve is endomorphic, precalculate beta and lambda + this.endo = this._getEndomorphism(conf); + this._endoWnafT1 = new Array(4); + this._endoWnafT2 = new Array(4); + } + inherits_browser(ShortCurve, base); + var short_1 = ShortCurve; + + ShortCurve.prototype._getEndomorphism = function _getEndomorphism(conf) { + // No efficient endomorphism + if (!this.zeroA || !this.g || !this.n || this.p.modn(3) !== 1) + return; + + // Compute beta and lambda, that lambda * P = (beta * Px; Py) + var beta; + var lambda; + if (conf.beta) { + beta = new bn(conf.beta, 16).toRed(this.red); + } else { + var betas = this._getEndoRoots(this.p); + // Choose the smallest beta + beta = betas[0].cmp(betas[1]) < 0 ? betas[0] : betas[1]; + beta = beta.toRed(this.red); + } + if (conf.lambda) { + lambda = new bn(conf.lambda, 16); + } else { + // Choose the lambda that is matching selected beta + var lambdas = this._getEndoRoots(this.n); + if (this.g.mul(lambdas[0]).x.cmp(this.g.x.redMul(beta)) === 0) { + lambda = lambdas[0]; + } else { + lambda = lambdas[1]; + assert$7(this.g.mul(lambda).x.cmp(this.g.x.redMul(beta)) === 0); + } + } + + // Get basis vectors, used for balanced length-two representation + var basis; + if (conf.basis) { + basis = conf.basis.map(function(vec) { + return { + a: new bn(vec.a, 16), + b: new bn(vec.b, 16) + }; + }); + } else { + basis = this._getEndoBasis(lambda); + } + + return { + beta: beta, + lambda: lambda, + basis: basis + }; + }; + + ShortCurve.prototype._getEndoRoots = function _getEndoRoots(num) { + // Find roots of for x^2 + x + 1 in F + // Root = (-1 +- Sqrt(-3)) / 2 + // + var red = num === this.p ? this.red : bn.mont(num); + var tinv = new bn(2).toRed(red).redInvm(); + var ntinv = tinv.redNeg(); + + var s = new bn(3).toRed(red).redNeg().redSqrt().redMul(tinv); + + var l1 = ntinv.redAdd(s).fromRed(); + var l2 = ntinv.redSub(s).fromRed(); + return [ l1, l2 ]; + }; + + ShortCurve.prototype._getEndoBasis = function _getEndoBasis(lambda) { + // aprxSqrt >= sqrt(this.n) + var aprxSqrt = this.n.ushrn(Math.floor(this.n.bitLength() / 2)); + + // 3.74 + // Run EGCD, until r(L + 1) < aprxSqrt + var u = lambda; + var v = this.n.clone(); + var x1 = new bn(1); + var y1 = new bn(0); + var x2 = new bn(0); + var y2 = new bn(1); + + // NOTE: all vectors are roots of: a + b * lambda = 0 (mod n) + var a0; + var b0; + // First vector + var a1; + var b1; + // Second vector + var a2; + var b2; + + var prevR; + var i = 0; + var r; + var x; + while (u.cmpn(0) !== 0) { + var q = v.div(u); + r = v.sub(q.mul(u)); + x = x2.sub(q.mul(x1)); + var y = y2.sub(q.mul(y1)); + + if (!a1 && r.cmp(aprxSqrt) < 0) { + a0 = prevR.neg(); + b0 = x1; + a1 = r.neg(); + b1 = x; + } else if (a1 && ++i === 2) { + break; + } + prevR = r; + + v = u; + u = r; + x2 = x1; + x1 = x; + y2 = y1; + y1 = y; + } + a2 = r.neg(); + b2 = x; + + var len1 = a1.sqr().add(b1.sqr()); + var len2 = a2.sqr().add(b2.sqr()); + if (len2.cmp(len1) >= 0) { + a2 = a0; + b2 = b0; + } + + // Normalize signs + if (a1.negative) { + a1 = a1.neg(); + b1 = b1.neg(); + } + if (a2.negative) { + a2 = a2.neg(); + b2 = b2.neg(); + } + + return [ + { a: a1, b: b1 }, + { a: a2, b: b2 } + ]; + }; + + ShortCurve.prototype._endoSplit = function _endoSplit(k) { + var basis = this.endo.basis; + var v1 = basis[0]; + var v2 = basis[1]; + + var c1 = v2.b.mul(k).divRound(this.n); + var c2 = v1.b.neg().mul(k).divRound(this.n); + + var p1 = c1.mul(v1.a); + var p2 = c2.mul(v2.a); + var q1 = c1.mul(v1.b); + var q2 = c2.mul(v2.b); + + // Calculate answer + var k1 = k.sub(p1).sub(p2); + var k2 = q1.add(q2).neg(); + return { k1: k1, k2: k2 }; + }; + + ShortCurve.prototype.pointFromX = function pointFromX(x, odd) { + x = new bn(x, 16); + if (!x.red) + x = x.toRed(this.red); + + var y2 = x.redSqr().redMul(x).redIAdd(x.redMul(this.a)).redIAdd(this.b); + var y = y2.redSqrt(); + if (y.redSqr().redSub(y2).cmp(this.zero) !== 0) + throw Error('invalid point'); + + // XXX Is there any way to tell if the number is odd without converting it + // to non-red form? + var isOdd = y.fromRed().isOdd(); + if (odd && !isOdd || !odd && isOdd) + y = y.redNeg(); + + return this.point(x, y); + }; + + ShortCurve.prototype.validate = function validate(point) { + if (point.inf) + return true; + + var x = point.x; + var y = point.y; + + var ax = this.a.redMul(x); + var rhs = x.redSqr().redMul(x).redIAdd(ax).redIAdd(this.b); + return y.redSqr().redISub(rhs).cmpn(0) === 0; + }; + + ShortCurve.prototype._endoWnafMulAdd = + function _endoWnafMulAdd(points, coeffs, jacobianResult) { + var npoints = this._endoWnafT1; + var ncoeffs = this._endoWnafT2; + for (var i = 0; i < points.length; i++) { + var split = this._endoSplit(coeffs[i]); + var p = points[i]; + var beta = p._getBeta(); + + if (split.k1.negative) { + split.k1.ineg(); + p = p.neg(true); + } + if (split.k2.negative) { + split.k2.ineg(); + beta = beta.neg(true); + } + + npoints[i * 2] = p; + npoints[i * 2 + 1] = beta; + ncoeffs[i * 2] = split.k1; + ncoeffs[i * 2 + 1] = split.k2; + } + var res = this._wnafMulAdd(1, npoints, ncoeffs, i * 2, jacobianResult); + + // Clean-up references to points and coefficients + for (var j = 0; j < i * 2; j++) { + npoints[j] = null; + ncoeffs[j] = null; + } + return res; + }; + + function Point$2(curve, x, y, isRed) { + base.BasePoint.call(this, curve, 'affine'); + if (x === null && y === null) { + this.x = null; + this.y = null; + this.inf = true; + } else { + this.x = new bn(x, 16); + this.y = new bn(y, 16); + // Force redgomery representation when loading from JSON + if (isRed) { + this.x.forceRed(this.curve.red); + this.y.forceRed(this.curve.red); + } + if (!this.x.red) + this.x = this.x.toRed(this.curve.red); + if (!this.y.red) + this.y = this.y.toRed(this.curve.red); + this.inf = false; + } + } + inherits_browser(Point$2, base.BasePoint); + + ShortCurve.prototype.point = function point(x, y, isRed) { + return new Point$2(this, x, y, isRed); + }; + + ShortCurve.prototype.pointFromJSON = function pointFromJSON(obj, red) { + return Point$2.fromJSON(this, obj, red); + }; + + Point$2.prototype._getBeta = function _getBeta() { + if (!this.curve.endo) + return; + + var pre = this.precomputed; + if (pre && pre.beta) + return pre.beta; + + var beta = this.curve.point(this.x.redMul(this.curve.endo.beta), this.y); + if (pre) { + var curve = this.curve; + var endoMul = function(p) { + return curve.point(p.x.redMul(curve.endo.beta), p.y); + }; + pre.beta = beta; + beta.precomputed = { + beta: null, + naf: pre.naf && { + wnd: pre.naf.wnd, + points: pre.naf.points.map(endoMul) + }, + doubles: pre.doubles && { + step: pre.doubles.step, + points: pre.doubles.points.map(endoMul) + } + }; + } + return beta; + }; + + Point$2.prototype.toJSON = function toJSON() { + if (!this.precomputed) + return [ this.x, this.y ]; + + return [ this.x, this.y, this.precomputed && { + doubles: this.precomputed.doubles && { + step: this.precomputed.doubles.step, + points: this.precomputed.doubles.points.slice(1) + }, + naf: this.precomputed.naf && { + wnd: this.precomputed.naf.wnd, + points: this.precomputed.naf.points.slice(1) + } + } ]; + }; + + Point$2.fromJSON = function fromJSON(curve, obj, red) { + if (typeof obj === 'string') + obj = JSON.parse(obj); + var res = curve.point(obj[0], obj[1], red); + if (!obj[2]) + return res; + + function obj2point(obj) { + return curve.point(obj[0], obj[1], red); + } + + var pre = obj[2]; + res.precomputed = { + beta: null, + doubles: pre.doubles && { + step: pre.doubles.step, + points: [ res ].concat(pre.doubles.points.map(obj2point)) + }, + naf: pre.naf && { + wnd: pre.naf.wnd, + points: [ res ].concat(pre.naf.points.map(obj2point)) + } + }; + return res; + }; + + Point$2.prototype.inspect = function inspect() { + if (this.isInfinity()) + return ''; + return ''; + }; + + Point$2.prototype.isInfinity = function isInfinity() { + return this.inf; + }; + + Point$2.prototype.add = function add(p) { + // O + P = P + if (this.inf) + return p; + + // P + O = P + if (p.inf) + return this; + + // P + P = 2P + if (this.eq(p)) + return this.dbl(); + + // P + (-P) = O + if (this.neg().eq(p)) + return this.curve.point(null, null); + + // P + Q = O + if (this.x.cmp(p.x) === 0) + return this.curve.point(null, null); + + var c = this.y.redSub(p.y); + if (c.cmpn(0) !== 0) + c = c.redMul(this.x.redSub(p.x).redInvm()); + var nx = c.redSqr().redISub(this.x).redISub(p.x); + var ny = c.redMul(this.x.redSub(nx)).redISub(this.y); + return this.curve.point(nx, ny); + }; + + Point$2.prototype.dbl = function dbl() { + if (this.inf) + return this; + + // 2P = O + var ys1 = this.y.redAdd(this.y); + if (ys1.cmpn(0) === 0) + return this.curve.point(null, null); + + var a = this.curve.a; + + var x2 = this.x.redSqr(); + var dyinv = ys1.redInvm(); + var c = x2.redAdd(x2).redIAdd(x2).redIAdd(a).redMul(dyinv); + + var nx = c.redSqr().redISub(this.x.redAdd(this.x)); + var ny = c.redMul(this.x.redSub(nx)).redISub(this.y); + return this.curve.point(nx, ny); + }; + + Point$2.prototype.getX = function getX() { + return this.x.fromRed(); + }; + + Point$2.prototype.getY = function getY() { + return this.y.fromRed(); + }; + + Point$2.prototype.mul = function mul(k) { + k = new bn(k, 16); + if (this.isInfinity()) + return this; + else if (this._hasDoubles(k)) + return this.curve._fixedNafMul(this, k); + else if (this.curve.endo) + return this.curve._endoWnafMulAdd([ this ], [ k ]); + else + return this.curve._wnafMul(this, k); + }; + + Point$2.prototype.mulAdd = function mulAdd(k1, p2, k2) { + var points = [ this, p2 ]; + var coeffs = [ k1, k2 ]; + if (this.curve.endo) + return this.curve._endoWnafMulAdd(points, coeffs); + else + return this.curve._wnafMulAdd(1, points, coeffs, 2); + }; + + Point$2.prototype.jmulAdd = function jmulAdd(k1, p2, k2) { + var points = [ this, p2 ]; + var coeffs = [ k1, k2 ]; + if (this.curve.endo) + return this.curve._endoWnafMulAdd(points, coeffs, true); + else + return this.curve._wnafMulAdd(1, points, coeffs, 2, true); + }; + + Point$2.prototype.eq = function eq(p) { + return this === p || + this.inf === p.inf && + (this.inf || this.x.cmp(p.x) === 0 && this.y.cmp(p.y) === 0); + }; + + Point$2.prototype.neg = function neg(_precompute) { + if (this.inf) + return this; + + var res = this.curve.point(this.x, this.y.redNeg()); + if (_precompute && this.precomputed) { + var pre = this.precomputed; + var negate = function(p) { + return p.neg(); + }; + res.precomputed = { + naf: pre.naf && { + wnd: pre.naf.wnd, + points: pre.naf.points.map(negate) + }, + doubles: pre.doubles && { + step: pre.doubles.step, + points: pre.doubles.points.map(negate) + } + }; + } + return res; + }; + + Point$2.prototype.toJ = function toJ() { + if (this.inf) + return this.curve.jpoint(null, null, null); + + var res = this.curve.jpoint(this.x, this.y, this.curve.one); + return res; + }; + + function JPoint(curve, x, y, z) { + base.BasePoint.call(this, curve, 'jacobian'); + if (x === null && y === null && z === null) { + this.x = this.curve.one; + this.y = this.curve.one; + this.z = new bn(0); + } else { + this.x = new bn(x, 16); + this.y = new bn(y, 16); + this.z = new bn(z, 16); + } + if (!this.x.red) + this.x = this.x.toRed(this.curve.red); + if (!this.y.red) + this.y = this.y.toRed(this.curve.red); + if (!this.z.red) + this.z = this.z.toRed(this.curve.red); + + this.zOne = this.z === this.curve.one; + } + inherits_browser(JPoint, base.BasePoint); + + ShortCurve.prototype.jpoint = function jpoint(x, y, z) { + return new JPoint(this, x, y, z); + }; + + JPoint.prototype.toP = function toP() { + if (this.isInfinity()) + return this.curve.point(null, null); + + var zinv = this.z.redInvm(); + var zinv2 = zinv.redSqr(); + var ax = this.x.redMul(zinv2); + var ay = this.y.redMul(zinv2).redMul(zinv); + + return this.curve.point(ax, ay); + }; + + JPoint.prototype.neg = function neg() { + return this.curve.jpoint(this.x, this.y.redNeg(), this.z); + }; + + JPoint.prototype.add = function add(p) { + // O + P = P + if (this.isInfinity()) + return p; + + // P + O = P + if (p.isInfinity()) + return this; + + // 12M + 4S + 7A + var pz2 = p.z.redSqr(); + var z2 = this.z.redSqr(); + var u1 = this.x.redMul(pz2); + var u2 = p.x.redMul(z2); + var s1 = this.y.redMul(pz2.redMul(p.z)); + var s2 = p.y.redMul(z2.redMul(this.z)); + + var h = u1.redSub(u2); + var r = s1.redSub(s2); + if (h.cmpn(0) === 0) { + if (r.cmpn(0) !== 0) + return this.curve.jpoint(null, null, null); + else + return this.dbl(); + } + + var h2 = h.redSqr(); + var h3 = h2.redMul(h); + var v = u1.redMul(h2); + + var nx = r.redSqr().redIAdd(h3).redISub(v).redISub(v); + var ny = r.redMul(v.redISub(nx)).redISub(s1.redMul(h3)); + var nz = this.z.redMul(p.z).redMul(h); + + return this.curve.jpoint(nx, ny, nz); + }; + + JPoint.prototype.mixedAdd = function mixedAdd(p) { + // O + P = P + if (this.isInfinity()) + return p.toJ(); + + // P + O = P + if (p.isInfinity()) + return this; + + // 8M + 3S + 7A + var z2 = this.z.redSqr(); + var u1 = this.x; + var u2 = p.x.redMul(z2); + var s1 = this.y; + var s2 = p.y.redMul(z2).redMul(this.z); + + var h = u1.redSub(u2); + var r = s1.redSub(s2); + if (h.cmpn(0) === 0) { + if (r.cmpn(0) !== 0) + return this.curve.jpoint(null, null, null); + else + return this.dbl(); + } + + var h2 = h.redSqr(); + var h3 = h2.redMul(h); + var v = u1.redMul(h2); + + var nx = r.redSqr().redIAdd(h3).redISub(v).redISub(v); + var ny = r.redMul(v.redISub(nx)).redISub(s1.redMul(h3)); + var nz = this.z.redMul(h); + + return this.curve.jpoint(nx, ny, nz); + }; + + JPoint.prototype.dblp = function dblp(pow) { + if (pow === 0) + return this; + if (this.isInfinity()) + return this; + if (!pow) + return this.dbl(); + + if (this.curve.zeroA || this.curve.threeA) { + var r = this; + for (var i = 0; i < pow; i++) + r = r.dbl(); + return r; + } + + // 1M + 2S + 1A + N * (4S + 5M + 8A) + // N = 1 => 6M + 6S + 9A + var a = this.curve.a; + var tinv = this.curve.tinv; + + var jx = this.x; + var jy = this.y; + var jz = this.z; + var jz4 = jz.redSqr().redSqr(); + + // Reuse results + var jyd = jy.redAdd(jy); + for (var i = 0; i < pow; i++) { + var jx2 = jx.redSqr(); + var jyd2 = jyd.redSqr(); + var jyd4 = jyd2.redSqr(); + var c = jx2.redAdd(jx2).redIAdd(jx2).redIAdd(a.redMul(jz4)); + + var t1 = jx.redMul(jyd2); + var nx = c.redSqr().redISub(t1.redAdd(t1)); + var t2 = t1.redISub(nx); + var dny = c.redMul(t2); + dny = dny.redIAdd(dny).redISub(jyd4); + var nz = jyd.redMul(jz); + if (i + 1 < pow) + jz4 = jz4.redMul(jyd4); + + jx = nx; + jz = nz; + jyd = dny; + } + + return this.curve.jpoint(jx, jyd.redMul(tinv), jz); + }; + + JPoint.prototype.dbl = function dbl() { + if (this.isInfinity()) + return this; + + if (this.curve.zeroA) + return this._zeroDbl(); + else if (this.curve.threeA) + return this._threeDbl(); + else + return this._dbl(); + }; + + JPoint.prototype._zeroDbl = function _zeroDbl() { + var nx; + var ny; + var nz; + // Z = 1 + if (this.zOne) { + // hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html + // #doubling-mdbl-2007-bl + // 1M + 5S + 14A + + // XX = X1^2 + var xx = this.x.redSqr(); + // YY = Y1^2 + var yy = this.y.redSqr(); + // YYYY = YY^2 + var yyyy = yy.redSqr(); + // S = 2 * ((X1 + YY)^2 - XX - YYYY) + var s = this.x.redAdd(yy).redSqr().redISub(xx).redISub(yyyy); + s = s.redIAdd(s); + // M = 3 * XX + a; a = 0 + var m = xx.redAdd(xx).redIAdd(xx); + // T = M ^ 2 - 2*S + var t = m.redSqr().redISub(s).redISub(s); + + // 8 * YYYY + var yyyy8 = yyyy.redIAdd(yyyy); + yyyy8 = yyyy8.redIAdd(yyyy8); + yyyy8 = yyyy8.redIAdd(yyyy8); + + // X3 = T + nx = t; + // Y3 = M * (S - T) - 8 * YYYY + ny = m.redMul(s.redISub(t)).redISub(yyyy8); + // Z3 = 2*Y1 + nz = this.y.redAdd(this.y); + } else { + // hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html + // #doubling-dbl-2009-l + // 2M + 5S + 13A + + // A = X1^2 + var a = this.x.redSqr(); + // B = Y1^2 + var b = this.y.redSqr(); + // C = B^2 + var c = b.redSqr(); + // D = 2 * ((X1 + B)^2 - A - C) + var d = this.x.redAdd(b).redSqr().redISub(a).redISub(c); + d = d.redIAdd(d); + // E = 3 * A + var e = a.redAdd(a).redIAdd(a); + // F = E^2 + var f = e.redSqr(); + + // 8 * C + var c8 = c.redIAdd(c); + c8 = c8.redIAdd(c8); + c8 = c8.redIAdd(c8); + + // X3 = F - 2 * D + nx = f.redISub(d).redISub(d); + // Y3 = E * (D - X3) - 8 * C + ny = e.redMul(d.redISub(nx)).redISub(c8); + // Z3 = 2 * Y1 * Z1 + nz = this.y.redMul(this.z); + nz = nz.redIAdd(nz); + } + + return this.curve.jpoint(nx, ny, nz); + }; + + JPoint.prototype._threeDbl = function _threeDbl() { + var nx; + var ny; + var nz; + // Z = 1 + if (this.zOne) { + // hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-3.html + // #doubling-mdbl-2007-bl + // 1M + 5S + 15A + + // XX = X1^2 + var xx = this.x.redSqr(); + // YY = Y1^2 + var yy = this.y.redSqr(); + // YYYY = YY^2 + var yyyy = yy.redSqr(); + // S = 2 * ((X1 + YY)^2 - XX - YYYY) + var s = this.x.redAdd(yy).redSqr().redISub(xx).redISub(yyyy); + s = s.redIAdd(s); + // M = 3 * XX + a + var m = xx.redAdd(xx).redIAdd(xx).redIAdd(this.curve.a); + // T = M^2 - 2 * S + var t = m.redSqr().redISub(s).redISub(s); + // X3 = T + nx = t; + // Y3 = M * (S - T) - 8 * YYYY + var yyyy8 = yyyy.redIAdd(yyyy); + yyyy8 = yyyy8.redIAdd(yyyy8); + yyyy8 = yyyy8.redIAdd(yyyy8); + ny = m.redMul(s.redISub(t)).redISub(yyyy8); + // Z3 = 2 * Y1 + nz = this.y.redAdd(this.y); + } else { + // hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-3.html#doubling-dbl-2001-b + // 3M + 5S + + // delta = Z1^2 + var delta = this.z.redSqr(); + // gamma = Y1^2 + var gamma = this.y.redSqr(); + // beta = X1 * gamma + var beta = this.x.redMul(gamma); + // alpha = 3 * (X1 - delta) * (X1 + delta) + var alpha = this.x.redSub(delta).redMul(this.x.redAdd(delta)); + alpha = alpha.redAdd(alpha).redIAdd(alpha); + // X3 = alpha^2 - 8 * beta + var beta4 = beta.redIAdd(beta); + beta4 = beta4.redIAdd(beta4); + var beta8 = beta4.redAdd(beta4); + nx = alpha.redSqr().redISub(beta8); + // Z3 = (Y1 + Z1)^2 - gamma - delta + nz = this.y.redAdd(this.z).redSqr().redISub(gamma).redISub(delta); + // Y3 = alpha * (4 * beta - X3) - 8 * gamma^2 + var ggamma8 = gamma.redSqr(); + ggamma8 = ggamma8.redIAdd(ggamma8); + ggamma8 = ggamma8.redIAdd(ggamma8); + ggamma8 = ggamma8.redIAdd(ggamma8); + ny = alpha.redMul(beta4.redISub(nx)).redISub(ggamma8); + } + + return this.curve.jpoint(nx, ny, nz); + }; + + JPoint.prototype._dbl = function _dbl() { + var a = this.curve.a; + + // 4M + 6S + 10A + var jx = this.x; + var jy = this.y; + var jz = this.z; + var jz4 = jz.redSqr().redSqr(); + + var jx2 = jx.redSqr(); + var jy2 = jy.redSqr(); + + var c = jx2.redAdd(jx2).redIAdd(jx2).redIAdd(a.redMul(jz4)); + + var jxd4 = jx.redAdd(jx); + jxd4 = jxd4.redIAdd(jxd4); + var t1 = jxd4.redMul(jy2); + var nx = c.redSqr().redISub(t1.redAdd(t1)); + var t2 = t1.redISub(nx); + + var jyd8 = jy2.redSqr(); + jyd8 = jyd8.redIAdd(jyd8); + jyd8 = jyd8.redIAdd(jyd8); + jyd8 = jyd8.redIAdd(jyd8); + var ny = c.redMul(t2).redISub(jyd8); + var nz = jy.redAdd(jy).redMul(jz); + + return this.curve.jpoint(nx, ny, nz); + }; + + JPoint.prototype.trpl = function trpl() { + if (!this.curve.zeroA) + return this.dbl().add(this); + + // hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#tripling-tpl-2007-bl + // 5M + 10S + ... + + // XX = X1^2 + var xx = this.x.redSqr(); + // YY = Y1^2 + var yy = this.y.redSqr(); + // ZZ = Z1^2 + var zz = this.z.redSqr(); + // YYYY = YY^2 + var yyyy = yy.redSqr(); + // M = 3 * XX + a * ZZ2; a = 0 + var m = xx.redAdd(xx).redIAdd(xx); + // MM = M^2 + var mm = m.redSqr(); + // E = 6 * ((X1 + YY)^2 - XX - YYYY) - MM + var e = this.x.redAdd(yy).redSqr().redISub(xx).redISub(yyyy); + e = e.redIAdd(e); + e = e.redAdd(e).redIAdd(e); + e = e.redISub(mm); + // EE = E^2 + var ee = e.redSqr(); + // T = 16*YYYY + var t = yyyy.redIAdd(yyyy); + t = t.redIAdd(t); + t = t.redIAdd(t); + t = t.redIAdd(t); + // U = (M + E)^2 - MM - EE - T + var u = m.redIAdd(e).redSqr().redISub(mm).redISub(ee).redISub(t); + // X3 = 4 * (X1 * EE - 4 * YY * U) + var yyu4 = yy.redMul(u); + yyu4 = yyu4.redIAdd(yyu4); + yyu4 = yyu4.redIAdd(yyu4); + var nx = this.x.redMul(ee).redISub(yyu4); + nx = nx.redIAdd(nx); + nx = nx.redIAdd(nx); + // Y3 = 8 * Y1 * (U * (T - U) - E * EE) + var ny = this.y.redMul(u.redMul(t.redISub(u)).redISub(e.redMul(ee))); + ny = ny.redIAdd(ny); + ny = ny.redIAdd(ny); + ny = ny.redIAdd(ny); + // Z3 = (Z1 + E)^2 - ZZ - EE + var nz = this.z.redAdd(e).redSqr().redISub(zz).redISub(ee); + + return this.curve.jpoint(nx, ny, nz); + }; + + JPoint.prototype.mul = function mul(k, kbase) { + k = new bn(k, kbase); + + return this.curve._wnafMul(this, k); + }; + + JPoint.prototype.eq = function eq(p) { + if (p.type === 'affine') + return this.eq(p.toJ()); + + if (this === p) + return true; + + // x1 * z2^2 == x2 * z1^2 + var z2 = this.z.redSqr(); + var pz2 = p.z.redSqr(); + if (this.x.redMul(pz2).redISub(p.x.redMul(z2)).cmpn(0) !== 0) + return false; + + // y1 * z2^3 == y2 * z1^3 + var z3 = z2.redMul(this.z); + var pz3 = pz2.redMul(p.z); + return this.y.redMul(pz3).redISub(p.y.redMul(z3)).cmpn(0) === 0; + }; + + JPoint.prototype.eqXToP = function eqXToP(x) { + var zs = this.z.redSqr(); + var rx = x.toRed(this.curve.red).redMul(zs); + if (this.x.cmp(rx) === 0) + return true; + + var xc = x.clone(); + var t = this.curve.redN.redMul(zs); + for (;;) { + xc.iadd(this.curve.n); + if (xc.cmp(this.curve.p) >= 0) + return false; + + rx.redIAdd(t); + if (this.x.cmp(rx) === 0) + return true; + } + }; + + JPoint.prototype.inspect = function inspect() { + if (this.isInfinity()) + return ''; + return ''; + }; + + JPoint.prototype.isInfinity = function isInfinity() { + // XXX This code assumes that zero is always zero in red + return this.z.cmpn(0) === 0; + }; + + function MontCurve(conf) { + base.call(this, 'mont', conf); + + this.a = new bn(conf.a, 16).toRed(this.red); + this.b = new bn(conf.b, 16).toRed(this.red); + this.i4 = new bn(4).toRed(this.red).redInvm(); + this.two = new bn(2).toRed(this.red); + // Note: this implementation is according to the original paper + // by P. Montgomery, NOT the one by D. J. Bernstein. + this.a24 = this.i4.redMul(this.a.redAdd(this.two)); + } + inherits_browser(MontCurve, base); + var mont = MontCurve; + + MontCurve.prototype.validate = function validate(point) { + var x = point.normalize().x; + var x2 = x.redSqr(); + var rhs = x2.redMul(x).redAdd(x2.redMul(this.a)).redAdd(x); + var y = rhs.redSqrt(); + + return y.redSqr().cmp(rhs) === 0; + }; + + function Point$1(curve, x, z) { + base.BasePoint.call(this, curve, 'projective'); + if (x === null && z === null) { + this.x = this.curve.one; + this.z = this.curve.zero; + } else { + this.x = new bn(x, 16); + this.z = new bn(z, 16); + if (!this.x.red) + this.x = this.x.toRed(this.curve.red); + if (!this.z.red) + this.z = this.z.toRed(this.curve.red); + } + } + inherits_browser(Point$1, base.BasePoint); + + MontCurve.prototype.decodePoint = function decodePoint(bytes, enc) { + var bytes = utils_1.toArray(bytes, enc); + + // TODO Curve448 + // Montgomery curve points must be represented in the compressed format + // https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-02#appendix-B + if (bytes.length === 33 && bytes[0] === 0x40) + bytes = bytes.slice(1, 33).reverse(); // point must be little-endian + if (bytes.length !== 32) + throw Error('Unknown point compression format'); + return this.point(bytes, 1); + }; + + MontCurve.prototype.point = function point(x, z) { + return new Point$1(this, x, z); + }; + + MontCurve.prototype.pointFromJSON = function pointFromJSON(obj) { + return Point$1.fromJSON(this, obj); + }; + + Point$1.prototype.precompute = function precompute() { + // No-op + }; + + Point$1.prototype._encode = function _encode(compact) { + var len = this.curve.p.byteLength(); + + // Note: the output should always be little-endian + // https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-02#appendix-B + if (compact) { + return [ 0x40 ].concat(this.getX().toArray('le', len)); + } else { + return this.getX().toArray('be', len); + } + }; + + Point$1.fromJSON = function fromJSON(curve, obj) { + return new Point$1(curve, obj[0], obj[1] || curve.one); + }; + + Point$1.prototype.inspect = function inspect() { + if (this.isInfinity()) + return ''; + return ''; + }; + + Point$1.prototype.isInfinity = function isInfinity() { + // XXX This code assumes that zero is always zero in red + return this.z.cmpn(0) === 0; + }; + + Point$1.prototype.dbl = function dbl() { + // http://hyperelliptic.org/EFD/g1p/auto-montgom-xz.html#doubling-dbl-1987-m-3 + // 2M + 2S + 4A + + // A = X1 + Z1 + var a = this.x.redAdd(this.z); + // AA = A^2 + var aa = a.redSqr(); + // B = X1 - Z1 + var b = this.x.redSub(this.z); + // BB = B^2 + var bb = b.redSqr(); + // C = AA - BB + var c = aa.redSub(bb); + // X3 = AA * BB + var nx = aa.redMul(bb); + // Z3 = C * (BB + A24 * C) + var nz = c.redMul(bb.redAdd(this.curve.a24.redMul(c))); + return this.curve.point(nx, nz); + }; + + Point$1.prototype.add = function add() { + throw Error('Not supported on Montgomery curve'); + }; + + Point$1.prototype.diffAdd = function diffAdd(p, diff) { + // http://hyperelliptic.org/EFD/g1p/auto-montgom-xz.html#diffadd-dadd-1987-m-3 + // 4M + 2S + 6A + + // A = X2 + Z2 + var a = this.x.redAdd(this.z); + // B = X2 - Z2 + var b = this.x.redSub(this.z); + // C = X3 + Z3 + var c = p.x.redAdd(p.z); + // D = X3 - Z3 + var d = p.x.redSub(p.z); + // DA = D * A + var da = d.redMul(a); + // CB = C * B + var cb = c.redMul(b); + // X5 = Z1 * (DA + CB)^2 + var nx = diff.z.redMul(da.redAdd(cb).redSqr()); + // Z5 = X1 * (DA - CB)^2 + var nz = diff.x.redMul(da.redISub(cb).redSqr()); + return this.curve.point(nx, nz); + }; + + Point$1.prototype.mul = function mul(k) { + k = new bn(k, 16); + + var t = k.clone(); + var a = this; // (N / 2) * Q + Q + var b = this.curve.point(null, null); // (N / 2) * Q + var c = this; // Q + + for (var bits = []; t.cmpn(0) !== 0; t.iushrn(1)) + bits.push(t.andln(1)); + + for (var i = bits.length - 1; i >= 0; i--) { + if (bits[i] === 0) { + // N * Q + Q = ((N / 2) * Q + Q)) + (N / 2) * Q + a = a.diffAdd(b, c); + // N * Q = 2 * ((N / 2) * Q + Q)) + b = b.dbl(); + } else { + // N * Q = ((N / 2) * Q + Q) + ((N / 2) * Q) + b = a.diffAdd(b, c); + // N * Q + Q = 2 * ((N / 2) * Q + Q) + a = a.dbl(); + } + } + return b; + }; + + Point$1.prototype.mulAdd = function mulAdd() { + throw Error('Not supported on Montgomery curve'); + }; + + Point$1.prototype.jumlAdd = function jumlAdd() { + throw Error('Not supported on Montgomery curve'); + }; + + Point$1.prototype.eq = function eq(other) { + return this.getX().cmp(other.getX()) === 0; + }; + + Point$1.prototype.normalize = function normalize() { + this.x = this.x.redMul(this.z.redInvm()); + this.z = this.curve.one; + return this; + }; + + Point$1.prototype.getX = function getX() { + // Normalize coordinates + this.normalize(); + + return this.x.fromRed(); + }; + + var assert$6 = utils_1.assert; + + function EdwardsCurve(conf) { + // NOTE: Important as we are creating point in Base.call() + this.twisted = (conf.a | 0) !== 1; + this.mOneA = this.twisted && (conf.a | 0) === -1; + this.extended = this.mOneA; + + base.call(this, 'edwards', conf); + + this.a = new bn(conf.a, 16).umod(this.red.m); + this.a = this.a.toRed(this.red); + this.c = new bn(conf.c, 16).toRed(this.red); + this.c2 = this.c.redSqr(); + this.d = new bn(conf.d, 16).toRed(this.red); + this.dd = this.d.redAdd(this.d); + + assert$6(!this.twisted || this.c.fromRed().cmpn(1) === 0); + this.oneC = (conf.c | 0) === 1; + } + inherits_browser(EdwardsCurve, base); + var edwards = EdwardsCurve; + + EdwardsCurve.prototype._mulA = function _mulA(num) { + if (this.mOneA) + return num.redNeg(); + else + return this.a.redMul(num); + }; + + EdwardsCurve.prototype._mulC = function _mulC(num) { + if (this.oneC) + return num; + else + return this.c.redMul(num); + }; + + // Just for compatibility with Short curve + EdwardsCurve.prototype.jpoint = function jpoint(x, y, z, t) { + return this.point(x, y, z, t); + }; + + EdwardsCurve.prototype.pointFromX = function pointFromX(x, odd) { + x = new bn(x, 16); + if (!x.red) + x = x.toRed(this.red); + + var x2 = x.redSqr(); + var rhs = this.c2.redSub(this.a.redMul(x2)); + var lhs = this.one.redSub(this.c2.redMul(this.d).redMul(x2)); + + var y2 = rhs.redMul(lhs.redInvm()); + var y = y2.redSqrt(); + if (y.redSqr().redSub(y2).cmp(this.zero) !== 0) + throw Error('invalid point'); + + var isOdd = y.fromRed().isOdd(); + if (odd && !isOdd || !odd && isOdd) + y = y.redNeg(); + + return this.point(x, y); + }; + + EdwardsCurve.prototype.pointFromY = function pointFromY(y, odd) { + y = new bn(y, 16); + if (!y.red) + y = y.toRed(this.red); + + // x^2 = (y^2 - c^2) / (c^2 d y^2 - a) + var y2 = y.redSqr(); + var lhs = y2.redSub(this.c2); + var rhs = y2.redMul(this.d).redMul(this.c2).redSub(this.a); + var x2 = lhs.redMul(rhs.redInvm()); + + if (x2.cmp(this.zero) === 0) { + if (odd) + throw Error('invalid point'); + else + return this.point(this.zero, y); + } + + var x = x2.redSqrt(); + if (x.redSqr().redSub(x2).cmp(this.zero) !== 0) + throw Error('invalid point'); + + if (x.fromRed().isOdd() !== odd) + x = x.redNeg(); + + return this.point(x, y); + }; + + EdwardsCurve.prototype.validate = function validate(point) { + if (point.isInfinity()) + return true; + + // Curve: A * X^2 + Y^2 = C^2 * (1 + D * X^2 * Y^2) + point.normalize(); + + var x2 = point.x.redSqr(); + var y2 = point.y.redSqr(); + var lhs = x2.redMul(this.a).redAdd(y2); + var rhs = this.c2.redMul(this.one.redAdd(this.d.redMul(x2).redMul(y2))); + + return lhs.cmp(rhs) === 0; + }; + + function Point(curve, x, y, z, t) { + base.BasePoint.call(this, curve, 'projective'); + if (x === null && y === null && z === null) { + this.x = this.curve.zero; + this.y = this.curve.one; + this.z = this.curve.one; + this.t = this.curve.zero; + this.zOne = true; + } else { + this.x = new bn(x, 16); + this.y = new bn(y, 16); + this.z = z ? new bn(z, 16) : this.curve.one; + this.t = t && new bn(t, 16); + if (!this.x.red) + this.x = this.x.toRed(this.curve.red); + if (!this.y.red) + this.y = this.y.toRed(this.curve.red); + if (!this.z.red) + this.z = this.z.toRed(this.curve.red); + if (this.t && !this.t.red) + this.t = this.t.toRed(this.curve.red); + this.zOne = this.z === this.curve.one; + + // Use extended coordinates + if (this.curve.extended && !this.t) { + this.t = this.x.redMul(this.y); + if (!this.zOne) + this.t = this.t.redMul(this.z.redInvm()); + } + } + } + inherits_browser(Point, base.BasePoint); + + EdwardsCurve.prototype.pointFromJSON = function pointFromJSON(obj) { + return Point.fromJSON(this, obj); + }; + + EdwardsCurve.prototype.point = function point(x, y, z, t) { + return new Point(this, x, y, z, t); + }; + + Point.fromJSON = function fromJSON(curve, obj) { + return new Point(curve, obj[0], obj[1], obj[2]); + }; + + Point.prototype.inspect = function inspect() { + if (this.isInfinity()) + return ''; + return ''; + }; + + Point.prototype.isInfinity = function isInfinity() { + // XXX This code assumes that zero is always zero in red + return this.x.cmpn(0) === 0 && + (this.y.cmp(this.z) === 0 || + (this.zOne && this.y.cmp(this.curve.c) === 0)); + }; + + Point.prototype._extDbl = function _extDbl() { + // hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html + // #doubling-dbl-2008-hwcd + // 4M + 4S + + // A = X1^2 + var a = this.x.redSqr(); + // B = Y1^2 + var b = this.y.redSqr(); + // C = 2 * Z1^2 + var c = this.z.redSqr(); + c = c.redIAdd(c); + // D = a * A + var d = this.curve._mulA(a); + // E = (X1 + Y1)^2 - A - B + var e = this.x.redAdd(this.y).redSqr().redISub(a).redISub(b); + // G = D + B + var g = d.redAdd(b); + // F = G - C + var f = g.redSub(c); + // H = D - B + var h = d.redSub(b); + // X3 = E * F + var nx = e.redMul(f); + // Y3 = G * H + var ny = g.redMul(h); + // T3 = E * H + var nt = e.redMul(h); + // Z3 = F * G + var nz = f.redMul(g); + return this.curve.point(nx, ny, nz, nt); + }; + + Point.prototype._projDbl = function _projDbl() { + // hyperelliptic.org/EFD/g1p/auto-twisted-projective.html + // #doubling-dbl-2008-bbjlp + // #doubling-dbl-2007-bl + // and others + // Generally 3M + 4S or 2M + 4S + + // B = (X1 + Y1)^2 + var b = this.x.redAdd(this.y).redSqr(); + // C = X1^2 + var c = this.x.redSqr(); + // D = Y1^2 + var d = this.y.redSqr(); + + var nx; + var ny; + var nz; + if (this.curve.twisted) { + // E = a * C + var e = this.curve._mulA(c); + // F = E + D + var f = e.redAdd(d); + if (this.zOne) { + // X3 = (B - C - D) * (F - 2) + nx = b.redSub(c).redSub(d).redMul(f.redSub(this.curve.two)); + // Y3 = F * (E - D) + ny = f.redMul(e.redSub(d)); + // Z3 = F^2 - 2 * F + nz = f.redSqr().redSub(f).redSub(f); + } else { + // H = Z1^2 + var h = this.z.redSqr(); + // J = F - 2 * H + var j = f.redSub(h).redISub(h); + // X3 = (B-C-D)*J + nx = b.redSub(c).redISub(d).redMul(j); + // Y3 = F * (E - D) + ny = f.redMul(e.redSub(d)); + // Z3 = F * J + nz = f.redMul(j); + } + } else { + // E = C + D + var e = c.redAdd(d); + // H = (c * Z1)^2 + var h = this.curve._mulC(this.z).redSqr(); + // J = E - 2 * H + var j = e.redSub(h).redSub(h); + // X3 = c * (B - E) * J + nx = this.curve._mulC(b.redISub(e)).redMul(j); + // Y3 = c * E * (C - D) + ny = this.curve._mulC(e).redMul(c.redISub(d)); + // Z3 = E * J + nz = e.redMul(j); + } + return this.curve.point(nx, ny, nz); + }; + + Point.prototype.dbl = function dbl() { + if (this.isInfinity()) + return this; + + // Double in extended coordinates + if (this.curve.extended) + return this._extDbl(); + else + return this._projDbl(); + }; + + Point.prototype._extAdd = function _extAdd(p) { + // hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html + // #addition-add-2008-hwcd-3 + // 8M + + // A = (Y1 - X1) * (Y2 - X2) + var a = this.y.redSub(this.x).redMul(p.y.redSub(p.x)); + // B = (Y1 + X1) * (Y2 + X2) + var b = this.y.redAdd(this.x).redMul(p.y.redAdd(p.x)); + // C = T1 * k * T2 + var c = this.t.redMul(this.curve.dd).redMul(p.t); + // D = Z1 * 2 * Z2 + var d = this.z.redMul(p.z.redAdd(p.z)); + // E = B - A + var e = b.redSub(a); + // F = D - C + var f = d.redSub(c); + // G = D + C + var g = d.redAdd(c); + // H = B + A + var h = b.redAdd(a); + // X3 = E * F + var nx = e.redMul(f); + // Y3 = G * H + var ny = g.redMul(h); + // T3 = E * H + var nt = e.redMul(h); + // Z3 = F * G + var nz = f.redMul(g); + return this.curve.point(nx, ny, nz, nt); + }; + + Point.prototype._projAdd = function _projAdd(p) { + // hyperelliptic.org/EFD/g1p/auto-twisted-projective.html + // #addition-add-2008-bbjlp + // #addition-add-2007-bl + // 10M + 1S + + // A = Z1 * Z2 + var a = this.z.redMul(p.z); + // B = A^2 + var b = a.redSqr(); + // C = X1 * X2 + var c = this.x.redMul(p.x); + // D = Y1 * Y2 + var d = this.y.redMul(p.y); + // E = d * C * D + var e = this.curve.d.redMul(c).redMul(d); + // F = B - E + var f = b.redSub(e); + // G = B + E + var g = b.redAdd(e); + // X3 = A * F * ((X1 + Y1) * (X2 + Y2) - C - D) + var tmp = this.x.redAdd(this.y).redMul(p.x.redAdd(p.y)).redISub(c).redISub(d); + var nx = a.redMul(f).redMul(tmp); + var ny; + var nz; + if (this.curve.twisted) { + // Y3 = A * G * (D - a * C) + ny = a.redMul(g).redMul(d.redSub(this.curve._mulA(c))); + // Z3 = F * G + nz = f.redMul(g); + } else { + // Y3 = A * G * (D - C) + ny = a.redMul(g).redMul(d.redSub(c)); + // Z3 = c * F * G + nz = this.curve._mulC(f).redMul(g); + } + return this.curve.point(nx, ny, nz); + }; + + Point.prototype.add = function add(p) { + if (this.isInfinity()) + return p; + if (p.isInfinity()) + return this; + + if (this.curve.extended) + return this._extAdd(p); + else + return this._projAdd(p); + }; + + Point.prototype.mul = function mul(k) { + if (this._hasDoubles(k)) + return this.curve._fixedNafMul(this, k); + else + return this.curve._wnafMul(this, k); + }; + + Point.prototype.mulAdd = function mulAdd(k1, p, k2) { + return this.curve._wnafMulAdd(1, [ this, p ], [ k1, k2 ], 2, false); + }; + + Point.prototype.jmulAdd = function jmulAdd(k1, p, k2) { + return this.curve._wnafMulAdd(1, [ this, p ], [ k1, k2 ], 2, true); + }; + + Point.prototype.normalize = function normalize() { + if (this.zOne) + return this; + + // Normalize coordinates + var zi = this.z.redInvm(); + this.x = this.x.redMul(zi); + this.y = this.y.redMul(zi); + if (this.t) + this.t = this.t.redMul(zi); + this.z = this.curve.one; + this.zOne = true; + return this; + }; + + Point.prototype.neg = function neg() { + return this.curve.point(this.x.redNeg(), + this.y, + this.z, + this.t && this.t.redNeg()); + }; + + Point.prototype.getX = function getX() { + this.normalize(); + return this.x.fromRed(); + }; + + Point.prototype.getY = function getY() { + this.normalize(); + return this.y.fromRed(); + }; + + Point.prototype.eq = function eq(other) { + return this === other || + this.getX().cmp(other.getX()) === 0 && + this.getY().cmp(other.getY()) === 0; + }; + + Point.prototype.eqXToP = function eqXToP(x) { + var rx = x.toRed(this.curve.red).redMul(this.z); + if (this.x.cmp(rx) === 0) + return true; + + var xc = x.clone(); + var t = this.curve.redN.redMul(this.z); + for (;;) { + xc.iadd(this.curve.n); + if (xc.cmp(this.curve.p) >= 0) + return false; + + rx.redIAdd(t); + if (this.x.cmp(rx) === 0) + return true; + } + }; + + // Compatibility with BaseCurve + Point.prototype.toP = Point.prototype.normalize; + Point.prototype.mixedAdd = Point.prototype.add; + + var curve_1 = createCommonjsModule(function (module, exports) { + + var curve = exports; + + curve.base = base; + curve.short = short_1; + curve.mont = mont; + curve.edwards = edwards; + }); + + var rotl32 = utils.rotl32; + var sum32 = utils.sum32; + var sum32_5 = utils.sum32_5; + var ft_1 = common.ft_1; + var BlockHash = common$1.BlockHash; + + var sha1_K = [ + 0x5A827999, 0x6ED9EBA1, + 0x8F1BBCDC, 0xCA62C1D6 + ]; + + function SHA1() { + if (!(this instanceof SHA1)) + return new SHA1(); + + BlockHash.call(this); + this.h = [ + 0x67452301, 0xefcdab89, 0x98badcfe, + 0x10325476, 0xc3d2e1f0 ]; + this.W = new Array(80); + } + + utils.inherits(SHA1, BlockHash); + var _1 = SHA1; + + SHA1.blockSize = 512; + SHA1.outSize = 160; + SHA1.hmacStrength = 80; + SHA1.padLength = 64; + + SHA1.prototype._update = function _update(msg, start) { + var W = this.W; + + for (var i = 0; i < 16; i++) + W[i] = msg[start + i]; + + for(; i < W.length; i++) + W[i] = rotl32(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1); + + var a = this.h[0]; + var b = this.h[1]; + var c = this.h[2]; + var d = this.h[3]; + var e = this.h[4]; + + for (i = 0; i < W.length; i++) { + var s = ~~(i / 20); + var t = sum32_5(rotl32(a, 5), ft_1(s, b, c, d), e, W[i], sha1_K[s]); + e = d; + d = c; + c = rotl32(b, 30); + b = a; + a = t; + } + + this.h[0] = sum32(this.h[0], a); + this.h[1] = sum32(this.h[1], b); + this.h[2] = sum32(this.h[2], c); + this.h[3] = sum32(this.h[3], d); + this.h[4] = sum32(this.h[4], e); + }; + + SHA1.prototype._digest = function digest(enc) { + if (enc === 'hex') + return utils.toHex32(this.h, 'big'); + else + return utils.split32(this.h, 'big'); + }; + + var sha1 = _1; + var sha224 = _224; + var sha256 = _256; + var sha384 = _384; + var sha512 = _512; + + var sha = { + sha1: sha1, + sha224: sha224, + sha256: sha256, + sha384: sha384, + sha512: sha512 + }; + + function Hmac(hash, key, enc) { + if (!(this instanceof Hmac)) + return new Hmac(hash, key, enc); + this.Hash = hash; + this.blockSize = hash.blockSize / 8; + this.outSize = hash.outSize / 8; + this.inner = null; + this.outer = null; + + this._init(utils.toArray(key, enc)); + } + var hmac = Hmac; + + Hmac.prototype._init = function init(key) { + // Shorten key, if needed + if (key.length > this.blockSize) + key = new this.Hash().update(key).digest(); + minimalisticAssert(key.length <= this.blockSize); + + // Add padding to key + for (var i = key.length; i < this.blockSize; i++) + key.push(0); + + for (i = 0; i < key.length; i++) + key[i] ^= 0x36; + this.inner = new this.Hash().update(key); + + // 0x36 ^ 0x5c = 0x6a + for (i = 0; i < key.length; i++) + key[i] ^= 0x6a; + this.outer = new this.Hash().update(key); + }; + + Hmac.prototype.update = function update(msg, enc) { + this.inner.update(msg, enc); + return this; + }; + + Hmac.prototype.digest = function digest(enc) { + this.outer.update(this.inner.digest()); + return this.outer.digest(enc); + }; + + var hash_1 = createCommonjsModule(function (module, exports) { + var hash = exports; + + hash.utils = utils; + hash.common = common$1; + hash.sha = sha; + hash.ripemd = ripemd; + hash.hmac = hmac; + + // Proxy hash functions to the main object + hash.sha1 = hash.sha.sha1; + hash.sha256 = hash.sha.sha256; + hash.sha224 = hash.sha.sha224; + hash.sha384 = hash.sha.sha384; + hash.sha512 = hash.sha.sha512; + hash.ripemd160 = hash.ripemd.ripemd160; + }); + + var secp256k1 = { + doubles: { + step: 4, + points: [ + [ + 'e60fce93b59e9ec53011aabc21c23e97b2a31369b87a5ae9c44ee89e2a6dec0a', + 'f7e3507399e595929db99f34f57937101296891e44d23f0be1f32cce69616821' + ], + [ + '8282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508', + '11f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf' + ], + [ + '175e159f728b865a72f99cc6c6fc846de0b93833fd2222ed73fce5b551e5b739', + 'd3506e0d9e3c79eba4ef97a51ff71f5eacb5955add24345c6efa6ffee9fed695' + ], + [ + '363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640', + '4e273adfc732221953b445397f3363145b9a89008199ecb62003c7f3bee9de9' + ], + [ + '8b4b5f165df3c2be8c6244b5b745638843e4a781a15bcd1b69f79a55dffdf80c', + '4aad0a6f68d308b4b3fbd7813ab0da04f9e336546162ee56b3eff0c65fd4fd36' + ], + [ + '723cbaa6e5db996d6bf771c00bd548c7b700dbffa6c0e77bcb6115925232fcda', + '96e867b5595cc498a921137488824d6e2660a0653779494801dc069d9eb39f5f' + ], + [ + 'eebfa4d493bebf98ba5feec812c2d3b50947961237a919839a533eca0e7dd7fa', + '5d9a8ca3970ef0f269ee7edaf178089d9ae4cdc3a711f712ddfd4fdae1de8999' + ], + [ + '100f44da696e71672791d0a09b7bde459f1215a29b3c03bfefd7835b39a48db0', + 'cdd9e13192a00b772ec8f3300c090666b7ff4a18ff5195ac0fbd5cd62bc65a09' + ], + [ + 'e1031be262c7ed1b1dc9227a4a04c017a77f8d4464f3b3852c8acde6e534fd2d', + '9d7061928940405e6bb6a4176597535af292dd419e1ced79a44f18f29456a00d' + ], + [ + 'feea6cae46d55b530ac2839f143bd7ec5cf8b266a41d6af52d5e688d9094696d', + 'e57c6b6c97dce1bab06e4e12bf3ecd5c981c8957cc41442d3155debf18090088' + ], + [ + 'da67a91d91049cdcb367be4be6ffca3cfeed657d808583de33fa978bc1ec6cb1', + '9bacaa35481642bc41f463f7ec9780e5dec7adc508f740a17e9ea8e27a68be1d' + ], + [ + '53904faa0b334cdda6e000935ef22151ec08d0f7bb11069f57545ccc1a37b7c0', + '5bc087d0bc80106d88c9eccac20d3c1c13999981e14434699dcb096b022771c8' + ], + [ + '8e7bcd0bd35983a7719cca7764ca906779b53a043a9b8bcaeff959f43ad86047', + '10b7770b2a3da4b3940310420ca9514579e88e2e47fd68b3ea10047e8460372a' + ], + [ + '385eed34c1cdff21e6d0818689b81bde71a7f4f18397e6690a841e1599c43862', + '283bebc3e8ea23f56701de19e9ebf4576b304eec2086dc8cc0458fe5542e5453' + ], + [ + '6f9d9b803ecf191637c73a4413dfa180fddf84a5947fbc9c606ed86c3fac3a7', + '7c80c68e603059ba69b8e2a30e45c4d47ea4dd2f5c281002d86890603a842160' + ], + [ + '3322d401243c4e2582a2147c104d6ecbf774d163db0f5e5313b7e0e742d0e6bd', + '56e70797e9664ef5bfb019bc4ddaf9b72805f63ea2873af624f3a2e96c28b2a0' + ], + [ + '85672c7d2de0b7da2bd1770d89665868741b3f9af7643397721d74d28134ab83', + '7c481b9b5b43b2eb6374049bfa62c2e5e77f17fcc5298f44c8e3094f790313a6' + ], + [ + '948bf809b1988a46b06c9f1919413b10f9226c60f668832ffd959af60c82a0a', + '53a562856dcb6646dc6b74c5d1c3418c6d4dff08c97cd2bed4cb7f88d8c8e589' + ], + [ + '6260ce7f461801c34f067ce0f02873a8f1b0e44dfc69752accecd819f38fd8e8', + 'bc2da82b6fa5b571a7f09049776a1ef7ecd292238051c198c1a84e95b2b4ae17' + ], + [ + 'e5037de0afc1d8d43d8348414bbf4103043ec8f575bfdc432953cc8d2037fa2d', + '4571534baa94d3b5f9f98d09fb990bddbd5f5b03ec481f10e0e5dc841d755bda' + ], + [ + 'e06372b0f4a207adf5ea905e8f1771b4e7e8dbd1c6a6c5b725866a0ae4fce725', + '7a908974bce18cfe12a27bb2ad5a488cd7484a7787104870b27034f94eee31dd' + ], + [ + '213c7a715cd5d45358d0bbf9dc0ce02204b10bdde2a3f58540ad6908d0559754', + '4b6dad0b5ae462507013ad06245ba190bb4850f5f36a7eeddff2c27534b458f2' + ], + [ + '4e7c272a7af4b34e8dbb9352a5419a87e2838c70adc62cddf0cc3a3b08fbd53c', + '17749c766c9d0b18e16fd09f6def681b530b9614bff7dd33e0b3941817dcaae6' + ], + [ + 'fea74e3dbe778b1b10f238ad61686aa5c76e3db2be43057632427e2840fb27b6', + '6e0568db9b0b13297cf674deccb6af93126b596b973f7b77701d3db7f23cb96f' + ], + [ + '76e64113f677cf0e10a2570d599968d31544e179b760432952c02a4417bdde39', + 'c90ddf8dee4e95cf577066d70681f0d35e2a33d2b56d2032b4b1752d1901ac01' + ], + [ + 'c738c56b03b2abe1e8281baa743f8f9a8f7cc643df26cbee3ab150242bcbb891', + '893fb578951ad2537f718f2eacbfbbbb82314eef7880cfe917e735d9699a84c3' + ], + [ + 'd895626548b65b81e264c7637c972877d1d72e5f3a925014372e9f6588f6c14b', + 'febfaa38f2bc7eae728ec60818c340eb03428d632bb067e179363ed75d7d991f' + ], + [ + 'b8da94032a957518eb0f6433571e8761ceffc73693e84edd49150a564f676e03', + '2804dfa44805a1e4d7c99cc9762808b092cc584d95ff3b511488e4e74efdf6e7' + ], + [ + 'e80fea14441fb33a7d8adab9475d7fab2019effb5156a792f1a11778e3c0df5d', + 'eed1de7f638e00771e89768ca3ca94472d155e80af322ea9fcb4291b6ac9ec78' + ], + [ + 'a301697bdfcd704313ba48e51d567543f2a182031efd6915ddc07bbcc4e16070', + '7370f91cfb67e4f5081809fa25d40f9b1735dbf7c0a11a130c0d1a041e177ea1' + ], + [ + '90ad85b389d6b936463f9d0512678de208cc330b11307fffab7ac63e3fb04ed4', + 'e507a3620a38261affdcbd9427222b839aefabe1582894d991d4d48cb6ef150' + ], + [ + '8f68b9d2f63b5f339239c1ad981f162ee88c5678723ea3351b7b444c9ec4c0da', + '662a9f2dba063986de1d90c2b6be215dbbea2cfe95510bfdf23cbf79501fff82' + ], + [ + 'e4f3fb0176af85d65ff99ff9198c36091f48e86503681e3e6686fd5053231e11', + '1e63633ad0ef4f1c1661a6d0ea02b7286cc7e74ec951d1c9822c38576feb73bc' + ], + [ + '8c00fa9b18ebf331eb961537a45a4266c7034f2f0d4e1d0716fb6eae20eae29e', + 'efa47267fea521a1a9dc343a3736c974c2fadafa81e36c54e7d2a4c66702414b' + ], + [ + 'e7a26ce69dd4829f3e10cec0a9e98ed3143d084f308b92c0997fddfc60cb3e41', + '2a758e300fa7984b471b006a1aafbb18d0a6b2c0420e83e20e8a9421cf2cfd51' + ], + [ + 'b6459e0ee3662ec8d23540c223bcbdc571cbcb967d79424f3cf29eb3de6b80ef', + '67c876d06f3e06de1dadf16e5661db3c4b3ae6d48e35b2ff30bf0b61a71ba45' + ], + [ + 'd68a80c8280bb840793234aa118f06231d6f1fc67e73c5a5deda0f5b496943e8', + 'db8ba9fff4b586d00c4b1f9177b0e28b5b0e7b8f7845295a294c84266b133120' + ], + [ + '324aed7df65c804252dc0270907a30b09612aeb973449cea4095980fc28d3d5d', + '648a365774b61f2ff130c0c35aec1f4f19213b0c7e332843967224af96ab7c84' + ], + [ + '4df9c14919cde61f6d51dfdbe5fee5dceec4143ba8d1ca888e8bd373fd054c96', + '35ec51092d8728050974c23a1d85d4b5d506cdc288490192ebac06cad10d5d' + ], + [ + '9c3919a84a474870faed8a9c1cc66021523489054d7f0308cbfc99c8ac1f98cd', + 'ddb84f0f4a4ddd57584f044bf260e641905326f76c64c8e6be7e5e03d4fc599d' + ], + [ + '6057170b1dd12fdf8de05f281d8e06bb91e1493a8b91d4cc5a21382120a959e5', + '9a1af0b26a6a4807add9a2daf71df262465152bc3ee24c65e899be932385a2a8' + ], + [ + 'a576df8e23a08411421439a4518da31880cef0fba7d4df12b1a6973eecb94266', + '40a6bf20e76640b2c92b97afe58cd82c432e10a7f514d9f3ee8be11ae1b28ec8' + ], + [ + '7778a78c28dec3e30a05fe9629de8c38bb30d1f5cf9a3a208f763889be58ad71', + '34626d9ab5a5b22ff7098e12f2ff580087b38411ff24ac563b513fc1fd9f43ac' + ], + [ + '928955ee637a84463729fd30e7afd2ed5f96274e5ad7e5cb09eda9c06d903ac', + 'c25621003d3f42a827b78a13093a95eeac3d26efa8a8d83fc5180e935bcd091f' + ], + [ + '85d0fef3ec6db109399064f3a0e3b2855645b4a907ad354527aae75163d82751', + '1f03648413a38c0be29d496e582cf5663e8751e96877331582c237a24eb1f962' + ], + [ + 'ff2b0dce97eece97c1c9b6041798b85dfdfb6d8882da20308f5404824526087e', + '493d13fef524ba188af4c4dc54d07936c7b7ed6fb90e2ceb2c951e01f0c29907' + ], + [ + '827fbbe4b1e880ea9ed2b2e6301b212b57f1ee148cd6dd28780e5e2cf856e241', + 'c60f9c923c727b0b71bef2c67d1d12687ff7a63186903166d605b68baec293ec' + ], + [ + 'eaa649f21f51bdbae7be4ae34ce6e5217a58fdce7f47f9aa7f3b58fa2120e2b3', + 'be3279ed5bbbb03ac69a80f89879aa5a01a6b965f13f7e59d47a5305ba5ad93d' + ], + [ + 'e4a42d43c5cf169d9391df6decf42ee541b6d8f0c9a137401e23632dda34d24f', + '4d9f92e716d1c73526fc99ccfb8ad34ce886eedfa8d8e4f13a7f7131deba9414' + ], + [ + '1ec80fef360cbdd954160fadab352b6b92b53576a88fea4947173b9d4300bf19', + 'aeefe93756b5340d2f3a4958a7abbf5e0146e77f6295a07b671cdc1cc107cefd' + ], + [ + '146a778c04670c2f91b00af4680dfa8bce3490717d58ba889ddb5928366642be', + 'b318e0ec3354028add669827f9d4b2870aaa971d2f7e5ed1d0b297483d83efd0' + ], + [ + 'fa50c0f61d22e5f07e3acebb1aa07b128d0012209a28b9776d76a8793180eef9', + '6b84c6922397eba9b72cd2872281a68a5e683293a57a213b38cd8d7d3f4f2811' + ], + [ + 'da1d61d0ca721a11b1a5bf6b7d88e8421a288ab5d5bba5220e53d32b5f067ec2', + '8157f55a7c99306c79c0766161c91e2966a73899d279b48a655fba0f1ad836f1' + ], + [ + 'a8e282ff0c9706907215ff98e8fd416615311de0446f1e062a73b0610d064e13', + '7f97355b8db81c09abfb7f3c5b2515888b679a3e50dd6bd6cef7c73111f4cc0c' + ], + [ + '174a53b9c9a285872d39e56e6913cab15d59b1fa512508c022f382de8319497c', + 'ccc9dc37abfc9c1657b4155f2c47f9e6646b3a1d8cb9854383da13ac079afa73' + ], + [ + '959396981943785c3d3e57edf5018cdbe039e730e4918b3d884fdff09475b7ba', + '2e7e552888c331dd8ba0386a4b9cd6849c653f64c8709385e9b8abf87524f2fd' + ], + [ + 'd2a63a50ae401e56d645a1153b109a8fcca0a43d561fba2dbb51340c9d82b151', + 'e82d86fb6443fcb7565aee58b2948220a70f750af484ca52d4142174dcf89405' + ], + [ + '64587e2335471eb890ee7896d7cfdc866bacbdbd3839317b3436f9b45617e073', + 'd99fcdd5bf6902e2ae96dd6447c299a185b90a39133aeab358299e5e9faf6589' + ], + [ + '8481bde0e4e4d885b3a546d3e549de042f0aa6cea250e7fd358d6c86dd45e458', + '38ee7b8cba5404dd84a25bf39cecb2ca900a79c42b262e556d64b1b59779057e' + ], + [ + '13464a57a78102aa62b6979ae817f4637ffcfed3c4b1ce30bcd6303f6caf666b', + '69be159004614580ef7e433453ccb0ca48f300a81d0942e13f495a907f6ecc27' + ], + [ + 'bc4a9df5b713fe2e9aef430bcc1dc97a0cd9ccede2f28588cada3a0d2d83f366', + 'd3a81ca6e785c06383937adf4b798caa6e8a9fbfa547b16d758d666581f33c1' + ], + [ + '8c28a97bf8298bc0d23d8c749452a32e694b65e30a9472a3954ab30fe5324caa', + '40a30463a3305193378fedf31f7cc0eb7ae784f0451cb9459e71dc73cbef9482' + ], + [ + '8ea9666139527a8c1dd94ce4f071fd23c8b350c5a4bb33748c4ba111faccae0', + '620efabbc8ee2782e24e7c0cfb95c5d735b783be9cf0f8e955af34a30e62b945' + ], + [ + 'dd3625faef5ba06074669716bbd3788d89bdde815959968092f76cc4eb9a9787', + '7a188fa3520e30d461da2501045731ca941461982883395937f68d00c644a573' + ], + [ + 'f710d79d9eb962297e4f6232b40e8f7feb2bc63814614d692c12de752408221e', + 'ea98e67232d3b3295d3b535532115ccac8612c721851617526ae47a9c77bfc82' + ] + ] + }, + naf: { + wnd: 7, + points: [ + [ + 'f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9', + '388f7b0f632de8140fe337e62a37f3566500a99934c2231b6cb9fd7584b8e672' + ], + [ + '2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4', + 'd8ac222636e5e3d6d4dba9dda6c9c426f788271bab0d6840dca87d3aa6ac62d6' + ], + [ + '5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc', + '6aebca40ba255960a3178d6d861a54dba813d0b813fde7b5a5082628087264da' + ], + [ + 'acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe', + 'cc338921b0a7d9fd64380971763b61e9add888a4375f8e0f05cc262ac64f9c37' + ], + [ + '774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb', + 'd984a032eb6b5e190243dd56d7b7b365372db1e2dff9d6a8301d74c9c953c61b' + ], + [ + 'f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8', + 'ab0902e8d880a89758212eb65cdaf473a1a06da521fa91f29b5cb52db03ed81' + ], + [ + 'd7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e', + '581e2872a86c72a683842ec228cc6defea40af2bd896d3a5c504dc9ff6a26b58' + ], + [ + 'defdea4cdb677750a420fee807eacf21eb9898ae79b9768766e4faa04a2d4a34', + '4211ab0694635168e997b0ead2a93daeced1f4a04a95c0f6cfb199f69e56eb77' + ], + [ + '2b4ea0a797a443d293ef5cff444f4979f06acfebd7e86d277475656138385b6c', + '85e89bc037945d93b343083b5a1c86131a01f60c50269763b570c854e5c09b7a' + ], + [ + '352bbf4a4cdd12564f93fa332ce333301d9ad40271f8107181340aef25be59d5', + '321eb4075348f534d59c18259dda3e1f4a1b3b2e71b1039c67bd3d8bcf81998c' + ], + [ + '2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f', + '2de1068295dd865b64569335bd5dd80181d70ecfc882648423ba76b532b7d67' + ], + [ + '9248279b09b4d68dab21a9b066edda83263c3d84e09572e269ca0cd7f5453714', + '73016f7bf234aade5d1aa71bdea2b1ff3fc0de2a887912ffe54a32ce97cb3402' + ], + [ + 'daed4f2be3a8bf278e70132fb0beb7522f570e144bf615c07e996d443dee8729', + 'a69dce4a7d6c98e8d4a1aca87ef8d7003f83c230f3afa726ab40e52290be1c55' + ], + [ + 'c44d12c7065d812e8acf28d7cbb19f9011ecd9e9fdf281b0e6a3b5e87d22e7db', + '2119a460ce326cdc76c45926c982fdac0e106e861edf61c5a039063f0e0e6482' + ], + [ + '6a245bf6dc698504c89a20cfded60853152b695336c28063b61c65cbd269e6b4', + 'e022cf42c2bd4a708b3f5126f16a24ad8b33ba48d0423b6efd5e6348100d8a82' + ], + [ + '1697ffa6fd9de627c077e3d2fe541084ce13300b0bec1146f95ae57f0d0bd6a5', + 'b9c398f186806f5d27561506e4557433a2cf15009e498ae7adee9d63d01b2396' + ], + [ + '605bdb019981718b986d0f07e834cb0d9deb8360ffb7f61df982345ef27a7479', + '2972d2de4f8d20681a78d93ec96fe23c26bfae84fb14db43b01e1e9056b8c49' + ], + [ + '62d14dab4150bf497402fdc45a215e10dcb01c354959b10cfe31c7e9d87ff33d', + '80fc06bd8cc5b01098088a1950eed0db01aa132967ab472235f5642483b25eaf' + ], + [ + '80c60ad0040f27dade5b4b06c408e56b2c50e9f56b9b8b425e555c2f86308b6f', + '1c38303f1cc5c30f26e66bad7fe72f70a65eed4cbe7024eb1aa01f56430bd57a' + ], + [ + '7a9375ad6167ad54aa74c6348cc54d344cc5dc9487d847049d5eabb0fa03c8fb', + 'd0e3fa9eca8726909559e0d79269046bdc59ea10c70ce2b02d499ec224dc7f7' + ], + [ + 'd528ecd9b696b54c907a9ed045447a79bb408ec39b68df504bb51f459bc3ffc9', + 'eecf41253136e5f99966f21881fd656ebc4345405c520dbc063465b521409933' + ], + [ + '49370a4b5f43412ea25f514e8ecdad05266115e4a7ecb1387231808f8b45963', + '758f3f41afd6ed428b3081b0512fd62a54c3f3afbb5b6764b653052a12949c9a' + ], + [ + '77f230936ee88cbbd73df930d64702ef881d811e0e1498e2f1c13eb1fc345d74', + '958ef42a7886b6400a08266e9ba1b37896c95330d97077cbbe8eb3c7671c60d6' + ], + [ + 'f2dac991cc4ce4b9ea44887e5c7c0bce58c80074ab9d4dbaeb28531b7739f530', + 'e0dedc9b3b2f8dad4da1f32dec2531df9eb5fbeb0598e4fd1a117dba703a3c37' + ], + [ + '463b3d9f662621fb1b4be8fbbe2520125a216cdfc9dae3debcba4850c690d45b', + '5ed430d78c296c3543114306dd8622d7c622e27c970a1de31cb377b01af7307e' + ], + [ + 'f16f804244e46e2a09232d4aff3b59976b98fac14328a2d1a32496b49998f247', + 'cedabd9b82203f7e13d206fcdf4e33d92a6c53c26e5cce26d6579962c4e31df6' + ], + [ + 'caf754272dc84563b0352b7a14311af55d245315ace27c65369e15f7151d41d1', + 'cb474660ef35f5f2a41b643fa5e460575f4fa9b7962232a5c32f908318a04476' + ], + [ + '2600ca4b282cb986f85d0f1709979d8b44a09c07cb86d7c124497bc86f082120', + '4119b88753c15bd6a693b03fcddbb45d5ac6be74ab5f0ef44b0be9475a7e4b40' + ], + [ + '7635ca72d7e8432c338ec53cd12220bc01c48685e24f7dc8c602a7746998e435', + '91b649609489d613d1d5e590f78e6d74ecfc061d57048bad9e76f302c5b9c61' + ], + [ + '754e3239f325570cdbbf4a87deee8a66b7f2b33479d468fbc1a50743bf56cc18', + '673fb86e5bda30fb3cd0ed304ea49a023ee33d0197a695d0c5d98093c536683' + ], + [ + 'e3e6bd1071a1e96aff57859c82d570f0330800661d1c952f9fe2694691d9b9e8', + '59c9e0bba394e76f40c0aa58379a3cb6a5a2283993e90c4167002af4920e37f5' + ], + [ + '186b483d056a033826ae73d88f732985c4ccb1f32ba35f4b4cc47fdcf04aa6eb', + '3b952d32c67cf77e2e17446e204180ab21fb8090895138b4a4a797f86e80888b' + ], + [ + 'df9d70a6b9876ce544c98561f4be4f725442e6d2b737d9c91a8321724ce0963f', + '55eb2dafd84d6ccd5f862b785dc39d4ab157222720ef9da217b8c45cf2ba2417' + ], + [ + '5edd5cc23c51e87a497ca815d5dce0f8ab52554f849ed8995de64c5f34ce7143', + 'efae9c8dbc14130661e8cec030c89ad0c13c66c0d17a2905cdc706ab7399a868' + ], + [ + '290798c2b6476830da12fe02287e9e777aa3fba1c355b17a722d362f84614fba', + 'e38da76dcd440621988d00bcf79af25d5b29c094db2a23146d003afd41943e7a' + ], + [ + 'af3c423a95d9f5b3054754efa150ac39cd29552fe360257362dfdecef4053b45', + 'f98a3fd831eb2b749a93b0e6f35cfb40c8cd5aa667a15581bc2feded498fd9c6' + ], + [ + '766dbb24d134e745cccaa28c99bf274906bb66b26dcf98df8d2fed50d884249a', + '744b1152eacbe5e38dcc887980da38b897584a65fa06cedd2c924f97cbac5996' + ], + [ + '59dbf46f8c94759ba21277c33784f41645f7b44f6c596a58ce92e666191abe3e', + 'c534ad44175fbc300f4ea6ce648309a042ce739a7919798cd85e216c4a307f6e' + ], + [ + 'f13ada95103c4537305e691e74e9a4a8dd647e711a95e73cb62dc6018cfd87b8', + 'e13817b44ee14de663bf4bc808341f326949e21a6a75c2570778419bdaf5733d' + ], + [ + '7754b4fa0e8aced06d4167a2c59cca4cda1869c06ebadfb6488550015a88522c', + '30e93e864e669d82224b967c3020b8fa8d1e4e350b6cbcc537a48b57841163a2' + ], + [ + '948dcadf5990e048aa3874d46abef9d701858f95de8041d2a6828c99e2262519', + 'e491a42537f6e597d5d28a3224b1bc25df9154efbd2ef1d2cbba2cae5347d57e' + ], + [ + '7962414450c76c1689c7b48f8202ec37fb224cf5ac0bfa1570328a8a3d7c77ab', + '100b610ec4ffb4760d5c1fc133ef6f6b12507a051f04ac5760afa5b29db83437' + ], + [ + '3514087834964b54b15b160644d915485a16977225b8847bb0dd085137ec47ca', + 'ef0afbb2056205448e1652c48e8127fc6039e77c15c2378b7e7d15a0de293311' + ], + [ + 'd3cc30ad6b483e4bc79ce2c9dd8bc54993e947eb8df787b442943d3f7b527eaf', + '8b378a22d827278d89c5e9be8f9508ae3c2ad46290358630afb34db04eede0a4' + ], + [ + '1624d84780732860ce1c78fcbfefe08b2b29823db913f6493975ba0ff4847610', + '68651cf9b6da903e0914448c6cd9d4ca896878f5282be4c8cc06e2a404078575' + ], + [ + '733ce80da955a8a26902c95633e62a985192474b5af207da6df7b4fd5fc61cd4', + 'f5435a2bd2badf7d485a4d8b8db9fcce3e1ef8e0201e4578c54673bc1dc5ea1d' + ], + [ + '15d9441254945064cf1a1c33bbd3b49f8966c5092171e699ef258dfab81c045c', + 'd56eb30b69463e7234f5137b73b84177434800bacebfc685fc37bbe9efe4070d' + ], + [ + 'a1d0fcf2ec9de675b612136e5ce70d271c21417c9d2b8aaaac138599d0717940', + 'edd77f50bcb5a3cab2e90737309667f2641462a54070f3d519212d39c197a629' + ], + [ + 'e22fbe15c0af8ccc5780c0735f84dbe9a790badee8245c06c7ca37331cb36980', + 'a855babad5cd60c88b430a69f53a1a7a38289154964799be43d06d77d31da06' + ], + [ + '311091dd9860e8e20ee13473c1155f5f69635e394704eaa74009452246cfa9b3', + '66db656f87d1f04fffd1f04788c06830871ec5a64feee685bd80f0b1286d8374' + ], + [ + '34c1fd04d301be89b31c0442d3e6ac24883928b45a9340781867d4232ec2dbdf', + '9414685e97b1b5954bd46f730174136d57f1ceeb487443dc5321857ba73abee' + ], + [ + 'f219ea5d6b54701c1c14de5b557eb42a8d13f3abbcd08affcc2a5e6b049b8d63', + '4cb95957e83d40b0f73af4544cccf6b1f4b08d3c07b27fb8d8c2962a400766d1' + ], + [ + 'd7b8740f74a8fbaab1f683db8f45de26543a5490bca627087236912469a0b448', + 'fa77968128d9c92ee1010f337ad4717eff15db5ed3c049b3411e0315eaa4593b' + ], + [ + '32d31c222f8f6f0ef86f7c98d3a3335ead5bcd32abdd94289fe4d3091aa824bf', + '5f3032f5892156e39ccd3d7915b9e1da2e6dac9e6f26e961118d14b8462e1661' + ], + [ + '7461f371914ab32671045a155d9831ea8793d77cd59592c4340f86cbc18347b5', + '8ec0ba238b96bec0cbdddcae0aa442542eee1ff50c986ea6b39847b3cc092ff6' + ], + [ + 'ee079adb1df1860074356a25aa38206a6d716b2c3e67453d287698bad7b2b2d6', + '8dc2412aafe3be5c4c5f37e0ecc5f9f6a446989af04c4e25ebaac479ec1c8c1e' + ], + [ + '16ec93e447ec83f0467b18302ee620f7e65de331874c9dc72bfd8616ba9da6b5', + '5e4631150e62fb40d0e8c2a7ca5804a39d58186a50e497139626778e25b0674d' + ], + [ + 'eaa5f980c245f6f038978290afa70b6bd8855897f98b6aa485b96065d537bd99', + 'f65f5d3e292c2e0819a528391c994624d784869d7e6ea67fb18041024edc07dc' + ], + [ + '78c9407544ac132692ee1910a02439958ae04877151342ea96c4b6b35a49f51', + 'f3e0319169eb9b85d5404795539a5e68fa1fbd583c064d2462b675f194a3ddb4' + ], + [ + '494f4be219a1a77016dcd838431aea0001cdc8ae7a6fc688726578d9702857a5', + '42242a969283a5f339ba7f075e36ba2af925ce30d767ed6e55f4b031880d562c' + ], + [ + 'a598a8030da6d86c6bc7f2f5144ea549d28211ea58faa70ebf4c1e665c1fe9b5', + '204b5d6f84822c307e4b4a7140737aec23fc63b65b35f86a10026dbd2d864e6b' + ], + [ + 'c41916365abb2b5d09192f5f2dbeafec208f020f12570a184dbadc3e58595997', + '4f14351d0087efa49d245b328984989d5caf9450f34bfc0ed16e96b58fa9913' + ], + [ + '841d6063a586fa475a724604da03bc5b92a2e0d2e0a36acfe4c73a5514742881', + '73867f59c0659e81904f9a1c7543698e62562d6744c169ce7a36de01a8d6154' + ], + [ + '5e95bb399a6971d376026947f89bde2f282b33810928be4ded112ac4d70e20d5', + '39f23f366809085beebfc71181313775a99c9aed7d8ba38b161384c746012865' + ], + [ + '36e4641a53948fd476c39f8a99fd974e5ec07564b5315d8bf99471bca0ef2f66', + 'd2424b1b1abe4eb8164227b085c9aa9456ea13493fd563e06fd51cf5694c78fc' + ], + [ + '336581ea7bfbbb290c191a2f507a41cf5643842170e914faeab27c2c579f726', + 'ead12168595fe1be99252129b6e56b3391f7ab1410cd1e0ef3dcdcabd2fda224' + ], + [ + '8ab89816dadfd6b6a1f2634fcf00ec8403781025ed6890c4849742706bd43ede', + '6fdcef09f2f6d0a044e654aef624136f503d459c3e89845858a47a9129cdd24e' + ], + [ + '1e33f1a746c9c5778133344d9299fcaa20b0938e8acff2544bb40284b8c5fb94', + '60660257dd11b3aa9c8ed618d24edff2306d320f1d03010e33a7d2057f3b3b6' + ], + [ + '85b7c1dcb3cec1b7ee7f30ded79dd20a0ed1f4cc18cbcfcfa410361fd8f08f31', + '3d98a9cdd026dd43f39048f25a8847f4fcafad1895d7a633c6fed3c35e999511' + ], + [ + '29df9fbd8d9e46509275f4b125d6d45d7fbe9a3b878a7af872a2800661ac5f51', + 'b4c4fe99c775a606e2d8862179139ffda61dc861c019e55cd2876eb2a27d84b' + ], + [ + 'a0b1cae06b0a847a3fea6e671aaf8adfdfe58ca2f768105c8082b2e449fce252', + 'ae434102edde0958ec4b19d917a6a28e6b72da1834aff0e650f049503a296cf2' + ], + [ + '4e8ceafb9b3e9a136dc7ff67e840295b499dfb3b2133e4ba113f2e4c0e121e5', + 'cf2174118c8b6d7a4b48f6d534ce5c79422c086a63460502b827ce62a326683c' + ], + [ + 'd24a44e047e19b6f5afb81c7ca2f69080a5076689a010919f42725c2b789a33b', + '6fb8d5591b466f8fc63db50f1c0f1c69013f996887b8244d2cdec417afea8fa3' + ], + [ + 'ea01606a7a6c9cdd249fdfcfacb99584001edd28abbab77b5104e98e8e3b35d4', + '322af4908c7312b0cfbfe369f7a7b3cdb7d4494bc2823700cfd652188a3ea98d' + ], + [ + 'af8addbf2b661c8a6c6328655eb96651252007d8c5ea31be4ad196de8ce2131f', + '6749e67c029b85f52a034eafd096836b2520818680e26ac8f3dfbcdb71749700' + ], + [ + 'e3ae1974566ca06cc516d47e0fb165a674a3dabcfca15e722f0e3450f45889', + '2aeabe7e4531510116217f07bf4d07300de97e4874f81f533420a72eeb0bd6a4' + ], + [ + '591ee355313d99721cf6993ffed1e3e301993ff3ed258802075ea8ced397e246', + 'b0ea558a113c30bea60fc4775460c7901ff0b053d25ca2bdeee98f1a4be5d196' + ], + [ + '11396d55fda54c49f19aa97318d8da61fa8584e47b084945077cf03255b52984', + '998c74a8cd45ac01289d5833a7beb4744ff536b01b257be4c5767bea93ea57a4' + ], + [ + '3c5d2a1ba39c5a1790000738c9e0c40b8dcdfd5468754b6405540157e017aa7a', + 'b2284279995a34e2f9d4de7396fc18b80f9b8b9fdd270f6661f79ca4c81bd257' + ], + [ + 'cc8704b8a60a0defa3a99a7299f2e9c3fbc395afb04ac078425ef8a1793cc030', + 'bdd46039feed17881d1e0862db347f8cf395b74fc4bcdc4e940b74e3ac1f1b13' + ], + [ + 'c533e4f7ea8555aacd9777ac5cad29b97dd4defccc53ee7ea204119b2889b197', + '6f0a256bc5efdf429a2fb6242f1a43a2d9b925bb4a4b3a26bb8e0f45eb596096' + ], + [ + 'c14f8f2ccb27d6f109f6d08d03cc96a69ba8c34eec07bbcf566d48e33da6593', + 'c359d6923bb398f7fd4473e16fe1c28475b740dd098075e6c0e8649113dc3a38' + ], + [ + 'a6cbc3046bc6a450bac24789fa17115a4c9739ed75f8f21ce441f72e0b90e6ef', + '21ae7f4680e889bb130619e2c0f95a360ceb573c70603139862afd617fa9b9f' + ], + [ + '347d6d9a02c48927ebfb86c1359b1caf130a3c0267d11ce6344b39f99d43cc38', + '60ea7f61a353524d1c987f6ecec92f086d565ab687870cb12689ff1e31c74448' + ], + [ + 'da6545d2181db8d983f7dcb375ef5866d47c67b1bf31c8cf855ef7437b72656a', + '49b96715ab6878a79e78f07ce5680c5d6673051b4935bd897fea824b77dc208a' + ], + [ + 'c40747cc9d012cb1a13b8148309c6de7ec25d6945d657146b9d5994b8feb1111', + '5ca560753be2a12fc6de6caf2cb489565db936156b9514e1bb5e83037e0fa2d4' + ], + [ + '4e42c8ec82c99798ccf3a610be870e78338c7f713348bd34c8203ef4037f3502', + '7571d74ee5e0fb92a7a8b33a07783341a5492144cc54bcc40a94473693606437' + ], + [ + '3775ab7089bc6af823aba2e1af70b236d251cadb0c86743287522a1b3b0dedea', + 'be52d107bcfa09d8bcb9736a828cfa7fac8db17bf7a76a2c42ad961409018cf7' + ], + [ + 'cee31cbf7e34ec379d94fb814d3d775ad954595d1314ba8846959e3e82f74e26', + '8fd64a14c06b589c26b947ae2bcf6bfa0149ef0be14ed4d80f448a01c43b1c6d' + ], + [ + 'b4f9eaea09b6917619f6ea6a4eb5464efddb58fd45b1ebefcdc1a01d08b47986', + '39e5c9925b5a54b07433a4f18c61726f8bb131c012ca542eb24a8ac07200682a' + ], + [ + 'd4263dfc3d2df923a0179a48966d30ce84e2515afc3dccc1b77907792ebcc60e', + '62dfaf07a0f78feb30e30d6295853ce189e127760ad6cf7fae164e122a208d54' + ], + [ + '48457524820fa65a4f8d35eb6930857c0032acc0a4a2de422233eeda897612c4', + '25a748ab367979d98733c38a1fa1c2e7dc6cc07db2d60a9ae7a76aaa49bd0f77' + ], + [ + 'dfeeef1881101f2cb11644f3a2afdfc2045e19919152923f367a1767c11cceda', + 'ecfb7056cf1de042f9420bab396793c0c390bde74b4bbdff16a83ae09a9a7517' + ], + [ + '6d7ef6b17543f8373c573f44e1f389835d89bcbc6062ced36c82df83b8fae859', + 'cd450ec335438986dfefa10c57fea9bcc521a0959b2d80bbf74b190dca712d10' + ], + [ + 'e75605d59102a5a2684500d3b991f2e3f3c88b93225547035af25af66e04541f', + 'f5c54754a8f71ee540b9b48728473e314f729ac5308b06938360990e2bfad125' + ], + [ + 'eb98660f4c4dfaa06a2be453d5020bc99a0c2e60abe388457dd43fefb1ed620c', + '6cb9a8876d9cb8520609af3add26cd20a0a7cd8a9411131ce85f44100099223e' + ], + [ + '13e87b027d8514d35939f2e6892b19922154596941888336dc3563e3b8dba942', + 'fef5a3c68059a6dec5d624114bf1e91aac2b9da568d6abeb2570d55646b8adf1' + ], + [ + 'ee163026e9fd6fe017c38f06a5be6fc125424b371ce2708e7bf4491691e5764a', + '1acb250f255dd61c43d94ccc670d0f58f49ae3fa15b96623e5430da0ad6c62b2' + ], + [ + 'b268f5ef9ad51e4d78de3a750c2dc89b1e626d43505867999932e5db33af3d80', + '5f310d4b3c99b9ebb19f77d41c1dee018cf0d34fd4191614003e945a1216e423' + ], + [ + 'ff07f3118a9df035e9fad85eb6c7bfe42b02f01ca99ceea3bf7ffdba93c4750d', + '438136d603e858a3a5c440c38eccbaddc1d2942114e2eddd4740d098ced1f0d8' + ], + [ + '8d8b9855c7c052a34146fd20ffb658bea4b9f69e0d825ebec16e8c3ce2b526a1', + 'cdb559eedc2d79f926baf44fb84ea4d44bcf50fee51d7ceb30e2e7f463036758' + ], + [ + '52db0b5384dfbf05bfa9d472d7ae26dfe4b851ceca91b1eba54263180da32b63', + 'c3b997d050ee5d423ebaf66a6db9f57b3180c902875679de924b69d84a7b375' + ], + [ + 'e62f9490d3d51da6395efd24e80919cc7d0f29c3f3fa48c6fff543becbd43352', + '6d89ad7ba4876b0b22c2ca280c682862f342c8591f1daf5170e07bfd9ccafa7d' + ], + [ + '7f30ea2476b399b4957509c88f77d0191afa2ff5cb7b14fd6d8e7d65aaab1193', + 'ca5ef7d4b231c94c3b15389a5f6311e9daff7bb67b103e9880ef4bff637acaec' + ], + [ + '5098ff1e1d9f14fb46a210fada6c903fef0fb7b4a1dd1d9ac60a0361800b7a00', + '9731141d81fc8f8084d37c6e7542006b3ee1b40d60dfe5362a5b132fd17ddc0' + ], + [ + '32b78c7de9ee512a72895be6b9cbefa6e2f3c4ccce445c96b9f2c81e2778ad58', + 'ee1849f513df71e32efc3896ee28260c73bb80547ae2275ba497237794c8753c' + ], + [ + 'e2cb74fddc8e9fbcd076eef2a7c72b0ce37d50f08269dfc074b581550547a4f7', + 'd3aa2ed71c9dd2247a62df062736eb0baddea9e36122d2be8641abcb005cc4a4' + ], + [ + '8438447566d4d7bedadc299496ab357426009a35f235cb141be0d99cd10ae3a8', + 'c4e1020916980a4da5d01ac5e6ad330734ef0d7906631c4f2390426b2edd791f' + ], + [ + '4162d488b89402039b584c6fc6c308870587d9c46f660b878ab65c82c711d67e', + '67163e903236289f776f22c25fb8a3afc1732f2b84b4e95dbda47ae5a0852649' + ], + [ + '3fad3fa84caf0f34f0f89bfd2dcf54fc175d767aec3e50684f3ba4a4bf5f683d', + 'cd1bc7cb6cc407bb2f0ca647c718a730cf71872e7d0d2a53fa20efcdfe61826' + ], + [ + '674f2600a3007a00568c1a7ce05d0816c1fb84bf1370798f1c69532faeb1a86b', + '299d21f9413f33b3edf43b257004580b70db57da0b182259e09eecc69e0d38a5' + ], + [ + 'd32f4da54ade74abb81b815ad1fb3b263d82d6c692714bcff87d29bd5ee9f08f', + 'f9429e738b8e53b968e99016c059707782e14f4535359d582fc416910b3eea87' + ], + [ + '30e4e670435385556e593657135845d36fbb6931f72b08cb1ed954f1e3ce3ff6', + '462f9bce619898638499350113bbc9b10a878d35da70740dc695a559eb88db7b' + ], + [ + 'be2062003c51cc3004682904330e4dee7f3dcd10b01e580bf1971b04d4cad297', + '62188bc49d61e5428573d48a74e1c655b1c61090905682a0d5558ed72dccb9bc' + ], + [ + '93144423ace3451ed29e0fb9ac2af211cb6e84a601df5993c419859fff5df04a', + '7c10dfb164c3425f5c71a3f9d7992038f1065224f72bb9d1d902a6d13037b47c' + ], + [ + 'b015f8044f5fcbdcf21ca26d6c34fb8197829205c7b7d2a7cb66418c157b112c', + 'ab8c1e086d04e813744a655b2df8d5f83b3cdc6faa3088c1d3aea1454e3a1d5f' + ], + [ + 'd5e9e1da649d97d89e4868117a465a3a4f8a18de57a140d36b3f2af341a21b52', + '4cb04437f391ed73111a13cc1d4dd0db1693465c2240480d8955e8592f27447a' + ], + [ + 'd3ae41047dd7ca065dbf8ed77b992439983005cd72e16d6f996a5316d36966bb', + 'bd1aeb21ad22ebb22a10f0303417c6d964f8cdd7df0aca614b10dc14d125ac46' + ], + [ + '463e2763d885f958fc66cdd22800f0a487197d0a82e377b49f80af87c897b065', + 'bfefacdb0e5d0fd7df3a311a94de062b26b80c61fbc97508b79992671ef7ca7f' + ], + [ + '7985fdfd127c0567c6f53ec1bb63ec3158e597c40bfe747c83cddfc910641917', + '603c12daf3d9862ef2b25fe1de289aed24ed291e0ec6708703a5bd567f32ed03' + ], + [ + '74a1ad6b5f76e39db2dd249410eac7f99e74c59cb83d2d0ed5ff1543da7703e9', + 'cc6157ef18c9c63cd6193d83631bbea0093e0968942e8c33d5737fd790e0db08' + ], + [ + '30682a50703375f602d416664ba19b7fc9bab42c72747463a71d0896b22f6da3', + '553e04f6b018b4fa6c8f39e7f311d3176290d0e0f19ca73f17714d9977a22ff8' + ], + [ + '9e2158f0d7c0d5f26c3791efefa79597654e7a2b2464f52b1ee6c1347769ef57', + '712fcdd1b9053f09003a3481fa7762e9ffd7c8ef35a38509e2fbf2629008373' + ], + [ + '176e26989a43c9cfeba4029c202538c28172e566e3c4fce7322857f3be327d66', + 'ed8cc9d04b29eb877d270b4878dc43c19aefd31f4eee09ee7b47834c1fa4b1c3' + ], + [ + '75d46efea3771e6e68abb89a13ad747ecf1892393dfc4f1b7004788c50374da8', + '9852390a99507679fd0b86fd2b39a868d7efc22151346e1a3ca4726586a6bed8' + ], + [ + '809a20c67d64900ffb698c4c825f6d5f2310fb0451c869345b7319f645605721', + '9e994980d9917e22b76b061927fa04143d096ccc54963e6a5ebfa5f3f8e286c1' + ], + [ + '1b38903a43f7f114ed4500b4eac7083fdefece1cf29c63528d563446f972c180', + '4036edc931a60ae889353f77fd53de4a2708b26b6f5da72ad3394119daf408f9' + ] + ] + } + }; + secp256k1.doubles; + secp256k1.naf; + + var curves_1 = createCommonjsModule(function (module, exports) { + + var curves = exports; + + + + + + var assert = utils_1.assert; + + function PresetCurve(options) { + if (options.type === 'short') + this.curve = new curve_1.short(options); + else if (options.type === 'edwards') + this.curve = new curve_1.edwards(options); + else if (options.type === 'mont') + this.curve = new curve_1.mont(options); + else throw Error('Unknown curve type.'); + this.g = this.curve.g; + this.n = this.curve.n; + this.hash = options.hash; + + assert(this.g.validate(), 'Invalid curve'); + assert(this.g.mul(this.n).isInfinity(), 'Invalid curve, n*G != O'); + } + curves.PresetCurve = PresetCurve; + + function defineCurve(name, options) { + Object.defineProperty(curves, name, { + configurable: true, + enumerable: true, + get: function() { + var curve = new PresetCurve(options); + Object.defineProperty(curves, name, { + configurable: true, + enumerable: true, + value: curve + }); + return curve; + } + }); + } + + defineCurve('p192', { + type: 'short', + prime: 'p192', + p: 'ffffffff ffffffff ffffffff fffffffe ffffffff ffffffff', + a: 'ffffffff ffffffff ffffffff fffffffe ffffffff fffffffc', + b: '64210519 e59c80e7 0fa7e9ab 72243049 feb8deec c146b9b1', + n: 'ffffffff ffffffff ffffffff 99def836 146bc9b1 b4d22831', + hash: hash_1.sha256, + gRed: false, + g: [ + '188da80e b03090f6 7cbf20eb 43a18800 f4ff0afd 82ff1012', + '07192b95 ffc8da78 631011ed 6b24cdd5 73f977a1 1e794811' + ] + }); + + defineCurve('p224', { + type: 'short', + prime: 'p224', + p: 'ffffffff ffffffff ffffffff ffffffff 00000000 00000000 00000001', + a: 'ffffffff ffffffff ffffffff fffffffe ffffffff ffffffff fffffffe', + b: 'b4050a85 0c04b3ab f5413256 5044b0b7 d7bfd8ba 270b3943 2355ffb4', + n: 'ffffffff ffffffff ffffffff ffff16a2 e0b8f03e 13dd2945 5c5c2a3d', + hash: hash_1.sha256, + gRed: false, + g: [ + 'b70e0cbd 6bb4bf7f 321390b9 4a03c1d3 56c21122 343280d6 115c1d21', + 'bd376388 b5f723fb 4c22dfe6 cd4375a0 5a074764 44d58199 85007e34' + ] + }); + + defineCurve('p256', { + type: 'short', + prime: null, + p: 'ffffffff 00000001 00000000 00000000 00000000 ffffffff ffffffff ffffffff', + a: 'ffffffff 00000001 00000000 00000000 00000000 ffffffff ffffffff fffffffc', + b: '5ac635d8 aa3a93e7 b3ebbd55 769886bc 651d06b0 cc53b0f6 3bce3c3e 27d2604b', + n: 'ffffffff 00000000 ffffffff ffffffff bce6faad a7179e84 f3b9cac2 fc632551', + hash: hash_1.sha256, + gRed: false, + g: [ + '6b17d1f2 e12c4247 f8bce6e5 63a440f2 77037d81 2deb33a0 f4a13945 d898c296', + '4fe342e2 fe1a7f9b 8ee7eb4a 7c0f9e16 2bce3357 6b315ece cbb64068 37bf51f5' + ] + }); + + defineCurve('p384', { + type: 'short', + prime: null, + p: 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ' + + 'fffffffe ffffffff 00000000 00000000 ffffffff', + a: 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ' + + 'fffffffe ffffffff 00000000 00000000 fffffffc', + b: 'b3312fa7 e23ee7e4 988e056b e3f82d19 181d9c6e fe814112 0314088f ' + + '5013875a c656398d 8a2ed19d 2a85c8ed d3ec2aef', + n: 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff c7634d81 ' + + 'f4372ddf 581a0db2 48b0a77a ecec196a ccc52973', + hash: hash_1.sha384, + gRed: false, + g: [ + 'aa87ca22 be8b0537 8eb1c71e f320ad74 6e1d3b62 8ba79b98 59f741e0 82542a38 ' + + '5502f25d bf55296c 3a545e38 72760ab7', + '3617de4a 96262c6f 5d9e98bf 9292dc29 f8f41dbd 289a147c e9da3113 b5f0b8c0 ' + + '0a60b1ce 1d7e819d 7a431d7c 90ea0e5f' + ] + }); + + defineCurve('p521', { + type: 'short', + prime: null, + p: '000001ff ffffffff ffffffff ffffffff ffffffff ffffffff ' + + 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ' + + 'ffffffff ffffffff ffffffff ffffffff ffffffff', + a: '000001ff ffffffff ffffffff ffffffff ffffffff ffffffff ' + + 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ' + + 'ffffffff ffffffff ffffffff ffffffff fffffffc', + b: '00000051 953eb961 8e1c9a1f 929a21a0 b68540ee a2da725b ' + + '99b315f3 b8b48991 8ef109e1 56193951 ec7e937b 1652c0bd ' + + '3bb1bf07 3573df88 3d2c34f1 ef451fd4 6b503f00', + n: '000001ff ffffffff ffffffff ffffffff ffffffff ffffffff ' + + 'ffffffff ffffffff fffffffa 51868783 bf2f966b 7fcc0148 ' + + 'f709a5d0 3bb5c9b8 899c47ae bb6fb71e 91386409', + hash: hash_1.sha512, + gRed: false, + g: [ + '000000c6 858e06b7 0404e9cd 9e3ecb66 2395b442 9c648139 ' + + '053fb521 f828af60 6b4d3dba a14b5e77 efe75928 fe1dc127 ' + + 'a2ffa8de 3348b3c1 856a429b f97e7e31 c2e5bd66', + '00000118 39296a78 9a3bc004 5c8a5fb4 2c7d1bd9 98f54449 ' + + '579b4468 17afbd17 273e662c 97ee7299 5ef42640 c550b901 ' + + '3fad0761 353c7086 a272c240 88be9476 9fd16650' + ] + }); + + // https://tools.ietf.org/html/rfc7748#section-4.1 + defineCurve('curve25519', { + type: 'mont', + prime: 'p25519', + p: '7fffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffed', + a: '76d06', + b: '1', + n: '1000000000000000 0000000000000000 14def9dea2f79cd6 5812631a5cf5d3ed', + cofactor: '8', + hash: hash_1.sha256, + gRed: false, + g: [ + '9' + ] + }); + + defineCurve('ed25519', { + type: 'edwards', + prime: 'p25519', + p: '7fffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffed', + a: '-1', + c: '1', + // -121665 * (121666^(-1)) (mod P) + d: '52036cee2b6ffe73 8cc740797779e898 00700a4d4141d8ab 75eb4dca135978a3', + n: '1000000000000000 0000000000000000 14def9dea2f79cd6 5812631a5cf5d3ed', + cofactor: '8', + hash: hash_1.sha256, + gRed: false, + g: [ + '216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51a', + // 4/5 + '6666666666666666666666666666666666666666666666666666666666666658' + ] + }); + + // https://tools.ietf.org/html/rfc5639#section-3.4 + defineCurve('brainpoolP256r1', { + type: 'short', + prime: null, + p: 'A9FB57DB A1EEA9BC 3E660A90 9D838D72 6E3BF623 D5262028 2013481D 1F6E5377', + a: '7D5A0975 FC2C3057 EEF67530 417AFFE7 FB8055C1 26DC5C6C E94A4B44 F330B5D9', + b: '26DC5C6C E94A4B44 F330B5D9 BBD77CBF 95841629 5CF7E1CE 6BCCDC18 FF8C07B6', + n: 'A9FB57DB A1EEA9BC 3E660A90 9D838D71 8C397AA3 B561A6F7 901E0E82 974856A7', + hash: hash_1.sha256, // or 384, or 512 + gRed: false, + g: [ + '8BD2AEB9CB7E57CB2C4B482FFC81B7AFB9DE27E1E3BD23C23A4453BD9ACE3262', + '547EF835C3DAC4FD97F8461A14611DC9C27745132DED8E545C1D54C72F046997' + ] + }); + + // https://tools.ietf.org/html/rfc5639#section-3.6 + defineCurve('brainpoolP384r1', { + type: 'short', + prime: null, + p: '8CB91E82 A3386D28 0F5D6F7E 50E641DF 152F7109 ED5456B4 12B1DA19 7FB71123' + + 'ACD3A729 901D1A71 87470013 3107EC53', + a: '7BC382C6 3D8C150C 3C72080A CE05AFA0 C2BEA28E 4FB22787 139165EF BA91F90F' + + '8AA5814A 503AD4EB 04A8C7DD 22CE2826', + b: '04A8C7DD 22CE2826 8B39B554 16F0447C 2FB77DE1 07DCD2A6 2E880EA5 3EEB62D5' + + '7CB43902 95DBC994 3AB78696 FA504C11', + n: '8CB91E82 A3386D28 0F5D6F7E 50E641DF 152F7109 ED5456B3 1F166E6C AC0425A7' + + 'CF3AB6AF 6B7FC310 3B883202 E9046565', + hash: hash_1.sha384, // or 512 + gRed: false, + g: [ + '1D1C64F068CF45FFA2A63A81B7C13F6B8847A3E77EF14FE3DB7FCAFE0CBD10' + + 'E8E826E03436D646AAEF87B2E247D4AF1E', + '8ABE1D7520F9C2A45CB1EB8E95CFD55262B70B29FEEC5864E19C054FF99129' + + '280E4646217791811142820341263C5315' + ] + }); + + // https://tools.ietf.org/html/rfc5639#section-3.7 + defineCurve('brainpoolP512r1', { + type: 'short', + prime: null, + p: 'AADD9DB8 DBE9C48B 3FD4E6AE 33C9FC07 CB308DB3 B3C9D20E D6639CCA 70330871' + + '7D4D9B00 9BC66842 AECDA12A E6A380E6 2881FF2F 2D82C685 28AA6056 583A48F3', + a: '7830A331 8B603B89 E2327145 AC234CC5 94CBDD8D 3DF91610 A83441CA EA9863BC' + + '2DED5D5A A8253AA1 0A2EF1C9 8B9AC8B5 7F1117A7 2BF2C7B9 E7C1AC4D 77FC94CA', + b: '3DF91610 A83441CA EA9863BC 2DED5D5A A8253AA1 0A2EF1C9 8B9AC8B5 7F1117A7' + + '2BF2C7B9 E7C1AC4D 77FC94CA DC083E67 984050B7 5EBAE5DD 2809BD63 8016F723', + n: 'AADD9DB8 DBE9C48B 3FD4E6AE 33C9FC07 CB308DB3 B3C9D20E D6639CCA 70330870' + + '553E5C41 4CA92619 41866119 7FAC1047 1DB1D381 085DDADD B5879682 9CA90069', + hash: hash_1.sha512, + gRed: false, + g: [ + '81AEE4BDD82ED9645A21322E9C4C6A9385ED9F70B5D916C1B43B62EEF4D009' + + '8EFF3B1F78E2D0D48D50D1687B93B97D5F7C6D5047406A5E688B352209BCB9F822', + '7DDE385D566332ECC0EABFA9CF7822FDF209F70024A57B1AA000C55B881F81' + + '11B2DCDE494A5F485E5BCA4BD88A2763AED1CA2B2FA8F0540678CD1E0F3AD80892' + ] + }); + + // https://en.bitcoin.it/wiki/Secp256k1 + var pre; + try { + pre = secp256k1; + } catch (e) { + pre = undefined; + } + + defineCurve('secp256k1', { + type: 'short', + prime: 'k256', + p: 'ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe fffffc2f', + a: '0', + b: '7', + n: 'ffffffff ffffffff ffffffff fffffffe baaedce6 af48a03b bfd25e8c d0364141', + h: '1', + hash: hash_1.sha256, + + // Precomputed endomorphism + beta: '7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee', + lambda: '5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72', + basis: [ + { + a: '3086d221a7d46bcde86c90e49284eb15', + b: '-e4437ed6010e88286f547fa90abfe4c3' + }, + { + a: '114ca50f7a8e2f3f657c1108d9d44cfd8', + b: '3086d221a7d46bcde86c90e49284eb15' + } + ], + + gRed: false, + g: [ + '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + '483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', + pre + ] + }); + }); + + function HmacDRBG(options) { + if (!(this instanceof HmacDRBG)) + return new HmacDRBG(options); + this.hash = options.hash; + this.predResist = !!options.predResist; + + this.outLen = this.hash.outSize; + this.minEntropy = options.minEntropy || this.hash.hmacStrength; + + this._reseed = null; + this.reseedInterval = null; + this.K = null; + this.V = null; + + var entropy = utils_1$1.toArray(options.entropy, options.entropyEnc || 'hex'); + var nonce = utils_1$1.toArray(options.nonce, options.nonceEnc || 'hex'); + var pers = utils_1$1.toArray(options.pers, options.persEnc || 'hex'); + minimalisticAssert(entropy.length >= (this.minEntropy / 8), + 'Not enough entropy. Minimum is: ' + this.minEntropy + ' bits'); + this._init(entropy, nonce, pers); + } + var hmacDrbg = HmacDRBG; + + HmacDRBG.prototype._init = function init(entropy, nonce, pers) { + var seed = entropy.concat(nonce).concat(pers); + + this.K = new Array(this.outLen / 8); + this.V = new Array(this.outLen / 8); + for (var i = 0; i < this.V.length; i++) { + this.K[i] = 0x00; + this.V[i] = 0x01; + } + + this._update(seed); + this._reseed = 1; + this.reseedInterval = 0x1000000000000; // 2^48 + }; + + HmacDRBG.prototype._hmac = function hmac() { + return new hash_1.hmac(this.hash, this.K); + }; + + HmacDRBG.prototype._update = function update(seed) { + var kmac = this._hmac() + .update(this.V) + .update([ 0x00 ]); + if (seed) + kmac = kmac.update(seed); + this.K = kmac.digest(); + this.V = this._hmac().update(this.V).digest(); + if (!seed) + return; + + this.K = this._hmac() + .update(this.V) + .update([ 0x01 ]) + .update(seed) + .digest(); + this.V = this._hmac().update(this.V).digest(); + }; + + HmacDRBG.prototype.reseed = function reseed(entropy, entropyEnc, add, addEnc) { + // Optional entropy enc + if (typeof entropyEnc !== 'string') { + addEnc = add; + add = entropyEnc; + entropyEnc = null; + } + + entropy = utils_1$1.toArray(entropy, entropyEnc); + add = utils_1$1.toArray(add, addEnc); + + minimalisticAssert(entropy.length >= (this.minEntropy / 8), + 'Not enough entropy. Minimum is: ' + this.minEntropy + ' bits'); + + this._update(entropy.concat(add || [])); + this._reseed = 1; + }; + + HmacDRBG.prototype.generate = function generate(len, enc, add, addEnc) { + if (this._reseed > this.reseedInterval) + throw Error('Reseed is required'); + + // Optional encoding + if (typeof enc !== 'string') { + addEnc = add; + add = enc; + enc = null; + } + + // Optional additional data + if (add) { + add = utils_1$1.toArray(add, addEnc || 'hex'); + this._update(add); + } + + var temp = []; + while (temp.length < len) { + this.V = this._hmac().update(this.V).digest(); + temp = temp.concat(this.V); + } + + var res = temp.slice(0, len); + this._update(add); + this._reseed++; + return utils_1$1.encode(res, enc); + }; + + var assert$5 = utils_1.assert; + + function KeyPair$1(ec, options) { + this.ec = ec; + this.priv = null; + this.pub = null; + + // KeyPair(ec, { priv: ..., pub: ... }) + if (options.priv) + this._importPrivate(options.priv, options.privEnc); + if (options.pub) + this._importPublic(options.pub, options.pubEnc); + } + var key$1 = KeyPair$1; + + KeyPair$1.fromPublic = function fromPublic(ec, pub, enc) { + if (pub instanceof KeyPair$1) + return pub; + + return new KeyPair$1(ec, { + pub: pub, + pubEnc: enc + }); + }; + + KeyPair$1.fromPrivate = function fromPrivate(ec, priv, enc) { + if (priv instanceof KeyPair$1) + return priv; + + return new KeyPair$1(ec, { + priv: priv, + privEnc: enc + }); + }; + + // TODO: should not validate for X25519 + KeyPair$1.prototype.validate = function validate() { + var pub = this.getPublic(); + + if (pub.isInfinity()) + return { result: false, reason: 'Invalid public key' }; + if (!pub.validate()) + return { result: false, reason: 'Public key is not a point' }; + if (!pub.mul(this.ec.curve.n).isInfinity()) + return { result: false, reason: 'Public key * N != O' }; + + return { result: true, reason: null }; + }; + + KeyPair$1.prototype.getPublic = function getPublic(enc, compact) { + if (!this.pub) + this.pub = this.ec.g.mul(this.priv); + + if (!enc) + return this.pub; + + return this.pub.encode(enc, compact); + }; + + KeyPair$1.prototype.getPrivate = function getPrivate(enc) { + if (enc === 'hex') + return this.priv.toString(16, 2); + else + return this.priv; + }; + + KeyPair$1.prototype._importPrivate = function _importPrivate(key, enc) { + this.priv = new bn(key, enc || 16); + + // For Curve25519/Curve448 we have a specific procedure. + // TODO Curve448 + if (this.ec.curve.type === 'mont') { + var one = this.ec.curve.one; + var mask = one.ushln(255 - 3).sub(one).ushln(3); + this.priv = this.priv.or(one.ushln(255 - 1)); + this.priv = this.priv.and(mask); + } else + // Ensure that the priv won't be bigger than n, otherwise we may fail + // in fixed multiplication method + this.priv = this.priv.umod(this.ec.curve.n); + }; + + KeyPair$1.prototype._importPublic = function _importPublic(key, enc) { + if (key.x || key.y) { + // Montgomery points only have an `x` coordinate. + // Weierstrass/Edwards points on the other hand have both `x` and + // `y` coordinates. + if (this.ec.curve.type === 'mont') { + assert$5(key.x, 'Need x coordinate'); + } else if (this.ec.curve.type === 'short' || + this.ec.curve.type === 'edwards') { + assert$5(key.x && key.y, 'Need both x and y coordinate'); + } + this.pub = this.ec.curve.point(key.x, key.y); + return; + } + this.pub = this.ec.curve.decodePoint(key, enc); + }; + + // ECDH + KeyPair$1.prototype.derive = function derive(pub) { + return pub.mul(this.priv).getX(); + }; + + // ECDSA + KeyPair$1.prototype.sign = function sign(msg, enc, options) { + return this.ec.sign(msg, this, enc, options); + }; + + KeyPair$1.prototype.verify = function verify(msg, signature) { + return this.ec.verify(msg, signature, this); + }; + + KeyPair$1.prototype.inspect = function inspect() { + return ''; + }; + + var assert$4 = utils_1.assert; + + function Signature$1(options, enc) { + if (options instanceof Signature$1) + return options; + + if (this._importDER(options, enc)) + return; + + assert$4(options.r && options.s, 'Signature without r or s'); + this.r = new bn(options.r, 16); + this.s = new bn(options.s, 16); + if (options.recoveryParam === undefined) + this.recoveryParam = null; + else + this.recoveryParam = options.recoveryParam; + } + var signature$1 = Signature$1; + + function Position() { + this.place = 0; + } + + function getLength(buf, p) { + var initial = buf[p.place++]; + if (!(initial & 0x80)) { + return initial; + } + var octetLen = initial & 0xf; + var val = 0; + for (var i = 0, off = p.place; i < octetLen; i++, off++) { + val <<= 8; + val |= buf[off]; + } + p.place = off; + return val; + } + + function rmPadding(buf) { + var i = 0; + var len = buf.length - 1; + while (!buf[i] && !(buf[i + 1] & 0x80) && i < len) { + i++; + } + if (i === 0) { + return buf; + } + return buf.slice(i); + } + + Signature$1.prototype._importDER = function _importDER(data, enc) { + data = utils_1.toArray(data, enc); + var p = new Position(); + if (data[p.place++] !== 0x30) { + return false; + } + var len = getLength(data, p); + if ((len + p.place) !== data.length) { + return false; + } + if (data[p.place++] !== 0x02) { + return false; + } + var rlen = getLength(data, p); + var r = data.slice(p.place, rlen + p.place); + p.place += rlen; + if (data[p.place++] !== 0x02) { + return false; + } + var slen = getLength(data, p); + if (data.length !== slen + p.place) { + return false; + } + var s = data.slice(p.place, slen + p.place); + if (r[0] === 0 && (r[1] & 0x80)) { + r = r.slice(1); + } + if (s[0] === 0 && (s[1] & 0x80)) { + s = s.slice(1); + } + + this.r = new bn(r); + this.s = new bn(s); + this.recoveryParam = null; + + return true; + }; + + function constructLength(arr, len) { + if (len < 0x80) { + arr.push(len); + return; + } + var octets = 1 + (Math.log(len) / Math.LN2 >>> 3); + arr.push(octets | 0x80); + while (--octets) { + arr.push((len >>> (octets << 3)) & 0xff); + } + arr.push(len); + } + + Signature$1.prototype.toDER = function toDER(enc) { + var r = this.r.toArray(); + var s = this.s.toArray(); + + // Pad values + if (r[0] & 0x80) + r = [ 0 ].concat(r); + // Pad values + if (s[0] & 0x80) + s = [ 0 ].concat(s); + + r = rmPadding(r); + s = rmPadding(s); + + while (!s[0] && !(s[1] & 0x80)) { + s = s.slice(1); + } + var arr = [ 0x02 ]; + constructLength(arr, r.length); + arr = arr.concat(r); + arr.push(0x02); + constructLength(arr, s.length); + var backHalf = arr.concat(s); + var res = [ 0x30 ]; + constructLength(res, backHalf.length); + res = res.concat(backHalf); + return utils_1.encode(res, enc); + }; + + var assert$3 = utils_1.assert; + + + + + function EC(options) { + if (!(this instanceof EC)) + return new EC(options); + + // Shortcut `elliptic.ec(curve-name)` + if (typeof options === 'string') { + assert$3(curves_1.hasOwnProperty(options), 'Unknown curve ' + options); + + options = curves_1[options]; + } + + // Shortcut for `elliptic.ec(elliptic.curves.curveName)` + if (options instanceof curves_1.PresetCurve) + options = { curve: options }; + + this.curve = options.curve.curve; + this.n = this.curve.n; + this.nh = this.n.ushrn(1); + this.g = this.curve.g; + + // Point on curve + this.g = options.curve.g; + this.g.precompute(options.curve.n.bitLength() + 1); + + // Hash function for DRBG + this.hash = options.hash || options.curve.hash; + } + var ec = EC; + + EC.prototype.keyPair = function keyPair(options) { + return new key$1(this, options); + }; + + EC.prototype.keyFromPrivate = function keyFromPrivate(priv, enc) { + return key$1.fromPrivate(this, priv, enc); + }; + + EC.prototype.keyFromPublic = function keyFromPublic(pub, enc) { + return key$1.fromPublic(this, pub, enc); + }; + + EC.prototype.genKeyPair = function genKeyPair(options) { + if (!options) + options = {}; + + // Instantiate Hmac_DRBG + var drbg = new hmacDrbg({ + hash: this.hash, + pers: options.pers, + persEnc: options.persEnc || 'utf8', + entropy: options.entropy || brorand(this.hash.hmacStrength), + entropyEnc: options.entropy && options.entropyEnc || 'utf8', + nonce: this.n.toArray() + }); + + // Key generation for curve25519 is simpler + if (this.curve.type === 'mont') { + var priv = new bn(drbg.generate(32)); + return this.keyFromPrivate(priv); + } + + var bytes = this.n.byteLength(); + var ns2 = this.n.sub(new bn(2)); + do { + var priv = new bn(drbg.generate(bytes)); + if (priv.cmp(ns2) > 0) + continue; + + priv.iaddn(1); + return this.keyFromPrivate(priv); + } while (true); + }; + + EC.prototype._truncateToN = function truncateToN(msg, truncOnly, bitSize) { + bitSize = bitSize || msg.byteLength() * 8; + var delta = bitSize - this.n.bitLength(); + if (delta > 0) + msg = msg.ushrn(delta); + if (!truncOnly && msg.cmp(this.n) >= 0) + return msg.sub(this.n); + else + return msg; + }; + + EC.prototype.truncateMsg = function truncateMSG(msg) { + // Bit size is only determined correctly for Uint8Arrays and hex strings + var bitSize; + if (msg instanceof Uint8Array) { + bitSize = msg.byteLength * 8; + msg = this._truncateToN(new bn(msg, 16), false, bitSize); + } else if (typeof msg === 'string') { + bitSize = msg.length * 4; + msg = this._truncateToN(new bn(msg, 16), false, bitSize); + } else { + msg = this._truncateToN(new bn(msg, 16)); + } + return msg; + }; + + EC.prototype.sign = function sign(msg, key, enc, options) { + if (typeof enc === 'object') { + options = enc; + enc = null; + } + if (!options) + options = {}; + + key = this.keyFromPrivate(key, enc); + msg = this.truncateMsg(msg); + + // Zero-extend key to provide enough entropy + var bytes = this.n.byteLength(); + var bkey = key.getPrivate().toArray('be', bytes); + + // Zero-extend nonce to have the same byte size as N + var nonce = msg.toArray('be', bytes); + + // Instantiate Hmac_DRBG + var drbg = new hmacDrbg({ + hash: this.hash, + entropy: bkey, + nonce: nonce, + pers: options.pers, + persEnc: options.persEnc || 'utf8' + }); + + // Number of bytes to generate + var ns1 = this.n.sub(new bn(1)); + + for (var iter = 0; true; iter++) { + var k = options.k ? + options.k(iter) : + new bn(drbg.generate(this.n.byteLength())); + k = this._truncateToN(k, true); + if (k.cmpn(1) <= 0 || k.cmp(ns1) >= 0) + continue; + + var kp = this.g.mul(k); + if (kp.isInfinity()) + continue; + + var kpX = kp.getX(); + var r = kpX.umod(this.n); + if (r.cmpn(0) === 0) + continue; + + var s = k.invm(this.n).mul(r.mul(key.getPrivate()).iadd(msg)); + s = s.umod(this.n); + if (s.cmpn(0) === 0) + continue; + + var recoveryParam = (kp.getY().isOdd() ? 1 : 0) | + (kpX.cmp(r) !== 0 ? 2 : 0); + + // Use complement of `s`, if it is > `n / 2` + if (options.canonical && s.cmp(this.nh) > 0) { + s = this.n.sub(s); + recoveryParam ^= 1; + } + + return new signature$1({ r: r, s: s, recoveryParam: recoveryParam }); + } + }; + + EC.prototype.verify = function verify(msg, signature, key, enc) { + key = this.keyFromPublic(key, enc); + signature = new signature$1(signature, 'hex'); + // Fallback to the old code + var ret = this._verify(this.truncateMsg(msg), signature, key) || + this._verify(this._truncateToN(new bn(msg, 16)), signature, key); + return ret; + }; + + EC.prototype._verify = function _verify(msg, signature, key) { + // Perform primitive values validation + var r = signature.r; + var s = signature.s; + if (r.cmpn(1) < 0 || r.cmp(this.n) >= 0) + return false; + if (s.cmpn(1) < 0 || s.cmp(this.n) >= 0) + return false; + + // Validate signature + var sinv = s.invm(this.n); + var u1 = sinv.mul(msg).umod(this.n); + var u2 = sinv.mul(r).umod(this.n); + + if (!this.curve._maxwellTrick) { + var p = this.g.mulAdd(u1, key.getPublic(), u2); + if (p.isInfinity()) + return false; + + return p.getX().umod(this.n).cmp(r) === 0; + } + + // NOTE: Greg Maxwell's trick, inspired by: + // https://git.io/vad3K + + var p = this.g.jmulAdd(u1, key.getPublic(), u2); + if (p.isInfinity()) + return false; + + // Compare `p.x` of Jacobian point with `r`, + // this will do `p.x == r * p.z^2` instead of multiplying `p.x` by the + // inverse of `p.z^2` + return p.eqXToP(r); + }; + + EC.prototype.recoverPubKey = function(msg, signature, j, enc) { + assert$3((3 & j) === j, 'The recovery param is more than two bits'); + signature = new signature$1(signature, enc); + + var n = this.n; + var e = new bn(msg); + var r = signature.r; + var s = signature.s; + + // A set LSB signifies that the y-coordinate is odd + var isYOdd = j & 1; + var isSecondKey = j >> 1; + if (r.cmp(this.curve.p.umod(this.curve.n)) >= 0 && isSecondKey) + throw Error('Unable to find sencond key candinate'); + + // 1.1. Let x = r + jn. + if (isSecondKey) + r = this.curve.pointFromX(r.add(this.curve.n), isYOdd); + else + r = this.curve.pointFromX(r, isYOdd); + + var rInv = signature.r.invm(n); + var s1 = n.sub(e).mul(rInv).umod(n); + var s2 = s.mul(rInv).umod(n); + + // 1.6.1 Compute Q = r^-1 (sR - eG) + // Q = r^-1 (sR + -eG) + return this.g.mulAdd(s1, r, s2); + }; + + EC.prototype.getKeyRecoveryParam = function(e, signature, Q, enc) { + signature = new signature$1(signature, enc); + if (signature.recoveryParam !== null) + return signature.recoveryParam; + + for (var i = 0; i < 4; i++) { + var Qprime; + try { + Qprime = this.recoverPubKey(e, signature, i); + } catch (e) { + continue; + } + + if (Qprime.eq(Q)) + return i; + } + throw Error('Unable to find valid recovery factor'); + }; + + var assert$2 = utils_1.assert; + var parseBytes$2 = utils_1.parseBytes; + var cachedProperty$1 = utils_1.cachedProperty; + + /** + * @param {EDDSA} eddsa - instance + * @param {Object} params - public/private key parameters + * + * @param {Array} [params.secret] - secret seed bytes + * @param {Point} [params.pub] - public key point (aka `A` in eddsa terms) + * @param {Array} [params.pub] - public key point encoded as bytes + * + */ + function KeyPair(eddsa, params) { + this.eddsa = eddsa; + if (params.hasOwnProperty('secret')) + this._secret = parseBytes$2(params.secret); + if (eddsa.isPoint(params.pub)) + this._pub = params.pub; + else { + this._pubBytes = parseBytes$2(params.pub); + if (this._pubBytes && this._pubBytes.length === 33 && + this._pubBytes[0] === 0x40) + this._pubBytes = this._pubBytes.slice(1, 33); + if (this._pubBytes && this._pubBytes.length !== 32) + throw Error('Unknown point compression format'); + } + } + + KeyPair.fromPublic = function fromPublic(eddsa, pub) { + if (pub instanceof KeyPair) + return pub; + return new KeyPair(eddsa, { pub: pub }); + }; + + KeyPair.fromSecret = function fromSecret(eddsa, secret) { + if (secret instanceof KeyPair) + return secret; + return new KeyPair(eddsa, { secret: secret }); + }; + + KeyPair.prototype.secret = function secret() { + return this._secret; + }; + + cachedProperty$1(KeyPair, 'pubBytes', function pubBytes() { + return this.eddsa.encodePoint(this.pub()); + }); + + cachedProperty$1(KeyPair, 'pub', function pub() { + if (this._pubBytes) + return this.eddsa.decodePoint(this._pubBytes); + return this.eddsa.g.mul(this.priv()); + }); + + cachedProperty$1(KeyPair, 'privBytes', function privBytes() { + var eddsa = this.eddsa; + var hash = this.hash(); + var lastIx = eddsa.encodingLength - 1; + + // https://tools.ietf.org/html/rfc8032#section-5.1.5 + var a = hash.slice(0, eddsa.encodingLength); + a[0] &= 248; + a[lastIx] &= 127; + a[lastIx] |= 64; + + return a; + }); + + cachedProperty$1(KeyPair, 'priv', function priv() { + return this.eddsa.decodeInt(this.privBytes()); + }); + + cachedProperty$1(KeyPair, 'hash', function hash() { + return this.eddsa.hash().update(this.secret()).digest(); + }); + + cachedProperty$1(KeyPair, 'messagePrefix', function messagePrefix() { + return this.hash().slice(this.eddsa.encodingLength); + }); + + KeyPair.prototype.sign = function sign(message) { + assert$2(this._secret, 'KeyPair can only verify'); + return this.eddsa.sign(message, this); + }; + + KeyPair.prototype.verify = function verify(message, sig) { + return this.eddsa.verify(message, sig, this); + }; + + KeyPair.prototype.getSecret = function getSecret(enc) { + assert$2(this._secret, 'KeyPair is public only'); + return utils_1.encode(this.secret(), enc); + }; + + KeyPair.prototype.getPublic = function getPublic(enc, compact) { + return utils_1.encode((compact ? [ 0x40 ] : []).concat(this.pubBytes()), enc); + }; + + var key = KeyPair; + + var assert$1 = utils_1.assert; + var cachedProperty = utils_1.cachedProperty; + var parseBytes$1 = utils_1.parseBytes; + + /** + * @param {EDDSA} eddsa - eddsa instance + * @param {Array|Object} sig - + * @param {Array|Point} [sig.R] - R point as Point or bytes + * @param {Array|bn} [sig.S] - S scalar as bn or bytes + * @param {Array} [sig.Rencoded] - R point encoded + * @param {Array} [sig.Sencoded] - S scalar encoded + */ + function Signature(eddsa, sig) { + this.eddsa = eddsa; + + if (typeof sig !== 'object') + sig = parseBytes$1(sig); + + if (Array.isArray(sig)) { + sig = { + R: sig.slice(0, eddsa.encodingLength), + S: sig.slice(eddsa.encodingLength) + }; + } + + assert$1(sig.R && sig.S, 'Signature without R or S'); + + if (eddsa.isPoint(sig.R)) + this._R = sig.R; + if (sig.S instanceof bn) + this._S = sig.S; + + this._Rencoded = Array.isArray(sig.R) ? sig.R : sig.Rencoded; + this._Sencoded = Array.isArray(sig.S) ? sig.S : sig.Sencoded; + } + + cachedProperty(Signature, 'S', function S() { + return this.eddsa.decodeInt(this.Sencoded()); + }); + + cachedProperty(Signature, 'R', function R() { + return this.eddsa.decodePoint(this.Rencoded()); + }); + + cachedProperty(Signature, 'Rencoded', function Rencoded() { + return this.eddsa.encodePoint(this.R()); + }); + + cachedProperty(Signature, 'Sencoded', function Sencoded() { + return this.eddsa.encodeInt(this.S()); + }); + + Signature.prototype.toBytes = function toBytes() { + return this.Rencoded().concat(this.Sencoded()); + }; + + Signature.prototype.toHex = function toHex() { + return utils_1.encode(this.toBytes(), 'hex').toUpperCase(); + }; + + var signature = Signature; + + var assert = utils_1.assert; + var parseBytes = utils_1.parseBytes; + + + + function EDDSA(curve) { + assert(curve === 'ed25519', 'only tested with ed25519 so far'); + + if (!(this instanceof EDDSA)) + return new EDDSA(curve); + + var curve = curves_1[curve].curve; + this.curve = curve; + this.g = curve.g; + this.g.precompute(curve.n.bitLength() + 1); + + this.pointClass = curve.point().constructor; + this.encodingLength = Math.ceil(curve.n.bitLength() / 8); + this.hash = hash_1.sha512; + } + + var eddsa = EDDSA; + + /** + * @param {Array|String} message - message bytes + * @param {Array|String|KeyPair} secret - secret bytes or a keypair + * @returns {Signature} - signature + */ + EDDSA.prototype.sign = function sign(message, secret) { + message = parseBytes(message); + var key = this.keyFromSecret(secret); + var r = this.hashInt(key.messagePrefix(), message); + var R = this.g.mul(r); + var Rencoded = this.encodePoint(R); + var s_ = this.hashInt(Rencoded, key.pubBytes(), message) + .mul(key.priv()); + var S = r.add(s_).umod(this.curve.n); + return this.makeSignature({ R: R, S: S, Rencoded: Rencoded }); + }; + + /** + * @param {Array} message - message bytes + * @param {Array|String|Signature} sig - sig bytes + * @param {Array|String|Point|KeyPair} pub - public key + * @returns {Boolean} - true if public key matches sig of message + */ + EDDSA.prototype.verify = function verify(message, sig, pub) { + message = parseBytes(message); + sig = this.makeSignature(sig); + var key = this.keyFromPublic(pub); + var h = this.hashInt(sig.Rencoded(), key.pubBytes(), message); + var SG = this.g.mul(sig.S()); + var RplusAh = sig.R().add(key.pub().mul(h)); + return RplusAh.eq(SG); + }; + + EDDSA.prototype.hashInt = function hashInt() { + var hash = this.hash(); + for (var i = 0; i < arguments.length; i++) + hash.update(arguments[i]); + return utils_1.intFromLE(hash.digest()).umod(this.curve.n); + }; + + EDDSA.prototype.keyPair = function keyPair(options) { + return new key(this, options); + }; + + EDDSA.prototype.keyFromPublic = function keyFromPublic(pub) { + return key.fromPublic(this, pub); + }; + + EDDSA.prototype.keyFromSecret = function keyFromSecret(secret) { + return key.fromSecret(this, secret); + }; + + EDDSA.prototype.genKeyPair = function genKeyPair(options) { + if (!options) + options = {}; + + // Instantiate Hmac_DRBG + var drbg = new hmacDrbg({ + hash: this.hash, + pers: options.pers, + persEnc: options.persEnc || 'utf8', + entropy: options.entropy || brorand(this.hash.hmacStrength), + entropyEnc: options.entropy && options.entropyEnc || 'utf8', + nonce: this.curve.n.toArray() + }); + + return this.keyFromSecret(drbg.generate(32)); + }; + + EDDSA.prototype.makeSignature = function makeSignature(sig) { + if (sig instanceof signature) + return sig; + return new signature(this, sig); + }; + + /** + * * https://tools.ietf.org/html/draft-josefsson-eddsa-ed25519-03#section-5.2 + * + * EDDSA defines methods for encoding and decoding points and integers. These are + * helper convenience methods, that pass along to utility functions implied + * parameters. + * + */ + EDDSA.prototype.encodePoint = function encodePoint(point) { + var enc = point.getY().toArray('le', this.encodingLength); + enc[this.encodingLength - 1] |= point.getX().isOdd() ? 0x80 : 0; + return enc; + }; + + EDDSA.prototype.decodePoint = function decodePoint(bytes) { + bytes = utils_1.parseBytes(bytes); + + var lastIx = bytes.length - 1; + var normed = bytes.slice(0, lastIx).concat(bytes[lastIx] & ~0x80); + var xIsOdd = (bytes[lastIx] & 0x80) !== 0; + + var y = utils_1.intFromLE(normed); + return this.curve.pointFromY(y, xIsOdd); + }; + + EDDSA.prototype.encodeInt = function encodeInt(num) { + return num.toArray('le', this.encodingLength); + }; + + EDDSA.prototype.decodeInt = function decodeInt(bytes) { + return utils_1.intFromLE(bytes); + }; + + EDDSA.prototype.isPoint = function isPoint(val) { + return val instanceof this.pointClass; + }; + + var elliptic_1 = createCommonjsModule(function (module, exports) { + + var elliptic = exports; + + elliptic.utils = utils_1; + elliptic.rand = brorand; + elliptic.curve = curve_1; + elliptic.curves = curves_1; + + // Protocols + elliptic.ec = ec; + elliptic.eddsa = eddsa; + }); + + var elliptic = /*#__PURE__*/Object.freeze({ + __proto__: null, + 'default': elliptic_1, + __moduleExports: elliptic_1 + }); + + exports.AEADEncryptedDataPacket = AEADEncryptedDataPacket; + exports.CleartextMessage = CleartextMessage; + exports.CompressedDataPacket = CompressedDataPacket; + exports.LiteralDataPacket = LiteralDataPacket; + exports.MarkerPacket = MarkerPacket; + exports.Message = Message; + exports.OnePassSignaturePacket = OnePassSignaturePacket; + exports.PacketList = PacketList; + exports.PrivateKey = PrivateKey; + exports.PublicKey = PublicKey; + exports.PublicKeyEncryptedSessionKeyPacket = PublicKeyEncryptedSessionKeyPacket; + exports.PublicKeyPacket = PublicKeyPacket; + exports.PublicSubkeyPacket = PublicSubkeyPacket; + exports.SecretKeyPacket = SecretKeyPacket; + exports.SecretSubkeyPacket = SecretSubkeyPacket; + exports.Signature = Signature$2; + exports.SignaturePacket = SignaturePacket; + exports.Subkey = Subkey; + exports.SymEncryptedIntegrityProtectedDataPacket = SymEncryptedIntegrityProtectedDataPacket; + exports.SymEncryptedSessionKeyPacket = SymEncryptedSessionKeyPacket; + exports.SymmetricallyEncryptedDataPacket = SymmetricallyEncryptedDataPacket; + exports.TrustPacket = TrustPacket; + exports.UnparseablePacket = UnparseablePacket; + exports.UserAttributePacket = UserAttributePacket; + exports.UserIDPacket = UserIDPacket; + exports.armor = armor; + exports.config = config; + exports.createCleartextMessage = createCleartextMessage; + exports.createMessage = createMessage; + exports.decrypt = decrypt; + exports.decryptKey = decryptKey; + exports.decryptSessionKeys = decryptSessionKeys; + exports.encrypt = encrypt; + exports.encryptKey = encryptKey; + exports.encryptSessionKey = encryptSessionKey; + exports.enums = enums; + exports.generateKey = generateKey; + exports.generateSessionKey = generateSessionKey; + exports.readCleartextMessage = readCleartextMessage; + exports.readKey = readKey; + exports.readKeys = readKeys; + exports.readMessage = readMessage; + exports.readPrivateKey = readPrivateKey; + exports.readPrivateKeys = readPrivateKeys; + exports.readSignature = readSignature; + exports.reformatKey = reformatKey; + exports.revokeKey = revokeKey; + exports.sign = sign; + exports.unarmor = unarmor; + exports.verify = verify; + + Object.defineProperty(exports, '__esModule', { value: true }); + + return exports; + +})({}); diff --git a/vendors/qr.js/AUTHORS.md b/vendors/qr.js/AUTHORS.md deleted file mode 100644 index 0fa53ceeee..0000000000 --- a/vendors/qr.js/AUTHORS.md +++ /dev/null @@ -1,7 +0,0 @@ -# Authors ordered by first contribution - -* tz -* Alasdair Mercer -* Alexandre Perrin -* Michael Mason -* Benjamin Besse diff --git a/vendors/qr.js/CHANGES.md b/vendors/qr.js/CHANGES.md deleted file mode 100644 index 08a1f2d7f3..0000000000 --- a/vendors/qr.js/CHANGES.md +++ /dev/null @@ -1,60 +0,0 @@ -## Version 1.1.4, 2015.11.11 - -* [#2](https://github.com/neocotic/qr.js/issues/2): Fix padding issues -* [#35](https://github.com/neocotic/qr.js/pull/35): Make the QR-code center-aligned -* [#38](https://github.com/neocotic/qr.js/pull/38): Update node-canvas dependency version to support Node.js v4 and above - -## Version 1.1.3, 2014.09.01 - -* [#23](https://github.com/neocotic/qr.js/issues/23): Revert back to [GPL License][] - -## Version 1.1.2, 2014.04.27 - -* [#20](https://github.com/neocotic/qr.js/issues/20): Fix "too many open files" bug - -## Version 1.1.1, 2013.12.03 - -* Fix bug with IIFE - -## Version 1.1.0, 2013.12.02 - -* [#9](https://github.com/neocotic/qr.js/issues/9): Fix RequireJS support -* [#13](https://github.com/neocotic/qr.js/issues/13): Remove [Ender][] support -* [#14](https://github.com/neocotic/qr.js/issues/14): Improve code formatting and style -* [#16](https://github.com/neocotic/qr.js/issues/16): Support different MIME types for `toDataURL` and other related functions -* [#17](https://github.com/neocotic/qr.js/issues/17): Remove unnecessary callback arguments from synchronous functions -* [#17](https://github.com/neocotic/qr.js/issues/17): Make `save` fully asynchronous -* [#17](https://github.com/neocotic/qr.js/issues/17): Add `saveSync` for synchronous saving -* [#18](https://github.com/neocotic/qr.js/issues/18): Add [Grunt][] build system -* [#18](https://github.com/neocotic/qr.js/issues/18): Generate source map as part of build -* [#18](https://github.com/neocotic/qr.js/issues/18): Improve developer documentation -* [#19](https://github.com/neocotic/qr.js/issues/19): Add support for [Bower][] -* Many small fixes and tweaks - -## Version 1.0.3, 2011.12.19 - -* [#3](https://github.com/neocotic/qr.js/issues/3): Rename `QRCode` to `qr` -* [#3](https://github.com/neocotic/qr.js/issues/3): Remove all deprecated methods -* [#4](https://github.com/neocotic/qr.js/issues/4): Reformat code and add additional, along with some original, code comments -* [#6](https://github.com/neocotic/qr.js/issues/6): Add support for [Node.js][], [CommonJS][] and [Ender][] -* [#6](https://github.com/neocotic/qr.js/issues/6): Add optional `callback` functionality to API methods -* [#7](https://github.com/neocotic/qr.js/issues/7): Allow `data` arguments to be an object or string value -* [#8](https://github.com/neocotic/qr.js/issues/8): Add `VERSION` property to the API -* [#8](https://github.com/neocotic/qr.js/issues/8): Add `toDataURL`, `save` and `noConflict` methods to the API -* Now distributed under the [MIT License][] - -## Version 1.0.2, 2011.08.31 - -* [#1](https://github.com/neocotic/qr.js/issues/1): Deprecate `generateCanvas` and `generateImage` and replaced with `canvas` and `image` respectively - -## Version 1.0.1, 2011.08.12 - -* Allow customisation of colours used when rendering - -[bower]: http://bower.io -[commonjs]: http://commonjs.org -[ender]: http://ender.no.de -[gpl license]: http://www.gnu.org/licenses/ -[grunt]: http://gruntjs.com -[mit license]: http://en.wikipedia.org/wiki/MIT_License -[node.js]: http://nodejs.org diff --git a/vendors/qr.js/CONTRIBUTING.md b/vendors/qr.js/CONTRIBUTING.md deleted file mode 100644 index de239d8be0..0000000000 --- a/vendors/qr.js/CONTRIBUTING.md +++ /dev/null @@ -1,59 +0,0 @@ -# Contributing - -Here are some guidelines that we'd like contributors to follow so that we can have a chance of -keeping things right. - -## Getting Starting - -* Make sure you have a [GitHub account](https://github.com/signup/free) -* Submit a ticket for your issue if one does not already exist - * Clearly describe the issue including steps to reproduce when it is a bug - * Include the earliest version that you know has the issue -* Fork the repository on GitHub -* Read the `INSTALL.md` file - -## Making Changes - -* Create a topic branch from where you want to base your work - * This is usually the master branch - * Only target release branches if you are certain your fix must be on that branch - * To quickly create a topic branch based on master; - `git branch fix/master/my_contribution master` then checkout the new branch with - `git checkout fix/master/my_contribution` - * Avoid working directly on the `master` branch -* Make commits of logical units -* Check for unnecessary whitespace with `git diff --check` before committing -* Make sure your commit messages are in the proper format -* Avoid updating the distributable file or annotated source code documentation - -``` -(#99999) Make the example in CONTRIBUTING imperative and concrete - -Without this patch applied the example commit message in the CONTRIBUTING document is not a -concrete example. This is a problem because the contributor is left to imagine what the commit -message should look like based on a description rather than an example. This patch fixes the -problem by making the example concrete and imperative. - -The first line is a real life imperative statement with a ticket number from our issue tracker. The -body describes the behavior without the patch, why this is a problem, and how the patch fixes the -problem when applied. -``` - -* Make sure you have added the necessary tests for your changes -* Run *all* the tests to assure nothing else was accidentally broken - -## Submitting Changes - -* Ensure you added your details to `AUTHORS.md` in the correct format - `Joe Bloggs ` -* Push your changes to a topic branch in your fork of the repository -* Submit a pull request to neocotic's repository -* Update your issue to mark that you have submitted code and are ready for it to be reviewed - * Include a link to the pull request in the issue - -# Additional Resources - -* [qr.js repository](https://github.com/neocotic/qr.js) -* [Issue tracker](https://github.com/neocotic/qr.js/issues) -* [General GitHub documentation](http://help.github.com) -* [GitHub pull request documentation](http://help.github.com/send-pull-requests) diff --git a/vendors/qr.js/INSTALL.md b/vendors/qr.js/INSTALL.md deleted file mode 100644 index d793d2b86d..0000000000 --- a/vendors/qr.js/INSTALL.md +++ /dev/null @@ -1,46 +0,0 @@ -This document is only relevant for those that want to contribute to the [qr.js][] open source -project (we love you guys!). If you are only interested in installing the tool look at `README.md`. - -## Build Requirements - -In order to build [qr.js][], you need to have the following install [git][] 1.7+ and [node.js][] -0.8+ (which includes [npm][]). - -### Canvas Support - -[qr.js][] heavily depends on [node-canvas][] to support the HTML5 canvas element in the [node.js][] -environment. Unfortunately, this library is dependant on [Cairo][], which is not managed by -[npm][]. Before you are able to build [qr.js][] (and it's dependencies), you must have [Cairo][] -installed. Please see their wiki on steps on how to do this on various platforms: - -https://github.com/LearnBoost/node-canvas/wiki/_pages - -## Building - -Follow these steps to build [qr.js][]; - -1. Clone a copy of the main [qr.js git repository](https://github.com/neocotic/qr.js) by running - `git clone git://github.com/neocotic/qr.js.git` -2. `cd` to the repository directory -3. Ensure you have all of the dependencies by entering `npm install` -4. Ensure you can run [Grunt][] by running `npm install -g grunt-cli` -5. To run the full test suite enter `grunt test` - * **Pro Tip:** You can easily run step 5 by just entering `grunt` -6. To update the optimized distributable file and documentation enter `grunt dist` - * Outputs to documentation to the `docs` directory - -## Important - -If you're planning on contributing to [qr.js][] please do **NOT** update the distributable file or -documentation (step 6) when submitting a pull request. We will not accept pull requests when these -files have been changed as we do this ourselves when finalizing a release. - -Read the `CONTRIBUTING.md` file for more information about submitting pull requests. - -[cairo]: http://cairographics.org -[git]: http://git-scm.com -[grunt]: http://gruntjs.com -[node.js]: http://nodejs.org -[node-canvas]: https://github.com/LearnBoost/node-canvas -[npm]: http://npmjs.org -[qr.js]: http://neocotic.com/qr.js diff --git a/vendors/qr.js/LICENSE.md b/vendors/qr.js/LICENSE.md deleted file mode 100644 index dec605db5c..0000000000 --- a/vendors/qr.js/LICENSE.md +++ /dev/null @@ -1,14 +0,0 @@ -Copyright (C) 2015 Alasdair Mercer, http://neocotic.com - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . diff --git a/vendors/qr.js/README.md b/vendors/qr.js/README.md deleted file mode 100644 index 74ec9a12c0..0000000000 --- a/vendors/qr.js/README.md +++ /dev/null @@ -1,290 +0,0 @@ - __ - __ _ __ /\_\ ____ - /'__`\/\`'__\ \/\ \ /',__\ - /\ \L\ \ \ \/__ \ \ \/\__, `\ - \ \___, \ \_\\_\_\ \ \/\____/ - \/___/\ \/_//_/\ \_\ \/___/ - \ \_\ \ \____/ - \/_/ \/___/ - -[qr.js][] is a pure JavaScript library for [QR code][] generation using canvas. - -* [Install](#install) -* [Examples](#examples) -* [API](#api) -* [Canvas Support](#canvas-support) -* [Bugs](#bugs) -* [Questions](#questions) - -## Install - -Install using the package manager for your desired environment(s): - -``` bash -# for node.js: -$ npm install qr-js -# OR; for the browser: -$ bower install qr-js -``` - -## Examples - -In the browser: - -``` html - - - - - - - -``` - -In [node.js][]: - -``` javascript -var qr = require('qr-js'); - -qr.saveSync('http://neocotic.com/qr.js', 'qrcode.png'); -``` - -## API - -### Standard Data - -The following configuration data options are recognised by all of the core API methods (all of -which are optional): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    PropertyDescriptionDefault
    backgroundBackground colour to be used#fff
    canvas<canvas> element in which the QR code should be renderedCreates a new element
    foregroundForeground colour to be used#000
    levelECC (error correction capacity) level to be appliedL
    sizeModule size of the generated QR code4
    valueValue to be encoded in the generated QR code""
    - -### `canvas([data|value])` -Renders a QR code in an HTML5 `` element for a given value. - -``` javascript -// Render the QR code on a newly created canvas element -var canvas = qr.canvas('http://neocotic.com/qr.js'); -// Re-render the QR code on an existing element -qr.canvas({ - canvas: canvas, - value: 'https://github.com/neocotic/qr.js' -}); -``` - -### `image([data|value])` -Renders a QR code in an HTML `` element for a given value. - -``` javascript -// Render the QR code on a newly created img element -var img = qr.image('http://neocotic.com/qr.js'); -// Re-render the QR code on an existing element -qr.image({ - image: img, - value: 'https://github.com/neocotic/qr.js' -}); -``` - -#### Additional Data -As well as the [Standard Data](#standard-data), this method also accepts the following additional -data options: - - - - - - - - - - - - - - - - - -
    PropertyDescriptionDefault
    image<img> element in which the QR code should be renderedCreates a new element
    mimeMIME type to process the QR code imageimage/png
    - -### `save([data|value][, path], callback)` -Saves a QR code, which has been rendered for a given value, to the user's file system. - -``` javascript -// Render a QR code to a PNG file -qr.save('http://neocotic.com/qr.js', 'qr.png', function(err) { - if (err) throw err; - - // ... -}); -// Render a QR code to a JPEG file -qr.save({ - mime: 'image/jpeg', - path: 'qr.jpg', - value: 'https://github.com/neocotic/qr.js' -}, function(err) { - if (err) throw err; - - // ... -}); -``` - -**Note:** Currently, in the browser, this just does it's best to force a download prompt. We will -try to improve on this in the future. - -#### Additional Data -As well as the [Standard Data](#standard-data), this method also accepts the following additional -data options: - - - - - - - - - - - - - - - - - -
    PropertyDescriptionDefault
    mimeMIME type to process the QR code imageimage/png
    pathPath to which the QR code should be saved
    Ignored in browsers
    Required if not specified as an argument
    - -### `saveSync([data|value][, path])` -Synchronous [`save(3)`](#savedatavalue-path-callback). - -### `toDataURL([data|value])` -Returns a data URL for rendered QR code. This is a convenient shorthand for dealing with the native -`HTMLCanvasElement.prototype.toDataURL` function. - -``` javascript -console.log(qr.toDataURL('http://neocotic.com/qr.js')); // "data:image/png;base64,iVBORw0KGgoAAAA..." -console.log(qr.toDataURL({ - mime: 'image/jpeg', - value: 'https://github.com/neocotic/qr.js' -})); // "data:image/jpeg;base64,/9j/4AAQSkZJRg..." -``` - -#### Additional Data -As well as the [Standard Data](#standard-data), this method also accepts the following additional -data options: - - - - - - - - - - - - -
    PropertyDescriptionDefault
    mimeMIME type to process the QR code imageimage/png
    - -### Miscellaneous - -#### `noConflict()` -Returns `qr` in a no-conflict state, reallocating the `qr` global variable name to its previous -owner, where possible. - -This is really just intended for use within a browser. - -``` html - - - -``` - -#### `VERSION` -The current version of `qr`. - -``` javascript -console.log(qr.VERSION); // "1.1.4" -``` - -## Canvas Support - -For browser users; their browser must support the HTML5 canvas element or the API will throw an -error immediately. - -For [node.js][] users; [qr.js][] heavily depends on [node-canvas][] to support the HTML5 canvas -element in the [node.js][] environment. Unfortunately, this library is dependant on [Cairo][], -which is not managed by [npm][]. Before you are able to install [qr.js][] (and it's dependencies), -you must have [Cairo][] installed. Please see their wiki on steps on how to do this on various -platforms: - -https://github.com/LearnBoost/node-canvas/wiki/_pages - -## Bugs - -If you have any problems with this library or would like to see the changes currently in -development you can do so here; - -https://github.com/neocotic/qr.js/issues - -## Questions? - -Take a look at `docs/qr.html` to get a better understanding of what the code is doing. - -If that doesn't help, feel free to follow me on Twitter, [@neocotic][]. - -However, if you want more information or examples of using this library please visit the project's -homepage; - -http://neocotic.com/qr.js - -[@neocotic]: https://twitter.com/neocotic -[cairo]: http://cairographics.org -[node.js]: http://nodejs.org -[node-canvas]: https://github.com/LearnBoost/node-canvas -[npm]: http://npmjs.org -[qr.js]: http://neocotic.com/qr.js -[qr code]: http://en.wikipedia.org/wiki/QR_code diff --git a/vendors/qr.js/bower.json b/vendors/qr.js/bower.json deleted file mode 100644 index 60ed2373ff..0000000000 --- a/vendors/qr.js/bower.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "qr-js", - "version": "1.1.4", - "description": "Library for QR code generation using canvas", - "homepage": "http://neocotic.com/qr.js", - "authors": [ - { - "name": "Alasdair Mercer", - "email": "mercer.alasdair@gmail.com", - "homepage": "http://neocotic.com" - } - ], - "license": "GPL-3.0", - "keywords": [ - "qr", - "code", - "encode", - "canvas", - "image" - ], - "repository": { - "type": "git", - "url": "https://github.com/neocotic/qr.js.git" - }, - "main": "qr.min.js", - "ignore": [ - "**/.*", - "docs", - "AUTHORS.md", - "CHANGES.md", - "CONTRIBUTING.md", - "Gruntfile.js", - "INSTALL.md", - "README.md", - "bower.json", - "package.json" - ] -} diff --git a/vendors/qr.js/package.json b/vendors/qr.js/package.json deleted file mode 100644 index 5f8bc01b77..0000000000 --- a/vendors/qr.js/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "qr-js", - "version": "1.1.4", - "description": "Library for QR code generation using canvas", - "homepage": "http://neocotic.com/qr.js", - "bugs": { - "url": "https://github.com/neocotic/qr.js/issues" - }, - "author": { - "name": "Alasdair Mercer", - "email": "mercer.alasdair@gmail.com", - "url": "http://neocotic.com" - }, - "license": "GPL-3.0", - "keywords": [ - "qr", - "code", - "encode", - "canvas", - "image" - ], - "repository": { - "type": "git", - "url": "https://github.com/neocotic/qr.js.git" - }, - "dependencies": { - "canvas": "^1.3.1" - }, - "devDependencies": { - "grunt": "^0.4.5", - "grunt-cli": "^0.1.13", - "grunt-contrib-jshint": "^0.11.3", - "grunt-contrib-uglify": "^0.10.0", - "grunt-docco": "^0.4.0" - }, - "main": "qr.js", - "scripts": { - "test": "grunt test" - } -} diff --git a/vendors/qr.js/qr.js b/vendors/qr.js/qr.js deleted file mode 100644 index e695bc194b..0000000000 --- a/vendors/qr.js/qr.js +++ /dev/null @@ -1,1217 +0,0 @@ -// [qr.js](http://neocotic.com/qr.js) -// (c) 2015 Alasdair Mercer -// Licensed under the GPL Version 3 license. -// Based on [jsqrencode](http://code.google.com/p/jsqrencode/) -// (c) 2010 tz@execpc.com -// Licensed under the GPL Version 3 license. -// For all details and documentation: -// - -(function (root) { - - 'use strict'; - - // Private constants - // ----------------- - - // Alignment pattern. - var ALIGNMENT_DELTA = [ - 0, 11, 15, 19, 23, 27, 31, - 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24, - 26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28 - ]; - // Default MIME type. - var DEFAULT_MIME = 'image/png'; - // MIME used to initiate a browser download prompt when `qr.save` is called. - var DOWNLOAD_MIME = 'image/octet-stream'; - // There are four elements per version. The first two indicate the number of blocks, then the - // data width, and finally the ECC width. - var ECC_BLOCKS = [ - 1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17, - 1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22, 1, 0, 16, 28, - 1, 0, 55, 15, 1, 0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22, - 1, 0, 80, 20, 2, 0, 32, 18, 2, 0, 24, 26, 4, 0, 9, 16, - 1, 0, 108, 26, 2, 0, 43, 24, 2, 2, 15, 18, 2, 2, 11, 22, - 2, 0, 68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28, - 2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4, 1, 13, 26, - 2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26, - 2, 0, 116, 30, 3, 2, 36, 22, 4, 4, 16, 20, 4, 4, 12, 24, - 2, 2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28, - 4, 0, 81, 20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24, - 2, 2, 92, 24, 6, 2, 36, 22, 4, 6, 20, 26, 7, 4, 14, 28, - 4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22, - 3, 1, 115, 30, 4, 5, 40, 24, 11, 5, 16, 20, 11, 5, 12, 24, - 5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24, - 5, 1, 98, 24, 7, 3, 45, 28, 15, 2, 19, 24, 3, 13, 15, 30, - 1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2, 17, 14, 28, - 5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2, 19, 14, 28, - 3, 4, 113, 28, 3, 11, 44, 26, 17, 4, 21, 26, 9, 16, 13, 26, - 3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28, - 4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30, - 2, 7, 111, 28, 17, 0, 46, 28, 7, 16, 24, 30, 34, 0, 13, 24, - 4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30, - 6, 4, 117, 30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30, - 8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30, 22, 13, 15, 30, - 10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30, - 8, 4, 122, 30, 22, 3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30, - 3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31, 15, 30, - 7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30, - 5, 10, 115, 30, 19, 10, 47, 28, 15, 25, 24, 30, 23, 25, 15, 30, - 13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15, 30, - 17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30, - 17, 1, 115, 30, 14, 21, 46, 28, 29, 19, 24, 30, 11, 46, 15, 30, - 13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16, 30, - 12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30, - 6, 14, 121, 30, 6, 34, 47, 28, 46, 10, 24, 30, 2, 64, 15, 30, - 17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15, 30, - 4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30, - 20, 4, 117, 30, 40, 7, 47, 28, 43, 22, 24, 30, 10, 67, 15, 30, - 19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15, 30 - ]; - // Map of human-readable ECC levels. - var ECC_LEVELS = { - L: 1, - M: 2, - Q: 3, - H: 4 - }; - // Final format bits with mask (level << 3 | mask). - var FINAL_FORMAT = [ - 0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976, /* L */ - 0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0, /* M */ - 0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed, /* Q */ - 0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b /* H */ - ]; - // Galois field exponent table. - var GALOIS_EXPONENT = [ - 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26, - 0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, - 0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23, - 0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1, - 0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0, - 0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2, - 0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce, - 0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc, - 0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54, - 0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73, - 0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff, - 0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41, - 0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6, - 0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09, - 0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16, - 0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00 - ]; - // Galois field log table. - var GALOIS_LOG = [ - 0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b, - 0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71, - 0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45, - 0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6, - 0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88, - 0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40, - 0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d, - 0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57, - 0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18, - 0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e, - 0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61, - 0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2, - 0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6, - 0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a, - 0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7, - 0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf - ]; - // *Badness* coefficients. - var N1 = 3; - var N2 = 3; - var N3 = 40; - var N4 = 10; - // Version pattern. - var VERSION_BLOCK = [ - 0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d, 0x928, 0xb78, 0x45d, 0xa17, 0x532, - 0x9a6, 0x683, 0x8c9, 0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75, 0x250, 0x9d5, - 0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64, 0x541, 0xc69 - ]; - // Mode for node.js file system file writes. - var WRITE_MODE = parseInt('0666', 8); - - // Private variables - // ----------------- - - // Run lengths for badness. - var badBuffer = []; - // Constructor for `canvas` elements in the node.js environment. - var Canvas; - // Data block. - var dataBlock; - // ECC data blocks and tables. - var eccBlock, neccBlock1, neccBlock2; - // ECC buffer. - var eccBuffer = []; - // ECC level (defaults to **L**). - var eccLevel = 1; - // Image buffer. - var frameBuffer = []; - // Fixed part of the image. - var frameMask = []; - // File system within the node.js environment. - var fs; - // Constructor for `img` elements in the node.js environment. - var Image; - // Indicates whether or not this script is running in node.js. - var inNode = false; - // Generator polynomial. - var polynomial = []; - // Save the previous value of the `qr` variable. - var previousQr = root.qr; - // Data input buffer. - var stringBuffer = []; - // Version for the data. - var version; - // Data width is based on `version`. - var width; - - // Private functions - // ----------------- - - // Create a new canvas using `document.createElement` unless script is running in node.js, in - // which case the `canvas` module is used. - function createCanvas() { - return inNode ? new Canvas() : root.document.createElement('canvas'); - } - - // Create a new image using `document.createElement` unless script is running in node.js, in - // which case the `canvas` module is used. - function createImage() { - return inNode ? new Image() : root.document.createElement('img'); - } - - // Force the canvas image to be downloaded in the browser. - // Optionally, a `callback` function can be specified which will be called upon completed. Since - // this is not an asynchronous operation, this is merely convenient and helps simplify the - // calling code. - function download(cvs, data, callback) { - var mime = data.mime || DEFAULT_MIME; - - root.location.href = cvs.toDataURL(mime).replace(mime, DOWNLOAD_MIME); - - if (typeof callback === 'function') callback(); - } - - // Normalize the `data` that is provided to the main API. - function normalizeData(data) { - if (typeof data === 'string') data = { value: data }; - return data || {}; - } - - // Override the `qr` API methods that require HTML5 canvas support to throw a relevant error. - function overrideAPI(qr) { - var methods = [ 'canvas', 'image', 'save', 'saveSync', 'toDataURL' ]; - var i; - - function overrideMethod(name) { - qr[name] = function () { - throw new Error(name + ' requires HTML5 canvas element support'); - }; - } - - for (i = 0; i < methods.length; i++) { - overrideMethod(methods[i]); - } - } - - // Asynchronously write the data of the rendered canvas to a given file path. - function writeFile(cvs, data, callback) { - if (typeof data.path !== 'string') { - return callback(new TypeError('Invalid path type: ' + typeof data.path)); - } - - var fd, buff; - - // Write the buffer to the open file stream once both prerequisites are met. - function writeBuffer() { - fs.write(fd, buff, 0, buff.length, 0, function (error) { - fs.close(fd); - - callback(error); - }); - } - - // Create a buffer of the canvas' data. - cvs.toBuffer(function (error, _buff) { - if (error) return callback(error); - - buff = _buff; - if (fd) { - writeBuffer(); - } - }); - - // Open a stream for the file to be written. - fs.open(data.path, 'w', WRITE_MODE, function (error, _fd) { - if (error) return callback(error); - - fd = _fd; - if (buff) { - writeBuffer(); - } - }); - } - - // Write the data of the rendered canvas to a given file path. - function writeFileSync(cvs, data) { - if (typeof data.path !== 'string') { - throw new TypeError('Invalid path type: ' + typeof data.path); - } - - var buff = cvs.toBuffer(); - var fd = fs.openSync(data.path, 'w', WRITE_MODE); - - try { - fs.writeSync(fd, buff, 0, buff.length, 0); - } finally { - fs.closeSync(fd); - } - } - - // Set bit to indicate cell in frame is immutable (symmetric around diagonal). - function setMask(x, y) { - var bit; - - if (x > y) { - bit = x; - x = y; - y = bit; - } - - bit = y; - bit *= y; - bit += y; - bit >>= 1; - bit += x; - - frameMask[bit] = 1; - } - - // Enter alignment pattern. Foreground colour to frame, background to mask. Frame will be merged - // with mask later. - function addAlignment(x, y) { - var i; - - frameBuffer[x + width * y] = 1; - - for (i = -2; i < 2; i++) { - frameBuffer[(x + i) + width * (y - 2)] = 1; - frameBuffer[(x - 2) + width * (y + i + 1)] = 1; - frameBuffer[(x + 2) + width * (y + i)] = 1; - frameBuffer[(x + i + 1) + width * (y + 2)] = 1; - } - - for (i = 0; i < 2; i++) { - setMask(x - 1, y + i); - setMask(x + 1, y - i); - setMask(x - i, y - 1); - setMask(x + i, y + 1); - } - } - - // Exponentiation mod N. - function modN(x) { - while (x >= 255) { - x -= 255; - x = (x >> 8) + (x & 255); - } - - return x; - } - - // Calculate and append `ecc` data to the `data` block. If block is in the string buffer the - // indices to buffers are used. - function appendData(data, dataLength, ecc, eccLength) { - var bit, i, j; - - for (i = 0; i < eccLength; i++) { - stringBuffer[ecc + i] = 0; - } - - for (i = 0; i < dataLength; i++) { - bit = GALOIS_LOG[stringBuffer[data + i] ^ stringBuffer[ecc]]; - - if (bit !== 255) { - for (j = 1; j < eccLength; j++) { - stringBuffer[ecc + j - 1] = stringBuffer[ecc + j] ^ - GALOIS_EXPONENT[modN(bit + polynomial[eccLength - j])]; - } - } else { - for (j = ecc; j < ecc + eccLength; j++) { - stringBuffer[j] = stringBuffer[j + 1]; - } - } - - stringBuffer[ecc + eccLength - 1] = bit === 255 ? 0 : - GALOIS_EXPONENT[modN(bit + polynomial[0])]; - } - } - - // Check mask since symmetricals use half. - function isMasked(x, y) { - var bit; - - if (x > y) { - bit = x; - x = y; - y = bit; - } - - bit = y; - bit += y * y; - bit >>= 1; - bit += x; - - return frameMask[bit] === 1; - } - - // Apply the selected mask out of the 8 options. - function applyMask(mask) { - var x, y, r3x, r3y; - - switch (mask) { - case 0: - for (y = 0; y < width; y++) { - for (x = 0; x < width; x++) { - if (!((x + y) & 1) && !isMasked(x, y)) { - frameBuffer[x + y * width] ^= 1; - } - } - } - - break; - case 1: - for (y = 0; y < width; y++) { - for (x = 0; x < width; x++) { - if (!(y & 1) && !isMasked(x, y)) { - frameBuffer[x + y * width] ^= 1; - } - } - } - - break; - case 2: - for (y = 0; y < width; y++) { - for (r3x = 0, x = 0; x < width; x++, r3x++) { - if (r3x === 3) r3x = 0; - - if (!r3x && !isMasked(x, y)) { - frameBuffer[x + y * width] ^= 1; - } - } - } - - break; - case 3: - for (r3y = 0, y = 0; y < width; y++, r3y++) { - if (r3y === 3) r3y = 0; - - for (r3x = r3y, x = 0; x < width; x++, r3x++) { - if (r3x === 3) r3x = 0; - - if (!r3x && !isMasked(x, y)) { - frameBuffer[x + y * width] ^= 1; - } - } - } - - break; - case 4: - for (y = 0; y < width; y++) { - for (r3x = 0, r3y = ((y >> 1) & 1), x = 0; x < width; x++, r3x++) { - if (r3x === 3) { - r3x = 0; - r3y = !r3y; - } - - if (!r3y && !isMasked(x, y)) { - frameBuffer[x + y * width] ^= 1; - } - } - } - - break; - case 5: - for (r3y = 0, y = 0; y < width; y++, r3y++) { - if (r3y === 3) r3y = 0; - - for (r3x = 0, x = 0; x < width; x++, r3x++) { - if (r3x === 3) r3x = 0; - - if (!((x & y & 1) + !(!r3x | !r3y)) && !isMasked(x, y)) { - frameBuffer[x + y * width] ^= 1; - } - } - } - - break; - case 6: - for (r3y = 0, y = 0; y < width; y++, r3y++) { - if (r3y === 3) r3y = 0; - - for (r3x = 0, x = 0; x < width; x++, r3x++) { - if (r3x === 3) r3x = 0; - - if (!(((x & y & 1) + (r3x && (r3x === r3y))) & 1) && !isMasked(x, y)) { - frameBuffer[x + y * width] ^= 1; - } - } - } - - break; - case 7: - for (r3y = 0, y = 0; y < width; y++, r3y++) { - if (r3y === 3) r3y = 0; - - for (r3x = 0, x = 0; x < width; x++, r3x++) { - if (r3x === 3) r3x = 0; - - if (!(((r3x && (r3x === r3y)) + ((x + y) & 1)) & 1) && !isMasked(x, y)) { - frameBuffer[x + y * width] ^= 1; - } - } - } - - break; - } - } - - // Using the table for the length of each run, calculate the amount of bad image. Long runs or - // those that look like finders are called twice; once for X and Y. - function getBadRuns(length) { - var badRuns = 0; - var i; - - for (i = 0; i <= length; i++) { - if (badBuffer[i] >= 5) { - badRuns += N1 + badBuffer[i] - 5; - } - } - - // FBFFFBF as in finder. - for (i = 3; i < length - 1; i += 2) { - if (badBuffer[i - 2] === badBuffer[i + 2] && - badBuffer[i + 2] === badBuffer[i - 1] && - badBuffer[i - 1] === badBuffer[i + 1] && - badBuffer[i - 1] * 3 === badBuffer[i] && - // Background around the foreground pattern? Not part of the specs. - (badBuffer[i - 3] === 0 || i + 3 > length || - badBuffer[i - 3] * 3 >= badBuffer[i] * 4 || - badBuffer[i + 3] * 3 >= badBuffer[i] * 4)) { - badRuns += N3; - } - } - - return badRuns; - } - - // Calculate how bad the masked image is (e.g. blocks, imbalance, runs, or finders). - function checkBadness() { - var b, b1, bad, big, bw, count, h, x, y; - bad = bw = count = 0; - - // Blocks of same colour. - for (y = 0; y < width - 1; y++) { - for (x = 0; x < width - 1; x++) { - // All foreground colour. - if ((frameBuffer[x + width * y] && - frameBuffer[(x + 1) + width * y] && - frameBuffer[x + width * (y + 1)] && - frameBuffer[(x + 1) + width * (y + 1)]) || - // All background colour. - !(frameBuffer[x + width * y] || - frameBuffer[(x + 1) + width * y] || - frameBuffer[x + width * (y + 1)] || - frameBuffer[(x + 1) + width * (y + 1)])) { - bad += N2; - } - } - } - - // X runs. - for (y = 0; y < width; y++) { - badBuffer[0] = 0; - - for (h = b = x = 0; x < width; x++) { - if ((b1 = frameBuffer[x + width * y]) === b) { - badBuffer[h]++; - } else { - badBuffer[++h] = 1; - } - - b = b1; - bw += b ? 1 : -1; - } - - bad += getBadRuns(h); - } - - if (bw < 0) bw = -bw; - - big = bw; - big += big << 2; - big <<= 1; - - while (big > width * width) { - big -= width * width; - count++; - } - - bad += count * N4; - - // Y runs. - for (x = 0; x < width; x++) { - badBuffer[0] = 0; - - for (h = b = y = 0; y < width; y++) { - if ((b1 = frameBuffer[x + width * y]) === b) { - badBuffer[h]++; - } else { - badBuffer[++h] = 1; - } - - b = b1; - } - - bad += getBadRuns(h); - } - - return bad; - } - - // Generate the encoded QR image for the string provided. - function generateFrame(str) { - var i, j, k, m, t, v, x, y; - - // Find the smallest version that fits the string. - t = str.length; - - version = 0; - - do { - version++; - - k = (eccLevel - 1) * 4 + (version - 1) * 16; - - neccBlock1 = ECC_BLOCKS[k++]; - neccBlock2 = ECC_BLOCKS[k++]; - dataBlock = ECC_BLOCKS[k++]; - eccBlock = ECC_BLOCKS[k]; - - k = dataBlock * (neccBlock1 + neccBlock2) + neccBlock2 - 3 + (version <= 9); - - if (t <= k) break; - } while (version < 40); - - // FIXME: Ensure that it fits insted of being truncated. - width = 17 + 4 * version; - - // Allocate, clear and setup data structures. - v = dataBlock + (dataBlock + eccBlock) * (neccBlock1 + neccBlock2) + neccBlock2; - - for (t = 0; t < v; t++) { - eccBuffer[t] = 0; - } - - stringBuffer = str.slice(0); - - for (t = 0; t < width * width; t++) { - frameBuffer[t] = 0; - } - - for (t = 0; t < (width * (width + 1) + 1) / 2; t++) { - frameMask[t] = 0; - } - - // Insert finders: Foreground colour to frame and background to mask. - for (t = 0; t < 3; t++) { - k = y = 0; - - if (t === 1) k = (width - 7); - if (t === 2) y = (width - 7); - - frameBuffer[(y + 3) + width * (k + 3)] = 1; - - for (x = 0; x < 6; x++) { - frameBuffer[(y + x) + width * k] = 1; - frameBuffer[y + width * (k + x + 1)] = 1; - frameBuffer[(y + 6) + width * (k + x)] = 1; - frameBuffer[(y + x + 1) + width * (k + 6)] = 1; - } - - for (x = 1; x < 5; x++) { - setMask(y + x, k + 1); - setMask(y + 1, k + x + 1); - setMask(y + 5, k + x); - setMask(y + x + 1, k + 5); - } - - for (x = 2; x < 4; x++) { - frameBuffer[(y + x) + width * (k + 2)] = 1; - frameBuffer[(y + 2) + width * (k + x + 1)] = 1; - frameBuffer[(y + 4) + width * (k + x)] = 1; - frameBuffer[(y + x + 1) + width * (k + 4)] = 1; - } - } - - // Alignment blocks. - if (version > 1) { - t = ALIGNMENT_DELTA[version]; - y = width - 7; - - for (;;) { - x = width - 7; - - while (x > t - 3) { - addAlignment(x, y); - - if (x < t) break; - - x -= t; - } - - if (y <= t + 9) break; - - y -= t; - - addAlignment(6, y); - addAlignment(y, 6); - } - } - - // Single foreground cell. - frameBuffer[8 + width * (width - 8)] = 1; - - // Timing gap (mask only). - for (y = 0; y < 7; y++) { - setMask(7, y); - setMask(width - 8, y); - setMask(7, y + width - 7); - } - - for (x = 0; x < 8; x++) { - setMask(x, 7); - setMask(x + width - 8, 7); - setMask(x, width - 8); - } - - // Reserve mask, format area. - for (x = 0; x < 9; x++) { - setMask(x, 8); - } - - for (x = 0; x < 8; x++) { - setMask(x + width - 8, 8); - setMask(8, x); - } - - for (y = 0; y < 7; y++) { - setMask(8, y + width - 7); - } - - // Timing row/column. - for (x = 0; x < width - 14; x++) { - if (x & 1) { - setMask(8 + x, 6); - setMask(6, 8 + x); - } else { - frameBuffer[(8 + x) + width * 6] = 1; - frameBuffer[6 + width * (8 + x)] = 1; - } - } - - // Version block. - if (version > 6) { - t = VERSION_BLOCK[version - 7]; - k = 17; - - for (x = 0; x < 6; x++) { - for (y = 0; y < 3; y++, k--) { - if (1 & (k > 11 ? version >> (k - 12) : t >> k)) { - frameBuffer[(5 - x) + width * (2 - y + width - 11)] = 1; - frameBuffer[(2 - y + width - 11) + width * (5 - x)] = 1; - } else { - setMask(5 - x, 2 - y + width - 11); - setMask(2 - y + width - 11, 5 - x); - } - } - } - } - - // Sync mask bits. Only set above for background cells, so now add the foreground. - for (y = 0; y < width; y++) { - for (x = 0; x <= y; x++) { - if (frameBuffer[x + width * y]) { - setMask(x, y); - } - } - } - - // Convert string to bit stream. 8-bit data to QR-coded 8-bit data (numeric, alphanum, or kanji - // not supported). - v = stringBuffer.length; - - // String to array. - for (i = 0; i < v; i++) { - eccBuffer[i] = stringBuffer.charCodeAt(i); - } - - stringBuffer = eccBuffer.slice(0); - - // Calculate max string length. - x = dataBlock * (neccBlock1 + neccBlock2) + neccBlock2; - - if (v >= x - 2) { - v = x - 2; - - if (version > 9) v--; - } - - // Shift and re-pack to insert length prefix. - i = v; - - if (version > 9) { - stringBuffer[i + 2] = 0; - stringBuffer[i + 3] = 0; - - while (i--) { - t = stringBuffer[i]; - - stringBuffer[i + 3] |= 255 & (t << 4); - stringBuffer[i + 2] = t >> 4; - } - - stringBuffer[2] |= 255 & (v << 4); - stringBuffer[1] = v >> 4; - stringBuffer[0] = 0x40 | (v >> 12); - } else { - stringBuffer[i + 1] = 0; - stringBuffer[i + 2] = 0; - - while (i--) { - t = stringBuffer[i]; - - stringBuffer[i + 2] |= 255 & (t << 4); - stringBuffer[i + 1] = t >> 4; - } - - stringBuffer[1] |= 255 & (v << 4); - stringBuffer[0] = 0x40 | (v >> 4); - } - - // Fill to end with pad pattern. - i = v + 3 - (version < 10); - - while (i < x) { - stringBuffer[i++] = 0xec; - stringBuffer[i++] = 0x11; - } - - // Calculate generator polynomial. - polynomial[0] = 1; - - for (i = 0; i < eccBlock; i++) { - polynomial[i + 1] = 1; - - for (j = i; j > 0; j--) { - polynomial[j] = polynomial[j] ? polynomial[j - 1] ^ - GALOIS_EXPONENT[modN(GALOIS_LOG[polynomial[j]] + i)] : polynomial[j - 1]; - } - - polynomial[0] = GALOIS_EXPONENT[modN(GALOIS_LOG[polynomial[0]] + i)]; - } - - // Use logs for generator polynomial to save calculation step. - for (i = 0; i <= eccBlock; i++) { - polynomial[i] = GALOIS_LOG[polynomial[i]]; - } - - // Append ECC to data buffer. - k = x; - y = 0; - - for (i = 0; i < neccBlock1; i++) { - appendData(y, dataBlock, k, eccBlock); - - y += dataBlock; - k += eccBlock; - } - - for (i = 0; i < neccBlock2; i++) { - appendData(y, dataBlock + 1, k, eccBlock); - - y += dataBlock + 1; - k += eccBlock; - } - - // Interleave blocks. - y = 0; - - for (i = 0; i < dataBlock; i++) { - for (j = 0; j < neccBlock1; j++) { - eccBuffer[y++] = stringBuffer[i + j * dataBlock]; - } - - for (j = 0; j < neccBlock2; j++) { - eccBuffer[y++] = stringBuffer[(neccBlock1 * dataBlock) + i + (j * (dataBlock + 1))]; - } - } - - for (j = 0; j < neccBlock2; j++) { - eccBuffer[y++] = stringBuffer[(neccBlock1 * dataBlock) + i + (j * (dataBlock + 1))]; - } - - for (i = 0; i < eccBlock; i++) { - for (j = 0; j < neccBlock1 + neccBlock2; j++) { - eccBuffer[y++] = stringBuffer[x + i + j * eccBlock]; - } - } - - stringBuffer = eccBuffer; - - // Pack bits into frame avoiding masked area. - x = y = width - 1; - k = v = 1; - - // inteleaved data and ECC codes. - m = (dataBlock + eccBlock) * (neccBlock1 + neccBlock2) + neccBlock2; - - for (i = 0; i < m; i++) { - t = stringBuffer[i]; - - for (j = 0; j < 8; j++, t <<= 1) { - if (0x80 & t) { - frameBuffer[x + width * y] = 1; - } - - // Find next fill position. - do { - if (v) { - x--; - } else { - x++; - - if (k) { - if (y !== 0) { - y--; - } else { - x -= 2; - k = !k; - - if (x === 6) { - x--; - y = 9; - } - } - } else { - if (y !== width - 1) { - y++; - } else { - x -= 2; - k = !k; - - if (x === 6) { - x--; - y -= 8; - } - } - } - } - - v = !v; - } while (isMasked(x, y)); - } - } - - // Save pre-mask copy of frame. - stringBuffer = frameBuffer.slice(0); - - t = 0; - y = 30000; - - // Using `for` instead of `while` since in original Arduino code if an early mask was *good - // enough* it wouldn't try for a better one since they get more complex and take longer. - for (k = 0; k < 8; k++) { - // Returns foreground-background imbalance. - applyMask(k); - - x = checkBadness(); - - // Is current mask better than previous best? - if (x < y) { - y = x; - t = k; - } - - // Don't increment `i` to a void redoing mask. - if (t === 7) break; - - // Reset for next pass. - frameBuffer = stringBuffer.slice(0); - } - - // Redo best mask as none were *good enough* (i.e. last wasn't `t`). - if (t !== k) { - applyMask(t); - } - - // Add in final mask/ECC level bytes. - y = FINAL_FORMAT[t + ((eccLevel - 1) << 3)]; - - // Low byte. - for (k = 0; k < 8; k++, y >>= 1) { - if (y & 1) { - frameBuffer[(width - 1 - k) + width * 8] = 1; - - if (k < 6) { - frameBuffer[8 + width * k] = 1; - } else { - frameBuffer[8 + width * (k + 1)] = 1; - } - } - } - - // High byte. - for (k = 0; k < 7; k++, y >>= 1) { - if (y & 1) { - frameBuffer[8 + width * (width - 7 + k)] = 1; - - if (k) { - frameBuffer[(6 - k) + width * 8] = 1; - } else { - frameBuffer[7 + width * 8] = 1; - } - } - } - - // Finally, return the image data. - return frameBuffer; - } - - // qr.js setup - // ----------- - - // Build the publicly exposed API. - var qr = { - - // Constants - // --------- - - // Current version of `qr`. - VERSION: '1.1.4', - - // QR functions - // ------------ - - // Generate the QR code using the data provided and render it on to a `` element. - // If no `` element is specified in the argument provided a new one will be created and - // used. - // ECC (error correction capacity) determines how many intential errors are contained in the QR - // code. - canvas: function(data) { - data = normalizeData(data); - - // Module size of the generated QR code (i.e. 1-10). - var size = data.size >= 1 && data.size <= 10 ? data.size : 4; - // Actual size of the QR code symbol and is scaled to 25 pixels (e.g. 1 = 25px, 3 = 75px). - size *= 25; - - // `` element used to render the QR code. - var cvs = data.canvas || createCanvas(); - // Retreive the 2D context of the canvas. - var c2d = cvs.getContext('2d'); - // Ensure the canvas has the correct dimensions. - c2d.canvas.width = size; - c2d.canvas.height = size; - // Fill the canvas with the correct background colour. - c2d.fillStyle = data.background || '#fff'; - c2d.fillRect(0, 0, size, size); - - // Determine the ECC level to be applied. - eccLevel = ECC_LEVELS[(data.level && data.level.toUpperCase()) || 'L']; - - // Generate the image frame for the given `value`. - var frame = generateFrame(data.value || ''); - - c2d.lineWidth = 1; - - // Determine the *pixel* size. - var px = size; - px /= width; - px = Math.floor(px); - - var offset = Math.floor((size - (px * width)) / 2); - - // Draw the QR code. - c2d.clearRect(0, 0, size, size); - c2d.fillStyle = data.background || '#fff'; - c2d.fillRect(0, 0, size, size); - c2d.fillStyle = data.foreground || '#000'; - - var i, j; - - for (i = 0; i < width; i++) { - for (j = 0; j < width; j++) { - if (frame[j * width + i]) { - c2d.fillRect(px * i + offset, px * j + offset, px, px); - } - } - } - - return cvs; - }, - - // Generate the QR code using the data provided and render it on to a `` element. - // If no `` element is specified in the argument provided a new one will be created and - // used. - // ECC (error correction capacity) determines how many intential errors are contained in the QR - // code. - image: function(data) { - data = normalizeData(data); - - // `` element only which the QR code is rendered. - var cvs = this.canvas(data); - // `` element used to display the QR code. - var img = data.image || createImage(); - - // Apply the QR code to `img`. - img.src = cvs.toDataURL(data.mime || DEFAULT_MIME); - img.height = cvs.height; - img.width = cvs.width; - - return img; - }, - - // Generate the QR code using the data provided and render it on to a `` element and - // save it as an image file. - // If no `` element is specified in the argument provided a new one will be created and - // used. - // ECC (error correction capacity) determines how many intential errors are contained in the QR - // code. - // If called in a browser the `path` property/argument is ignored and will simply prompt the - // user to choose a location and file name. However, if called within node.js the file will be - // saved to specified path. - // A `callback` function must be provided which will be called once the saving process has - // started. If an error occurs it will be passed as the first argument to this function, - // otherwise this argument will be `null`. - save: function(data, path, callback) { - data = normalizeData(data); - - switch (typeof path) { - case 'function': - callback = path; - path = null; - break; - case 'string': - data.path = path; - break; - } - - // Callback function is required. - if (typeof callback !== 'function') { - throw new TypeError('Invalid callback type: ' + typeof callback); - } - - var completed = false; - // `` element only which the QR code is rendered. - var cvs = this.canvas(data); - - // Simple function to try and ensure that the `callback` function is only called once. - function done(error) { - if (!completed) { - completed = true; - - callback(error); - } - } - - if (inNode) { - writeFile(cvs, data, done); - } else { - download(cvs, data, done); - } - }, - - // Generate the QR code using the data provided and render it on to a `` element and - // save it as an image file. - // If no `` element is specified in the argument provided a new one will be created and - // used. - // ECC (error correction capacity) determines how many intential errors are contained in the QR - // code. - // If called in a browser the `path` property/argument is ignored and will simply prompt the - // user to choose a location and file name. However, if called within node.js the file will be - // saved to specified path. - saveSync: function(data, path) { - data = normalizeData(data); - - if (typeof path === 'string') data.path = path; - - // `` element only which the QR code is rendered. - var cvs = this.canvas(data); - - if (inNode) { - writeFileSync(cvs, data); - } else { - download(cvs, data); - } - }, - - // Generate the QR code using the data provided and render it on to a `` element before - // returning its data URI. - // If no `` element is specified in the argument provided a new one will be created and - // used. - // ECC (error correction capacity) determines how many intential errors are contained in the QR - // code. - toDataURL: function(data) { - data = normalizeData(data); - - return this.canvas(data).toDataURL(data.mime || DEFAULT_MIME); - }, - - // Utility functions - // ----------------- - - // Run qr.js in *noConflict* mode, returning the `qr` variable to its previous owner. - // Returns a reference to `qr`. - noConflict: function() { - root.qr = previousQr; - return this; - } - - }; - - // Support - // ------- - - // Export `qr` for node.js and CommonJS. - if (typeof exports !== 'undefined') { - inNode = true; - - if (typeof module !== 'undefined' && module.exports) { - exports = module.exports = qr; - } - exports.qr = qr; - - // Import required node.js modules. - Canvas = require('canvas'); - Image = Canvas.Image; - fs = require('fs'); - } else if (typeof define === 'function' && define.amd) { - define(function () { - return qr; - }); - } else { - // In non-HTML5 browser so strip base functionality. - if (!root.HTMLCanvasElement) { - overrideAPI(qr); - } - - root.qr = qr; - } - -})(this); diff --git a/vendors/qr.js/qr.min.js b/vendors/qr.js/qr.min.js deleted file mode 100644 index 01d79b2f35..0000000000 --- a/vendors/qr.js/qr.min.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! qr-js v1.1.4 | (c) 2015 Alasdair Mercer | GPL v3 License -jsqrencode | (c) 2010 tz@execpc.com | GPL v3 License -*/ -!function(a){"use strict";function b(){return T?new r:a.document.createElement("canvas")}function c(){return T?new x:a.document.createElement("img")}function d(b,c,d){var e=c.mime||B;a.location.href=b.toDataURL(e).replace(e,C),"function"==typeof d&&d()}function e(a){return"string"==typeof a&&(a={value:a}),a||{}}function f(a){function b(b){a[b]=function(){throw new Error(b+" requires HTML5 canvas element support")}}var c,d=["canvas","image","save","saveSync","toDataURL"];for(c=0;cb&&(c=a,a=b,b=c),c=b,c*=b,c+=b,c>>=1,c+=a,S[c]=1}function j(a,b){var c;for(R[a+z*b]=1,c=-2;2>c;c++)R[a+c+z*(b-2)]=1,R[a-2+z*(b+c+1)]=1,R[a+2+z*(b+c)]=1,R[a+c+1+z*(b+2)]=1;for(c=0;2>c;c++)i(a-1,b+c),i(a+1,b-c),i(a-c,b-1),i(a+c,b+1)}function k(a){for(;a>=255;)a-=255,a=(a>>8)+(255&a);return a}function l(a,b,c,d){var e,f,g;for(f=0;d>f;f++)W[c+f]=0;for(f=0;b>f;f++){if(e=H[W[a+f]^W[c]],255!==e)for(g=1;d>g;g++)W[c+g-1]=W[c+g]^G[k(e+U[d-g])];else for(g=c;c+d>g;g++)W[g]=W[g+1];W[c+d-1]=255===e?0:G[k(e+U[0])]}}function m(a,b){var c;return a>b&&(c=a,a=b,b=c),c=b,c+=b*b,c>>=1,c+=a,1===S[c]}function n(a){var b,c,d,e;switch(a){case 0:for(c=0;z>c;c++)for(b=0;z>b;b++)b+c&1||m(b,c)||(R[b+c*z]^=1);break;case 1:for(c=0;z>c;c++)for(b=0;z>b;b++)1&c||m(b,c)||(R[b+c*z]^=1);break;case 2:for(c=0;z>c;c++)for(d=0,b=0;z>b;b++,d++)3===d&&(d=0),d||m(b,c)||(R[b+c*z]^=1);break;case 3:for(e=0,c=0;z>c;c++,e++)for(3===e&&(e=0),d=e,b=0;z>b;b++,d++)3===d&&(d=0),d||m(b,c)||(R[b+c*z]^=1);break;case 4:for(c=0;z>c;c++)for(d=0,e=c>>1&1,b=0;z>b;b++,d++)3===d&&(d=0,e=!e),e||m(b,c)||(R[b+c*z]^=1);break;case 5:for(e=0,c=0;z>c;c++,e++)for(3===e&&(e=0),d=0,b=0;z>b;b++,d++)3===d&&(d=0),(b&c&1)+!(!d|!e)||m(b,c)||(R[b+c*z]^=1);break;case 6:for(e=0,c=0;z>c;c++,e++)for(3===e&&(e=0),d=0,b=0;z>b;b++,d++)3===d&&(d=0),(b&c&1)+(d&&d===e)&1||m(b,c)||(R[b+c*z]^=1);break;case 7:for(e=0,c=0;z>c;c++,e++)for(3===e&&(e=0),d=0,b=0;z>b;b++,d++)3===d&&(d=0),(d&&d===e)+(b+c&1)&1||m(b,c)||(R[b+c*z]^=1)}}function o(a){var b,c=0;for(b=0;a>=b;b++)O[b]>=5&&(c+=I+O[b]-5);for(b=3;a-1>b;b+=2)O[b-2]===O[b+2]&&O[b+2]===O[b-1]&&O[b-1]===O[b+1]&&3*O[b-1]===O[b]&&(0===O[b-3]||b+3>a||3*O[b-3]>=4*O[b]||3*O[b+3]>=4*O[b])&&(c+=K);return c}function p(){var a,b,c,d,e,f,g,h,i;for(c=e=f=0,i=0;z-1>i;i++)for(h=0;z-1>h;h++)(R[h+z*i]&&R[h+1+z*i]&&R[h+z*(i+1)]&&R[h+1+z*(i+1)]||!(R[h+z*i]||R[h+1+z*i]||R[h+z*(i+1)]||R[h+1+z*(i+1)]))&&(c+=J);for(i=0;z>i;i++){for(O[0]=0,g=a=h=0;z>h;h++)(b=R[h+z*i])===a?O[g]++:O[++g]=1,a=b,e+=a?1:-1;c+=o(g)}for(0>e&&(e=-e),d=e,d+=d<<2,d<<=1;d>z*z;)d-=z*z,f++;for(c+=f*L,h=0;z>h;h++){for(O[0]=0,g=a=i=0;z>i;i++)(b=R[h+z*i])===a?O[g]++:O[++g]=1,a=b;c+=o(g)}return c}function q(a){var b,c,d,e,f,g,h,o;f=a.length,y=0;do if(y++,d=4*(Q-1)+16*(y-1),u=D[d++],v=D[d++],s=D[d++],t=D[d],d=s*(u+v)+v-3+(9>=y),d>=f)break;while(40>y);for(z=17+4*y,g=s+(s+t)*(u+v)+v,f=0;g>f;f++)P[f]=0;for(W=a.slice(0),f=0;z*z>f;f++)R[f]=0;for(f=0;(z*(z+1)+1)/2>f;f++)S[f]=0;for(f=0;3>f;f++){for(d=o=0,1===f&&(d=z-7),2===f&&(o=z-7),R[o+3+z*(d+3)]=1,h=0;6>h;h++)R[o+h+z*d]=1,R[o+z*(d+h+1)]=1,R[o+6+z*(d+h)]=1,R[o+h+1+z*(d+6)]=1;for(h=1;5>h;h++)i(o+h,d+1),i(o+1,d+h+1),i(o+5,d+h),i(o+h+1,d+5);for(h=2;4>h;h++)R[o+h+z*(d+2)]=1,R[o+2+z*(d+h+1)]=1,R[o+4+z*(d+h)]=1,R[o+h+1+z*(d+4)]=1}if(y>1)for(f=A[y],o=z-7;;){for(h=z-7;h>f-3&&(j(h,o),!(f>h));)h-=f;if(f+9>=o)break;o-=f,j(6,o),j(o,6)}for(R[8+z*(z-8)]=1,o=0;7>o;o++)i(7,o),i(z-8,o),i(7,o+z-7);for(h=0;8>h;h++)i(h,7),i(h+z-8,7),i(h,z-8);for(h=0;9>h;h++)i(h,8);for(h=0;8>h;h++)i(h+z-8,8),i(8,h);for(o=0;7>o;o++)i(8,o+z-7);for(h=0;z-14>h;h++)1&h?(i(8+h,6),i(6,8+h)):(R[8+h+6*z]=1,R[6+z*(8+h)]=1);if(y>6)for(f=M[y-7],d=17,h=0;6>h;h++)for(o=0;3>o;o++,d--)1&(d>11?y>>d-12:f>>d)?(R[5-h+z*(2-o+z-11)]=1,R[2-o+z-11+z*(5-h)]=1):(i(5-h,2-o+z-11),i(2-o+z-11,5-h));for(o=0;z>o;o++)for(h=0;o>=h;h++)R[h+z*o]&&i(h,o);for(g=W.length,b=0;g>b;b++)P[b]=W.charCodeAt(b);if(W=P.slice(0),h=s*(u+v)+v,g>=h-2&&(g=h-2,y>9&&g--),b=g,y>9){for(W[b+2]=0,W[b+3]=0;b--;)f=W[b],W[b+3]|=255&f<<4,W[b+2]=f>>4;W[2]|=255&g<<4,W[1]=g>>4,W[0]=64|g>>12}else{for(W[b+1]=0,W[b+2]=0;b--;)f=W[b],W[b+2]|=255&f<<4,W[b+1]=f>>4;W[1]|=255&g<<4,W[0]=64|g>>4}for(b=g+3-(10>y);h>b;)W[b++]=236,W[b++]=17;for(U[0]=1,b=0;t>b;b++){for(U[b+1]=1,c=b;c>0;c--)U[c]=U[c]?U[c-1]^G[k(H[U[c]]+b)]:U[c-1];U[0]=G[k(H[U[0]]+b)]}for(b=0;t>=b;b++)U[b]=H[U[b]];for(d=h,o=0,b=0;u>b;b++)l(o,s,d,t),o+=s,d+=t;for(b=0;v>b;b++)l(o,s+1,d,t),o+=s+1,d+=t;for(o=0,b=0;s>b;b++){for(c=0;u>c;c++)P[o++]=W[b+c*s];for(c=0;v>c;c++)P[o++]=W[u*s+b+c*(s+1)]}for(c=0;v>c;c++)P[o++]=W[u*s+b+c*(s+1)];for(b=0;t>b;b++)for(c=0;u+v>c;c++)P[o++]=W[h+b+c*t];for(W=P,h=o=z-1,d=g=1,e=(s+t)*(u+v)+v,b=0;e>b;b++)for(f=W[b],c=0;8>c;c++,f<<=1){128&f&&(R[h+z*o]=1);do g?h--:(h++,d?0!==o?o--:(h-=2,d=!d,6===h&&(h--,o=9)):o!==z-1?o++:(h-=2,d=!d,6===h&&(h--,o-=8))),g=!g;while(m(h,o))}for(W=R.slice(0),f=0,o=3e4,d=0;8>d&&(n(d),h=p(),o>h&&(o=h,f=d),7!==f);d++)R=W.slice(0);for(f!==d&&n(f),o=F[f+(Q-1<<3)],d=0;8>d;d++,o>>=1)1&o&&(R[z-1-d+8*z]=1,6>d?R[8+z*d]=1:R[8+z*(d+1)]=1);for(d=0;7>d;d++,o>>=1)1&o&&(R[8+z*(z-7+d)]=1,d?R[6-d+8*z]=1:R[7+8*z]=1);return R}var r,s,t,u,v,w,x,y,z,A=[0,11,15,19,23,27,31,16,18,20,22,24,26,28,20,22,24,24,26,28,28,22,24,24,26,26,28,28,24,24,26,26,26,28,28,24,26,26,26,28,28],B="image/png",C="image/octet-stream",D=[1,0,19,7,1,0,16,10,1,0,13,13,1,0,9,17,1,0,34,10,1,0,28,16,1,0,22,22,1,0,16,28,1,0,55,15,1,0,44,26,2,0,17,18,2,0,13,22,1,0,80,20,2,0,32,18,2,0,24,26,4,0,9,16,1,0,108,26,2,0,43,24,2,2,15,18,2,2,11,22,2,0,68,18,4,0,27,16,4,0,19,24,4,0,15,28,2,0,78,20,4,0,31,18,2,4,14,18,4,1,13,26,2,0,97,24,2,2,38,22,4,2,18,22,4,2,14,26,2,0,116,30,3,2,36,22,4,4,16,20,4,4,12,24,2,2,68,18,4,1,43,26,6,2,19,24,6,2,15,28,4,0,81,20,1,4,50,30,4,4,22,28,3,8,12,24,2,2,92,24,6,2,36,22,4,6,20,26,7,4,14,28,4,0,107,26,8,1,37,22,8,4,20,24,12,4,11,22,3,1,115,30,4,5,40,24,11,5,16,20,11,5,12,24,5,1,87,22,5,5,41,24,5,7,24,30,11,7,12,24,5,1,98,24,7,3,45,28,15,2,19,24,3,13,15,30,1,5,107,28,10,1,46,28,1,15,22,28,2,17,14,28,5,1,120,30,9,4,43,26,17,1,22,28,2,19,14,28,3,4,113,28,3,11,44,26,17,4,21,26,9,16,13,26,3,5,107,28,3,13,41,26,15,5,24,30,15,10,15,28,4,4,116,28,17,0,42,26,17,6,22,28,19,6,16,30,2,7,111,28,17,0,46,28,7,16,24,30,34,0,13,24,4,5,121,30,4,14,47,28,11,14,24,30,16,14,15,30,6,4,117,30,6,14,45,28,11,16,24,30,30,2,16,30,8,4,106,26,8,13,47,28,7,22,24,30,22,13,15,30,10,2,114,28,19,4,46,28,28,6,22,28,33,4,16,30,8,4,122,30,22,3,45,28,8,26,23,30,12,28,15,30,3,10,117,30,3,23,45,28,4,31,24,30,11,31,15,30,7,7,116,30,21,7,45,28,1,37,23,30,19,26,15,30,5,10,115,30,19,10,47,28,15,25,24,30,23,25,15,30,13,3,115,30,2,29,46,28,42,1,24,30,23,28,15,30,17,0,115,30,10,23,46,28,10,35,24,30,19,35,15,30,17,1,115,30,14,21,46,28,29,19,24,30,11,46,15,30,13,6,115,30,14,23,46,28,44,7,24,30,59,1,16,30,12,7,121,30,12,26,47,28,39,14,24,30,22,41,15,30,6,14,121,30,6,34,47,28,46,10,24,30,2,64,15,30,17,4,122,30,29,14,46,28,49,10,24,30,24,46,15,30,4,18,122,30,13,32,46,28,48,14,24,30,42,32,15,30,20,4,117,30,40,7,47,28,43,22,24,30,10,67,15,30,19,6,118,30,18,31,47,28,34,34,24,30,20,61,15,30],E={L:1,M:2,Q:3,H:4},F=[30660,29427,32170,30877,26159,25368,27713,26998,21522,20773,24188,23371,17913,16590,20375,19104,13663,12392,16177,14854,9396,8579,11994,11245,5769,5054,7399,6608,1890,597,3340,2107],G=[1,2,4,8,16,32,64,128,29,58,116,232,205,135,19,38,76,152,45,90,180,117,234,201,143,3,6,12,24,48,96,192,157,39,78,156,37,74,148,53,106,212,181,119,238,193,159,35,70,140,5,10,20,40,80,160,93,186,105,210,185,111,222,161,95,190,97,194,153,47,94,188,101,202,137,15,30,60,120,240,253,231,211,187,107,214,177,127,254,225,223,163,91,182,113,226,217,175,67,134,17,34,68,136,13,26,52,104,208,189,103,206,129,31,62,124,248,237,199,147,59,118,236,197,151,51,102,204,133,23,46,92,184,109,218,169,79,158,33,66,132,21,42,84,168,77,154,41,82,164,85,170,73,146,57,114,228,213,183,115,230,209,191,99,198,145,63,126,252,229,215,179,123,246,241,255,227,219,171,75,150,49,98,196,149,55,110,220,165,87,174,65,130,25,50,100,200,141,7,14,28,56,112,224,221,167,83,166,81,162,89,178,121,242,249,239,195,155,43,86,172,69,138,9,18,36,72,144,61,122,244,245,247,243,251,235,203,139,11,22,44,88,176,125,250,233,207,131,27,54,108,216,173,71,142,0],H=[255,0,1,25,2,50,26,198,3,223,51,238,27,104,199,75,4,100,224,14,52,141,239,129,28,193,105,248,200,8,76,113,5,138,101,47,225,36,15,33,53,147,142,218,240,18,130,69,29,181,194,125,106,39,249,185,201,154,9,120,77,228,114,166,6,191,139,98,102,221,48,253,226,152,37,179,16,145,34,136,54,208,148,206,143,150,219,189,241,210,19,92,131,56,70,64,30,66,182,163,195,72,126,110,107,58,40,84,250,133,186,61,202,94,155,159,10,21,121,43,78,212,229,172,115,243,167,87,7,112,192,247,140,128,99,13,103,74,222,237,49,197,254,24,227,165,153,119,38,184,180,124,17,68,146,217,35,32,137,46,55,63,209,91,149,188,207,205,144,135,151,178,220,252,190,97,242,86,211,171,20,42,93,158,132,60,57,83,71,109,65,162,31,45,67,216,183,123,164,118,196,23,73,236,127,12,111,246,108,161,59,82,41,157,85,170,251,96,134,177,187,204,62,90,203,89,95,176,156,169,160,81,11,245,22,235,122,117,44,215,79,174,213,233,230,231,173,232,116,214,244,234,168,80,88,175],I=3,J=3,K=40,L=10,M=[3220,1468,2713,1235,3062,1890,2119,1549,2344,2936,1117,2583,1330,2470,1667,2249,2028,3780,481,4011,142,3098,831,3445,592,2517,1776,2234,1951,2827,1070,2660,1345,3177],N=parseInt("0666",8),O=[],P=[],Q=1,R=[],S=[],T=!1,U=[],V=a.qr,W=[],X={VERSION:"1.1.4",canvas:function(a){a=e(a);var c=a.size>=1&&a.size<=10?a.size:4;c*=25;var d=a.canvas||b(),f=d.getContext("2d");f.canvas.width=c,f.canvas.height=c,f.fillStyle=a.background||"#fff",f.fillRect(0,0,c,c),Q=E[a.level&&a.level.toUpperCase()||"L"];var g=q(a.value||"");f.lineWidth=1;var h=c;h/=z,h=Math.floor(h);var i=Math.floor((c-h*z)/2);f.clearRect(0,0,c,c),f.fillStyle=a.background||"#fff",f.fillRect(0,0,c,c),f.fillStyle=a.foreground||"#000";var j,k;for(j=0;z>j;j++)for(k=0;z>k;k++)g[k*z+j]&&f.fillRect(h*j+i,h*k+i,h,h);return d},image:function(a){a=e(a);var b=this.canvas(a),d=a.image||c();return d.src=b.toDataURL(a.mime||B),d.height=b.height,d.width=b.width,d},save:function(a,b,c){function f(a){h||(h=!0,c(a))}switch(a=e(a),typeof b){case"function":c=b,b=null;break;case"string":a.path=b}if("function"!=typeof c)throw new TypeError("Invalid callback type: "+typeof c);var h=!1,i=this.canvas(a);T?g(i,a,f):d(i,a,f)},saveSync:function(a,b){a=e(a),"string"==typeof b&&(a.path=b);var c=this.canvas(a);T?h(c,a):d(c,a)},toDataURL:function(a){return a=e(a),this.canvas(a).toDataURL(a.mime||B)},noConflict:function(){return a.qr=V,this}};"undefined"!=typeof exports?(T=!0,"undefined"!=typeof module&&module.exports&&(exports=module.exports=X),exports.qr=X,r=require("canvas"),x=r.Image,w=require("fs")):"function"==typeof define&&define.amd?define(function(){return X}):(a.HTMLCanvasElement||f(X),a.qr=X)}(this); -//# sourceMappingURL=qr.min.map \ No newline at end of file diff --git a/vendors/qr.js/qr.min.map b/vendors/qr.js/qr.min.map deleted file mode 100644 index 7886d1ed88..0000000000 --- a/vendors/qr.js/qr.min.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["qr.js"],"names":["root","createCanvas","inNode","Canvas","document","createElement","createImage","Image","download","cvs","data","callback","mime","DEFAULT_MIME","location","href","toDataURL","replace","DOWNLOAD_MIME","normalizeData","value","overrideAPI","qr","overrideMethod","name","Error","i","methods","length","writeFile","writeBuffer","fs","write","fd","buff","error","close","path","TypeError","toBuffer","_buff","open","WRITE_MODE","_fd","writeFileSync","openSync","writeSync","closeSync","setMask","x","y","bit","frameMask","addAlignment","frameBuffer","width","modN","appendData","dataLength","ecc","eccLength","j","stringBuffer","GALOIS_LOG","GALOIS_EXPONENT","polynomial","isMasked","applyMask","mask","r3x","r3y","getBadRuns","badRuns","badBuffer","N1","N3","checkBadness","b","b1","bad","big","bw","count","h","N2","N4","generateFrame","str","k","m","t","v","version","eccLevel","neccBlock1","ECC_BLOCKS","neccBlock2","dataBlock","eccBlock","eccBuffer","slice","ALIGNMENT_DELTA","VERSION_BLOCK","charCodeAt","FINAL_FORMAT","ECC_LEVELS","L","M","Q","H","parseInt","previousQr","VERSION","canvas","size","c2d","getContext","height","fillStyle","background","fillRect","level","toUpperCase","frame","lineWidth","px","Math","floor","offset","clearRect","foreground","image","this","img","src","save","done","completed","saveSync","noConflict","exports","module","require","define","amd","HTMLCanvasElement"],"mappings":";;;CASA,SAAWA,GAET,YAsKA,SAASC,KACP,MAAOC,GAAS,GAAIC,GAAWH,EAAKI,SAASC,cAAc,UAK7D,QAASC,KACP,MAAOJ,GAAS,GAAIK,GAAUP,EAAKI,SAASC,cAAc,OAO5D,QAASG,GAASC,EAAKC,EAAMC,GAC3B,GAAIC,GAAOF,EAAKE,MAAQC,CAExBb,GAAKc,SAASC,KAAON,EAAIO,UAAUJ,GAAMK,QAAQL,EAAMM,GAE/B,kBAAbP,IAAyBA,IAItC,QAASQ,GAAcT,GAErB,MADoB,gBAATA,KAAmBA,GAASU,MAAOV,IACvCA,MAIT,QAASW,GAAYC,GAInB,QAASC,GAAeC,GACtBF,EAAGE,GAAQ,WACT,KAAM,IAAIC,OAAMD,EAAO,2CAL3B,GACIE,GADAC,GAAY,SAAU,QAAS,OAAQ,WAAY,YASvD,KAAKD,EAAI,EAAGA,EAAIC,EAAQC,OAAQF,IAC9BH,EAAeI,EAAQD,IAK3B,QAASG,GAAUpB,EAAKC,EAAMC,GAQ5B,QAASmB,KACPC,EAAGC,MAAMC,EAAIC,EAAM,EAAGA,EAAKN,OAAQ,EAAG,SAAUO,GAC9CJ,EAAGK,MAAMH,GAETtB,EAASwB,KAXb,GAAyB,gBAAdzB,GAAK2B,KACd,MAAO1B,GAAS,GAAI2B,WAAU,4BAA+B5B,GAAK2B,MAGpE,IAAIJ,GAAIC,CAYRzB,GAAI8B,SAAS,SAAUJ,EAAOK,GAC5B,MAAIL,GAAcxB,EAASwB,IAE3BD,EAAOM,OACHP,GACFH,QAKJC,EAAGU,KAAK/B,EAAK2B,KAAM,IAAKK,EAAY,SAAUP,EAAOQ,GACnD,MAAIR,GAAcxB,EAASwB,IAE3BF,EAAKU,OACDT,GACFJ,QAMN,QAASc,GAAcnC,EAAKC,GAC1B,GAAyB,gBAAdA,GAAK2B,KACd,KAAM,IAAIC,WAAU,4BAA+B5B,GAAK2B,KAG1D,IAAIH,GAAOzB,EAAI8B,WACXN,EAAKF,EAAGc,SAASnC,EAAK2B,KAAM,IAAKK,EAErC,KACEX,EAAGe,UAAUb,EAAIC,EAAM,EAAGA,EAAKN,OAAQ,GACvC,QACAG,EAAGgB,UAAUd,IAKjB,QAASe,GAAQC,EAAGC,GAClB,GAAIC,EAEAF,GAAIC,IACNC,EAAMF,EACNA,EAAMC,EACNA,EAAMC,GAGRA,EAAQD,EACRC,GAAQD,EACRC,GAAQD,EACRC,IAAQ,EACRA,GAAQF,EAERG,EAAUD,GAAO,EAKnB,QAASE,GAAaJ,EAAGC,GACvB,GAAIxB,EAIJ,KAFA4B,EAAYL,EAAIM,EAAQL,GAAK,EAExBxB,EAAI,GAAQ,EAAJA,EAAOA,IAClB4B,EAAaL,EAAIvB,EAAS6B,GAASL,EAAI,IAAU,EACjDI,EAAaL,EAAI,EAASM,GAASL,EAAIxB,EAAI,IAAM,EACjD4B,EAAaL,EAAI,EAASM,GAASL,EAAIxB,IAAU,EACjD4B,EAAaL,EAAIvB,EAAI,EAAK6B,GAASL,EAAI,IAAU,CAGnD,KAAKxB,EAAI,EAAO,EAAJA,EAAOA,IACjBsB,EAAQC,EAAI,EAAGC,EAAIxB,GACnBsB,EAAQC,EAAI,EAAGC,EAAIxB,GACnBsB,EAAQC,EAAIvB,EAAGwB,EAAI,GACnBF,EAAQC,EAAIvB,EAAGwB,EAAI,GAKvB,QAASM,GAAKP,GACZ,KAAOA,GAAK,KACVA,GAAK,IACLA,GAAMA,GAAK,IAAU,IAAJA,EAGnB,OAAOA,GAKT,QAASQ,GAAW/C,EAAMgD,EAAYC,EAAKC,GACzC,GAAIT,GAAKzB,EAAGmC,CAEZ,KAAKnC,EAAI,EAAOkC,EAAJlC,EAAeA,IACzBoC,EAAaH,EAAMjC,GAAK,CAG1B,KAAKA,EAAI,EAAOgC,EAAJhC,EAAgBA,IAAK,CAG/B,GAFAyB,EAAMY,EAAWD,EAAapD,EAAOgB,GAAKoC,EAAaH,IAE3C,MAARR,EACF,IAAKU,EAAI,EAAOD,EAAJC,EAAeA,IACzBC,EAAaH,EAAME,EAAI,GAAKC,EAAaH,EAAME,GAC3CG,EAAgBR,EAAKL,EAAMc,EAAWL,EAAYC,SAGxD,KAAKA,EAAIF,EAASA,EAAMC,EAAVC,EAAqBA,IACjCC,EAAaD,GAAKC,EAAaD,EAAI,EAIvCC,GAAaH,EAAMC,EAAY,GAAa,MAART,EAAc,EAC9Ca,EAAgBR,EAAKL,EAAMc,EAAW,MAK9C,QAASC,GAASjB,EAAGC,GACnB,GAAIC,EAaJ,OAXIF,GAAIC,IACNC,EAAMF,EACNA,EAAMC,EACNA,EAAMC,GAGRA,EAAQD,EACRC,GAAQD,EAAIA,EACZC,IAAQ,EACRA,GAAQF,EAEkB,IAAnBG,EAAUD,GAInB,QAASgB,GAAUC,GACjB,GAAInB,GAAGC,EAAGmB,EAAKC,CAEf,QAAQF,GACN,IAAK,GACH,IAAKlB,EAAI,EAAOK,EAAJL,EAAWA,IACrB,IAAKD,EAAI,EAAOM,EAAJN,EAAWA,IACdA,EAAIC,EAAK,GAAOgB,EAASjB,EAAGC,KACjCI,EAAYL,EAAIC,EAAIK,IAAU,EAKpC,MACF,KAAK,GACH,IAAKL,EAAI,EAAOK,EAAJL,EAAWA,IACrB,IAAKD,EAAI,EAAOM,EAAJN,EAAWA,IACX,EAAJC,GAAWgB,EAASjB,EAAGC,KAC3BI,EAAYL,EAAIC,EAAIK,IAAU,EAKpC,MACF,KAAK,GACH,IAAKL,EAAI,EAAOK,EAAJL,EAAWA,IACrB,IAAKmB,EAAM,EAAGpB,EAAI,EAAOM,EAAJN,EAAWA,IAAKoB,IACvB,IAARA,IAAWA,EAAM,GAEhBA,GAAQH,EAASjB,EAAGC,KACvBI,EAAYL,EAAIC,EAAIK,IAAU,EAKpC,MACF,KAAK,GACH,IAAKe,EAAM,EAAGpB,EAAI,EAAOK,EAAJL,EAAWA,IAAKoB,IAGnC,IAFY,IAARA,IAAWA,EAAM,GAEhBD,EAAMC,EAAKrB,EAAI,EAAOM,EAAJN,EAAWA,IAAKoB,IACzB,IAARA,IAAWA,EAAM,GAEhBA,GAAQH,EAASjB,EAAGC,KACvBI,EAAYL,EAAIC,EAAIK,IAAU,EAKpC,MACF,KAAK,GACH,IAAKL,EAAI,EAAOK,EAAJL,EAAWA,IACrB,IAAKmB,EAAM,EAAGC,EAAQpB,GAAK,EAAK,EAAID,EAAI,EAAOM,EAAJN,EAAWA,IAAKoB,IAC7C,IAARA,IACFA,EAAM,EACNC,GAAOA,GAGJA,GAAQJ,EAASjB,EAAGC,KACvBI,EAAYL,EAAIC,EAAIK,IAAU,EAKpC,MACF,KAAK,GACH,IAAKe,EAAM,EAAGpB,EAAI,EAAOK,EAAJL,EAAWA,IAAKoB,IAGnC,IAFY,IAARA,IAAWA,EAAM,GAEhBD,EAAM,EAAGpB,EAAI,EAAOM,EAAJN,EAAWA,IAAKoB,IACvB,IAARA,IAAWA,EAAM,IAEdpB,EAAIC,EAAI,MAAQmB,GAAOC,IAAUJ,EAASjB,EAAGC,KAClDI,EAAYL,EAAIC,EAAIK,IAAU,EAKpC,MACF,KAAK,GACH,IAAKe,EAAM,EAAGpB,EAAI,EAAOK,EAAJL,EAAWA,IAAKoB,IAGnC,IAFY,IAARA,IAAWA,EAAM,GAEhBD,EAAM,EAAGpB,EAAI,EAAOM,EAAJN,EAAWA,IAAKoB,IACvB,IAARA,IAAWA,EAAM,IAEbpB,EAAIC,EAAI,IAAMmB,GAAQA,IAAQC,GAAS,GAAOJ,EAASjB,EAAGC,KAChEI,EAAYL,EAAIC,EAAIK,IAAU,EAKpC,MACF,KAAK,GACH,IAAKe,EAAM,EAAGpB,EAAI,EAAOK,EAAJL,EAAWA,IAAKoB,IAGnC,IAFY,IAARA,IAAWA,EAAM,GAEhBD,EAAM,EAAGpB,EAAI,EAAOM,EAAJN,EAAWA,IAAKoB,IACvB,IAARA,IAAWA,EAAM,IAEbA,GAAQA,IAAQC,IAAUrB,EAAIC,EAAK,GAAM,GAAOgB,EAASjB,EAAGC,KAClEI,EAAYL,EAAIC,EAAIK,IAAU,IAW1C,QAASgB,GAAW3C,GAClB,GACIF,GADA8C,EAAU,CAGd,KAAK9C,EAAI,EAAQE,GAALF,EAAaA,IACnB+C,EAAU/C,IAAM,IAClB8C,GAAWE,EAAKD,EAAU/C,GAAK,EAKnC,KAAKA,EAAI,EAAOE,EAAS,EAAbF,EAAgBA,GAAK,EAC3B+C,EAAU/C,EAAI,KAAO+C,EAAU/C,EAAI,IACnC+C,EAAU/C,EAAI,KAAO+C,EAAU/C,EAAI,IACnC+C,EAAU/C,EAAI,KAAO+C,EAAU/C,EAAI,IAChB,EAAnB+C,EAAU/C,EAAI,KAAW+C,EAAU/C,KAEb,IAArB+C,EAAU/C,EAAI,IAAYA,EAAI,EAAIE,GACf,EAAnB6C,EAAU/C,EAAI,IAAyB,EAAf+C,EAAU/C,IACf,EAAnB+C,EAAU/C,EAAI,IAAyB,EAAf+C,EAAU/C,MACrC8C,GAAWG,EAIf,OAAOH,GAIT,QAASI,KACP,GAAIC,GAAGC,EAAIC,EAAKC,EAAKC,EAAIC,EAAOC,EAAGlC,EAAGC,CAItC,KAHA6B,EAAME,EAAKC,EAAQ,EAGdhC,EAAI,EAAOK,EAAQ,EAAZL,EAAeA,IACzB,IAAKD,EAAI,EAAOM,EAAQ,EAAZN,EAAeA,KAEpBK,EAAYL,EAAIM,EAAQL,IACxBI,EAAaL,EAAI,EAAKM,EAAQL,IAC9BI,EAAYL,EAAIM,GAASL,EAAI,KAC7BI,EAAaL,EAAI,EAAKM,GAASL,EAAI,OAElCI,EAAYL,EAAIM,EAAQL,IACxBI,EAAaL,EAAI,EAAKM,EAAQL,IAC9BI,EAAYL,EAAIM,GAASL,EAAI,KAC7BI,EAAaL,EAAI,EAAKM,GAASL,EAAI,QACvC6B,GAAOK,EAMb,KAAKlC,EAAI,EAAOK,EAAJL,EAAWA,IAAK,CAG1B,IAFAuB,EAAU,GAAK,EAEVU,EAAIN,EAAI5B,EAAI,EAAOM,EAAJN,EAAWA,KACxB6B,EAAKxB,EAAYL,EAAIM,EAAQL,MAAQ2B,EACxCJ,EAAUU,KAEVV,IAAYU,GAAK,EAGnBN,EAAMC,EACNG,GAAMJ,EAAI,EAAI,EAGhBE,IAAOR,EAAWY,GASpB,IANS,EAALF,IAAQA,GAAMA,GAElBD,EAAQC,EACRD,GAAQA,GAAO,EACfA,IAAQ,EAEDA,EAAMzB,EAAQA,GACnByB,GAAOzB,EAAQA,EACf2B,GAMF,KAHAH,GAAOG,EAAQG,EAGVpC,EAAI,EAAOM,EAAJN,EAAWA,IAAK,CAG1B,IAFAwB,EAAU,GAAK,EAEVU,EAAIN,EAAI3B,EAAI,EAAOK,EAAJL,EAAWA,KACxB4B,EAAKxB,EAAYL,EAAIM,EAAQL,MAAQ2B,EACxCJ,EAAUU,KAEVV,IAAYU,GAAK,EAGnBN,EAAIC,CAGNC,IAAOR,EAAWY,GAGpB,MAAOJ,GAIT,QAASO,GAAcC,GACrB,GAAI7D,GAAGmC,EAAG2B,EAAGC,EAAGC,EAAGC,EAAG1C,EAAGC,CAGzBwC,GAAIH,EAAI3D,OAERgE,EAAU,CAEV,GAYE,IAXAA,IAEAJ,EAAqB,GAAhBK,EAAW,GAAyB,IAAfD,EAAU,GAEpCE,EAAaC,EAAWP,KACxBQ,EAAaD,EAAWP,KACxBS,EAAaF,EAAWP,KACxBU,EAAaH,EAAWP,GAExBA,EAAIS,GAAaH,EAAaE,GAAcA,EAAa,GAAgB,GAAXJ,GAErDJ,GAALE,EAAQ,YACK,GAAVE,EAQT,KALArC,EAAQ,GAAK,EAAIqC,EAGjBD,EAAIM,GAAaA,EAAYC,IAAaJ,EAAaE,GAAcA,EAEhEN,EAAI,EAAOC,EAAJD,EAAOA,IACjBS,EAAUT,GAAK,CAKjB,KAFA5B,EAAeyB,EAAIa,MAAM,GAEpBV,EAAI,EAAOnC,EAAQA,EAAZmC,EAAmBA,IAC7BpC,EAAYoC,GAAK,CAGnB,KAAKA,EAAI,GAAQnC,GAASA,EAAQ,GAAK,GAAK,EAAhCmC,EAAmCA,IAC7CtC,EAAUsC,GAAK,CAIjB,KAAKA,EAAI,EAAO,EAAJA,EAAOA,IAAK,CAQtB,IAPAF,EAAItC,EAAI,EAEE,IAANwC,IAASF,EAAKjC,EAAQ,GAChB,IAANmC,IAASxC,EAAKK,EAAQ,GAE1BD,EAAaJ,EAAI,EAAKK,GAASiC,EAAI,IAAM,EAEpCvC,EAAI,EAAO,EAAJA,EAAOA,IACjBK,EAAaJ,EAAID,EAAKM,EAAQiC,GAAK,EACnClC,EAAYJ,EAAIK,GAASiC,EAAIvC,EAAI,IAAM,EACvCK,EAAaJ,EAAI,EAAKK,GAASiC,EAAIvC,IAAM,EACzCK,EAAaJ,EAAID,EAAI,EAAKM,GAASiC,EAAI,IAAM,CAG/C,KAAKvC,EAAI,EAAO,EAAJA,EAAOA,IACjBD,EAAQE,EAAID,EAAGuC,EAAI,GACnBxC,EAAQE,EAAI,EAAGsC,EAAIvC,EAAI,GACvBD,EAAQE,EAAI,EAAGsC,EAAIvC,GACnBD,EAAQE,EAAID,EAAI,EAAGuC,EAAI,EAGzB,KAAKvC,EAAI,EAAO,EAAJA,EAAOA,IACjBK,EAAaJ,EAAID,EAAKM,GAASiC,EAAI,IAAM,EACzClC,EAAaJ,EAAI,EAAKK,GAASiC,EAAIvC,EAAI,IAAM,EAC7CK,EAAaJ,EAAI,EAAKK,GAASiC,EAAIvC,IAAM,EACzCK,EAAaJ,EAAID,EAAI,EAAKM,GAASiC,EAAI,IAAM,EAKjD,GAAII,EAAU,EAIZ,IAHAF,EAAIW,EAAgBT,GACpB1C,EAAIK,EAAQ,IAEH,CAGP,IAFAN,EAAIM,EAAQ,EAELN,EAAIyC,EAAI,IACbrC,EAAaJ,EAAGC,KAERwC,EAAJzC,KAEJA,GAAKyC,CAGP,IAASA,EAAI,GAATxC,EAAY,KAEhBA,IAAKwC,EAELrC,EAAa,EAAGH,GAChBG,EAAaH,EAAG,GAQpB,IAHAI,EAAY,EAAIC,GAASA,EAAQ,IAAM,EAGlCL,EAAI,EAAO,EAAJA,EAAOA,IACjBF,EAAQ,EAAGE,GACXF,EAAQO,EAAQ,EAAGL,GACnBF,EAAQ,EAAGE,EAAIK,EAAQ,EAGzB,KAAKN,EAAI,EAAO,EAAJA,EAAOA,IACjBD,EAAQC,EAAG,GACXD,EAAQC,EAAIM,EAAQ,EAAG,GACvBP,EAAQC,EAAGM,EAAQ,EAIrB,KAAKN,EAAI,EAAO,EAAJA,EAAOA,IACjBD,EAAQC,EAAG,EAGb,KAAKA,EAAI,EAAO,EAAJA,EAAOA,IACjBD,EAAQC,EAAIM,EAAQ,EAAG,GACvBP,EAAQ,EAAGC,EAGb,KAAKC,EAAI,EAAO,EAAJA,EAAOA,IACjBF,EAAQ,EAAGE,EAAIK,EAAQ,EAIzB,KAAKN,EAAI,EAAOM,EAAQ,GAAZN,EAAgBA,IAClB,EAAJA,GACFD,EAAQ,EAAIC,EAAG,GACfD,EAAQ,EAAG,EAAIC,KAEfK,EAAa,EAAIL,EAAa,EAARM,GAAa,EACnCD,EAAY,EAAIC,GAAS,EAAIN,IAAM,EAKvC,IAAI2C,EAAU,EAIZ,IAHAF,EAAIY,EAAcV,EAAU,GAC5BJ,EAAI,GAECvC,EAAI,EAAO,EAAJA,EAAOA,IACjB,IAAKC,EAAI,EAAO,EAAJA,EAAOA,IAAKsC,IAClB,GAAKA,EAAI,GAAKI,GAAYJ,EAAI,GAAME,GAAKF,IAC3ClC,EAAa,EAAIL,EAAKM,GAAS,EAAIL,EAAIK,EAAQ,KAAO,EACtDD,EAAa,EAAIJ,EAAIK,EAAQ,GAAMA,GAAS,EAAIN,IAAM,IAEtDD,EAAQ,EAAIC,EAAG,EAAIC,EAAIK,EAAQ,IAC/BP,EAAQ,EAAIE,EAAIK,EAAQ,GAAI,EAAIN,GAOxC,KAAKC,EAAI,EAAOK,EAAJL,EAAWA,IACrB,IAAKD,EAAI,EAAQC,GAALD,EAAQA,IACdK,EAAYL,EAAIM,EAAQL,IAC1BF,EAAQC,EAAGC,EAUjB,KAHAyC,EAAI7B,EAAalC,OAGZF,EAAI,EAAOiE,EAAJjE,EAAOA,IACjByE,EAAUzE,GAAKoC,EAAayC,WAAW7E,EAiBzC,IAdAoC,EAAeqC,EAAUC,MAAM,GAG/BnD,EAAIgD,GAAaH,EAAaE,GAAcA,EAExCL,GAAK1C,EAAI,IACX0C,EAAI1C,EAAI,EAEJ2C,EAAU,GAAGD,KAInBjE,EAAIiE,EAEAC,EAAU,EAAG,CAIf,IAHA9B,EAAapC,EAAI,GAAK,EACtBoC,EAAapC,EAAI,GAAK,EAEfA,KACLgE,EAAI5B,EAAapC,GAEjBoC,EAAapC,EAAI,IAAM,IAAOgE,GAAK,EACnC5B,EAAapC,EAAI,GAAKgE,GAAK,CAG7B5B,GAAa,IAAM,IAAO6B,GAAK,EAC/B7B,EAAa,GAAK6B,GAAK,EACvB7B,EAAa,GAAK,GAAQ6B,GAAK,OAC1B,CAIL,IAHA7B,EAAapC,EAAI,GAAK,EACtBoC,EAAapC,EAAI,GAAK,EAEfA,KACLgE,EAAI5B,EAAapC,GAEjBoC,EAAapC,EAAI,IAAM,IAAOgE,GAAK,EACnC5B,EAAapC,EAAI,GAAKgE,GAAK,CAG7B5B,GAAa,IAAM,IAAO6B,GAAK,EAC/B7B,EAAa,GAAK,GAAQ6B,GAAK,EAMjC,IAFAjE,EAAIiE,EAAI,GAAe,GAAVC,GAEF3C,EAAJvB,GACLoC,EAAapC,KAAO,IACpBoC,EAAapC,KAAO,EAMtB,KAFAuC,EAAW,GAAK,EAEXvC,EAAI,EAAOwE,EAAJxE,EAAcA,IAAK,CAG7B,IAFAuC,EAAWvC,EAAI,GAAK,EAEfmC,EAAInC,EAAGmC,EAAI,EAAGA,IACjBI,EAAWJ,GAAKI,EAAWJ,GAAKI,EAAWJ,EAAI,GAC3CG,EAAgBR,EAAKO,EAAWE,EAAWJ,IAAMnC,IAAMuC,EAAWJ,EAAI,EAG5EI,GAAW,GAAKD,EAAgBR,EAAKO,EAAWE,EAAW,IAAMvC,IAInE,IAAKA,EAAI,EAAQwE,GAALxE,EAAeA,IACzBuC,EAAWvC,GAAKqC,EAAWE,EAAWvC,GAOxC,KAHA8D,EAAIvC,EACJC,EAAI,EAECxB,EAAI,EAAOoE,EAAJpE,EAAgBA,IAC1B+B,EAAWP,EAAG+C,EAAWT,EAAGU,GAE5BhD,GAAK+C,EACLT,GAAKU,CAGP,KAAKxE,EAAI,EAAOsE,EAAJtE,EAAgBA,IAC1B+B,EAAWP,EAAG+C,EAAY,EAAGT,EAAGU,GAEhChD,GAAK+C,EAAY,EACjBT,GAAKU,CAMP,KAFAhD,EAAI,EAECxB,EAAI,EAAOuE,EAAJvE,EAAeA,IAAK,CAC9B,IAAKmC,EAAI,EAAOiC,EAAJjC,EAAgBA,IAC1BsC,EAAUjD,KAAOY,EAAapC,EAAImC,EAAIoC,EAGxC,KAAKpC,EAAI,EAAOmC,EAAJnC,EAAgBA,IAC1BsC,EAAUjD,KAAOY,EAAcgC,EAAaG,EAAavE,EAAKmC,GAAKoC,EAAY,IAInF,IAAKpC,EAAI,EAAOmC,EAAJnC,EAAgBA,IAC1BsC,EAAUjD,KAAOY,EAAcgC,EAAaG,EAAavE,EAAKmC,GAAKoC,EAAY,GAGjF,KAAKvE,EAAI,EAAOwE,EAAJxE,EAAcA,IACxB,IAAKmC,EAAI,EAAOiC,EAAaE,EAAjBnC,EAA6BA,IACvCsC,EAAUjD,KAAOY,EAAab,EAAIvB,EAAImC,EAAIqC,EAa9C,KATApC,EAAeqC,EAGflD,EAAIC,EAAIK,EAAQ,EAChBiC,EAAIG,EAAI,EAGRF,GAAKQ,EAAYC,IAAaJ,EAAaE,GAAcA,EAEpDtE,EAAI,EAAO+D,EAAJ/D,EAAOA,IAGjB,IAFAgE,EAAI5B,EAAapC,GAEZmC,EAAI,EAAO,EAAJA,EAAOA,IAAK6B,IAAM,EAAG,CAC3B,IAAOA,IACTpC,EAAYL,EAAIM,EAAQL,GAAK,EAI/B,GACMyC,GACF1C,KAEAA,IAEIuC,EACQ,IAANtC,EACFA,KAEAD,GAAK,EACLuC,GAAMA,EAEI,IAANvC,IACFA,IACAC,EAAI,IAIJA,IAAMK,EAAQ,EAChBL,KAEAD,GAAK,EACLuC,GAAMA,EAEI,IAANvC,IACFA,IACAC,GAAK,KAMbyC,GAAKA,QACEzB,EAASjB,EAAGC,IAYzB,IAPAY,EAAeR,EAAY8C,MAAM,GAEjCV,EAAI,EACJxC,EAAI,IAICsC,EAAI,EAAO,EAAJA,IAEVrB,EAAUqB,GAEVvC,EAAI2B,IAGI1B,EAAJD,IACFC,EAAID,EACJyC,EAAIF,GAII,IAANE,GAbaF,IAgBjBlC,EAAcQ,EAAasC,MAAM,EAYnC,KARIV,IAAMF,GACRrB,EAAUuB,GAIZxC,EAAIsD,EAAad,GAAMG,EAAW,GAAM,IAGnCL,EAAI,EAAO,EAAJA,EAAOA,IAAKtC,IAAM,EACpB,EAAJA,IACFI,EAAaC,EAAQ,EAAIiC,EAAa,EAARjC,GAAa,EAEnC,EAAJiC,EACFlC,EAAY,EAAIC,EAAQiC,GAAK,EAE7BlC,EAAY,EAAIC,GAASiC,EAAI,IAAM,EAMzC,KAAKA,EAAI,EAAO,EAAJA,EAAOA,IAAKtC,IAAM,EACpB,EAAJA,IACFI,EAAY,EAAIC,GAASA,EAAQ,EAAIiC,IAAM,EAEvCA,EACFlC,EAAa,EAAIkC,EAAa,EAARjC,GAAa,EAEnCD,EAAY,EAAY,EAARC,GAAa,EAMnC,OAAOD,GA/8BT,GA6HInD,GAEA8F,EAEAC,EAAUJ,EAAYE,EAUtBjE,EAEAxB,EAUAqF,EAEArC,EAzJA8C,GACF,EAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GACxB,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAChE,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,IAG9DxF,EAAe,YAEfK,EAAgB,qBAGhB6E,GACF,EAAI,EAAI,GAAK,EAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,EAAK,GACzE,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GACzE,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GACzE,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,EAAK,GACzE,EAAI,EAAI,IAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GACzE,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GACzE,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GACzE,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GACzE,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GACzE,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GACzE,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,EAAI,EAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GACzE,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GACzE,EAAI,EAAI,GAAK,GAAO,EAAI,EAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GAAO,EAAI,GAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,GAAI,EAAI,GAAK,GAAO,EAAI,GAAI,GAAK,GAAO,EAAI,GAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,EAAI,EAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GAAO,EAAI,GAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,EAAI,GAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GAAO,EAAI,GAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,EAAI,GAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,GAAI,EAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,GAAI,EAAI,GAAK,GAAO,EAAI,GAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,EAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,EAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,EAAI,GAAI,GAAK,GAAO,EAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,GAAI,EAAI,IAAK,GAAO,GAAI,EAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,GAAI,EAAI,GAAK,GAAO,EAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,EAAI,GAAI,IAAK,GAAO,EAAI,GAAI,GAAK,GAAO,EAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,EAAI,EAAI,IAAK,GAAO,GAAI,EAAI,GAAK,GAAO,EAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,EAAI,GAAI,IAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,GAAI,EAAI,IAAK,GAAO,EAAI,GAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,GAAI,EAAI,IAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,GAAI,EAAI,IAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,GAAI,EAAI,IAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GAAO,GAAI,EAAI,GAAK,GACzE,GAAI,EAAI,IAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,EAAI,GAAI,IAAK,GAAO,EAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GAAO,EAAI,GAAI,GAAK,GACzE,GAAI,EAAI,IAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,EAAI,GAAI,IAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,GAAI,EAAI,IAAK,GAAO,GAAI,EAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GACzE,GAAI,EAAI,IAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,GAAO,GAAI,GAAI,GAAK,IAGvEU,GACFC,EAAG,EACHC,EAAG,EACHC,EAAG,EACHC,EAAG,GAGDL,GACF,MAAQ,MAAQ,MAAQ,MAAQ,MAAQ,MAAQ,MAAQ,MACxD,MAAQ,MAAQ,MAAQ,MAAQ,MAAQ,MAAQ,MAAQ,MACxD,MAAQ,MAAQ,MAAQ,MAAQ,KAAQ,KAAQ,MAAQ,MACxD,KAAQ,KAAQ,KAAQ,KAAQ,KAAQ,IAAQ,KAAQ,MAGtDxC,GACF,EAAM,EAAM,EAAM,EAAM,GAAM,GAAM,GAAM,IAAM,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,GAAM,GAC1F,GAAM,IAAM,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,EAAM,EAAM,GAAM,GAAM,GAAM,GAAM,IAC1F,IAAM,GAAM,GAAM,IAAM,GAAM,GAAM,IAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAC1F,GAAM,IAAM,EAAM,GAAM,GAAM,GAAM,GAAM,IAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAC1F,GAAM,IAAM,GAAM,IAAM,IAAM,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,GAAM,GAAM,GAAM,IAAM,IAC1F,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAAM,IAAM,IAAM,IAC1F,IAAM,IAAM,GAAM,IAAM,GAAM,GAAM,GAAM,IAAM,GAAM,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAC1F,IAAM,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAAM,IAAM,IAAM,IAAM,IAAM,GAAM,IAAM,IAC1F,IAAM,GAAM,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,GAAM,IAAM,GAAM,GAAM,IAAM,GAAM,GAAM,GAC1F,IAAM,GAAM,IAAM,GAAM,GAAM,IAAM,GAAM,IAAM,GAAM,IAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAC1F,IAAM,IAAM,IAAM,GAAM,IAAM,IAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAC1F,IAAM,IAAM,IAAM,GAAM,IAAM,GAAM,GAAM,IAAM,IAAM,GAAM,IAAM,IAAM,IAAM,GAAM,IAAM,GAC1F,IAAM,GAAM,GAAM,IAAM,IAAM,IAAM,EAAM,GAAM,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,GAAM,IAC1F,GAAM,IAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAAM,GAAM,IAAM,GAAM,IAAM,EAC1F,GAAM,GAAM,GAAM,IAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAAM,GAC1F,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAAM,GAAM,IAAM,IAAM,IAAM,GAAM,IAAM,GAGxFD,GACF,IAAM,EAAM,EAAM,GAAM,EAAM,GAAM,GAAM,IAAM,EAAM,IAAM,GAAM,IAAM,GAAM,IAAM,IAAM,GAC1F,EAAM,IAAM,IAAM,GAAM,GAAM,IAAM,IAAM,IAAM,GAAM,IAAM,IAAM,IAAM,IAAM,EAAM,GAAM,IAC1F,EAAM,IAAM,IAAM,GAAM,IAAM,GAAM,GAAM,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,GAAM,IAAM,GAC1F,GAAM,IAAM,IAAM,IAAM,IAAM,GAAM,IAAM,IAAM,IAAM,IAAM,EAAM,IAAM,GAAM,IAAM,IAAM,IAC1F,EAAM,IAAM,IAAM,GAAM,IAAM,IAAM,GAAM,IAAM,IAAM,IAAM,GAAM,IAAM,GAAM,IAAM,GAAM,IAC1F,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAAM,GAAM,IAAM,GAAM,GAAM,GAC1F,GAAM,GAAM,IAAM,IAAM,IAAM,GAAM,IAAM,IAAM,IAAM,GAAM,GAAM,GAAM,IAAM,IAAM,IAAM,GAC1F,IAAM,GAAM,IAAM,IAAM,GAAM,GAAM,IAAM,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAC1F,EAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAAM,GAAM,IAAM,GAAM,IAAM,IAAM,GAAM,IAAM,IAAM,GAC1F,IAAM,IAAM,IAAM,IAAM,GAAM,IAAM,IAAM,IAAM,GAAM,GAAM,IAAM,IAAM,GAAM,GAAM,IAAM,GAC1F,GAAM,GAAM,IAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAC1F,IAAM,GAAM,IAAM,IAAM,GAAM,GAAM,GAAM,IAAM,IAAM,GAAM,GAAM,GAAM,GAAM,IAAM,GAAM,IAC1F,GAAM,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAAM,GAAM,IAAM,IAAM,GAAM,IAAM,IAC1F,IAAM,IAAM,GAAM,GAAM,GAAM,IAAM,GAAM,IAAM,IAAM,GAAM,IAAM,IAAM,IAAM,IAAM,GAAM,GAC1F,IAAM,GAAM,GAAM,IAAM,IAAM,IAAM,IAAM,GAAM,GAAM,IAAM,GAAM,IAAM,IAAM,IAAM,GAAM,IAC1F,GAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,GAAM,GAAM,KAGxFW,EAAK,EACLU,EAAK,EACLT,EAAK,GACLU,EAAK,GAELiB,GACF,KAAO,KAAO,KAAO,KAAO,KAAO,KAAO,KAAO,KAAO,KAAO,KAAO,KAAO,KAAO,KACpF,KAAO,KAAO,KAAO,KAAO,KAAO,IAAO,KAAO,IAAO,KAAO,IAAO,KAAO,IAAO,KACpF,KAAO,KAAO,KAAO,KAAO,KAAO,KAAO,KAAO,MAG/C5D,EAAaoE,SAAS,OAAQ,GAM9BrC,KAQA0B,KAEAN,EAAW,EAEXvC,KAEAF,KAMAlD,GAAS,EAET+D,KAEA8C,EAAa/G,EAAKsB,GAElBwC,KAi0BAxC,GAMF0F,QAAS,QAUTC,OAAQ,SAASvG,GACfA,EAAOS,EAAcT,EAGrB,IAAIwG,GAAOxG,EAAKwG,MAAQ,GAAKxG,EAAKwG,MAAQ,GAAKxG,EAAKwG,KAAO,CAE3DA,IAAQ,EAGR,IAAIzG,GAAMC,EAAKuG,QAAUhH,IAErBkH,EAAM1G,EAAI2G,WAAW,KAEzBD,GAAIF,OAAO1D,MAAS2D,EACpBC,EAAIF,OAAOI,OAASH,EAEpBC,EAAIG,UAAY5G,EAAK6G,YAAc,OACnCJ,EAAIK,SAAS,EAAG,EAAGN,EAAMA,GAGzBrB,EAAWY,EAAY/F,EAAK+G,OAAS/G,EAAK+G,MAAMC,eAAkB,IAGlE,IAAIC,GAAQrC,EAAc5E,EAAKU,OAAS,GAExC+F,GAAIS,UAAY,CAGhB,IAAIC,GAAKX,CACTW,IAAMtE,EACNsE,EAAMC,KAAKC,MAAMF,EAEjB,IAAIG,GAASF,KAAKC,OAAOb,EAAQW,EAAKtE,GAAU,EAGhD4D,GAAIc,UAAU,EAAG,EAAGf,EAAMA,GAC1BC,EAAIG,UAAY5G,EAAK6G,YAAc,OACnCJ,EAAIK,SAAS,EAAG,EAAGN,EAAMA,GACzBC,EAAIG,UAAY5G,EAAKwH,YAAc,MAEnC,IAAIxG,GAAGmC,CAEP,KAAKnC,EAAI,EAAO6B,EAAJ7B,EAAWA,IACrB,IAAKmC,EAAI,EAAON,EAAJM,EAAWA,IACjB8D,EAAM9D,EAAIN,EAAQ7B,IACpByF,EAAIK,SAASK,EAAKnG,EAAIsG,EAAQH,EAAKhE,EAAImE,EAAQH,EAAIA,EAKzD,OAAOpH,IAQT0H,MAAO,SAASzH,GACdA,EAAOS,EAAcT,EAGrB,IAAID,GAAM2H,KAAKnB,OAAOvG,GAElB2H,EAAM3H,EAAKyH,OAAS7H,GAOxB,OAJA+H,GAAIC,IAAS7H,EAAIO,UAAUN,EAAKE,MAAQC,GACxCwH,EAAIhB,OAAS5G,EAAI4G,OACjBgB,EAAI9E,MAAS9C,EAAI8C,MAEV8E,GAeTE,KAAM,SAAS7H,EAAM2B,EAAM1B,GAuBzB,QAAS6H,GAAKrG,GACPsG,IACHA,GAAY,EAEZ9H,EAASwB,IAxBb,OAFAzB,EAAOS,EAAcT,SAEN2B,IACb,IAAK,WACH1B,EAAW0B,EACXA,EAAO,IACP,MACF,KAAK,SACH3B,EAAK2B,KAAOA,EAKhB,GAAwB,kBAAb1B,GACT,KAAM,IAAI2B,WAAU,gCAAmC3B,GAGzD,IAAI8H,IAAY,EAEZhI,EAAM2H,KAAKnB,OAAOvG,EAWlBR,GACF2B,EAAUpB,EAAKC,EAAM8H,GAErBhI,EAASC,EAAKC,EAAM8H,IAaxBE,SAAU,SAAShI,EAAM2B,GACvB3B,EAAOS,EAAcT,GAED,gBAAT2B,KAAmB3B,EAAK2B,KAAOA,EAG1C,IAAI5B,GAAM2H,KAAKnB,OAAOvG,EAElBR,GACF0C,EAAcnC,EAAKC,GAEnBF,EAASC,EAAKC,IAUlBM,UAAW,SAASN,GAGlB,MAFAA,GAAOS,EAAcT,GAEd0H,KAAKnB,OAAOvG,GAAMM,UAAUN,EAAKE,MAAQC,IAQlD8H,WAAY,WAEV,MADA3I,GAAKsB,GAAKyF,EACHqB,MASY,oBAAZQ,UACT1I,GAAS,EAEa,mBAAX2I,SAA0BA,OAAOD,UAC1CA,QAAUC,OAAOD,QAAUtH,GAE7BsH,QAAQtH,GAAKA,EAGbnB,EAAS2I,QAAQ,UACjBvI,EAAQJ,EAAOI,MACfwB,EAAK+G,QAAQ,OACc,kBAAXC,SAAyBA,OAAOC,IAChDD,OAAO,WACL,MAAOzH,MAIJtB,EAAKiJ,mBACR5H,EAAYC,GAGdtB,EAAKsB,GAAKA,IAGX8G","file":"qr.min.js"} \ No newline at end of file diff --git a/vendors/routes/crossroads.js b/vendors/routes/crossroads.js new file mode 100644 index 0000000000..84223a2dfb --- /dev/null +++ b/vendors/routes/crossroads.js @@ -0,0 +1,166 @@ +/** @license + * Crossroads.js + * Released under the MIT license + * Author: Miller Medeiros + * Version: 0.7.1 - Build: 93 (2012/02/02 09:29 AM) + */ + +(global => { + + const isFunction = obj => typeof obj === 'function'; + + // Crossroads -------- + //==================== + + global.Crossroads = class Crossroads { + + constructor() { + this._routes = []; + } + + addRoute(pattern, callback) { + var route = new Route(pattern, callback, this); + this._routes.push(route); + return route; + } + + parse(request) { + request = request || ''; + var i = 0, + routes = this._routes, + n = routes.length, + route; + //should be decrement loop since higher priorities are added at the end of array + while (n--) { + route = routes[n]; + if ((!i || route.greedy) && route.match(request)) { + route.callback?.(...route._getParamsArray(request)); + ++i; + } + } + } + } + + // Route -------------- + //===================== + + class Route { + + constructor(pattern, callback, router) { + var isRegexPattern = pattern instanceof RegExp; + Object.assign(this, { + greedy: false, + rules: {}, + _router: router, + _pattern: pattern, + _paramsIds: isRegexPattern ? null : captureVals(PARAMS_REGEXP, pattern), + _optionalParamsIds: isRegexPattern ? null : captureVals(OPTIONAL_PARAMS_REGEXP, pattern), + _matchRegexp: isRegexPattern ? pattern : compilePattern(pattern), + callback: isFunction(callback) ? callback : null + }); + } + + match(request) { + // validate params even if regexp. + var values = this._getParamsObject(request); + return this._matchRegexp.test(request) + && 0 == Object.entries(this.rules).filter(([key, validationRule]) => { + var val = values[key], + isValid = false; + if (key === 'normalize_' + || (val == null && this._optionalParamsIds?.includes(key))) { + isValid = true; + } + else if (validationRule instanceof RegExp) { + isValid = validationRule.test(val); + } + else if (Array.isArray(validationRule)) { + isValid = validationRule.includes(val); + } + else if (isFunction(validationRule)) { + isValid = validationRule(val, request, values); + } + // fail silently if validationRule is from an unsupported type + return !isValid; + }).length; + } + + _getParamsObject(request) { + var values = getParamValues(request, this._matchRegexp) || [], + n = values.length; + if (this._paramsIds) { + while (n--) { + values[this._paramsIds[n]] = values[n]; + } + } + return values; + } + + _getParamsArray(request) { + var norm = this.rules.normalize_; + return isFunction(norm) + ? norm(request, this._getParamsObject(request)) + : getParamValues(request, this._matchRegexp); + } + + } + + + + // Pattern Lexer ------ + //===================== + + const + ESCAPE_CHARS_REGEXP = /[\\.+*?^$[\](){}/'#]/g, //match chars that should be escaped on string regexp + UNNECESSARY_SLASHES_REGEXP = /\/$/g, //trailing slash + OPTIONAL_SLASHES_REGEXP = /([:}]|\w(?=\/))\/?(:)/g, //slash between `::` or `}:` or `\w:`. $1 = before, $2 = after + REQUIRED_SLASHES_REGEXP = /([:}])\/?(\{)/g, //used to insert slash between `:{` and `}{` + + REQUIRED_PARAMS_REGEXP = /\{([^}]+)\}/g, //match everything between `{ }` + OPTIONAL_PARAMS_REGEXP = /:([^:]+):/g, //match everything between `: :` + PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g, //capture everything between `{ }` or `: :` + + //used to save params during compile (avoid escaping things that + //shouldn't be escaped). + SAVE_REQUIRED_PARAMS = '__CR_RP__', + SAVE_OPTIONAL_PARAMS = '__CR_OP__', + SAVE_REQUIRED_SLASHES = '__CR_RS__', + SAVE_OPTIONAL_SLASHES = '__CR_OS__', + SAVED_REQUIRED_REGEXP = new RegExp(SAVE_REQUIRED_PARAMS, 'g'), + SAVED_OPTIONAL_REGEXP = new RegExp(SAVE_OPTIONAL_PARAMS, 'g'), + SAVED_OPTIONAL_SLASHES_REGEXP = new RegExp(SAVE_OPTIONAL_SLASHES, 'g'), + SAVED_REQUIRED_SLASHES_REGEXP = new RegExp(SAVE_REQUIRED_SLASHES, 'g'), + + captureVals = (regex, pattern) => { + var vals = [], match; + while ((match = regex.exec(pattern))) { + vals.push(match[1]); + } + return vals; + }, + + getParamValues = (request, regexp) => { + var vals = regexp.exec(request); + vals?.shift(); + return vals; + }, + compilePattern = pattern => { + return new RegExp('^' + (pattern + ? pattern + // tokenize, save chars that shouldn't be escaped + .replace(UNNECESSARY_SLASHES_REGEXP, '') + .replace(OPTIONAL_SLASHES_REGEXP, '$1'+ SAVE_OPTIONAL_SLASHES +'$2') + .replace(REQUIRED_SLASHES_REGEXP, '$1'+ SAVE_REQUIRED_SLASHES +'$2') + .replace(OPTIONAL_PARAMS_REGEXP, SAVE_OPTIONAL_PARAMS) + .replace(REQUIRED_PARAMS_REGEXP, SAVE_REQUIRED_PARAMS) + .replace(ESCAPE_CHARS_REGEXP, '\\$&') + // untokenize + .replace(SAVED_OPTIONAL_SLASHES_REGEXP, '\\/?') + .replace(SAVED_REQUIRED_SLASHES_REGEXP, '\\/') + .replace(SAVED_OPTIONAL_REGEXP, '([^\\/]+)?/?') + .replace(SAVED_REQUIRED_REGEXP, '([^\\/]+)') + : '' + ) + '/?$'); //trailing slash is optional + }; + +})(this); diff --git a/vendors/routes/crossroads.min.js b/vendors/routes/crossroads.min.js deleted file mode 100644 index 759f3f1a28..0000000000 --- a/vendors/routes/crossroads.min.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - Crossroads.js - Released under the MIT license - Author: Miller Medeiros - Version: 0.7.1 - Build: 93 (2012/02/02 09:29 AM) -*/ -(function(f){f(["signals"],function(g){function f(a,b){if(a.indexOf)return a.indexOf(b);else{for(var c=a.length;c--;)if(a[c]===b)return c;return-1}}function i(a,b){return"[object "+b+"]"===Object.prototype.toString.call(a)}function n(a){return a===null||a==="null"?null:a==="true"?!0:a==="false"?!1:a===m||a==="undefined"?m:a===""||isNaN(a)?a:parseFloat(a)}function k(){this._routes=[];this.bypassed=new g.Signal;this.routed=new g.Signal}function o(a,b,c,e){var d=i(a,"RegExp");this._router=e;this._pattern= -a;this._paramsIds=d?null:h.getParamIds(this._pattern);this._optionalParamsIds=d?null:h.getOptionalParamsIds(this._pattern);this._matchRegexp=d?a:h.compilePattern(a);this.matched=new g.Signal;b&&this.matched.add(b);this._priority=c||0}var j,h,m;k.prototype={normalizeFn:null,create:function(){return new k},shouldTypecast:!1,addRoute:function(a,b,c){a=new o(a,b,c,this);this._sortedInsert(a);return a},removeRoute:function(a){var b=f(this._routes,a);b!==-1&&this._routes.splice(b,1);a._destroy()},removeAllRoutes:function(){for(var a= -this.getNumRoutes();a--;)this._routes[a]._destroy();this._routes.length=0},parse:function(a){var a=a||"",b=this._getMatchedRoutes(a),c=0,e=b.length,d;if(e)for(;c + * @author Miller Medeiros + * @version 1.1.2 (2012/10/31 03:19 PM) + * Released under the MIT License + */ + +(global => { + + //-------------------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------------------- + + const + _hashValRegexp = /#(.*)$/, + _hashRegexp = /^[#/]+/, + _hashTrim = /^\/+/g, + _trimHash = hash => hash?.replace(_hashTrim, '') || '', + _getWindowHash = () => { + //parsed full URL instead of getting window.location.hash because Firefox decode hash value (and all the other browsers don't) + var result = _hashValRegexp.exec( location.href ); + return result?.[1] ? decodeURIComponent(result[1]) : ''; + }, + _registerChange = newHash => { + if (_hash !== newHash) { + var oldHash = _hash; + _hash = newHash; //should come before event dispatch to make sure user can get proper value inside event handler + _dispatch(_trimHash(newHash), _trimHash(oldHash)); + } + }, + _setHash = (path, replace) => { + path = path ? '/' + path.replace(_hashRegexp, '') : path; + if (path !== _hash){ + // we should store raw value + _registerChange(path); + if (path === _hash) { + path = '#' + encodeURI(path) + // we check if path is still === _hash to avoid error in + // case of multiple consecutive redirects [issue #39] + replace + ? location.replace(path) + : (location.hash = path); + } + } + }, + _dispatch = (...args) => hasher.active && _bindings.forEach(callback => callback(...args)), + + //-------------------------------------------------------------------------------------- + // Public (API) + //-------------------------------------------------------------------------------------- + + hasher = /** @lends hasher */ { + clear : () => { + _bindings = []; + hasher.active = true; + }, + + /** + * Signal dispatched when hash value changes. + * - pass current hash as 1st parameter to listeners and previous hash value as 2nd parameter. + * @type signals.Signal + */ + active : true, + add : callback => _bindings.push(callback), + + /** + * Start listening/dispatching changes in the hash/history. + *
      + *
    • hasher won't dispatch CHANGE events by manually typing a new value or pressing the back/forward buttons before calling this method.
    • + *
    + */ + init : () => _dispatch(_trimHash(_hash)), + + /** + * Set Hash value, generating a new history record. + * @param {...string} path Hash value without '#'. + * @example hasher.setHash('lorem/ipsum/dolor') -> '#/lorem/ipsum/dolor' + */ + setHash : path => _setHash(path), + + /** + * Set Hash value without keeping previous hash on the history record. + * @param {...string} path Hash value without '#'. + * @example hasher.replaceHash('lorem/ipsum/dolor') -> '#/lorem/ipsum/dolor' + */ + replaceHash : path => _setHash(path, true) + }; + + var _hash = _getWindowHash(), + _bindings = []; + + addEventListener('hashchange', () => _registerChange(_getWindowHash())); + + global.hasher = hasher; +})(this); diff --git a/vendors/routes/hasher.min.js b/vendors/routes/hasher.min.js deleted file mode 100644 index 642ebf954b..0000000000 --- a/vendors/routes/hasher.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Hasher - * @author Miller Medeiros - * @version 1.1.2 (2012/10/31 03:19 PM) - * Released under the MIT License - */ -(function(a){a("hasher",["signals"],function(b){var c=(function(k){var o=25,q=k.document,n=k.history,w=b.Signal,f,u,m,E,d,C,s=/#(.*)$/,j=/(\?.*)|(\#.*)/,g=/^\#/,i=(!+"\v1"),A=("onhashchange" in k)&&q.documentMode!==7,e=i&&!A,r=(location.protocol==="file:");function t(G){if(!G){return""}var F=new RegExp("^\\"+f.prependHash+"|\\"+f.appendHash+"$","g");return G.replace(F,"")}function D(){var F=s.exec(f.getURL());return(F&&F[1])?decodeURIComponent(F[1]):""}function z(){return(d)?d.contentWindow.frameHash:null}function y(){d=q.createElement("iframe");d.src="about:blank";d.style.display="none";q.body.appendChild(d)}function h(){if(d&&u!==z()){var F=d.contentWindow.document;F.open();F.write(""+q.title+'