diff --git a/.dockerignore b/.dockerignore index 7fff0265d6..b92827b7c8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,5 @@ public/styles .git .idea .cd .babelrc -.DS_Store \ No newline at end of file +.DS_Store +debug \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000000..f8777509a3 --- /dev/null +++ b/.env @@ -0,0 +1,23 @@ +_APP_ENV=production +_APP_ENV=development +_APP_OPTIONS_ABUSE=disabled +_APP_OPTIONS_FORCE_HTTPS=disabled +_APP_OPENSSL_KEY_V1=your-secret-key +_APP_DOMAIN=demo.appwrite.io +_APP_DOMAIN_TARGET=demo.appwrite.io +_APP_REDIS_HOST=redis +_APP_REDIS_PORT=6379 +_APP_DB_HOST=mariadb +_APP_DB_PORT=3306 +_APP_DB_SCHEMA=appwrite +_APP_DB_USER=user +_APP_DB_PASS=password +_APP_INFLUXDB_HOST=influxdb +_APP_INFLUXDB_PORT=8086 +_APP_STATSD_HOST=telegraf +_APP_STATSD_PORT=8125 +_APP_SMTP_HOST=maildev +_APP_SMTP_PORT=25 +_APP_STORAGE_LIMIT=100000000 +_APP_FUNCTIONS_TIMEOUT=900 +_APP_FUNCTIONS_CONTAINERS=10 \ No newline at end of file diff --git a/.gitignore b/.gitignore index fa6a24789b..d0b2a74730 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,7 @@ /.vscode/ /vendor/ /node_modules/ -/storage/uploads/ -/storage/cache -/storage/db -/storage/influxdb /tests/resources/storage/ /.idea/ .DS_Store -.php_cs.cache -.env \ No newline at end of file +.php_cs.cache \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index a6fbe5944e..89db541ea3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,12 @@ notifications: services: - docker +before_install: + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + - sudo apt-get update + - sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce + # cache: # directories: # - docker_images @@ -25,9 +31,11 @@ services: # - docker save -o docker_images/images.tar appwrite_appwrite install: +- docker --version - docker-compose up -d - sleep 90 script: - docker ps -- docker exec appwrite test +- docker-compose logs appwrite +- docker exec appwrite test \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index f5ffdc97fb..e27b89b088 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,19 @@ - Upgraded ClamAV container image to version 1.0.11 ([#412](https://github.com/appwrite/appwrite/issues/412)) - Optimised function execution by using fully-qualified function calls - Added support for boolean 'true' and 'false' in query strings alongside 1 and 0 +- Added pagination for projects list on the console home page. +- Updated storage calculation to match IEC standards +- Now using Alpine as base Docker image +- New and consistent response format for all API object + new response examples in the docs + - Removed user roles attribute from user object (can be fetched from /v1/teams/memberships) ** + - Removed type attribute from session object response (used only internally) + - ** - might be changed before merging to master + +## Breaking Changes (Read before upgrading!) +- **Deprecated** `first` and `last` query params for documents list route in the database API +- **Deprecated** Deprectaed Pubjabi Translations ('pn') +- Switched order of limit and offset params in all the SDKs `listDocuments` method for better consistency +- Default `limit` param value in all the SDKs `listDocuments` method is now 25 for better consistency ## Bug Fixes @@ -41,10 +54,6 @@ - New OAuth adapter for PayPal sandbox - Fixed a bug making read permission overwrite write permission in some cases -## Breaking Changes -- **Deprecated** `first` and `last` query params for documents list route in the database API -- **Deprecated** Deprectaed Pubjabi Translations ('pn') - ## Security - Access to Health API now requires authentication with an API Key with access to `health.read` scope allowed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e6d85ab42..f007f9cf10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,8 +107,6 @@ Currently, all of the Appwrite microservices are intended to communicate using t Security and privacy are extremely important to Appwrite, developers, and users alike. Make sure to follow the best industry standards and practices. - - ## Dependencies Please avoid introducing new dependencies to Appwrite without consulting the team. New dependencies can be very helpful but also introduce new security and privacy issues, complexity, and impact total docker image size. @@ -148,7 +146,7 @@ After finishing the installation process, you can start writing and editing code To build a new version of the Appwrite server, all you need to do is run the build.sh file like this: ```bash -bash ./build.sh 1.0.0 +bash ./build.sh X.X.X ``` Before running the command, make sure you have proper write permissions to the Appwrite docker hub team. @@ -167,6 +165,27 @@ To run tests manually, run phpunit from your command line: docker exec appwrite test ``` +## Benchmarking + +You can use WRK Docker image to benchmark the server performance. Benchmarking is extremely useful when you want to compare how the server behaves before and after a change has been applied. Replace [APPWRITE_HOSTNAME_OR_IP] with your Appwrite server hostname or IP. Note that localhost is not accessible from inside the WRK container. + +``` + Options: + -c, --connections Connections to keep open + -d, --duration Duration of test + -t, --threads Number of threads to use + + -s, --script Load Lua script file + -H, --header Add header to request + --latency Print latency statistics + --timeout Socket/request timeout + -v, --version Print version details +``` + +```bash +docker run --rm skandyla/wrk -t3 -c100 -d30 https://[APPWRITE_HOSTNAME_OR_IP] +``` + ## Code Maintenance We use some automation tools to help us keep a healthy code base. diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 index 8a86a910fc..2eeb7f84f3 --- a/Dockerfile +++ b/Dockerfile @@ -1,56 +1,65 @@ -FROM ubuntu:18.04 AS builder - -LABEL maintainer="team@appwrite.io" +FROM composer:2.0 as step0 ARG TESTING=false - -ENV TZ=Asia/Tel_Aviv \ - DEBIAN_FRONTEND=noninteractive \ - PHP_VERSION=7.4 \ - PHP_REDIS_VERSION=5.2.1 - -RUN \ - apt-get update && \ - apt-get install -y --no-install-recommends --no-install-suggests ca-certificates software-properties-common wget git openssl && \ - LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php && \ - apt-get update && \ - apt-get install -y --no-install-recommends --no-install-suggests make php$PHP_VERSION php$PHP_VERSION-dev zip unzip php$PHP_VERSION-zip && \ - # Redis Extension - wget -q https://github.com/phpredis/phpredis/archive/$PHP_REDIS_VERSION.tar.gz && \ - tar -xf $PHP_REDIS_VERSION.tar.gz && \ - cd phpredis-$PHP_REDIS_VERSION && \ - phpize$PHP_VERSION && \ - ./configure && \ - make && \ - # Composer - wget https://getcomposer.org/composer.phar && \ - chmod +x ./composer.phar && \ - mv ./composer.phar /usr/bin/composer && \ - #Brotli - cd / && \ - git clone https://github.com/eustas/ngx_brotli.git && \ - cd ngx_brotli && git submodule update --init && cd .. +ENV TESTING=$TESTING WORKDIR /usr/local/src/ -# Updating PHP Dependencies and Auto-loading... - -ENV TESTING=$TESTING - -COPY composer.* /usr/local/src/ +COPY composer.lock /usr/local/src/ +COPY composer.json /usr/local/src/ RUN composer update --ignore-platform-reqs --optimize-autoloader \ --no-plugins --no-scripts --prefer-dist \ `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` -FROM ubuntu:18.04 +FROM php:7.4-cli-alpine as step1 + +ENV TZ=Asia/Tel_Aviv \ + PHP_REDIS_VERSION=5.3.0 \ + PHP_SWOOLE_VERSION=4.5.2 \ + PHP_XDEBUG_VERSION=sdebug_2_9-beta + +RUN \ + apk add --no-cache --virtual .deps \ + make \ + automake \ + autoconf \ + gcc \ + g++ \ + tar \ + wget \ + git \ + zlib-dev \ + brotli-dev + +RUN docker-php-ext-install sockets + +RUN \ + # Redis Extension + wget -q https://github.com/phpredis/phpredis/archive/$PHP_REDIS_VERSION.tar.gz && \ + tar -xf $PHP_REDIS_VERSION.tar.gz && \ + cd phpredis-$PHP_REDIS_VERSION && \ + phpize && \ + ./configure && \ + make && make install && \ + cd .. && \ + ## Swoole Extension + git clone https://github.com/swoole/swoole-src.git && \ + cd swoole-src && \ + git checkout v$PHP_SWOOLE_VERSION && \ + phpize && \ + ./configure --enable-sockets --enable-http2 && \ + make && make install && \ + cd .. + +FROM php:7.4-cli-alpine as final + LABEL maintainer="team@appwrite.io" ARG VERSION=dev ENV TZ=Asia/Tel_Aviv \ - DEBIAN_FRONTEND=noninteractive \ - PHP_VERSION=7.4 \ + _APP_SERVER=swoole \ _APP_ENV=production \ _APP_DOMAIN=localhost \ _APP_DOMAIN_TARGET=localhost \ @@ -59,7 +68,7 @@ ENV TZ=Asia/Tel_Aviv \ _APP_OPTIONS_ABUSE=enabled \ _APP_OPTIONS_FORCE_HTTPS=disabled \ _APP_OPENSSL_KEY_V1=your-secret-key \ - _APP_STORAGE_LIMIT=104857600 \ + _APP_STORAGE_LIMIT=100000000 \ _APP_STORAGE_ANTIVIRUS=enabled \ _APP_REDIS_HOST=redis \ _APP_REDIS_PORT=6379 \ @@ -74,106 +83,91 @@ ENV TZ=Asia/Tel_Aviv \ _APP_STATSD_PORT=8125 \ _APP_SMTP_HOST=smtp \ _APP_SMTP_PORT=25 \ + _APP_FUNCTIONS_TIMEOUT=900 \ + _APP_FUNCTIONS_CONTAINERS=10 \ _APP_SETUP=self-hosted \ _APP_VERSION=$VERSION #ENV _APP_SMTP_SECURE '' #ENV _APP_SMTP_USERNAME '' #ENV _APP_SMTP_PASSWORD '' -COPY --from=builder /phpredis-5.2.1/modules/redis.so /usr/lib/php/20190902/ -COPY --from=builder /phpredis-5.2.1/modules/redis.so /usr/lib/php/20190902/ -COPY --from=builder /ngx_brotli /ngx_brotli - RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone RUN \ - apt-get update && \ - apt-get install -y --no-install-recommends --no-install-suggests wget ca-certificates software-properties-common build-essential libpcre3-dev zlib1g-dev libssl-dev openssl gnupg htop supervisor && \ - LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php && \ - add-apt-repository universe && \ - add-apt-repository ppa:certbot/certbot && \ - apt-get update && \ - apt-get install -y --no-install-recommends --no-install-suggests php$PHP_VERSION php$PHP_VERSION-fpm \ - php$PHP_VERSION-mysqlnd php$PHP_VERSION-curl php$PHP_VERSION-imagick php$PHP_VERSION-mbstring php$PHP_VERSION-dom webp certbot && \ - # Nginx - wget http://nginx.org/download/nginx-1.19.0.tar.gz && \ - tar -xzvf nginx-1.19.0.tar.gz && rm nginx-1.19.0.tar.gz && \ - cd nginx-1.19.0 && \ - ./configure --prefix=/usr/share/nginx \ - --sbin-path=/usr/sbin/nginx \ - --modules-path=/usr/lib/nginx/modules \ - --conf-path=/etc/nginx/nginx.conf \ - --error-log-path=/var/log/nginx/error.log \ - --http-log-path=/var/log/nginx/access.log \ - --pid-path=/run/nginx.pid \ - --lock-path=/var/lock/nginx.lock \ - --user=www-data \ - --group=www-data \ - --build=Ubuntu \ - --with-http_gzip_static_module \ - --with-http_ssl_module \ - --with-http_v2_module \ - --add-module=/ngx_brotli && \ - make && \ - make install && \ - rm -rf ../nginx-1.19.0 && \ - # Redis Extension - echo extension=redis.so >> /etc/php/$PHP_VERSION/fpm/conf.d/redis.ini && \ - echo extension=redis.so >> /etc/php/$PHP_VERSION/cli/conf.d/redis.ini && \ - # Cleanup - cd ../ && \ - apt-get purge -y --auto-remove wget software-properties-common build-essential libpcre3-dev zlib1g-dev libssl-dev gnupg && \ - apt-get clean && \ - rm -rf /ngx_brotli && \ - rm -rf /var/lib/apt/lists/* + apk update \ + && apk add --no-cache --virtual .deps \ + make \ + automake \ + autoconf \ + gcc \ + g++ \ + curl-dev \ + && apk add --no-cache \ + libstdc++ \ + yaml-dev \ + imagemagick \ + imagemagick-dev \ + certbot \ + docker-cli \ + && pecl install imagick yaml \ + && docker-php-ext-enable imagick yaml \ + && docker-php-ext-install sockets opcache pdo_mysql \ + && apk del .deps -# Set Upload Limit (default to 100MB) -RUN echo "upload_max_filesize = ${_APP_STORAGE_LIMIT}" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini -RUN echo "post_max_size = ${_APP_STORAGE_LIMIT}" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini +WORKDIR /usr/src/code -# Add logs file -RUN echo "" >> /var/log/appwrite.log +COPY --from=step0 /usr/local/src/vendor /usr/src/code/vendor +COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190902/swoole.so /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ +COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190902/redis.so /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ -# Nginx Configuration (with self-signed ssl certificates) -COPY ./docker/nginx.conf.template /etc/nginx/nginx.conf.template -COPY ./docker/ssl/cert.pem /etc/nginx/ssl/cert.pem -COPY ./docker/ssl/key.pem /etc/nginx/ssl/key.pem - -# PHP Configuration -RUN mkdir -p /var/run/php -COPY ./docker/www.conf /etc/php/$PHP_VERSION/fpm/pool.d/www.conf - -# Add PHP Source Code -COPY ./app /usr/share/nginx/html/app +# Add Source Code +COPY ./app /usr/src/code/app COPY ./bin /usr/local/bin -COPY ./docs /usr/share/nginx/html/docs -COPY ./public /usr/share/nginx/html/public -COPY ./src /usr/share/nginx/html/src -COPY --from=builder /usr/local/src/vendor /usr/share/nginx/html/vendor +COPY ./docs /usr/src/code/docs +COPY ./public /usr/src/code/public +COPY ./src /usr/src/code/src +# Set Volumes RUN mkdir -p /storage/uploads && \ mkdir -p /storage/cache && \ mkdir -p /storage/config && \ mkdir -p /storage/certificates && \ + mkdir -p /storage/functions && \ + mkdir -p /storage/debug && \ chown -Rf www-data.www-data /storage/uploads && chmod -Rf 0755 /storage/uploads && \ chown -Rf www-data.www-data /storage/cache && chmod -Rf 0755 /storage/cache && \ chown -Rf www-data.www-data /storage/config && chmod -Rf 0755 /storage/config && \ - chown -Rf www-data.www-data /storage/certificates && chmod -Rf 0755 /storage/certificates - -# Supervisord Conf -COPY ./docker/supervisord.conf /etc/supervisord.conf + chown -Rf www-data.www-data /storage/certificates && chmod -Rf 0755 /storage/certificates && \ + chown -Rf www-data.www-data /storage/functions && chmod -Rf 0755 /storage/functions && \ + chown -Rf www-data.www-data /storage/debug && chmod -Rf 0755 /storage/debug # Executables -RUN chmod +x /usr/local/bin/start -RUN chmod +x /usr/local/bin/doctor -RUN chmod +x /usr/local/bin/migrate -RUN chmod +x /usr/local/bin/test +RUN chmod +x /usr/local/bin/doctor && \ + chmod +x /usr/local/bin/install && \ + chmod +x /usr/local/bin/migrate && \ + chmod +x /usr/local/bin/schedule && \ + chmod +x /usr/local/bin/ssl && \ + chmod +x /usr/local/bin/test && \ + chmod +x /usr/local/bin/worker-audits && \ + chmod +x /usr/local/bin/worker-certificates && \ + chmod +x /usr/local/bin/worker-deletes && \ + chmod +x /usr/local/bin/worker-functions && \ + chmod +x /usr/local/bin/worker-mails && \ + chmod +x /usr/local/bin/worker-tasks && \ + chmod +x /usr/local/bin/worker-usage && \ + chmod +x /usr/local/bin/worker-webhooks # Letsencrypt Permissions RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/ +# Enable Extensions +RUN echo extension=swoole.so >> /usr/local/etc/php/conf.d/swoole.ini +RUN echo extension=redis.so >> /usr/local/etc/php/conf.d/redis.ini + +RUN echo "opcache.preload_user=www-data" >> /usr/local/etc/php/conf.d/appwrite.ini +RUN echo "opcache.preload=/usr/src/code/app/preload.php" >> /usr/local/etc/php/conf.d/appwrite.ini +RUN echo "opcache.enable_cli = 1" >> /usr/local/etc/php/conf.d/appwrite.ini + EXPOSE 80 -WORKDIR /usr/share/nginx/html - -CMD ["/bin/bash", "/usr/local/bin/start"] +CMD [ "php", "app/http.php", "-dopcache.preload=opcache.preload=/usr/src/code/app/preload.php" ] diff --git a/Dockerfile.alpine b/Dockerfile.alpine new file mode 100755 index 0000000000..e60b97585c --- /dev/null +++ b/Dockerfile.alpine @@ -0,0 +1,178 @@ +FROM composer:2.0 as step0 + +ARG TESTING=false +ENV TESTING=$TESTING + +WORKDIR /usr/local/src/ + +COPY composer.lock /usr/local/src/ +COPY composer.json /usr/local/src/ + +RUN composer update --ignore-platform-reqs --optimize-autoloader \ + --no-plugins --no-scripts --prefer-dist \ + `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` + +FROM php:8.0.0alpha1-cli-alpine as step1 + +ENV TZ=Asia/Tel_Aviv \ + DEBIAN_FRONTEND=noninteractive \ + PHP_REDIS_VERSION=5.3.0 \ + PHP_SWOOLE_VERSION=4.5.2 \ + PHP_XDEBUG_VERSION=sdebug_2_9-beta + +RUN \ + apk update && \ + apk add ca-certificates gcc make g++ autoconf wget git openssl make zip unzip brotli zlib + +RUN docker-php-ext-install sockets + +RUN \ + # Redis Extension + wget -q https://github.com/phpredis/phpredis/archive/$PHP_REDIS_VERSION.tar.gz && \ + tar -xf $PHP_REDIS_VERSION.tar.gz && \ + cd phpredis-$PHP_REDIS_VERSION && \ + phpize && \ + ./configure && \ + make && make install && \ + cd .. && \ + ## Swoole Extension + git clone https://github.com/swoole/swoole-src.git && \ + cd swoole-src && \ + #git checkout v$PHP_SWOOLE_VERSION && \ + phpize && \ + ./configure --enable-sockets --enable-http2 && \ + make && make install && \ + cd .. + ## XDebug Extension + # git clone https://github.com/swoole/sdebug.git && \ + # cd sdebug && \ + # git checkout $PHP_XDEBUG_VERSION && \ + # phpize && \ + # ./configure --enable-xdebug && \ + # make clean && make && make install + # cd .. && \ + # Meminfo Extension + # git clone https://github.com/BitOne/php-meminfo.git && \ + # cd php-meminfo && \ + # git checkout v1.0.5 && \ + # cd extension/php7 && \ + # phpize && \ + # ./configure --enable-meminfo && \ + # make && make install + +FROM php:8.0.0alpha1-cli-alpine as final + +LABEL maintainer="team@appwrite.io" + +ARG VERSION=dev + +ENV TZ=Asia/Tel_Aviv \ + DEBIAN_FRONTEND=noninteractive \ + _APP_SERVER=swoole \ + _APP_ENV=production \ + _APP_DOMAIN=localhost \ + _APP_DOMAIN_TARGET=localhost \ + _APP_HOME=https://appwrite.io \ + _APP_EDITION=community \ + _APP_OPTIONS_ABUSE=enabled \ + _APP_OPTIONS_FORCE_HTTPS=disabled \ + _APP_OPENSSL_KEY_V1=your-secret-key \ + _APP_STORAGE_LIMIT=100000000 \ + _APP_STORAGE_ANTIVIRUS=enabled \ + _APP_REDIS_HOST=redis \ + _APP_REDIS_PORT=6379 \ + _APP_DB_HOST=mariadb \ + _APP_DB_PORT=3306 \ + _APP_DB_USER=root \ + _APP_DB_PASS=password \ + _APP_DB_SCHEMA=appwrite \ + _APP_INFLUXDB_HOST=influxdb \ + _APP_INFLUXDB_PORT=8086 \ + _APP_STATSD_HOST=telegraf \ + _APP_STATSD_PORT=8125 \ + _APP_SMTP_HOST=smtp \ + _APP_SMTP_PORT=25 \ + _APP_SETUP=self-hosted \ + _APP_VERSION=$VERSION +#ENV _APP_SMTP_SECURE '' +#ENV _APP_SMTP_USERNAME '' +#ENV _APP_SMTP_PASSWORD '' + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN \ + apk update && \ + apk add libwebp certbot htop procps \ + oniguruma libcurl imagemagick-libs yaml-dev brotli-libs zlib-dev + # pecl install imagick yaml && \ + # docker-php-ext-enable imagick yaml + +RUN docker-php-ext-install sockets curl opcache pdo pdo_mysql + +WORKDIR /usr/src/code + +COPY --from=step0 /usr/local/src/vendor /usr/src/code/vendor +COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190128/swoole.so /usr/local/lib/php/extensions/no-debug-non-zts-20190128/ +COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190128/redis.so /usr/local/lib/php/extensions/no-debug-non-zts-20190128/ +# COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190128/xdebug.so /usr/local/lib/php/extensions/no-debug-non-zts-20190128/ +# COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190128/meminfo.so /usr/local/lib/php/extensions/no-debug-non-zts-20190128/ + +# Add Source Code +COPY ./app /usr/src/code/app +COPY ./bin /usr/local/bin +COPY ./docs /usr/src/code/docs +COPY ./public /usr/src/code/public +COPY ./src /usr/src/code/src + +# Set Volumes +RUN mkdir -p /storage/uploads && \ + mkdir -p /storage/cache && \ + mkdir -p /storage/config && \ + mkdir -p /storage/certificates && \ + mkdir -p /storage/functions && \ + mkdir -p /storage/debug && \ + chown -Rf www-data.www-data /storage/uploads && chmod -Rf 0755 /storage/uploads && \ + chown -Rf www-data.www-data /storage/cache && chmod -Rf 0755 /storage/cache && \ + chown -Rf www-data.www-data /storage/config && chmod -Rf 0755 /storage/config && \ + chown -Rf www-data.www-data /storage/certificates && chmod -Rf 0755 /storage/certificates && \ + chown -Rf www-data.www-data /storage/functions && chmod -Rf 0755 /storage/functions && \ + chown -Rf www-data.www-data /storage/debug && chmod -Rf 0755 /storage/debug + +# Executables +RUN chmod +x /usr/local/bin/doctor +RUN chmod +x /usr/local/bin/migrate +RUN chmod +x /usr/local/bin/schedule +RUN chmod +x /usr/local/bin/test +RUN chmod +x /usr/local/bin/worker-audits +RUN chmod +x /usr/local/bin/worker-certificates +RUN chmod +x /usr/local/bin/worker-deletes +RUN chmod +x /usr/local/bin/worker-functions +RUN chmod +x /usr/local/bin/worker-mails +RUN chmod +x /usr/local/bin/worker-tasks +RUN chmod +x /usr/local/bin/worker-usage +RUN chmod +x /usr/local/bin/worker-webhooks + +# Letsencrypt Permissions +RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/ + +# Enable Extensions +RUN echo extension=swoole.so >> /usr/local/etc/php/conf.d/swoole.ini +RUN echo extension=redis.so >> /usr/local/etc/php/conf.d/redis.ini +# RUN echo zend_extension=xdebug.so >> /usr/local/etc/php/conf.d/xdebug.ini +# RUN echo extension=meminfo.so >> /usr/local/etc/php/conf.d/meminfo.ini + +RUN echo "opcache.preload_user=www-data" >> /usr/local/etc/php/conf.d/appwrite.ini +RUN echo "opcache.preload=/usr/src/code/app/preload.php" >> /usr/local/etc/php/conf.d/appwrite.ini +RUN echo "opcache.enable_cli = 1" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.profiler_enable = 1" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.profiler_output_dir = /tmp/" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.profiler_enable_trigger = 1" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.trace_format = 1" >> /usr/local/etc/php/conf.d/appwrite.ini + +EXPOSE 80 + +#, "-dxdebug.auto_trace=1" +#, "-dxdebug.profiler_enable=1" +#, "-dopcache.preload=opcache.preload=/usr/src/code/app/preload.php" + +CMD [ "php", "app/server.php", "-dopcache.preload=opcache.preload=/usr/src/code/app/preload.php" ] \ No newline at end of file diff --git a/Dockerfile.debian b/Dockerfile.debian new file mode 100755 index 0000000000..0d55f0a6f5 --- /dev/null +++ b/Dockerfile.debian @@ -0,0 +1,180 @@ +FROM composer:2.0 as step0 + +ARG TESTING=false +ENV TESTING=$TESTING + +WORKDIR /usr/local/src/ + +COPY composer.lock /usr/local/src/ +COPY composer.json /usr/local/src/ + +RUN composer update --ignore-platform-reqs --optimize-autoloader \ + --no-plugins --no-scripts --prefer-dist \ + `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` + +FROM php:7.4-cli as step1 + +ENV TZ=Asia/Tel_Aviv \ + DEBIAN_FRONTEND=noninteractive \ + PHP_REDIS_VERSION=5.3.0 \ + PHP_SWOOLE_VERSION=4.5.2 \ + PHP_XDEBUG_VERSION=sdebug_2_9-beta + +RUN \ + apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests ca-certificates software-properties-common wget git openssl make zip unzip libbrotli-dev libz-dev + +RUN docker-php-ext-install sockets + +RUN \ + # Redis Extension + wget -q https://github.com/phpredis/phpredis/archive/$PHP_REDIS_VERSION.tar.gz && \ + tar -xf $PHP_REDIS_VERSION.tar.gz && \ + cd phpredis-$PHP_REDIS_VERSION && \ + phpize && \ + ./configure && \ + make && make install && \ + cd .. && \ + ## Swoole Extension + git clone https://github.com/swoole/swoole-src.git && \ + cd swoole-src && \ + git checkout v$PHP_SWOOLE_VERSION && \ + phpize && \ + ./configure --enable-sockets --enable-http2 && \ + make && make install && \ + cd .. + ## XDebug Extension + # git clone https://github.com/swoole/sdebug.git && \ + # cd sdebug && \ + # git checkout $PHP_XDEBUG_VERSION && \ + # phpize && \ + # ./configure --enable-xdebug && \ + # make clean && make && make install + # cd .. && \ + # Meminfo Extension + # git clone https://github.com/BitOne/php-meminfo.git && \ + # cd php-meminfo && \ + # git checkout v1.0.5 && \ + # cd extension/php7 && \ + # phpize && \ + # ./configure --enable-meminfo && \ + # make && make install + +FROM php:7.4-cli as final + +LABEL maintainer="team@appwrite.io" + +ARG VERSION=dev + +ENV TZ=Asia/Tel_Aviv \ + DEBIAN_FRONTEND=noninteractive \ + _APP_SERVER=swoole \ + _APP_ENV=production \ + _APP_DOMAIN=localhost \ + _APP_DOMAIN_TARGET=localhost \ + _APP_HOME=https://appwrite.io \ + _APP_EDITION=community \ + _APP_OPTIONS_ABUSE=enabled \ + _APP_OPTIONS_FORCE_HTTPS=disabled \ + _APP_OPENSSL_KEY_V1=your-secret-key \ + _APP_STORAGE_LIMIT=100000000 \ + _APP_STORAGE_ANTIVIRUS=enabled \ + _APP_REDIS_HOST=redis \ + _APP_REDIS_PORT=6379 \ + _APP_DB_HOST=mariadb \ + _APP_DB_PORT=3306 \ + _APP_DB_USER=root \ + _APP_DB_PASS=password \ + _APP_DB_SCHEMA=appwrite \ + _APP_INFLUXDB_HOST=influxdb \ + _APP_INFLUXDB_PORT=8086 \ + _APP_STATSD_HOST=telegraf \ + _APP_STATSD_PORT=8125 \ + _APP_SMTP_HOST=smtp \ + _APP_SMTP_PORT=25 \ + _APP_FUNCTIONS_TIMEOUT=900 \ + _APP_FUNCTIONS_CONTAINERS=10 \ + _APP_SETUP=self-hosted \ + _APP_VERSION=$VERSION +#ENV _APP_SMTP_SECURE '' +#ENV _APP_SMTP_USERNAME '' +#ENV _APP_SMTP_PASSWORD '' + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN \ + apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests webp certbot htop procps docker.io \ + libonig-dev libcurl4-gnutls-dev libmagickwand-dev libyaml-dev libbrotli-dev libz-dev && \ + pecl install imagick yaml && \ + docker-php-ext-enable imagick yaml + +RUN docker-php-ext-install sockets curl opcache pdo pdo_mysql + +WORKDIR /usr/src/code + +COPY --from=step0 /usr/local/src/vendor /usr/src/code/vendor +COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190902/swoole.so /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ +COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190902/redis.so /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ +# COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190902/xdebug.so /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ +# COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190902/meminfo.so /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ + +# Add Source Code +COPY ./app /usr/src/code/app +COPY ./bin /usr/local/bin +COPY ./docs /usr/src/code/docs +COPY ./public /usr/src/code/public +COPY ./src /usr/src/code/src + +# Set Volumes +RUN mkdir -p /storage/uploads && \ + mkdir -p /storage/cache && \ + mkdir -p /storage/config && \ + mkdir -p /storage/certificates && \ + mkdir -p /storage/functions && \ + mkdir -p /storage/debug && \ + chown -Rf www-data.www-data /storage/uploads && chmod -Rf 0755 /storage/uploads && \ + chown -Rf www-data.www-data /storage/cache && chmod -Rf 0755 /storage/cache && \ + chown -Rf www-data.www-data /storage/config && chmod -Rf 0755 /storage/config && \ + chown -Rf www-data.www-data /storage/certificates && chmod -Rf 0755 /storage/certificates && \ + chown -Rf www-data.www-data /storage/functions && chmod -Rf 0755 /storage/functions && \ + chown -Rf www-data.www-data /storage/debug && chmod -Rf 0755 /storage/debug + +# Executables +RUN chmod +x /usr/local/bin/doctor +RUN chmod +x /usr/local/bin/migrate +RUN chmod +x /usr/local/bin/schedule +RUN chmod +x /usr/local/bin/test +RUN chmod +x /usr/local/bin/worker-audits +RUN chmod +x /usr/local/bin/worker-certificates +RUN chmod +x /usr/local/bin/worker-deletes +RUN chmod +x /usr/local/bin/worker-functions +RUN chmod +x /usr/local/bin/worker-mails +RUN chmod +x /usr/local/bin/worker-tasks +RUN chmod +x /usr/local/bin/worker-usage +RUN chmod +x /usr/local/bin/worker-webhooks + +# Letsencrypt Permissions +RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/ + +# Enable Extensions +RUN echo extension=swoole.so >> /usr/local/etc/php/conf.d/swoole.ini +RUN echo extension=redis.so >> /usr/local/etc/php/conf.d/redis.ini +# RUN echo zend_extension=xdebug.so >> /usr/local/etc/php/conf.d/xdebug.ini +# RUN echo extension=meminfo.so >> /usr/local/etc/php/conf.d/meminfo.ini + +RUN echo "opcache.preload_user=www-data" >> /usr/local/etc/php/conf.d/appwrite.ini +RUN echo "opcache.preload=/usr/src/code/app/preload.php" >> /usr/local/etc/php/conf.d/appwrite.ini +RUN echo "opcache.enable_cli = 1" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.profiler_enable = 1" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.profiler_output_dir = /tmp/" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.profiler_enable_trigger = 1" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.trace_format = 1" >> /usr/local/etc/php/conf.d/appwrite.ini + +EXPOSE 80 + +#, "-dxdebug.auto_trace=1" +#, "-dxdebug.profiler_enable=1" +#, "-dopcache.preload=opcache.preload=/usr/src/code/app/preload.php" + +CMD [ "php", "app/server.php", "-dopcache.preload=opcache.preload=/usr/src/code/app/preload.php" ] \ No newline at end of file diff --git a/Dockerfile.nginx b/Dockerfile.nginx new file mode 100644 index 0000000000..3bd88a7d34 --- /dev/null +++ b/Dockerfile.nginx @@ -0,0 +1,185 @@ +FROM ubuntu:18.04 AS builder + +LABEL maintainer="team@appwrite.io" + +ARG TESTING=false + +ENV TZ=Asia/Tel_Aviv \ + DEBIAN_FRONTEND=noninteractive \ + PHP_VERSION=7.4 \ + PHP_REDIS_VERSION=5.2.1 + +RUN \ + apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests ca-certificates software-properties-common wget git openssl && \ + LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php && \ + apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests make php$PHP_VERSION php$PHP_VERSION-dev zip unzip php$PHP_VERSION-zip && \ + # Redis Extension + wget -q https://github.com/phpredis/phpredis/archive/$PHP_REDIS_VERSION.tar.gz && \ + tar -xf $PHP_REDIS_VERSION.tar.gz && \ + cd phpredis-$PHP_REDIS_VERSION && \ + phpize$PHP_VERSION && \ + ./configure && \ + make && \ + # Composer + wget https://getcomposer.org/composer.phar && \ + chmod +x ./composer.phar && \ + mv ./composer.phar /usr/bin/composer && \ + #Brotli + cd / && \ + git clone https://github.com/eustas/ngx_brotli.git && \ + cd ngx_brotli && git submodule update --init && cd .. + +WORKDIR /usr/local/src/ + +# Updating PHP Dependencies and Auto-loading... + +ENV TESTING=$TESTING + +COPY composer.* /usr/local/src/ + +RUN composer update --ignore-platform-reqs --optimize-autoloader \ + --no-plugins --no-scripts --prefer-dist \ + `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` + +FROM ubuntu:18.04 +LABEL maintainer="team@appwrite.io" + +ARG VERSION=dev + +ENV TZ=Asia/Tel_Aviv \ + DEBIAN_FRONTEND=noninteractive \ + PHP_VERSION=7.4 \ + _APP_SERVER=nginx \ + _APP_ENV=production \ + _APP_DOMAIN=localhost \ + _APP_DOMAIN_TARGET=localhost \ + _APP_HOME=https://appwrite.io \ + _APP_EDITION=community \ + _APP_OPTIONS_ABUSE=enabled \ + _APP_OPTIONS_FORCE_HTTPS=disabled \ + _APP_OPENSSL_KEY_V1=your-secret-key \ + _APP_STORAGE_LIMIT=100000000 \ + _APP_STORAGE_ANTIVIRUS=enabled \ + _APP_REDIS_HOST=redis \ + _APP_REDIS_PORT=6379 \ + _APP_DB_HOST=mariadb \ + _APP_DB_PORT=3306 \ + _APP_DB_USER=root \ + _APP_DB_PASS=password \ + _APP_DB_SCHEMA=appwrite \ + _APP_INFLUXDB_HOST=influxdb \ + _APP_INFLUXDB_PORT=8086 \ + _APP_STATSD_HOST=telegraf \ + _APP_STATSD_PORT=8125 \ + _APP_SMTP_HOST=smtp \ + _APP_SMTP_PORT=25 \ + _APP_SETUP=self-hosted \ + _APP_VERSION=$VERSION +#ENV _APP_SMTP_SECURE '' +#ENV _APP_SMTP_USERNAME '' +#ENV _APP_SMTP_PASSWORD '' + +COPY --from=builder /phpredis-5.2.1/modules/redis.so /usr/lib/php/20190902/ +COPY --from=builder /phpredis-5.2.1/modules/redis.so /usr/lib/php/20190902/ +COPY --from=builder /ngx_brotli /ngx_brotli + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN \ + apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests wget ca-certificates software-properties-common build-essential libpcre3-dev zlib1g-dev libssl-dev openssl gnupg htop supervisor && \ + LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php && \ + add-apt-repository universe && \ + add-apt-repository ppa:certbot/certbot && \ + apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests php$PHP_VERSION php$PHP_VERSION-fpm \ + php$PHP_VERSION-mysqlnd php$PHP_VERSION-curl php$PHP_VERSION-imagick php$PHP_VERSION-mbstring php$PHP_VERSION-dom certbot && \ + # Nginx + wget http://nginx.org/download/nginx-1.19.0.tar.gz && \ + tar -xzvf nginx-1.19.0.tar.gz && rm nginx-1.19.0.tar.gz && \ + cd nginx-1.19.0 && \ + ./configure --prefix=/usr/share/nginx \ + --sbin-path=/usr/sbin/nginx \ + --modules-path=/usr/lib/nginx/modules \ + --conf-path=/etc/nginx/nginx.conf \ + --error-log-path=/var/log/nginx/error.log \ + --http-log-path=/var/log/nginx/access.log \ + --pid-path=/run/nginx.pid \ + --lock-path=/var/lock/nginx.lock \ + --user=www-data \ + --group=www-data \ + --build=Ubuntu \ + --with-http_gzip_static_module \ + --with-http_ssl_module \ + --with-http_v2_module \ + --add-module=/ngx_brotli && \ + make && \ + make install && \ + rm -rf ../nginx-1.19.0 && \ + # Redis Extension + echo extension=redis.so >> /etc/php/$PHP_VERSION/fpm/conf.d/redis.ini && \ + echo extension=redis.so >> /etc/php/$PHP_VERSION/cli/conf.d/redis.ini && \ + # Cleanup + cd ../ && \ + apt-get purge -y --auto-remove wget software-properties-common build-essential libpcre3-dev zlib1g-dev libssl-dev gnupg && \ + apt-get clean && \ + rm -rf /ngx_brotli && \ + rm -rf /var/lib/apt/lists/* + +# Set Upload Limit (default to 100MB) +RUN echo "upload_max_filesize = ${_APP_STORAGE_LIMIT}" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini +RUN echo "post_max_size = ${_APP_STORAGE_LIMIT}" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini +RUN echo "opcache.preload_user=www-data" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini +RUN echo "opcache.preload=/usr/src/code/app/preload.php" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini +RUN echo "opcache.enable_cli = 1" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini + +# Add logs file +RUN echo "" >> /var/log/appwrite.log + +# Nginx Configuration (with self-signed ssl certificates) +COPY ./docker/nginx.conf.template /etc/nginx/nginx.conf.template +COPY ./docker/ssl/cert.pem /etc/nginx/ssl/cert.pem +COPY ./docker/ssl/key.pem /etc/nginx/ssl/key.pem + +# PHP Configuration +RUN mkdir -p /var/run/php +COPY ./docker/www.conf /etc/php/$PHP_VERSION/fpm/pool.d/www.conf + +# Add PHP Source Code +COPY ./app /usr/src/code/app +COPY ./bin /usr/local/bin +COPY ./docs /usr/src/code/docs +COPY ./public /usr/src/code/public +COPY ./src /usr/src/code/src +COPY --from=builder /usr/local/src/vendor /usr/src/code/vendor + +RUN mkdir -p /storage/uploads && \ + mkdir -p /storage/cache && \ + mkdir -p /storage/config && \ + mkdir -p /storage/certificates && \ + mkdir -p /storage/functions && \ + chown -Rf www-data.www-data /storage/uploads && chmod -Rf 0755 /storage/uploads && \ + chown -Rf www-data.www-data /storage/cache && chmod -Rf 0755 /storage/cache && \ + chown -Rf www-data.www-data /storage/config && chmod -Rf 0755 /storage/config && \ + chown -Rf www-data.www-data /storage/certificates && chmod -Rf 0755 /storage/certificates && \ + chown -Rf www-data.www-data /storage/functions && chmod -Rf 0755 /storage/functions + +# Supervisord Conf +COPY ./docker/supervisord.conf /etc/supervisord.conf + +# Executables +RUN chmod +x /usr/local/bin/start +RUN chmod +x /usr/local/bin/doctor +RUN chmod +x /usr/local/bin/migrate +RUN chmod +x /usr/local/bin/test + +# Letsencrypt Permissions +RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/ + +EXPOSE 80 + +WORKDIR /usr/src/code + +CMD ["/bin/bash", "/usr/local/bin/start"] diff --git a/Dockerfile.php8 b/Dockerfile.php8 new file mode 100755 index 0000000000..2cc74ef76d --- /dev/null +++ b/Dockerfile.php8 @@ -0,0 +1,178 @@ +FROM composer:2.0 as step0 + +ARG TESTING=false +ENV TESTING=$TESTING + +WORKDIR /usr/local/src/ + +COPY composer.lock /usr/local/src/ +COPY composer.json /usr/local/src/ + +RUN composer update --ignore-platform-reqs --optimize-autoloader \ + --no-plugins --no-scripts --prefer-dist \ + `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` + +FROM php:8.0-rc as step1 + +ENV TZ=Asia/Tel_Aviv \ + DEBIAN_FRONTEND=noninteractive \ + PHP_REDIS_VERSION=5.3.0 \ + PHP_SWOOLE_VERSION=4.5.2 \ + PHP_XDEBUG_VERSION=sdebug_2_9-beta + +RUN \ + apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests ca-certificates software-properties-common wget git openssl make zip unzip libbrotli-dev libz-dev + +RUN docker-php-ext-install sockets + +RUN \ + # Redis Extension + wget -q https://github.com/phpredis/phpredis/archive/$PHP_REDIS_VERSION.tar.gz && \ + tar -xf $PHP_REDIS_VERSION.tar.gz && \ + cd phpredis-$PHP_REDIS_VERSION && \ + phpize && \ + ./configure && \ + make && make install && \ + cd .. && \ + ## Swoole Extension + git clone https://github.com/swoole/swoole-src.git && \ + cd swoole-src && \ + #git checkout v$PHP_SWOOLE_VERSION && \ + phpize && \ + ./configure --enable-sockets --enable-http2 && \ + make && make install && \ + cd .. + ## XDebug Extension + # git clone https://github.com/swoole/sdebug.git && \ + # cd sdebug && \ + # git checkout $PHP_XDEBUG_VERSION && \ + # phpize && \ + # ./configure --enable-xdebug && \ + # make clean && make && make install + # cd .. && \ + # Meminfo Extension + # git clone https://github.com/BitOne/php-meminfo.git && \ + # cd php-meminfo && \ + # git checkout v1.0.5 && \ + # cd extension/php7 && \ + # phpize && \ + # ./configure --enable-meminfo && \ + # make && make install + +FROM php:8.0-rc as final + +LABEL maintainer="team@appwrite.io" + +ARG VERSION=dev + +ENV TZ=Asia/Tel_Aviv \ + DEBIAN_FRONTEND=noninteractive \ + _APP_SERVER=swoole \ + _APP_ENV=production \ + _APP_DOMAIN=localhost \ + _APP_DOMAIN_TARGET=localhost \ + _APP_HOME=https://appwrite.io \ + _APP_EDITION=community \ + _APP_OPTIONS_ABUSE=enabled \ + _APP_OPTIONS_FORCE_HTTPS=disabled \ + _APP_OPENSSL_KEY_V1=your-secret-key \ + _APP_STORAGE_LIMIT=100000000 \ + _APP_STORAGE_ANTIVIRUS=enabled \ + _APP_REDIS_HOST=redis \ + _APP_REDIS_PORT=6379 \ + _APP_DB_HOST=mariadb \ + _APP_DB_PORT=3306 \ + _APP_DB_USER=root \ + _APP_DB_PASS=password \ + _APP_DB_SCHEMA=appwrite \ + _APP_INFLUXDB_HOST=influxdb \ + _APP_INFLUXDB_PORT=8086 \ + _APP_STATSD_HOST=telegraf \ + _APP_STATSD_PORT=8125 \ + _APP_SMTP_HOST=smtp \ + _APP_SMTP_PORT=25 \ + _APP_SETUP=self-hosted \ + _APP_VERSION=$VERSION +#ENV _APP_SMTP_SECURE '' +#ENV _APP_SMTP_USERNAME '' +#ENV _APP_SMTP_PASSWORD '' + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN \ + apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests webp certbot htop procps \ + libonig-dev libcurl4-gnutls-dev libmagickwand-dev libyaml-dev libbrotli-dev libz-dev + # pecl install imagick yaml && \ + # docker-php-ext-enable imagick yaml + +RUN docker-php-ext-install sockets curl opcache pdo pdo_mysql + +WORKDIR /usr/src/code + +COPY --from=step0 /usr/local/src/vendor /usr/src/code/vendor +COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190902/swoole.so /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ +COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190902/redis.so /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ +# COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190902/xdebug.so /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ +# COPY --from=step1 /usr/local/lib/php/extensions/no-debug-non-zts-20190902/meminfo.so /usr/local/lib/php/extensions/no-debug-non-zts-20190902/ + +# Add Source Code +COPY ./app /usr/src/code/app +COPY ./bin /usr/local/bin +COPY ./docs /usr/src/code/docs +COPY ./public /usr/src/code/public +COPY ./src /usr/src/code/src + +# Set Volumes +RUN mkdir -p /storage/uploads && \ + mkdir -p /storage/cache && \ + mkdir -p /storage/config && \ + mkdir -p /storage/certificates && \ + mkdir -p /storage/functions && \ + mkdir -p /storage/debug && \ + chown -Rf www-data.www-data /storage/uploads && chmod -Rf 0755 /storage/uploads && \ + chown -Rf www-data.www-data /storage/cache && chmod -Rf 0755 /storage/cache && \ + chown -Rf www-data.www-data /storage/config && chmod -Rf 0755 /storage/config && \ + chown -Rf www-data.www-data /storage/certificates && chmod -Rf 0755 /storage/certificates && \ + chown -Rf www-data.www-data /storage/functions && chmod -Rf 0755 /storage/functions && \ + chown -Rf www-data.www-data /storage/debug && chmod -Rf 0755 /storage/debug + +# Executables +RUN chmod +x /usr/local/bin/doctor +RUN chmod +x /usr/local/bin/migrate +RUN chmod +x /usr/local/bin/schedule +RUN chmod +x /usr/local/bin/test +RUN chmod +x /usr/local/bin/worker-audits +RUN chmod +x /usr/local/bin/worker-certificates +RUN chmod +x /usr/local/bin/worker-deletes +RUN chmod +x /usr/local/bin/worker-functions +RUN chmod +x /usr/local/bin/worker-mails +RUN chmod +x /usr/local/bin/worker-tasks +RUN chmod +x /usr/local/bin/worker-usage +RUN chmod +x /usr/local/bin/worker-webhooks + +# Letsencrypt Permissions +RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/ + +# Enable Extensions +RUN echo extension=swoole.so >> /usr/local/etc/php/conf.d/swoole.ini +RUN echo extension=redis.so >> /usr/local/etc/php/conf.d/redis.ini +# RUN echo zend_extension=xdebug.so >> /usr/local/etc/php/conf.d/xdebug.ini +# RUN echo extension=meminfo.so >> /usr/local/etc/php/conf.d/meminfo.ini + +RUN echo "opcache.preload_user=www-data" >> /usr/local/etc/php/conf.d/appwrite.ini +RUN echo "opcache.preload=/usr/src/code/app/preload.php" >> /usr/local/etc/php/conf.d/appwrite.ini +RUN echo "opcache.enable_cli = 1" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.profiler_enable = 1" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.profiler_output_dir = /tmp/" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.profiler_enable_trigger = 1" >> /usr/local/etc/php/conf.d/appwrite.ini +# RUN echo "xdebug.trace_format = 1" >> /usr/local/etc/php/conf.d/appwrite.ini + +EXPOSE 80 + +#, "-dxdebug.auto_trace=1" +#, "-dxdebug.profiler_enable=1" +#, "-dopcache.preload=opcache.preload=/usr/src/code/app/preload.php" + +CMD [ "php", "app/server.php", "-dopcache.preload=opcache.preload=/usr/src/code/app/preload.php" ] \ No newline at end of file diff --git a/README.md b/README.md index 85626c8d20..a48e764fcf 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,8 @@ The easiest way to start running your Appwrite server is by running our docker-c ```bash docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ - --volume "$(pwd)"/appwrite:/install/appwrite:rw \ - -e version=0.6.2 \ - appwrite/install + --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ + appwrite/appwrite:0.7.0 ``` ### Windows @@ -62,9 +61,8 @@ docker run -it --rm \ ```cmd docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ - --volume "%cd%"/appwrite:/install/appwrite:rw ^ - -e version=0.6.2 ^ - appwrite/install + --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ + appwrite/appwrite:0.7.0 ``` #### PowerShell @@ -72,9 +70,8 @@ docker run -it --rm ^ ```powershell docker run -it --rm , --volume /var/run/docker.sock:/var/run/docker.sock , - --volume ${pwd}/appwrite:/install/appwrite:rw , - -e version=0.6.2 , - appwrite/install + --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw , + appwrite/appwrite:0.7.0 ``` Once the Docker installation completes, go to http://localhost to access the Appwrite console from your browser. Please note that on non-linux native hosts, the server might take a few minutes to start after installation completes. @@ -99,6 +96,7 @@ Getting started with Appwrite is as easy as creating a new project, choosing you * [**Teams**](https://appwrite.io/docs/client/teams) - Manage and group users in teams. Manage memberships, invites, and user roles within a team. * [**Database**](https://appwrite.io/docs/client/database) - Manage database collections and documents. Read, create, update, and delete documents and filter lists of documents collections using an advanced filter with graph-like capabilities. * [**Storage**](https://appwrite.io/docs/client/storage) - Manage storage files. Read, create, delete, and preview files. Manipulate the preview of your files to fit your app perfectly. All files are scanned by ClamAV and stored in a secure and encrypted way. +* [**Functions**](https://appwrite.io/docs/server/functions) - Customize your Appwrite server by executing your custom code in an isolate environment, you can trigger your code on any Appwrite system event, manually or using a CRON schedule. * [**Locale**](https://appwrite.io/docs/client/locale) - Track your user's location, and manage your app locale-based data. * [**Avatars**](https://appwrite.io/docs/client/avatars) - Manage your users' avatars, countries' flags, browser icons, credit card symbols, and generate QR codes. diff --git a/app/cli.php b/app/cli.php new file mode 100644 index 0000000000..9eede27a34 --- /dev/null +++ b/app/cli.php @@ -0,0 +1,25 @@ +#!/bin/env php +task('version') + ->desc('Get the server version') + ->action(function () { + Console::log(App::getEnv('_APP_VERSION', 'UNKNOWN')); + }); + +$cli->run(); \ No newline at end of file diff --git a/app/config/collections.php b/app/config/collections.php index f40488950e..5720b66d4f 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1,7 +1,6 @@ 'Localhost', 'type' => 'web', 'hostname' => 'localhost', - ], - [ - '$collection' => Database::SYSTEM_COLLECTION_PLATFORMS, - 'name' => 'Current Host', - 'type' => 'web', - 'hostname' => \parse_url('https://'.$request->getServer('HTTP_HOST'), PHP_URL_HOST), - ], + ], // Current host is added on app init ], 'legalName' => '', 'legalCountry' => '', @@ -50,9 +43,9 @@ $collections = [ 'legalCity' => '', 'legalAddress' => '', 'legalTaxId' => '', - 'authWhitelistEmails' => (!empty($request->getServer('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', $request->getServer('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], - 'authWhitelistIPs' => (!empty($request->getServer('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', $request->getServer('_APP_CONSOLE_WHITELIST_IPS', null)) : [], - 'authWhitelistDomains' => (!empty($request->getServer('_APP_CONSOLE_WHITELIST_DOMAINS', null))) ? \explode(',', $request->getServer('_APP_CONSOLE_WHITELIST_DOMAINS', null)) : [], + 'authWhitelistEmails' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], + 'authWhitelistIPs' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [], + 'authWhitelistDomains' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_DOMAINS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_DOMAINS', null)) : [], ], Database::SYSTEM_COLLECTION_COLLECTIONS => [ '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, @@ -65,7 +58,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Name', 'key' => 'name', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -74,7 +67,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Date Created', 'key' => 'dateCreated', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => false, 'array' => false, @@ -83,7 +76,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Date Updated', 'key' => 'dateUpdated', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => false, 'array' => false, @@ -92,7 +85,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Structure', 'key' => 'structure', - 'type' => 'boolean', + 'type' => Database::SYSTEM_VAR_TYPE_BOOLEAN, 'default' => false, 'required' => true, 'array' => false, @@ -101,7 +94,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Rules', 'key' => 'rules', - 'type' => 'document', + 'type' => Database::SYSTEM_VAR_TYPE_DOCUMENT, 'default' => [], 'required' => true, 'array' => true, @@ -120,7 +113,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Label', 'key' => 'label', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -129,7 +122,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Key', 'key' => 'key', - 'type' => 'key', + 'type' => Database::SYSTEM_VAR_TYPE_KEY, 'default' => '', 'required' => true, 'array' => false, @@ -138,7 +131,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Type', 'key' => 'type', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -147,7 +140,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Default', 'key' => 'default', - 'type' => 'wildcard', + 'type' => Database::SYSTEM_VAR_TYPE_WILDCARD, 'default' => '', 'required' => false, 'array' => false, @@ -156,7 +149,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Required', 'key' => 'required', - 'type' => 'boolean', + 'type' => Database::SYSTEM_VAR_TYPE_BOOLEAN, 'default' => true, 'required' => true, 'array' => false, @@ -165,7 +158,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Array', 'key' => 'array', - 'type' => 'boolean', + 'type' => Database::SYSTEM_VAR_TYPE_BOOLEAN, 'default' => true, 'required' => true, 'array' => false, @@ -174,7 +167,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'list', 'key' => 'list', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, //'default' => '', 'required' => false, 'array' => true, @@ -192,7 +185,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Name', 'key' => 'name', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -201,7 +194,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Email', 'key' => 'email', - 'type' => 'email', + 'type' => Database::SYSTEM_VAR_TYPE_EMAIL, 'default' => '', 'required' => true, 'array' => false, @@ -210,7 +203,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Status', 'key' => 'status', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => true, 'array' => false, @@ -219,7 +212,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Password', 'key' => 'password', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -228,7 +221,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Password Update Date', 'key' => 'password-update', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => true, 'array' => false, @@ -237,7 +230,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Prefs', 'key' => 'prefs', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -246,7 +239,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Registration Date', 'key' => 'registration', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => true, 'array' => false, @@ -255,7 +248,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Email Verification Status', 'key' => 'emailVerification', - 'type' => 'boolean', + 'type' => Database::SYSTEM_VAR_TYPE_BOOLEAN, 'default' => '', 'required' => true, 'array' => false, @@ -264,7 +257,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Reset', 'key' => 'reset', - 'type' => 'boolean', + 'type' => Database::SYSTEM_VAR_TYPE_BOOLEAN, 'default' => '', 'required' => true, 'array' => false, @@ -273,7 +266,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Tokens', 'key' => 'tokens', - 'type' => 'document', + 'type' => Database::SYSTEM_VAR_TYPE_DOCUMENT, 'default' => [], 'required' => false, 'array' => true, @@ -283,7 +276,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Memberships', 'key' => 'memberships', - 'type' => 'document', + 'type' => Database::SYSTEM_VAR_TYPE_DOCUMENT, 'default' => [], 'required' => false, 'array' => true, @@ -302,7 +295,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Type', 'key' => 'type', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => null, 'required' => true, 'array' => false, @@ -311,7 +304,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Secret', 'key' => 'secret', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -320,7 +313,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Expire', 'key' => 'expire', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => true, 'array' => false, @@ -329,7 +322,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'User Agent', 'key' => 'userAgent', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -338,7 +331,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'IP', 'key' => 'ip', - 'type' => 'ip', + 'type' => Database::SYSTEM_VAR_TYPE_IP, 'default' => '', 'required' => true, 'array' => false, @@ -356,7 +349,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Team ID', 'key' => 'teamId', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => true, 'array' => false, @@ -365,7 +358,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'User ID', 'key' => 'userId', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => true, 'array' => false, @@ -374,7 +367,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Roles', 'key' => 'roles', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => true, @@ -383,7 +376,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Invited', 'key' => 'invited', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => false, //FIXME SHOULD BE REQUIRED 'array' => false, @@ -392,7 +385,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Joined', 'key' => 'joined', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => true, 'array' => false, @@ -401,7 +394,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Confirm', 'key' => 'confirm', - 'type' => 'boolean', + 'type' => Database::SYSTEM_VAR_TYPE_BOOLEAN, 'default' => '', 'required' => true, 'array' => false, @@ -410,7 +403,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Secret', 'key' => 'secret', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -428,7 +421,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Name', 'key' => 'name', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -437,7 +430,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Date Created', 'key' => 'dateCreated', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => false, 'array' => false, @@ -446,7 +439,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Sum', 'key' => 'sum', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => false, 'array' => false, @@ -464,7 +457,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Name', 'key' => 'name', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => true, 'array' => false, @@ -473,7 +466,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Description', 'key' => 'description', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => false, 'array' => false, @@ -482,7 +475,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Logo', 'key' => 'logo', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, ], @@ -490,7 +483,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'URL', 'key' => 'url', - 'type' => 'url', + 'type' => Database::SYSTEM_VAR_TYPE_URL, 'default' => '', 'required' => false, ], @@ -498,7 +491,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Team ID', 'key' => 'teamId', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => 0, 'required' => true, 'array' => false, @@ -507,7 +500,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Legal Name', 'key' => 'legalName', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, ], @@ -515,7 +508,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Legal Country', 'key' => 'legalCountry', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, ], @@ -523,7 +516,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Legal State', 'key' => 'legalState', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, ], @@ -531,7 +524,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Legal City', 'key' => 'legalCity', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, ], @@ -539,7 +532,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Legal Address', 'key' => 'legalAddress', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, ], @@ -547,7 +540,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Legal Tax ID', 'key' => 'legalTaxId', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, ], @@ -555,7 +548,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Webhooks', 'key' => 'webhooks', - 'type' => 'document', + 'type' => Database::SYSTEM_VAR_TYPE_DOCUMENT, 'default' => [], 'required' => false, 'array' => true, @@ -565,7 +558,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'API Keys', 'key' => 'keys', - 'type' => 'document', + 'type' => Database::SYSTEM_VAR_TYPE_DOCUMENT, 'default' => [], 'required' => false, 'array' => true, @@ -575,7 +568,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Tasks', 'key' => 'tasks', - 'type' => 'document', + 'type' => Database::SYSTEM_VAR_TYPE_DOCUMENT, 'default' => [], 'required' => false, 'array' => true, @@ -585,7 +578,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Platforms', 'key' => 'platforms', - 'type' => 'document', + 'type' => Database::SYSTEM_VAR_TYPE_DOCUMENT, 'default' => [], 'required' => false, 'array' => true, @@ -595,7 +588,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Domains', 'key' => 'domains', - 'type' => 'document', + 'type' => Database::SYSTEM_VAR_TYPE_DOCUMENT, 'default' => [], 'required' => false, 'array' => true, @@ -614,7 +607,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Name', 'key' => 'name', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => true, 'array' => false, @@ -623,7 +616,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Events', 'key' => 'events', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => false, 'array' => true, @@ -632,7 +625,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'URL', 'key' => 'url', - 'type' => 'url', + 'type' => Database::SYSTEM_VAR_TYPE_URL, 'default' => '', 'required' => false, 'array' => false, @@ -641,7 +634,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Security', 'key' => 'security', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_BOOLEAN, 'default' => '', 'required' => false, 'array' => false, @@ -650,7 +643,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'HTTP User', 'key' => 'httpUser', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -659,7 +652,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'HTTP Password', 'key' => 'httpPass', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -677,7 +670,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Name', 'key' => 'name', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => true, 'array' => false, @@ -686,7 +679,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Scopes', 'key' => 'scopes', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => false, 'array' => true, @@ -695,7 +688,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Secret', 'key' => 'secret', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, ], @@ -712,7 +705,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Name', 'key' => 'name', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => true, 'array' => false, @@ -721,7 +714,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Status', 'key' => 'status', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -730,7 +723,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Updated', 'key' => 'updated', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => false, 'array' => false, @@ -739,7 +732,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Schedule', 'key' => 'schedule', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -748,7 +741,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Previous', 'key' => 'previous', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => false, 'array' => false, @@ -757,7 +750,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Next', 'key' => 'next', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => false, 'array' => false, @@ -766,7 +759,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Duration', 'key' => 'duration', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => false, 'array' => false, @@ -775,7 +768,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Delay', 'key' => 'delay', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => false, 'array' => false, @@ -784,7 +777,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Security', 'key' => 'security', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_BOOLEAN, 'default' => '', 'required' => false, 'array' => false, @@ -793,7 +786,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'HTTP Method', 'key' => 'httpMethod', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -802,7 +795,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'HTTP URL', 'key' => 'httpUrl', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -811,7 +804,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'HTTP Headers', 'key' => 'httpHeaders', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => false, 'array' => true, @@ -820,7 +813,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'HTTP User', 'key' => 'httpUser', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -829,7 +822,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'HTTP Password', 'key' => 'httpPass', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -838,7 +831,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Log', 'key' => 'log', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -847,7 +840,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Failures', 'key' => 'failures', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => false, 'array' => false, @@ -865,7 +858,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Type', 'key' => 'type', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => true, 'array' => false, @@ -874,7 +867,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Name', 'key' => 'name', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => true, 'array' => false, @@ -883,7 +876,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Date Created', 'key' => 'dateCreated', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => false, 'array' => false, @@ -892,7 +885,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Date Updated', 'key' => 'dateUpdated', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => false, 'array' => false, @@ -901,7 +894,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Key', 'key' => 'key', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -910,7 +903,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Store', 'key' => 'store', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -919,7 +912,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Hostname', 'key' => 'hostname', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -937,7 +930,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Domain', 'key' => 'domain', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => true, 'array' => false, @@ -946,7 +939,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Updated', 'key' => 'updated', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => false, 'array' => false, @@ -955,7 +948,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Top Level Domain', 'key' => 'tld', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -964,7 +957,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Registerable Domain', 'key' => 'registerable', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -973,7 +966,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Verification', 'key' => 'verification', - 'type' => 'boolean', + 'type' => Database::SYSTEM_VAR_TYPE_BOOLEAN, 'default' => false, 'required' => true, 'array' => false, @@ -982,7 +975,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Certificate ID', 'key' => 'certificateId', - 'type' => 'key', + 'type' => Database::SYSTEM_VAR_TYPE_KEY, 'default' => '', 'required' => false, 'array' => false, @@ -1000,7 +993,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Domain', 'key' => 'domain', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, 'required' => true, 'array' => false, @@ -1009,7 +1002,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Issue Date', 'key' => 'issueDate', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => false, 'array' => false, @@ -1018,7 +1011,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Renew Date', 'key' => 'renewDate', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => false, 'array' => false, @@ -1027,7 +1020,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Attempts', 'key' => 'attempts', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => false, 'array' => false, @@ -1036,7 +1029,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Log', 'key' => 'log', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -1054,7 +1047,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Date Created', 'key' => 'dateCreated', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => false, 'array' => false, @@ -1063,7 +1056,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Folder ID', 'key' => 'folderId', - 'type' => 'key', + 'type' => Database::SYSTEM_VAR_TYPE_KEY, 'default' => 0, 'required' => false, 'array' => false, @@ -1072,7 +1065,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Name', 'key' => 'name', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -1081,7 +1074,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Path', 'key' => 'path', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -1090,7 +1083,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Signature', 'key' => 'signature', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -1099,7 +1092,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Mime Type', 'key' => 'mimeType', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -1108,7 +1101,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Original Size', 'key' => 'sizeOriginal', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => true, 'array' => false, @@ -1117,7 +1110,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Actual Size', 'key' => 'sizeActual', - 'type' => 'numeric', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => 0, 'required' => true, 'array' => false, @@ -1126,7 +1119,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Algorithm', 'key' => 'algorithm', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -1135,7 +1128,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Token', 'key' => 'token', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => true, 'array' => false, @@ -1144,7 +1137,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Comment', 'key' => 'comment', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -1153,7 +1146,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'File OpenSSL Version', 'key' => 'fileOpenSSLVersion', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -1162,7 +1155,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'File OpenSSL Cipher', 'key' => 'fileOpenSSLCipher', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -1171,7 +1164,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'File OpenSSL Tag', 'key' => 'fileOpenSSLTag', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -1180,7 +1173,260 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'File OpenSSL IV', 'key' => 'fileOpenSSLIV', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + ], + ], + ], + Database::SYSTEM_COLLECTION_FUNCTIONS => [ + '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, + '$id' => Database::SYSTEM_COLLECTION_FUNCTIONS, + '$permissions' => ['read' => ['*']], + 'name' => 'Functions', + 'structure' => true, + 'rules' => [ + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Date Created', + 'key' => 'dateCreated', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'default' => 0, + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Date Updated', + 'key' => 'dateUpdated', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'default' => 0, + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Status', + 'key' => 'status', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Name', + 'key' => 'name', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Env', + 'key' => 'env', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Tag', + 'key' => 'tag', + 'type' => Database::SYSTEM_VAR_TYPE_KEY, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Vars', + 'key' => 'vars', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + 'filter' => ['json', 'encrypt'] + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Events', + 'key' => 'events', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => true, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Schedule', + 'key' => 'schedule', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Previous', + 'key' => 'previous', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Next', + 'key' => 'next', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Timeout', + 'key' => 'timeout', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'default' => '', + 'required' => false, + 'array' => false, + ], + ], + ], + Database::SYSTEM_COLLECTION_TAGS => [ + '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, + '$id' => Database::SYSTEM_COLLECTION_TAGS, + '$permissions' => ['read' => ['*']], + 'name' => 'Tags', + 'structure' => true, + 'rules' => [ + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Date Created', + 'key' => 'dateCreated', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'default' => 0, + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Function ID', + 'key' => 'functionId', + 'type' => Database::SYSTEM_VAR_TYPE_KEY, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Command', + 'key' => 'command', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Code Path', + 'key' => 'codePath', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Code Size', + 'key' => 'codeSize', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'default' => '', + 'required' => false, + 'array' => false, + ], + ], + ], + Database::SYSTEM_COLLECTION_EXECUTIONS => [ + '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, + '$id' => Database::SYSTEM_COLLECTION_EXECUTIONS, + '$permissions' => ['read' => ['*']], + 'name' => 'Executions', + 'structure' => true, + 'rules' => [ + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Date Created', + 'key' => 'dateCreated', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'default' => 0, + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Function ID', + 'key' => 'functionId', + 'type' => Database::SYSTEM_VAR_TYPE_KEY, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Tag ID', + 'key' => 'tagId', + 'type' => Database::SYSTEM_VAR_TYPE_KEY, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Status', + 'key' => 'status', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Exit Code', + 'key' => 'exitCode', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Stdout', + 'key' => 'stdout', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Stderr', + 'key' => 'stderr', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Time', + 'key' => 'time', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, 'default' => '', 'required' => false, 'array' => false, @@ -1208,7 +1454,7 @@ foreach ($providers as $index => $provider) { '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'OAuth2 '.\ucfirst($index).' ID', 'key' => 'usersOauth2'.\ucfirst($index).'Appid', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -1218,7 +1464,7 @@ foreach ($providers as $index => $provider) { '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'OAuth2 '.\ucfirst($index).' Secret', 'key' => 'usersOauth2'.\ucfirst($index).'Secret', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -1228,7 +1474,7 @@ foreach ($providers as $index => $provider) { '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'OAuth2 '.\ucfirst($index).' ID', 'key' => 'oauth2'.\ucfirst($index), - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, @@ -1238,7 +1484,7 @@ foreach ($providers as $index => $provider) { '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'OAuth2 '.\ucfirst($index).' Access Token', 'key' => 'oauth2'.\ucfirst($index).'AccessToken', - 'type' => 'text', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, 'array' => false, diff --git a/app/config/environments.php b/app/config/environments.php new file mode 100644 index 0000000000..cd312555a5 --- /dev/null +++ b/app/config/environments.php @@ -0,0 +1,46 @@ + [ + 'name' => 'Node.js', + 'version' => '14.5', + 'base' => 'node:14.5-alpine', + 'image' => 'appwrite/env-node:14.5', + 'logo' => 'node.png', + ], + 'php-7.4' => [ + 'name' => 'PHP', + 'version' => '7.4', + 'base' => 'php:7.4-cli-alpine', + 'image' => 'appwrite/env-php:7.4', + 'logo' => 'php.png', + ], + 'ruby-2.7' => [ + 'name' => 'Ruby', + 'version' => '2.7', + 'base' => 'ruby:2.7-alpine', + 'image' => 'appwrite/env-ruby:2.7', + 'logo' => 'ruby.png', + ], + 'python-3.8' => [ + 'name' => 'Python', + 'version' => '3.8', + 'base' => 'python:3.8-alpine', + 'image' => 'appwrite/env-python:3.8', + 'logo' => 'python.png', + ], + 'deno-1.2' => [ + 'name' => 'Deno', + 'version' => '1.2', + 'base' => 'hayd/deno:alpine-1.2.0', + 'image' => 'appwrite/env-deno:1.2', + 'logo' => 'deno.png', + ], + // 'dart-2.8' => [ + // 'name' => 'Dart', + // 'version' => '2.8', + // 'base' => 'google/dart:2.8', + // 'image' => 'appwrite/env-dart:2.8', + // 'logo' => 'dart.png', + // ], +]; \ No newline at end of file diff --git a/app/config/locales.php b/app/config/locale/codes.php similarity index 100% rename from app/config/locales.php rename to app/config/locale/codes.php diff --git a/app/config/currencies.php b/app/config/locale/currencies.php similarity index 100% rename from app/config/currencies.php rename to app/config/locale/currencies.php diff --git a/app/config/eu.php b/app/config/locale/eu.php similarity index 100% rename from app/config/eu.php rename to app/config/locale/eu.php diff --git a/app/config/languages.php b/app/config/locale/languages.php similarity index 100% rename from app/config/languages.php rename to app/config/locale/languages.php diff --git a/app/config/phones.php b/app/config/locale/phones.php similarity index 100% rename from app/config/phones.php rename to app/config/locale/phones.php diff --git a/app/config/locales/templates/_base.tpl b/app/config/locale/templates/email-base.tpl similarity index 100% rename from app/config/locales/templates/_base.tpl rename to app/config/locale/templates/email-base.tpl diff --git a/app/config/locales/templates/_cta.tpl b/app/config/locale/templates/email-cta.tpl similarity index 100% rename from app/config/locales/templates/_cta.tpl rename to app/config/locale/templates/email-cta.tpl diff --git a/app/config/locales/af.continents.php b/app/config/locale/translations/af.continents.php similarity index 100% rename from app/config/locales/af.continents.php rename to app/config/locale/translations/af.continents.php diff --git a/app/config/locales/af.countries.php b/app/config/locale/translations/af.countries.php similarity index 100% rename from app/config/locales/af.countries.php rename to app/config/locale/translations/af.countries.php diff --git a/app/config/locales/af.php b/app/config/locale/translations/af.php similarity index 100% rename from app/config/locales/af.php rename to app/config/locale/translations/af.php diff --git a/app/config/locales/ar.continents.php b/app/config/locale/translations/ar.continents.php similarity index 100% rename from app/config/locales/ar.continents.php rename to app/config/locale/translations/ar.continents.php diff --git a/app/config/locales/ar.countries.php b/app/config/locale/translations/ar.countries.php similarity index 100% rename from app/config/locales/ar.countries.php rename to app/config/locale/translations/ar.countries.php diff --git a/app/config/locales/ar.php b/app/config/locale/translations/ar.php similarity index 100% rename from app/config/locales/ar.php rename to app/config/locale/translations/ar.php diff --git a/app/config/locales/bn.continents.php b/app/config/locale/translations/bn.continents.php similarity index 100% rename from app/config/locales/bn.continents.php rename to app/config/locale/translations/bn.continents.php diff --git a/app/config/locales/bn.countries.php b/app/config/locale/translations/bn.countries.php similarity index 100% rename from app/config/locales/bn.countries.php rename to app/config/locale/translations/bn.countries.php diff --git a/app/config/locales/bn.php b/app/config/locale/translations/bn.php similarity index 100% rename from app/config/locales/bn.php rename to app/config/locale/translations/bn.php diff --git a/app/config/locales/cat.continents.php b/app/config/locale/translations/cat.continents.php similarity index 100% rename from app/config/locales/cat.continents.php rename to app/config/locale/translations/cat.continents.php diff --git a/app/config/locales/cat.countries.php b/app/config/locale/translations/cat.countries.php similarity index 100% rename from app/config/locales/cat.countries.php rename to app/config/locale/translations/cat.countries.php diff --git a/app/config/locales/cat.php b/app/config/locale/translations/cat.php similarity index 100% rename from app/config/locales/cat.php rename to app/config/locale/translations/cat.php diff --git a/app/config/locales/cz.continents.php b/app/config/locale/translations/cz.continents.php similarity index 100% rename from app/config/locales/cz.continents.php rename to app/config/locale/translations/cz.continents.php diff --git a/app/config/locales/cz.countries.php b/app/config/locale/translations/cz.countries.php similarity index 100% rename from app/config/locales/cz.countries.php rename to app/config/locale/translations/cz.countries.php diff --git a/app/config/locales/cz.php b/app/config/locale/translations/cz.php similarity index 100% rename from app/config/locales/cz.php rename to app/config/locale/translations/cz.php diff --git a/app/config/locales/de.continents.php b/app/config/locale/translations/de.continents.php similarity index 100% rename from app/config/locales/de.continents.php rename to app/config/locale/translations/de.continents.php diff --git a/app/config/locales/de.countries.php b/app/config/locale/translations/de.countries.php similarity index 100% rename from app/config/locales/de.countries.php rename to app/config/locale/translations/de.countries.php diff --git a/app/config/locales/de.php b/app/config/locale/translations/de.php similarity index 100% rename from app/config/locales/de.php rename to app/config/locale/translations/de.php diff --git a/app/config/locales/en.continents.php b/app/config/locale/translations/en.continents.php similarity index 100% rename from app/config/locales/en.continents.php rename to app/config/locale/translations/en.continents.php diff --git a/app/config/locales/en.countries.php b/app/config/locale/translations/en.countries.php similarity index 100% rename from app/config/locales/en.countries.php rename to app/config/locale/translations/en.countries.php diff --git a/app/config/locales/en.php b/app/config/locale/translations/en.php similarity index 100% rename from app/config/locales/en.php rename to app/config/locale/translations/en.php diff --git a/app/config/locales/es.continents.php b/app/config/locale/translations/es.continents.php similarity index 100% rename from app/config/locales/es.continents.php rename to app/config/locale/translations/es.continents.php diff --git a/app/config/locales/es.countries.php b/app/config/locale/translations/es.countries.php similarity index 100% rename from app/config/locales/es.countries.php rename to app/config/locale/translations/es.countries.php diff --git a/app/config/locales/es.php b/app/config/locale/translations/es.php similarity index 100% rename from app/config/locales/es.php rename to app/config/locale/translations/es.php diff --git a/app/config/locales/fi.continents.php b/app/config/locale/translations/fi.continents.php similarity index 100% rename from app/config/locales/fi.continents.php rename to app/config/locale/translations/fi.continents.php diff --git a/app/config/locales/fi.countries.php b/app/config/locale/translations/fi.countries.php similarity index 100% rename from app/config/locales/fi.countries.php rename to app/config/locale/translations/fi.countries.php diff --git a/app/config/locales/fi.php b/app/config/locale/translations/fi.php similarity index 100% rename from app/config/locales/fi.php rename to app/config/locale/translations/fi.php diff --git a/app/config/locales/fo.continents.php b/app/config/locale/translations/fo.continents.php similarity index 100% rename from app/config/locales/fo.continents.php rename to app/config/locale/translations/fo.continents.php diff --git a/app/config/locales/fo.countries.php b/app/config/locale/translations/fo.countries.php similarity index 100% rename from app/config/locales/fo.countries.php rename to app/config/locale/translations/fo.countries.php diff --git a/app/config/locales/fo.php b/app/config/locale/translations/fo.php similarity index 100% rename from app/config/locales/fo.php rename to app/config/locale/translations/fo.php diff --git a/app/config/locales/fr.continents.php b/app/config/locale/translations/fr.continents.php similarity index 100% rename from app/config/locales/fr.continents.php rename to app/config/locale/translations/fr.continents.php diff --git a/app/config/locales/fr.countries.php b/app/config/locale/translations/fr.countries.php similarity index 100% rename from app/config/locales/fr.countries.php rename to app/config/locale/translations/fr.countries.php diff --git a/app/config/locales/fr.php b/app/config/locale/translations/fr.php similarity index 100% rename from app/config/locales/fr.php rename to app/config/locale/translations/fr.php diff --git a/app/config/locales/gr.continents.php b/app/config/locale/translations/gr.continents.php similarity index 100% rename from app/config/locales/gr.continents.php rename to app/config/locale/translations/gr.continents.php diff --git a/app/config/locales/gr.countries.php b/app/config/locale/translations/gr.countries.php similarity index 100% rename from app/config/locales/gr.countries.php rename to app/config/locale/translations/gr.countries.php diff --git a/app/config/locales/gr.php b/app/config/locale/translations/gr.php similarity index 100% rename from app/config/locales/gr.php rename to app/config/locale/translations/gr.php diff --git a/app/config/locales/he.continents.php b/app/config/locale/translations/he.continents.php similarity index 100% rename from app/config/locales/he.continents.php rename to app/config/locale/translations/he.continents.php diff --git a/app/config/locales/he.countries.php b/app/config/locale/translations/he.countries.php similarity index 100% rename from app/config/locales/he.countries.php rename to app/config/locale/translations/he.countries.php diff --git a/app/config/locales/he.php b/app/config/locale/translations/he.php similarity index 100% rename from app/config/locales/he.php rename to app/config/locale/translations/he.php diff --git a/app/config/locales/hi.continents.php b/app/config/locale/translations/hi.continents.php similarity index 100% rename from app/config/locales/hi.continents.php rename to app/config/locale/translations/hi.continents.php diff --git a/app/config/locales/hi.countries.php b/app/config/locale/translations/hi.countries.php similarity index 100% rename from app/config/locales/hi.countries.php rename to app/config/locale/translations/hi.countries.php diff --git a/app/config/locales/hi.php b/app/config/locale/translations/hi.php similarity index 100% rename from app/config/locales/hi.php rename to app/config/locale/translations/hi.php diff --git a/app/config/locales/hu.continents.php b/app/config/locale/translations/hu.continents.php similarity index 100% rename from app/config/locales/hu.continents.php rename to app/config/locale/translations/hu.continents.php diff --git a/app/config/locales/hu.countries.php b/app/config/locale/translations/hu.countries.php similarity index 100% rename from app/config/locales/hu.countries.php rename to app/config/locale/translations/hu.countries.php diff --git a/app/config/locales/hu.php b/app/config/locale/translations/hu.php similarity index 100% rename from app/config/locales/hu.php rename to app/config/locale/translations/hu.php diff --git a/app/config/locales/hy.continents.php b/app/config/locale/translations/hy.continents.php similarity index 100% rename from app/config/locales/hy.continents.php rename to app/config/locale/translations/hy.continents.php diff --git a/app/config/locales/hy.countries.php b/app/config/locale/translations/hy.countries.php similarity index 100% rename from app/config/locales/hy.countries.php rename to app/config/locale/translations/hy.countries.php diff --git a/app/config/locales/hy.php b/app/config/locale/translations/hy.php similarity index 100% rename from app/config/locales/hy.php rename to app/config/locale/translations/hy.php diff --git a/app/config/locales/id.continents.php b/app/config/locale/translations/id.continents.php similarity index 100% rename from app/config/locales/id.continents.php rename to app/config/locale/translations/id.continents.php diff --git a/app/config/locales/id.countries.php b/app/config/locale/translations/id.countries.php similarity index 100% rename from app/config/locales/id.countries.php rename to app/config/locale/translations/id.countries.php diff --git a/app/config/locales/id.php b/app/config/locale/translations/id.php similarity index 100% rename from app/config/locales/id.php rename to app/config/locale/translations/id.php diff --git a/app/config/locales/is.continents.php b/app/config/locale/translations/is.continents.php similarity index 100% rename from app/config/locales/is.continents.php rename to app/config/locale/translations/is.continents.php diff --git a/app/config/locales/is.countries.php b/app/config/locale/translations/is.countries.php similarity index 100% rename from app/config/locales/is.countries.php rename to app/config/locale/translations/is.countries.php diff --git a/app/config/locales/is.php b/app/config/locale/translations/is.php similarity index 100% rename from app/config/locales/is.php rename to app/config/locale/translations/is.php diff --git a/app/config/locales/it.continents.php b/app/config/locale/translations/it.continents.php similarity index 100% rename from app/config/locales/it.continents.php rename to app/config/locale/translations/it.continents.php diff --git a/app/config/locales/it.countries.php b/app/config/locale/translations/it.countries.php similarity index 100% rename from app/config/locales/it.countries.php rename to app/config/locale/translations/it.countries.php diff --git a/app/config/locales/it.php b/app/config/locale/translations/it.php similarity index 100% rename from app/config/locales/it.php rename to app/config/locale/translations/it.php diff --git a/app/config/locales/ja.continents.php b/app/config/locale/translations/ja.continents.php similarity index 100% rename from app/config/locales/ja.continents.php rename to app/config/locale/translations/ja.continents.php diff --git a/app/config/locales/ja.countries.php b/app/config/locale/translations/ja.countries.php similarity index 100% rename from app/config/locales/ja.countries.php rename to app/config/locale/translations/ja.countries.php diff --git a/app/config/locales/ja.php b/app/config/locale/translations/ja.php similarity index 100% rename from app/config/locales/ja.php rename to app/config/locale/translations/ja.php diff --git a/app/config/locales/jv.continents.php b/app/config/locale/translations/jv.continents.php similarity index 100% rename from app/config/locales/jv.continents.php rename to app/config/locale/translations/jv.continents.php diff --git a/app/config/locales/jv.countries.php b/app/config/locale/translations/jv.countries.php similarity index 100% rename from app/config/locales/jv.countries.php rename to app/config/locale/translations/jv.countries.php diff --git a/app/config/locales/jv.php b/app/config/locale/translations/jv.php similarity index 100% rename from app/config/locales/jv.php rename to app/config/locale/translations/jv.php diff --git a/app/config/locales/km.continents.php b/app/config/locale/translations/km.continents.php similarity index 100% rename from app/config/locales/km.continents.php rename to app/config/locale/translations/km.continents.php diff --git a/app/config/locales/km.countries.php b/app/config/locale/translations/km.countries.php similarity index 100% rename from app/config/locales/km.countries.php rename to app/config/locale/translations/km.countries.php diff --git a/app/config/locales/km.php b/app/config/locale/translations/km.php similarity index 100% rename from app/config/locales/km.php rename to app/config/locale/translations/km.php diff --git a/app/config/locales/ko.continents.php b/app/config/locale/translations/ko.continents.php similarity index 100% rename from app/config/locales/ko.continents.php rename to app/config/locale/translations/ko.continents.php diff --git a/app/config/locales/ko.countries.php b/app/config/locale/translations/ko.countries.php similarity index 100% rename from app/config/locales/ko.countries.php rename to app/config/locale/translations/ko.countries.php diff --git a/app/config/locales/ko.php b/app/config/locale/translations/ko.php similarity index 100% rename from app/config/locales/ko.php rename to app/config/locale/translations/ko.php diff --git a/app/config/locales/lt.continents.php b/app/config/locale/translations/lt.continents.php similarity index 100% rename from app/config/locales/lt.continents.php rename to app/config/locale/translations/lt.continents.php diff --git a/app/config/locales/lt.countries.php b/app/config/locale/translations/lt.countries.php similarity index 100% rename from app/config/locales/lt.countries.php rename to app/config/locale/translations/lt.countries.php diff --git a/app/config/locales/lt.php b/app/config/locale/translations/lt.php similarity index 100% rename from app/config/locales/lt.php rename to app/config/locale/translations/lt.php diff --git a/app/config/locales/ml.continents.php b/app/config/locale/translations/ml.continents.php similarity index 100% rename from app/config/locales/ml.continents.php rename to app/config/locale/translations/ml.continents.php diff --git a/app/config/locales/ml.countries.php b/app/config/locale/translations/ml.countries.php similarity index 100% rename from app/config/locales/ml.countries.php rename to app/config/locale/translations/ml.countries.php diff --git a/app/config/locales/ml.php b/app/config/locale/translations/ml.php similarity index 100% rename from app/config/locales/ml.php rename to app/config/locale/translations/ml.php diff --git a/app/config/locales/ms.continents.php b/app/config/locale/translations/ms.continents.php similarity index 100% rename from app/config/locales/ms.continents.php rename to app/config/locale/translations/ms.continents.php diff --git a/app/config/locales/ms.countries.php b/app/config/locale/translations/ms.countries.php similarity index 100% rename from app/config/locales/ms.countries.php rename to app/config/locale/translations/ms.countries.php diff --git a/app/config/locales/ms.php b/app/config/locale/translations/ms.php similarity index 100% rename from app/config/locales/ms.php rename to app/config/locale/translations/ms.php diff --git a/app/config/locales/nl.continents.php b/app/config/locale/translations/nl.continents.php similarity index 100% rename from app/config/locales/nl.continents.php rename to app/config/locale/translations/nl.continents.php diff --git a/app/config/locales/nl.countries.php b/app/config/locale/translations/nl.countries.php similarity index 100% rename from app/config/locales/nl.countries.php rename to app/config/locale/translations/nl.countries.php diff --git a/app/config/locales/nl.php b/app/config/locale/translations/nl.php similarity index 100% rename from app/config/locales/nl.php rename to app/config/locale/translations/nl.php diff --git a/app/config/locales/no.continents.php b/app/config/locale/translations/no.continents.php similarity index 100% rename from app/config/locales/no.continents.php rename to app/config/locale/translations/no.continents.php diff --git a/app/config/locales/no.countries.php b/app/config/locale/translations/no.countries.php similarity index 100% rename from app/config/locales/no.countries.php rename to app/config/locale/translations/no.countries.php diff --git a/app/config/locales/no.php b/app/config/locale/translations/no.php similarity index 100% rename from app/config/locales/no.php rename to app/config/locale/translations/no.php diff --git a/app/config/locales/ph.continents.php b/app/config/locale/translations/ph.continents.php similarity index 100% rename from app/config/locales/ph.continents.php rename to app/config/locale/translations/ph.continents.php diff --git a/app/config/locales/ph.countries.php b/app/config/locale/translations/ph.countries.php similarity index 100% rename from app/config/locales/ph.countries.php rename to app/config/locale/translations/ph.countries.php diff --git a/app/config/locales/ph.php b/app/config/locale/translations/ph.php similarity index 100% rename from app/config/locales/ph.php rename to app/config/locale/translations/ph.php diff --git a/app/config/locales/pl.continents.php b/app/config/locale/translations/pl.continents.php similarity index 100% rename from app/config/locales/pl.continents.php rename to app/config/locale/translations/pl.continents.php diff --git a/app/config/locales/pl.countries.php b/app/config/locale/translations/pl.countries.php similarity index 100% rename from app/config/locales/pl.countries.php rename to app/config/locale/translations/pl.countries.php diff --git a/app/config/locales/pl.php b/app/config/locale/translations/pl.php similarity index 100% rename from app/config/locales/pl.php rename to app/config/locale/translations/pl.php diff --git a/app/config/locales/pt-br.continents.php b/app/config/locale/translations/pt-br.continents.php similarity index 100% rename from app/config/locales/pt-br.continents.php rename to app/config/locale/translations/pt-br.continents.php diff --git a/app/config/locales/pt-br.countries.php b/app/config/locale/translations/pt-br.countries.php similarity index 100% rename from app/config/locales/pt-br.countries.php rename to app/config/locale/translations/pt-br.countries.php diff --git a/app/config/locales/pt-br.php b/app/config/locale/translations/pt-br.php similarity index 100% rename from app/config/locales/pt-br.php rename to app/config/locale/translations/pt-br.php diff --git a/app/config/locales/pt-pt.continents.php b/app/config/locale/translations/pt-pt.continents.php similarity index 100% rename from app/config/locales/pt-pt.continents.php rename to app/config/locale/translations/pt-pt.continents.php diff --git a/app/config/locales/pt-pt.countries.php b/app/config/locale/translations/pt-pt.countries.php similarity index 100% rename from app/config/locales/pt-pt.countries.php rename to app/config/locale/translations/pt-pt.countries.php diff --git a/app/config/locales/pt-pt.php b/app/config/locale/translations/pt-pt.php similarity index 100% rename from app/config/locales/pt-pt.php rename to app/config/locale/translations/pt-pt.php diff --git a/app/config/locales/ro.continents.php b/app/config/locale/translations/ro.continents.php similarity index 100% rename from app/config/locales/ro.continents.php rename to app/config/locale/translations/ro.continents.php diff --git a/app/config/locales/ro.countries.php b/app/config/locale/translations/ro.countries.php similarity index 100% rename from app/config/locales/ro.countries.php rename to app/config/locale/translations/ro.countries.php diff --git a/app/config/locales/ro.php b/app/config/locale/translations/ro.php similarity index 100% rename from app/config/locales/ro.php rename to app/config/locale/translations/ro.php diff --git a/app/config/locales/ru.continents.php b/app/config/locale/translations/ru.continents.php similarity index 100% rename from app/config/locales/ru.continents.php rename to app/config/locale/translations/ru.continents.php diff --git a/app/config/locales/ru.countries.php b/app/config/locale/translations/ru.countries.php similarity index 100% rename from app/config/locales/ru.countries.php rename to app/config/locale/translations/ru.countries.php diff --git a/app/config/locales/ru.php b/app/config/locale/translations/ru.php similarity index 100% rename from app/config/locales/ru.php rename to app/config/locale/translations/ru.php diff --git a/app/config/locales/si.continents.php b/app/config/locale/translations/si.continents.php similarity index 100% rename from app/config/locales/si.continents.php rename to app/config/locale/translations/si.continents.php diff --git a/app/config/locales/si.countries.php b/app/config/locale/translations/si.countries.php similarity index 100% rename from app/config/locales/si.countries.php rename to app/config/locale/translations/si.countries.php diff --git a/app/config/locales/si.php b/app/config/locale/translations/si.php similarity index 100% rename from app/config/locales/si.php rename to app/config/locale/translations/si.php diff --git a/app/config/locales/sl.continents.php b/app/config/locale/translations/sl.continents.php similarity index 100% rename from app/config/locales/sl.continents.php rename to app/config/locale/translations/sl.continents.php diff --git a/app/config/locales/sl.countries.php b/app/config/locale/translations/sl.countries.php similarity index 100% rename from app/config/locales/sl.countries.php rename to app/config/locale/translations/sl.countries.php diff --git a/app/config/locales/sl.php b/app/config/locale/translations/sl.php similarity index 100% rename from app/config/locales/sl.php rename to app/config/locale/translations/sl.php diff --git a/app/config/locales/sq.continents.php b/app/config/locale/translations/sq.continents.php similarity index 100% rename from app/config/locales/sq.continents.php rename to app/config/locale/translations/sq.continents.php diff --git a/app/config/locales/sq.countries.php b/app/config/locale/translations/sq.countries.php similarity index 100% rename from app/config/locales/sq.countries.php rename to app/config/locale/translations/sq.countries.php diff --git a/app/config/locales/sq.php b/app/config/locale/translations/sq.php similarity index 100% rename from app/config/locales/sq.php rename to app/config/locale/translations/sq.php diff --git a/app/config/locales/sv.continents.php b/app/config/locale/translations/sv.continents.php similarity index 100% rename from app/config/locales/sv.continents.php rename to app/config/locale/translations/sv.continents.php diff --git a/app/config/locales/sv.countries.php b/app/config/locale/translations/sv.countries.php similarity index 100% rename from app/config/locales/sv.countries.php rename to app/config/locale/translations/sv.countries.php diff --git a/app/config/locales/sv.php b/app/config/locale/translations/sv.php similarity index 100% rename from app/config/locales/sv.php rename to app/config/locale/translations/sv.php diff --git a/app/config/locales/ta.continents.php b/app/config/locale/translations/ta.continents.php similarity index 100% rename from app/config/locales/ta.continents.php rename to app/config/locale/translations/ta.continents.php diff --git a/app/config/locales/ta.countries.php b/app/config/locale/translations/ta.countries.php similarity index 100% rename from app/config/locales/ta.countries.php rename to app/config/locale/translations/ta.countries.php diff --git a/app/config/locales/ta.php b/app/config/locale/translations/ta.php similarity index 100% rename from app/config/locales/ta.php rename to app/config/locale/translations/ta.php diff --git a/app/config/locales/templates/af.email.auth.confirm.tpl b/app/config/locale/translations/templates/af.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/af.email.auth.confirm.tpl rename to app/config/locale/translations/templates/af.email.auth.confirm.tpl diff --git a/app/config/locales/templates/af.email.auth.invitation.tpl b/app/config/locale/translations/templates/af.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/af.email.auth.invitation.tpl rename to app/config/locale/translations/templates/af.email.auth.invitation.tpl diff --git a/app/config/locales/templates/af.email.auth.recovery.tpl b/app/config/locale/translations/templates/af.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/af.email.auth.recovery.tpl rename to app/config/locale/translations/templates/af.email.auth.recovery.tpl diff --git a/app/config/locales/templates/alb.email.auth.confirm.tpl b/app/config/locale/translations/templates/alb.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/alb.email.auth.confirm.tpl rename to app/config/locale/translations/templates/alb.email.auth.confirm.tpl diff --git a/app/config/locales/templates/alb.email.auth.invitation.tpl b/app/config/locale/translations/templates/alb.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/alb.email.auth.invitation.tpl rename to app/config/locale/translations/templates/alb.email.auth.invitation.tpl diff --git a/app/config/locales/templates/alb.email.auth.recovery.tpl b/app/config/locale/translations/templates/alb.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/alb.email.auth.recovery.tpl rename to app/config/locale/translations/templates/alb.email.auth.recovery.tpl diff --git a/app/config/locales/templates/ar.email.auth.confirm.tpl b/app/config/locale/translations/templates/ar.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/ar.email.auth.confirm.tpl rename to app/config/locale/translations/templates/ar.email.auth.confirm.tpl diff --git a/app/config/locales/templates/ar.email.auth.invitation.tpl b/app/config/locale/translations/templates/ar.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/ar.email.auth.invitation.tpl rename to app/config/locale/translations/templates/ar.email.auth.invitation.tpl diff --git a/app/config/locales/templates/ar.email.auth.recovery.tpl b/app/config/locale/translations/templates/ar.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/ar.email.auth.recovery.tpl rename to app/config/locale/translations/templates/ar.email.auth.recovery.tpl diff --git a/app/config/locales/templates/bn.email.auth.confirm.tpl b/app/config/locale/translations/templates/bn.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/bn.email.auth.confirm.tpl rename to app/config/locale/translations/templates/bn.email.auth.confirm.tpl diff --git a/app/config/locales/templates/bn.email.auth.invitation.tpl b/app/config/locale/translations/templates/bn.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/bn.email.auth.invitation.tpl rename to app/config/locale/translations/templates/bn.email.auth.invitation.tpl diff --git a/app/config/locales/templates/bn.email.auth.recovery.tpl b/app/config/locale/translations/templates/bn.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/bn.email.auth.recovery.tpl rename to app/config/locale/translations/templates/bn.email.auth.recovery.tpl diff --git a/app/config/locales/templates/cat.email.auth.confirm.tpl b/app/config/locale/translations/templates/cat.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/cat.email.auth.confirm.tpl rename to app/config/locale/translations/templates/cat.email.auth.confirm.tpl diff --git a/app/config/locales/templates/cat.email.auth.invitation.tpl b/app/config/locale/translations/templates/cat.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/cat.email.auth.invitation.tpl rename to app/config/locale/translations/templates/cat.email.auth.invitation.tpl diff --git a/app/config/locales/templates/cat.email.auth.recovery.tpl b/app/config/locale/translations/templates/cat.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/cat.email.auth.recovery.tpl rename to app/config/locale/translations/templates/cat.email.auth.recovery.tpl diff --git a/app/config/locales/templates/cz.email.auth.confirm.tpl b/app/config/locale/translations/templates/cz.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/cz.email.auth.confirm.tpl rename to app/config/locale/translations/templates/cz.email.auth.confirm.tpl diff --git a/app/config/locales/templates/cz.email.auth.invitation.tpl b/app/config/locale/translations/templates/cz.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/cz.email.auth.invitation.tpl rename to app/config/locale/translations/templates/cz.email.auth.invitation.tpl diff --git a/app/config/locales/templates/cz.email.auth.recovery.tpl b/app/config/locale/translations/templates/cz.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/cz.email.auth.recovery.tpl rename to app/config/locale/translations/templates/cz.email.auth.recovery.tpl diff --git a/app/config/locales/templates/de.email.auth.confirm.tpl b/app/config/locale/translations/templates/de.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/de.email.auth.confirm.tpl rename to app/config/locale/translations/templates/de.email.auth.confirm.tpl diff --git a/app/config/locales/templates/de.email.auth.invitation.tpl b/app/config/locale/translations/templates/de.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/de.email.auth.invitation.tpl rename to app/config/locale/translations/templates/de.email.auth.invitation.tpl diff --git a/app/config/locales/templates/de.email.auth.recovery.tpl b/app/config/locale/translations/templates/de.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/de.email.auth.recovery.tpl rename to app/config/locale/translations/templates/de.email.auth.recovery.tpl diff --git a/app/config/locales/templates/en.email.auth.confirm.tpl b/app/config/locale/translations/templates/en.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/en.email.auth.confirm.tpl rename to app/config/locale/translations/templates/en.email.auth.confirm.tpl diff --git a/app/config/locales/templates/en.email.auth.invitation.tpl b/app/config/locale/translations/templates/en.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/en.email.auth.invitation.tpl rename to app/config/locale/translations/templates/en.email.auth.invitation.tpl diff --git a/app/config/locales/templates/en.email.auth.recovery.tpl b/app/config/locale/translations/templates/en.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/en.email.auth.recovery.tpl rename to app/config/locale/translations/templates/en.email.auth.recovery.tpl diff --git a/app/config/locales/templates/es.email.auth.confirm.tpl b/app/config/locale/translations/templates/es.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/es.email.auth.confirm.tpl rename to app/config/locale/translations/templates/es.email.auth.confirm.tpl diff --git a/app/config/locales/templates/es.email.auth.invitation.tpl b/app/config/locale/translations/templates/es.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/es.email.auth.invitation.tpl rename to app/config/locale/translations/templates/es.email.auth.invitation.tpl diff --git a/app/config/locales/templates/es.email.auth.recovery.tpl b/app/config/locale/translations/templates/es.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/es.email.auth.recovery.tpl rename to app/config/locale/translations/templates/es.email.auth.recovery.tpl diff --git a/app/config/locales/templates/fi.email.auth.confirm.tpl b/app/config/locale/translations/templates/fi.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/fi.email.auth.confirm.tpl rename to app/config/locale/translations/templates/fi.email.auth.confirm.tpl diff --git a/app/config/locales/templates/fi.email.auth.invitation.tpl b/app/config/locale/translations/templates/fi.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/fi.email.auth.invitation.tpl rename to app/config/locale/translations/templates/fi.email.auth.invitation.tpl diff --git a/app/config/locales/templates/fi.email.auth.recovery.tpl b/app/config/locale/translations/templates/fi.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/fi.email.auth.recovery.tpl rename to app/config/locale/translations/templates/fi.email.auth.recovery.tpl diff --git a/app/config/locales/templates/fo.email.auth.confirm.tpl b/app/config/locale/translations/templates/fo.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/fo.email.auth.confirm.tpl rename to app/config/locale/translations/templates/fo.email.auth.confirm.tpl diff --git a/app/config/locales/templates/fo.email.auth.invitation.tpl b/app/config/locale/translations/templates/fo.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/fo.email.auth.invitation.tpl rename to app/config/locale/translations/templates/fo.email.auth.invitation.tpl diff --git a/app/config/locales/templates/fo.email.auth.recovery.tpl b/app/config/locale/translations/templates/fo.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/fo.email.auth.recovery.tpl rename to app/config/locale/translations/templates/fo.email.auth.recovery.tpl diff --git a/app/config/locales/templates/fr.email.auth.confirm.tpl b/app/config/locale/translations/templates/fr.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/fr.email.auth.confirm.tpl rename to app/config/locale/translations/templates/fr.email.auth.confirm.tpl diff --git a/app/config/locales/templates/fr.email.auth.invitation.tpl b/app/config/locale/translations/templates/fr.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/fr.email.auth.invitation.tpl rename to app/config/locale/translations/templates/fr.email.auth.invitation.tpl diff --git a/app/config/locales/templates/fr.email.auth.recovery.tpl b/app/config/locale/translations/templates/fr.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/fr.email.auth.recovery.tpl rename to app/config/locale/translations/templates/fr.email.auth.recovery.tpl diff --git a/app/config/locales/templates/gr.email.auth.confirm.tpl b/app/config/locale/translations/templates/gr.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/gr.email.auth.confirm.tpl rename to app/config/locale/translations/templates/gr.email.auth.confirm.tpl diff --git a/app/config/locales/templates/gr.email.auth.invitation.tpl b/app/config/locale/translations/templates/gr.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/gr.email.auth.invitation.tpl rename to app/config/locale/translations/templates/gr.email.auth.invitation.tpl diff --git a/app/config/locales/templates/gr.email.auth.recovery.tpl b/app/config/locale/translations/templates/gr.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/gr.email.auth.recovery.tpl rename to app/config/locale/translations/templates/gr.email.auth.recovery.tpl diff --git a/app/config/locales/templates/he.email.auth.confirm.tpl b/app/config/locale/translations/templates/he.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/he.email.auth.confirm.tpl rename to app/config/locale/translations/templates/he.email.auth.confirm.tpl diff --git a/app/config/locales/templates/he.email.auth.invitation.tpl b/app/config/locale/translations/templates/he.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/he.email.auth.invitation.tpl rename to app/config/locale/translations/templates/he.email.auth.invitation.tpl diff --git a/app/config/locales/templates/he.email.auth.recovery.tpl b/app/config/locale/translations/templates/he.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/he.email.auth.recovery.tpl rename to app/config/locale/translations/templates/he.email.auth.recovery.tpl diff --git a/app/config/locales/templates/hi.email.auth.confirm.tpl b/app/config/locale/translations/templates/hi.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/hi.email.auth.confirm.tpl rename to app/config/locale/translations/templates/hi.email.auth.confirm.tpl diff --git a/app/config/locales/templates/hi.email.auth.invitation.tpl b/app/config/locale/translations/templates/hi.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/hi.email.auth.invitation.tpl rename to app/config/locale/translations/templates/hi.email.auth.invitation.tpl diff --git a/app/config/locales/templates/hi.email.auth.recovery.tpl b/app/config/locale/translations/templates/hi.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/hi.email.auth.recovery.tpl rename to app/config/locale/translations/templates/hi.email.auth.recovery.tpl diff --git a/app/config/locales/templates/hu.email.auth.confirm.tpl b/app/config/locale/translations/templates/hu.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/hu.email.auth.confirm.tpl rename to app/config/locale/translations/templates/hu.email.auth.confirm.tpl diff --git a/app/config/locales/templates/hu.email.auth.invitation.tpl b/app/config/locale/translations/templates/hu.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/hu.email.auth.invitation.tpl rename to app/config/locale/translations/templates/hu.email.auth.invitation.tpl diff --git a/app/config/locales/templates/hu.email.auth.recovery.tpl b/app/config/locale/translations/templates/hu.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/hu.email.auth.recovery.tpl rename to app/config/locale/translations/templates/hu.email.auth.recovery.tpl diff --git a/app/config/locales/templates/hy.email.auth.confirm.tpl b/app/config/locale/translations/templates/hy.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/hy.email.auth.confirm.tpl rename to app/config/locale/translations/templates/hy.email.auth.confirm.tpl diff --git a/app/config/locales/templates/hy.email.auth.invitation.tpl b/app/config/locale/translations/templates/hy.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/hy.email.auth.invitation.tpl rename to app/config/locale/translations/templates/hy.email.auth.invitation.tpl diff --git a/app/config/locales/templates/hy.email.auth.recovery.tpl b/app/config/locale/translations/templates/hy.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/hy.email.auth.recovery.tpl rename to app/config/locale/translations/templates/hy.email.auth.recovery.tpl diff --git a/app/config/locales/templates/id.email.auth.confirm.tpl b/app/config/locale/translations/templates/id.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/id.email.auth.confirm.tpl rename to app/config/locale/translations/templates/id.email.auth.confirm.tpl diff --git a/app/config/locales/templates/id.email.auth.invitation.tpl b/app/config/locale/translations/templates/id.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/id.email.auth.invitation.tpl rename to app/config/locale/translations/templates/id.email.auth.invitation.tpl diff --git a/app/config/locales/templates/id.email.auth.recovery.tpl b/app/config/locale/translations/templates/id.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/id.email.auth.recovery.tpl rename to app/config/locale/translations/templates/id.email.auth.recovery.tpl diff --git a/app/config/locales/templates/is.email.auth.confirm.tpl b/app/config/locale/translations/templates/is.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/is.email.auth.confirm.tpl rename to app/config/locale/translations/templates/is.email.auth.confirm.tpl diff --git a/app/config/locales/templates/is.email.auth.invitation.tpl b/app/config/locale/translations/templates/is.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/is.email.auth.invitation.tpl rename to app/config/locale/translations/templates/is.email.auth.invitation.tpl diff --git a/app/config/locales/templates/is.email.auth.recovery.tpl b/app/config/locale/translations/templates/is.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/is.email.auth.recovery.tpl rename to app/config/locale/translations/templates/is.email.auth.recovery.tpl diff --git a/app/config/locales/templates/it.email.auth.confirm.tpl b/app/config/locale/translations/templates/it.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/it.email.auth.confirm.tpl rename to app/config/locale/translations/templates/it.email.auth.confirm.tpl diff --git a/app/config/locales/templates/it.email.auth.invitation.tpl b/app/config/locale/translations/templates/it.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/it.email.auth.invitation.tpl rename to app/config/locale/translations/templates/it.email.auth.invitation.tpl diff --git a/app/config/locales/templates/it.email.auth.recovery.tpl b/app/config/locale/translations/templates/it.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/it.email.auth.recovery.tpl rename to app/config/locale/translations/templates/it.email.auth.recovery.tpl diff --git a/app/config/locales/templates/ja.email.auth.confirm.tpl b/app/config/locale/translations/templates/ja.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/ja.email.auth.confirm.tpl rename to app/config/locale/translations/templates/ja.email.auth.confirm.tpl diff --git a/app/config/locales/templates/ja.email.auth.invitation.tpl b/app/config/locale/translations/templates/ja.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/ja.email.auth.invitation.tpl rename to app/config/locale/translations/templates/ja.email.auth.invitation.tpl diff --git a/app/config/locales/templates/ja.email.auth.recovery.tpl b/app/config/locale/translations/templates/ja.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/ja.email.auth.recovery.tpl rename to app/config/locale/translations/templates/ja.email.auth.recovery.tpl diff --git a/app/config/locales/templates/jv.email.auth.confirm.tpl b/app/config/locale/translations/templates/jv.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/jv.email.auth.confirm.tpl rename to app/config/locale/translations/templates/jv.email.auth.confirm.tpl diff --git a/app/config/locales/templates/jv.email.auth.invitation.tpl b/app/config/locale/translations/templates/jv.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/jv.email.auth.invitation.tpl rename to app/config/locale/translations/templates/jv.email.auth.invitation.tpl diff --git a/app/config/locales/templates/jv.email.auth.recovery.tpl b/app/config/locale/translations/templates/jv.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/jv.email.auth.recovery.tpl rename to app/config/locale/translations/templates/jv.email.auth.recovery.tpl diff --git a/app/config/locales/templates/km.email.auth.confirm.tpl b/app/config/locale/translations/templates/km.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/km.email.auth.confirm.tpl rename to app/config/locale/translations/templates/km.email.auth.confirm.tpl diff --git a/app/config/locales/templates/km.email.auth.invitation.tpl b/app/config/locale/translations/templates/km.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/km.email.auth.invitation.tpl rename to app/config/locale/translations/templates/km.email.auth.invitation.tpl diff --git a/app/config/locales/templates/km.email.auth.recovery.tpl b/app/config/locale/translations/templates/km.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/km.email.auth.recovery.tpl rename to app/config/locale/translations/templates/km.email.auth.recovery.tpl diff --git a/app/config/locales/templates/ko.email.auth.confirm.tpl b/app/config/locale/translations/templates/ko.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/ko.email.auth.confirm.tpl rename to app/config/locale/translations/templates/ko.email.auth.confirm.tpl diff --git a/app/config/locales/templates/ko.email.auth.invitation.tpl b/app/config/locale/translations/templates/ko.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/ko.email.auth.invitation.tpl rename to app/config/locale/translations/templates/ko.email.auth.invitation.tpl diff --git a/app/config/locales/templates/ko.email.auth.recovery.tpl b/app/config/locale/translations/templates/ko.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/ko.email.auth.recovery.tpl rename to app/config/locale/translations/templates/ko.email.auth.recovery.tpl diff --git a/app/config/locales/templates/lt.email.auth.confirm.tpl b/app/config/locale/translations/templates/lt.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/lt.email.auth.confirm.tpl rename to app/config/locale/translations/templates/lt.email.auth.confirm.tpl diff --git a/app/config/locales/templates/lt.email.auth.invitation.tpl b/app/config/locale/translations/templates/lt.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/lt.email.auth.invitation.tpl rename to app/config/locale/translations/templates/lt.email.auth.invitation.tpl diff --git a/app/config/locales/templates/lt.email.auth.recovery.tpl b/app/config/locale/translations/templates/lt.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/lt.email.auth.recovery.tpl rename to app/config/locale/translations/templates/lt.email.auth.recovery.tpl diff --git a/app/config/locales/templates/ml.email.auth.confirm.tpl b/app/config/locale/translations/templates/ml.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/ml.email.auth.confirm.tpl rename to app/config/locale/translations/templates/ml.email.auth.confirm.tpl diff --git a/app/config/locales/templates/ml.email.auth.invitation.tpl b/app/config/locale/translations/templates/ml.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/ml.email.auth.invitation.tpl rename to app/config/locale/translations/templates/ml.email.auth.invitation.tpl diff --git a/app/config/locales/templates/ml.email.auth.recovery.tpl b/app/config/locale/translations/templates/ml.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/ml.email.auth.recovery.tpl rename to app/config/locale/translations/templates/ml.email.auth.recovery.tpl diff --git a/app/config/locales/templates/my.email.auth.confirm.tpl b/app/config/locale/translations/templates/my.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/my.email.auth.confirm.tpl rename to app/config/locale/translations/templates/my.email.auth.confirm.tpl diff --git a/app/config/locales/templates/my.email.auth.invitation.tpl b/app/config/locale/translations/templates/my.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/my.email.auth.invitation.tpl rename to app/config/locale/translations/templates/my.email.auth.invitation.tpl diff --git a/app/config/locales/templates/my.email.auth.recovery.tpl b/app/config/locale/translations/templates/my.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/my.email.auth.recovery.tpl rename to app/config/locale/translations/templates/my.email.auth.recovery.tpl diff --git a/app/config/locales/templates/nl.email.auth.confirm.tpl b/app/config/locale/translations/templates/nl.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/nl.email.auth.confirm.tpl rename to app/config/locale/translations/templates/nl.email.auth.confirm.tpl diff --git a/app/config/locales/templates/nl.email.auth.invitation.tpl b/app/config/locale/translations/templates/nl.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/nl.email.auth.invitation.tpl rename to app/config/locale/translations/templates/nl.email.auth.invitation.tpl diff --git a/app/config/locales/templates/nl.email.auth.recovery.tpl b/app/config/locale/translations/templates/nl.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/nl.email.auth.recovery.tpl rename to app/config/locale/translations/templates/nl.email.auth.recovery.tpl diff --git a/app/config/locales/templates/no.email.auth.confirm.tpl b/app/config/locale/translations/templates/no.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/no.email.auth.confirm.tpl rename to app/config/locale/translations/templates/no.email.auth.confirm.tpl diff --git a/app/config/locales/templates/no.email.auth.invitation.tpl b/app/config/locale/translations/templates/no.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/no.email.auth.invitation.tpl rename to app/config/locale/translations/templates/no.email.auth.invitation.tpl diff --git a/app/config/locales/templates/no.email.auth.recovery.tpl b/app/config/locale/translations/templates/no.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/no.email.auth.recovery.tpl rename to app/config/locale/translations/templates/no.email.auth.recovery.tpl diff --git a/app/config/locales/templates/ph.email.auth.confirm.tpl b/app/config/locale/translations/templates/ph.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/ph.email.auth.confirm.tpl rename to app/config/locale/translations/templates/ph.email.auth.confirm.tpl diff --git a/app/config/locales/templates/ph.email.auth.invitation.tpl b/app/config/locale/translations/templates/ph.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/ph.email.auth.invitation.tpl rename to app/config/locale/translations/templates/ph.email.auth.invitation.tpl diff --git a/app/config/locales/templates/ph.email.auth.recovery.tpl b/app/config/locale/translations/templates/ph.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/ph.email.auth.recovery.tpl rename to app/config/locale/translations/templates/ph.email.auth.recovery.tpl diff --git a/app/config/locales/templates/pl.email.auth.confirm.tpl b/app/config/locale/translations/templates/pl.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/pl.email.auth.confirm.tpl rename to app/config/locale/translations/templates/pl.email.auth.confirm.tpl diff --git a/app/config/locales/templates/pl.email.auth.invitation.tpl b/app/config/locale/translations/templates/pl.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/pl.email.auth.invitation.tpl rename to app/config/locale/translations/templates/pl.email.auth.invitation.tpl diff --git a/app/config/locales/templates/pl.email.auth.recovery.tpl b/app/config/locale/translations/templates/pl.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/pl.email.auth.recovery.tpl rename to app/config/locale/translations/templates/pl.email.auth.recovery.tpl diff --git a/app/config/locales/templates/pt-br.email.auth.confirm.tpl b/app/config/locale/translations/templates/pt-br.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/pt-br.email.auth.confirm.tpl rename to app/config/locale/translations/templates/pt-br.email.auth.confirm.tpl diff --git a/app/config/locales/templates/pt-br.email.auth.invitation.tpl b/app/config/locale/translations/templates/pt-br.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/pt-br.email.auth.invitation.tpl rename to app/config/locale/translations/templates/pt-br.email.auth.invitation.tpl diff --git a/app/config/locales/templates/pt-br.email.auth.recovery.tpl b/app/config/locale/translations/templates/pt-br.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/pt-br.email.auth.recovery.tpl rename to app/config/locale/translations/templates/pt-br.email.auth.recovery.tpl diff --git a/app/config/locales/templates/pt-pt.email.auth.confirm.tpl b/app/config/locale/translations/templates/pt-pt.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/pt-pt.email.auth.confirm.tpl rename to app/config/locale/translations/templates/pt-pt.email.auth.confirm.tpl diff --git a/app/config/locales/templates/pt-pt.email.auth.invitation.tpl b/app/config/locale/translations/templates/pt-pt.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/pt-pt.email.auth.invitation.tpl rename to app/config/locale/translations/templates/pt-pt.email.auth.invitation.tpl diff --git a/app/config/locales/templates/pt-pt.email.auth.recovery.tpl b/app/config/locale/translations/templates/pt-pt.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/pt-pt.email.auth.recovery.tpl rename to app/config/locale/translations/templates/pt-pt.email.auth.recovery.tpl diff --git a/app/config/locales/templates/ro.email.auth.confirm.tpl b/app/config/locale/translations/templates/ro.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/ro.email.auth.confirm.tpl rename to app/config/locale/translations/templates/ro.email.auth.confirm.tpl diff --git a/app/config/locales/templates/ro.email.auth.invitation.tpl b/app/config/locale/translations/templates/ro.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/ro.email.auth.invitation.tpl rename to app/config/locale/translations/templates/ro.email.auth.invitation.tpl diff --git a/app/config/locales/templates/ro.email.auth.recovery.tpl b/app/config/locale/translations/templates/ro.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/ro.email.auth.recovery.tpl rename to app/config/locale/translations/templates/ro.email.auth.recovery.tpl diff --git a/app/config/locales/templates/ru.email.auth.confirm.tpl b/app/config/locale/translations/templates/ru.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/ru.email.auth.confirm.tpl rename to app/config/locale/translations/templates/ru.email.auth.confirm.tpl diff --git a/app/config/locales/templates/ru.email.auth.invitation.tpl b/app/config/locale/translations/templates/ru.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/ru.email.auth.invitation.tpl rename to app/config/locale/translations/templates/ru.email.auth.invitation.tpl diff --git a/app/config/locales/templates/ru.email.auth.recovery.tpl b/app/config/locale/translations/templates/ru.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/ru.email.auth.recovery.tpl rename to app/config/locale/translations/templates/ru.email.auth.recovery.tpl diff --git a/app/config/locales/templates/si.email.auth.confirm.tpl b/app/config/locale/translations/templates/si.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/si.email.auth.confirm.tpl rename to app/config/locale/translations/templates/si.email.auth.confirm.tpl diff --git a/app/config/locales/templates/si.email.auth.invitation.tpl b/app/config/locale/translations/templates/si.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/si.email.auth.invitation.tpl rename to app/config/locale/translations/templates/si.email.auth.invitation.tpl diff --git a/app/config/locales/templates/si.email.auth.recovery.tpl b/app/config/locale/translations/templates/si.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/si.email.auth.recovery.tpl rename to app/config/locale/translations/templates/si.email.auth.recovery.tpl diff --git a/app/config/locales/templates/sl.email.auth.confirm.tpl b/app/config/locale/translations/templates/sl.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/sl.email.auth.confirm.tpl rename to app/config/locale/translations/templates/sl.email.auth.confirm.tpl diff --git a/app/config/locales/templates/sl.email.auth.invitation.tpl b/app/config/locale/translations/templates/sl.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/sl.email.auth.invitation.tpl rename to app/config/locale/translations/templates/sl.email.auth.invitation.tpl diff --git a/app/config/locales/templates/sl.email.auth.recovery.tpl b/app/config/locale/translations/templates/sl.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/sl.email.auth.recovery.tpl rename to app/config/locale/translations/templates/sl.email.auth.recovery.tpl diff --git a/app/config/locales/templates/sv.email.auth.confirm.tpl b/app/config/locale/translations/templates/sv.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/sv.email.auth.confirm.tpl rename to app/config/locale/translations/templates/sv.email.auth.confirm.tpl diff --git a/app/config/locales/templates/sv.email.auth.invitation.tpl b/app/config/locale/translations/templates/sv.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/sv.email.auth.invitation.tpl rename to app/config/locale/translations/templates/sv.email.auth.invitation.tpl diff --git a/app/config/locales/templates/sv.email.auth.recovery.tpl b/app/config/locale/translations/templates/sv.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/sv.email.auth.recovery.tpl rename to app/config/locale/translations/templates/sv.email.auth.recovery.tpl diff --git a/app/config/locales/templates/ta.email.auth.confirm.tpl b/app/config/locale/translations/templates/ta.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/ta.email.auth.confirm.tpl rename to app/config/locale/translations/templates/ta.email.auth.confirm.tpl diff --git a/app/config/locales/templates/ta.email.auth.invitation.tpl b/app/config/locale/translations/templates/ta.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/ta.email.auth.invitation.tpl rename to app/config/locale/translations/templates/ta.email.auth.invitation.tpl diff --git a/app/config/locales/templates/ta.email.auth.recovery.tpl b/app/config/locale/translations/templates/ta.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/ta.email.auth.recovery.tpl rename to app/config/locale/translations/templates/ta.email.auth.recovery.tpl diff --git a/app/config/locales/templates/th.email.auth.confirm.tpl b/app/config/locale/translations/templates/th.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/th.email.auth.confirm.tpl rename to app/config/locale/translations/templates/th.email.auth.confirm.tpl diff --git a/app/config/locales/templates/th.email.auth.invitation.tpl b/app/config/locale/translations/templates/th.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/th.email.auth.invitation.tpl rename to app/config/locale/translations/templates/th.email.auth.invitation.tpl diff --git a/app/config/locales/templates/th.email.auth.recovery.tpl b/app/config/locale/translations/templates/th.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/th.email.auth.recovery.tpl rename to app/config/locale/translations/templates/th.email.auth.recovery.tpl diff --git a/app/config/locales/templates/tr.email.auth.confirm.tpl b/app/config/locale/translations/templates/tr.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/tr.email.auth.confirm.tpl rename to app/config/locale/translations/templates/tr.email.auth.confirm.tpl diff --git a/app/config/locales/templates/tr.email.auth.invitation.tpl b/app/config/locale/translations/templates/tr.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/tr.email.auth.invitation.tpl rename to app/config/locale/translations/templates/tr.email.auth.invitation.tpl diff --git a/app/config/locales/templates/tr.email.auth.recovery.tpl b/app/config/locale/translations/templates/tr.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/tr.email.auth.recovery.tpl rename to app/config/locale/translations/templates/tr.email.auth.recovery.tpl diff --git a/app/config/locales/templates/ua.email.auth.confirm.tpl b/app/config/locale/translations/templates/ua.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/ua.email.auth.confirm.tpl rename to app/config/locale/translations/templates/ua.email.auth.confirm.tpl diff --git a/app/config/locales/templates/ua.email.auth.invitation.tpl b/app/config/locale/translations/templates/ua.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/ua.email.auth.invitation.tpl rename to app/config/locale/translations/templates/ua.email.auth.invitation.tpl diff --git a/app/config/locales/templates/ua.email.auth.recovery.tpl b/app/config/locale/translations/templates/ua.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/ua.email.auth.recovery.tpl rename to app/config/locale/translations/templates/ua.email.auth.recovery.tpl diff --git a/app/config/locales/templates/vi.email.auth.confirm.tpl b/app/config/locale/translations/templates/vi.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/vi.email.auth.confirm.tpl rename to app/config/locale/translations/templates/vi.email.auth.confirm.tpl diff --git a/app/config/locales/templates/vi.email.auth.invitation.tpl b/app/config/locale/translations/templates/vi.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/vi.email.auth.invitation.tpl rename to app/config/locale/translations/templates/vi.email.auth.invitation.tpl diff --git a/app/config/locales/templates/vi.email.auth.recovery.tpl b/app/config/locale/translations/templates/vi.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/vi.email.auth.recovery.tpl rename to app/config/locale/translations/templates/vi.email.auth.recovery.tpl diff --git a/app/config/locales/templates/zh-cn.email.auth.confirm.tpl b/app/config/locale/translations/templates/zh-cn.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/zh-cn.email.auth.confirm.tpl rename to app/config/locale/translations/templates/zh-cn.email.auth.confirm.tpl diff --git a/app/config/locales/templates/zh-cn.email.auth.invitation.tpl b/app/config/locale/translations/templates/zh-cn.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/zh-cn.email.auth.invitation.tpl rename to app/config/locale/translations/templates/zh-cn.email.auth.invitation.tpl diff --git a/app/config/locales/templates/zh-cn.email.auth.recovery.tpl b/app/config/locale/translations/templates/zh-cn.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/zh-cn.email.auth.recovery.tpl rename to app/config/locale/translations/templates/zh-cn.email.auth.recovery.tpl diff --git a/app/config/locales/templates/zh-tw.email.auth.confirm.tpl b/app/config/locale/translations/templates/zh-tw.email.auth.confirm.tpl similarity index 100% rename from app/config/locales/templates/zh-tw.email.auth.confirm.tpl rename to app/config/locale/translations/templates/zh-tw.email.auth.confirm.tpl diff --git a/app/config/locales/templates/zh-tw.email.auth.invitation.tpl b/app/config/locale/translations/templates/zh-tw.email.auth.invitation.tpl similarity index 100% rename from app/config/locales/templates/zh-tw.email.auth.invitation.tpl rename to app/config/locale/translations/templates/zh-tw.email.auth.invitation.tpl diff --git a/app/config/locales/templates/zh-tw.email.auth.recovery.tpl b/app/config/locale/translations/templates/zh-tw.email.auth.recovery.tpl similarity index 100% rename from app/config/locales/templates/zh-tw.email.auth.recovery.tpl rename to app/config/locale/translations/templates/zh-tw.email.auth.recovery.tpl diff --git a/app/config/locales/th.continents.php b/app/config/locale/translations/th.continents.php similarity index 100% rename from app/config/locales/th.continents.php rename to app/config/locale/translations/th.continents.php diff --git a/app/config/locales/th.countries.php b/app/config/locale/translations/th.countries.php similarity index 100% rename from app/config/locales/th.countries.php rename to app/config/locale/translations/th.countries.php diff --git a/app/config/locales/th.php b/app/config/locale/translations/th.php similarity index 100% rename from app/config/locales/th.php rename to app/config/locale/translations/th.php diff --git a/app/config/locales/tr.continents.php b/app/config/locale/translations/tr.continents.php similarity index 100% rename from app/config/locales/tr.continents.php rename to app/config/locale/translations/tr.continents.php diff --git a/app/config/locales/tr.countries.php b/app/config/locale/translations/tr.countries.php similarity index 100% rename from app/config/locales/tr.countries.php rename to app/config/locale/translations/tr.countries.php diff --git a/app/config/locales/tr.php b/app/config/locale/translations/tr.php similarity index 100% rename from app/config/locales/tr.php rename to app/config/locale/translations/tr.php diff --git a/app/config/locales/ua.continents.php b/app/config/locale/translations/ua.continents.php similarity index 100% rename from app/config/locales/ua.continents.php rename to app/config/locale/translations/ua.continents.php diff --git a/app/config/locales/ua.countries.php b/app/config/locale/translations/ua.countries.php similarity index 100% rename from app/config/locales/ua.countries.php rename to app/config/locale/translations/ua.countries.php diff --git a/app/config/locales/ua.php b/app/config/locale/translations/ua.php similarity index 100% rename from app/config/locales/ua.php rename to app/config/locale/translations/ua.php diff --git a/app/config/locales/vi.continents.php b/app/config/locale/translations/vi.continents.php similarity index 100% rename from app/config/locales/vi.continents.php rename to app/config/locale/translations/vi.continents.php diff --git a/app/config/locales/vi.countries.php b/app/config/locale/translations/vi.countries.php similarity index 100% rename from app/config/locales/vi.countries.php rename to app/config/locale/translations/vi.countries.php diff --git a/app/config/locales/vi.php b/app/config/locale/translations/vi.php similarity index 100% rename from app/config/locales/vi.php rename to app/config/locale/translations/vi.php diff --git a/app/config/locales/zh-cn.continents.php b/app/config/locale/translations/zh-cn.continents.php similarity index 100% rename from app/config/locales/zh-cn.continents.php rename to app/config/locale/translations/zh-cn.continents.php diff --git a/app/config/locales/zh-cn.countries.php b/app/config/locale/translations/zh-cn.countries.php similarity index 100% rename from app/config/locales/zh-cn.countries.php rename to app/config/locale/translations/zh-cn.countries.php diff --git a/app/config/locales/zh-cn.php b/app/config/locale/translations/zh-cn.php similarity index 100% rename from app/config/locales/zh-cn.php rename to app/config/locale/translations/zh-cn.php diff --git a/app/config/locales/zh-tw.continents.php b/app/config/locale/translations/zh-tw.continents.php similarity index 100% rename from app/config/locales/zh-tw.continents.php rename to app/config/locale/translations/zh-tw.continents.php diff --git a/app/config/locales/zh-tw.countries.php b/app/config/locale/translations/zh-tw.countries.php similarity index 100% rename from app/config/locales/zh-tw.countries.php rename to app/config/locale/translations/zh-tw.countries.php diff --git a/app/config/locales/zh-tw.php b/app/config/locale/translations/zh-tw.php similarity index 100% rename from app/config/locales/zh-tw.php rename to app/config/locale/translations/zh-tw.php diff --git a/app/config/roles.php b/app/config/roles.php index 4d2420ec58..0a4ec4369c 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -37,6 +37,8 @@ $admins = [ 'users.write', 'collections.read', 'collections.write', + 'functions.read', + 'functions.write', 'platforms.read', 'platforms.write', 'keys.read', diff --git a/app/config/scopes.php b/app/config/scopes.php index 59ad2a0859..88c67d70dd 100644 --- a/app/config/scopes.php +++ b/app/config/scopes.php @@ -11,6 +11,9 @@ return [ // List of publicly visible scopes 'documents.write', 'files.read', 'files.write', + 'functions.read', + 'functions.write', + 'health.read', // 'platforms.read', // 'platforms.write', // 'keys.read', diff --git a/app/config/services.php b/app/config/services.php index cc5df8c8e2..e500f9a241 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -3,89 +3,96 @@ return [ '/' => [ 'name' => 'Homepage', - 'controller' => 'controllers/web/home.php', + 'controller' => 'web/home.php', 'sdk' => false, 'tests' => false, ], 'console/' => [ 'name' => 'Console', - 'controller' => 'controllers/web/console.php', + 'controller' => 'web/console.php', 'sdk' => false, 'tests' => false, ], 'v1/account' => [ 'name' => 'Account', 'description' => '/docs/services/account.md', - 'controller' => 'controllers/api/account.php', + 'controller' => 'api/account.php', 'sdk' => true, 'tests' => false, ], 'v1/avatars' => [ 'name' => 'Avatars', 'description' => '/docs/services/avatars.md', - 'controller' => 'controllers/api/avatars.php', + 'controller' => 'api/avatars.php', 'sdk' => true, 'tests' => false, ], 'v1/database' => [ 'name' => 'Database', 'description' => '/docs/services/database.md', - 'controller' => 'controllers/api/database.php', + 'controller' => 'api/database.php', 'sdk' => true, 'tests' => false, ], 'v1/locale' => [ 'name' => 'Locale', 'description' => '/docs/services/locale.md', - 'controller' => 'controllers/api/locale.php', + 'controller' => 'api/locale.php', 'sdk' => true, 'tests' => false, ], 'v1/health' => [ 'name' => 'Health', 'description' => '/docs/services/health.md', - 'controller' => 'controllers/api/health.php', + 'controller' => 'api/health.php', 'sdk' => true, 'tests' => false, ], 'v1/projects' => [ 'name' => 'Projects', - 'controller' => 'controllers/api/projects.php', + 'controller' => 'api/projects.php', 'sdk' => true, 'tests' => false, ], 'v1/storage' => [ 'name' => 'Storage', 'description' => '/docs/services/storage.md', - 'controller' => 'controllers/api/storage.php', + 'controller' => 'api/storage.php', 'sdk' => true, 'tests' => false, ], 'v1/teams' => [ 'name' => 'Teams', 'description' => '/docs/services/teams.md', - 'controller' => 'controllers/api/teams.php', + 'controller' => 'api/teams.php', 'sdk' => true, 'tests' => false, ], 'v1/users' => [ 'name' => 'Users', 'description' => '/docs/services/users.md', - 'controller' => 'controllers/api/users.php', + 'controller' => 'api/users.php', + 'sdk' => true, + 'tests' => false, + ], + 'v1/functions' => [ + 'name' => 'Users', + 'description' => '/docs/services/functions.md', + 'controller' => 'api/functions.php', 'sdk' => true, 'tests' => false, ], 'v1/mock' => [ 'name' => 'Mock', 'description' => '', - 'controller' => 'controllers/mock.php', + 'controller' => 'mock.php', 'sdk' => false, 'tests' => true, ], 'v1/graphql' => [ 'name' => 'GraphQL', 'description' => 'GraphQL Endpoint', - 'controller' => 'controllers/api/graphql.php', + 'controller' => 'api/graphql.php', 'sdk' => false, 'tests' => false, ], diff --git a/app/config/storage/inputs.php b/app/config/storage/inputs.php new file mode 100644 index 0000000000..c580316c53 --- /dev/null +++ b/app/config/storage/inputs.php @@ -0,0 +1,8 @@ + 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', +]; \ No newline at end of file diff --git a/app/config/storage/logos.php b/app/config/storage/logos.php new file mode 100644 index 0000000000..3170762dca --- /dev/null +++ b/app/config/storage/logos.php @@ -0,0 +1,43 @@ + __DIR__.'/logos/none.png', + + // Video Files + 'video/mp4' => __DIR__.'/logos/video.png', + 'video/x-flv' => __DIR__.'/logos/video.png', + 'application/x-mpegURL' => __DIR__.'/logos/video.png', + 'video/MP2T' => __DIR__.'/logos/video.png', + 'video/3gpp' => __DIR__.'/logos/video.png', + 'video/quicktime' => __DIR__.'/logos/video.png', + 'video/x-msvideo' => __DIR__.'/logos/video.png', + 'video/x-ms-wmv' => __DIR__.'/logos/video.png', + + // // Microsoft Word + 'application/msword' => __DIR__.'/logos/word.png', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => __DIR__.'/logos/word.png', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => __DIR__.'/logos/word.png', + 'application/vnd.ms-word.document.macroEnabled.12' => __DIR__.'/logos/word.png', + + // // Microsoft Excel + 'application/vnd.ms-excel' => __DIR__.'/logos/excel.png', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => __DIR__.'/logos/excel.png', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => __DIR__.'/logos/excel.png', + 'application/vnd.ms-excel.sheet.macroEnabled.12' => __DIR__.'/logos/excel.png', + 'application/vnd.ms-excel.template.macroEnabled.12' => __DIR__.'/logos/excel.png', + 'application/vnd.ms-excel.addin.macroEnabled.12' => __DIR__.'/logos/excel.png', + 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => __DIR__.'/logos/excel.png', + + // // Microsoft Power Point + 'application/vnd.ms-powerpoint' => __DIR__.'/logos/ppt.png', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => __DIR__.'/logos/ppt.png', + 'application/vnd.openxmlformats-officedocument.presentationml.template' => __DIR__.'/logos/ppt.png', + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => __DIR__.'/logos/ppt.png', + 'application/vnd.ms-powerpoint.addin.macroEnabled.12' => __DIR__.'/logos/ppt.png', + 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' => __DIR__.'/logos/ppt.png', + 'application/vnd.ms-powerpoint.template.macroEnabled.12' => __DIR__.'/logos/ppt.png', + 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' => __DIR__.'/logos/ppt.png', + + // Adobe PDF + 'application/pdf' => __DIR__.'/logos/pdf.png', +]; \ No newline at end of file diff --git a/app/config/files/excel.png b/app/config/storage/logos/excel.png similarity index 100% rename from app/config/files/excel.png rename to app/config/storage/logos/excel.png diff --git a/app/config/files/none.png b/app/config/storage/logos/none.png similarity index 100% rename from app/config/files/none.png rename to app/config/storage/logos/none.png diff --git a/app/config/files/pdf.png b/app/config/storage/logos/pdf.png similarity index 100% rename from app/config/files/pdf.png rename to app/config/storage/logos/pdf.png diff --git a/app/config/files/ppt.png b/app/config/storage/logos/ppt.png similarity index 100% rename from app/config/files/ppt.png rename to app/config/storage/logos/ppt.png diff --git a/app/config/files/video.png b/app/config/storage/logos/video.png similarity index 100% rename from app/config/files/video.png rename to app/config/storage/logos/video.png diff --git a/app/config/files/word.png b/app/config/storage/logos/word.png similarity index 100% rename from app/config/files/word.png rename to app/config/storage/logos/word.png diff --git a/app/config/storage/mimes.php b/app/config/storage/mimes.php new file mode 100644 index 0000000000..242a990b8d --- /dev/null +++ b/app/config/storage/mimes.php @@ -0,0 +1,50 @@ + 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + 'webp' => 'image/webp', +]; \ No newline at end of file diff --git a/app/config/variables.php b/app/config/variables.php new file mode 100644 index 0000000000..c1b9982764 --- /dev/null +++ b/app/config/variables.php @@ -0,0 +1,136 @@ + '_APP_ENV', + 'default' => 'production', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_OPTIONS_ABUSE', + 'default' => 'enabled', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_OPTIONS_FORCE_HTTPS', + 'default' => 'enabled', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_OPENSSL_KEY_V1', + 'default' => 'your-secret-key', + 'required' => true, + 'question' => 'Choose a secret API key, make sure to make a backup of your key in a secure location', + ], + [ + 'name' => '_APP_DOMAIN', + 'default' => 'localhost', + 'required' => true, + 'question' => 'Enter your Appwrite hostname', + ], + [ + 'name' => '_APP_DOMAIN_TARGET', + 'default' => 'localhost', + 'required' => true, + 'question' => "Enter a DNS A record hostname to serve as a CNAME for your custom domains.\nYou can use the same value as used for the Appwrite hostname.", + ], + [ + 'name' => '_APP_REDIS_HOST', + 'default' => 'redis', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_REDIS_PORT', + 'default' => '6379', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_DB_HOST', + 'default' => 'mariadb', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_DB_PORT', + 'default' => '3306', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_DB_SCHEMA', + 'default' => 'appwrite', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_DB_USER', + 'default' => 'user', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_DB_PASS', + 'default' => 'password', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_INFLUXDB_HOST', + 'default' => 'influxdb', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_INFLUXDB_PORT', + 'default' => '8086', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_STATSD_HOST', + 'default' => 'telegraf', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_STATSD_PORT', + 'default' => '8125', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_SMTP_HOST', + 'default' => 'smtp', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_SMTP_PORT', + 'default' => '25', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_STORAGE_LIMIT', + 'default' => '100000000', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_FUNCTIONS_TIMEOUT', + 'default' => '900', + 'required' => false, + 'question' => '', + ], + [ + 'name' => '_APP_FUNCTIONS_CONTAINERS', + 'default' => '10', + 'required' => false, + 'question' => '', + ], +]; \ No newline at end of file diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 8ba5784412..48ad948263 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1,10 +1,7 @@ init(function() use (&$oauth2Keys) { +App::init(function() use (&$oauth2Keys) { foreach (Config::getParam('providers') as $key => $provider) { if (!$provider['enabled']) { continue; @@ -43,9 +39,9 @@ $utopia->init(function() use (&$oauth2Keys) { $oauth2Keys[] = 'oauth2'.\ucfirst($key); $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } -}, 'account'); +}, [], 'account'); -$utopia->post('/v1/account') +App::post('/v1/account') ->desc('Create Account') ->groups(['api', 'account']) ->label('webhook', 'account.create') @@ -58,94 +54,99 @@ $utopia->post('/v1/account') ->param('email', '', function () { return new Email(); }, 'User email.') ->param('password', '', function () { return new Password(); }, 'User password. Must be between 6 to 32 chars.') ->param('name', '', function () { return new Text(100); }, 'User name.', true) - ->action( - function ($email, $password, $name) use ($request, $response, $audit, $projectDB, $project, $webhook, $oauth2Keys) { - if ('console' === $project->getId()) { - $whitlistEmails = $project->getAttribute('authWhitelistEmails'); - $whitlistIPs = $project->getAttribute('authWhitelistIPs'); - $whitlistDomains = $project->getAttribute('authWhitelistDomains'); + ->action(function ($email, $password, $name, $request, $response, $project, $projectDB, $webhooks, $audits) use ($oauth2Keys) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ - if (!empty($whitlistEmails) && !\in_array($email, $whitlistEmails)) { - throw new Exception('Console registration is restricted to specific emails. Contact your administrator for more information.', 401); - } + if ('console' === $project->getId()) { + $whitlistEmails = $project->getAttribute('authWhitelistEmails'); + $whitlistIPs = $project->getAttribute('authWhitelistIPs'); + $whitlistDomains = $project->getAttribute('authWhitelistDomains'); - if (!empty($whitlistIPs) && !\in_array($request->getIP(), $whitlistIPs)) { - throw new Exception('Console registration is restricted to specific IPs. Contact your administrator for more information.', 401); - } - - if (!empty($whitlistDomains) && !\in_array(\substr(\strrchr($email, '@'), 1), $whitlistDomains)) { - throw new Exception('Console registration is restricted to specific domains. Contact your administrator for more information.', 401); - } + if (!empty($whitlistEmails) && !\in_array($email, $whitlistEmails)) { + throw new Exception('Console registration is restricted to specific emails. Contact your administrator for more information.', 401); } - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); - - if (!empty($profile)) { - throw new Exception('Account already exists', 409); + if (!empty($whitlistIPs) && !\in_array($request->getIP(), $whitlistIPs)) { + throw new Exception('Console registration is restricted to specific IPs. Contact your administrator for more information.', 401); } - Authorization::disable(); - - try { - $user = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_USERS, - '$permissions' => [ - 'read' => ['*'], - 'write' => ['user:{self}'], - ], - 'email' => $email, - 'emailVerification' => false, - 'status' => Auth::USER_STATUS_UNACTIVATED, - 'password' => Auth::passwordHash($password), - 'password-update' => \time(), - 'registration' => \time(), - 'reset' => false, - 'name' => $name, - ], ['email' => $email]); - } catch (Duplicate $th) { - throw new Exception('Account already exists', 409); + if (!empty($whitlistDomains) && !\in_array(\substr(\strrchr($email, '@'), 1), $whitlistDomains)) { + throw new Exception('Console registration is restricted to specific domains. Contact your administrator for more information.', 401); } - - Authorization::enable(); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - - $webhook - ->setParam('payload', [ - 'name' => $name, - 'email' => $email, - ]) - ; - - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.create') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); } - ); -$utopia->post('/v1/account/sessions') + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); + + if (!empty($profile)) { + throw new Exception('Account already exists', 409); + } + + Authorization::disable(); + + try { + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['user:{self}'], + ], + 'email' => $email, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => Auth::passwordHash($password), + 'password-update' => \time(), + 'registration' => \time(), + 'reset' => false, + 'name' => $name, + ], ['email' => $email]); + } catch (Duplicate $th) { + throw new Exception('Account already exists', 409); + } + + Authorization::enable(); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $webhooks + ->setParam('payload', [ + 'name' => $name, + 'email' => $email, + ]) + ; + + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.create') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'email', + 'registration', + 'name', + ], + $oauth2Keys + )), ['roles' => Authorization::getRoles()])); + }, ['request', 'response', 'project', 'projectDB', 'webhooks', 'audits']); + +App::post('/v1/account/sessions') ->desc('Create Account Session') ->groups(['api', 'account']) ->label('webhook', 'account.sessions.create') @@ -158,84 +159,90 @@ $utopia->post('/v1/account/sessions') ->label('abuse-key', 'url:{url},email:{param-email}') ->param('email', '', function () { return new Email(); }, 'User email.') ->param('password', '', function () { return new Password(); }, 'User password. Must be between 6 to 32 chars.') - ->action( - function ($email, $password) use ($response, $request, $projectDB, $audit, $webhook) { - $protocol = Config::getParam('protocol'); - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); + ->action(function ($email, $password, $request, $response, $projectDB, $webhooks, $audits) { + /** @var Appwrite\Swoole\Request $request */ + /** @var Appwrite\Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ - if (false == $profile || !Auth::passwordVerify($password, $profile->getAttribute('password'))) { - $audit - //->setParam('userId', $profile->getId()) - ->setParam('event', 'account.sesssions.failed') - ->setParam('resource', 'users/'.($profile ? $profile->getId() : '')) - ; + $protocol = $request->getProtocol(); + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); - throw new Exception('Invalid credentials', 401); // Wrong password or username - } - - $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; - $secret = Auth::tokenGenerator(); - $session = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, - '$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]], - 'type' => Auth::TOKEN_TYPE_LOGIN, - 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak - 'expire' => $expiry, - 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), - 'ip' => $request->getIP(), - ]); - - Authorization::setRole('user:'.$profile->getId()); - - $session = $projectDB->createDocument($session->getArrayCopy()); - - if (false === $session) { - throw new Exception('Failed saving session to DB', 500); - } - - $profile->setAttribute('tokens', $session, Document::SET_TYPE_APPEND); - - $profile = $projectDB->updateDocument($profile->getArrayCopy()); - - if (false === $profile) { - throw new Exception('Failed saving user to DB', 500); - } - - $webhook - ->setParam('payload', [ - 'name' => $profile->getAttribute('name', ''), - 'email' => $profile->getAttribute('email', ''), - ]) + if (false == $profile || !Auth::passwordVerify($password, $profile->getAttribute('password'))) { + $audits + //->setParam('userId', $profile->getId()) + ->setParam('event', 'account.sesssions.failed') + ->setParam('resource', 'users/'.($profile ? $profile->getId() : '')) ; - $audit - ->setParam('userId', $profile->getId()) - ->setParam('event', 'account.sessions.create') - ->setParam('resource', 'users/'.$profile->getId()) - ; + throw new Exception('Invalid credentials', 401); // Wrong password or username + } - if (!Config::getParam('domainVerification')) { - $response - ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($profile->getId(), $secret)])) - ; - } - + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $secret = Auth::tokenGenerator(); + $session = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]], + 'type' => Auth::TOKEN_TYPE_LOGIN, + 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak + 'expire' => $expiry, + 'userAgent' => $request->getUserAgent('UNKNOWN'), + 'ip' => $request->getIP(), + ]); + + Authorization::setRole('user:'.$profile->getId()); + + $session = $projectDB->createDocument($session->getArrayCopy()); + + if (false === $session) { + throw new Exception('Failed saving session to DB', 500); + } + + $profile->setAttribute('tokens', $session, Document::SET_TYPE_APPEND); + + $profile = $projectDB->updateDocument($profile->getArrayCopy()); + + if (false === $profile) { + throw new Exception('Failed saving user to DB', 500); + } + + $webhooks + ->setParam('payload', [ + 'name' => $profile->getAttribute('name', ''), + 'email' => $profile->getAttribute('email', ''), + ]) + ; + + $audits + ->setParam('userId', $profile->getId()) + ->setParam('event', 'account.sessions.create') + ->setParam('resource', 'users/'.$profile->getId()) + ; + + if (!Config::getParam('domainVerification')) { $response - ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($profile->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($profile->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($session->getArrayCopy(['$id', 'type', 'expire'])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($profile->getId(), $secret)])) ; } - ); + + $response + ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($profile->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($profile->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->setStatusCode(Response::STATUS_CODE_CREATED) + ; + + $response->dynamic($session, Response::MODEL_SESSION); + ; + }, ['request', 'response', 'projectDB', 'webhooks', 'audits']); -$utopia->get('/v1/account/sessions/oauth2/:provider') +App::get('/v1/account/sessions/oauth2/:provider') ->desc('Create Account Session with OAuth2') ->groups(['api', 'account']) ->label('error', __DIR__.'/../../views/general/error.phtml') @@ -250,43 +257,45 @@ $utopia->get('/v1/account/sessions/oauth2/:provider') ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('providers'), function($node) {return (!$node['mock']);}))).'.') - ->param('success', $oauthDefaultSuccess, function () use ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true) - ->param('failure', $oauthDefaultFailure, function () use ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true) + ->param('success', $oauthDefaultSuccess, function ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients']) + ->param('failure', $oauthDefaultFailure, function ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients']) ->param('scopes', [], function () { return new ArrayList(new Text(128)); }, 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes.', true) - ->action( - function ($provider, $success, $failure, $scopes) use ($response, $request, $project) { - $protocol = Config::getParam('protocol'); - $callback = $protocol.'://'.$request->getServer('HTTP_HOST').'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId(); - $appId = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Appid', ''); - $appSecret = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'); + ->action(function ($provider, $success, $failure, $scopes, $request, $response, $project) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ - $appSecret = \json_decode($appSecret, true); + $protocol = $request->getProtocol(); + $callback = $protocol.'://'.$request->getHostname().'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId(); + $appId = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Appid', ''); + $appSecret = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'); - if (!empty($appSecret) && isset($appSecret['version'])) { - $key = $request->getServer('_APP_OPENSSL_KEY_V'.$appSecret['version']); - $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); - } + $appSecret = \json_decode($appSecret, true); - if (empty($appId) || empty($appSecret)) { - throw new Exception('This provider is disabled. Please configure the provider app ID and app secret key from your '.APP_NAME.' console to continue.', 412); - } - - $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); - - if (!\class_exists($classname)) { - throw new Exception('Provider is not supported', 501); - } - - $oauth2 = new $classname($appId, $appSecret, $callback, ['success' => $success, 'failure' => $failure], $scopes); - - $response - ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - ->addHeader('Pragma', 'no-cache') - ->redirect($oauth2->getLoginURL()); + if (!empty($appSecret) && isset($appSecret['version'])) { + $key = App::getEnv('_APP_OPENSSL_KEY_V'.$appSecret['version']); + $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); } - ); -$utopia->get('/v1/account/sessions/oauth2/callback/:provider/:projectId') + if (empty($appId) || empty($appSecret)) { + throw new Exception('This provider is disabled. Please configure the provider app ID and app secret key from your '.APP_NAME.' console to continue.', 412); + } + + $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); + + if (!\class_exists($classname)) { + throw new Exception('Provider is not supported', 501); + } + + $oauth2 = new $classname($appId, $appSecret, $callback, ['success' => $success, 'failure' => $failure], $scopes); + + $response + ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->addHeader('Pragma', 'no-cache') + ->redirect($oauth2->getLoginURL()); + }, ['request', 'response', 'project']); + +App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 Callback') ->groups(['api', 'account']) ->label('error', __DIR__.'/../../views/general/error.phtml') @@ -296,20 +305,21 @@ $utopia->get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') ->param('code', '', function () { return new Text(1024); }, 'OAuth2 code.') ->param('state', '', function () { return new Text(2048); }, 'Login state params.', true) - ->action( - function ($projectId, $provider, $code, $state) use ($response) { - $domain = Config::getParam('domain'); - $protocol = Config::getParam('protocol'); - - $response - ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - ->addHeader('Pragma', 'no-cache') - ->redirect($protocol.'://'.$domain.'/v1/account/sessions/oauth2/'.$provider.'/redirect?' - .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); - } - ); + ->action(function ($projectId, $provider, $code, $state, $request, $response) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ -$utopia->post('/v1/account/sessions/oauth2/callback/:provider/:projectId') + $domain = $request->getHostname(); + $protocol = $request->getProtocol(); + + $response + ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->addHeader('Pragma', 'no-cache') + ->redirect($protocol.'://'.$domain.'/v1/account/sessions/oauth2/'.$provider.'/redirect?' + .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); + }, ['request', 'response']); + +App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 Callback') ->groups(['api', 'account']) ->label('error', __DIR__.'/../../views/general/error.phtml') @@ -320,20 +330,21 @@ $utopia->post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') ->param('code', '', function () { return new Text(1024); }, 'OAuth2 code.') ->param('state', '', function () { return new Text(2048); }, 'Login state params.', true) - ->action( - function ($projectId, $provider, $code, $state) use ($response) { - $domain = Config::getParam('domain'); - $protocol = Config::getParam('protocol'); - - $response - ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - ->addHeader('Pragma', 'no-cache') - ->redirect($protocol.'://'.$domain.'/v1/account/sessions/oauth2/'.$provider.'/redirect?' - .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); - } - ); + ->action(function ($projectId, $provider, $code, $state, $request, $response) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ -$utopia->get('/v1/account/sessions/oauth2/:provider/redirect') + $domain = $request->getHostname(); + $protocol = $request->getProtocol(); + + $response + ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->addHeader('Pragma', 'no-cache') + ->redirect($protocol.'://'.$domain.'/v1/account/sessions/oauth2/'.$provider.'/redirect?' + .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); + }, ['request', 'response']); + +App::get('/v1/account/sessions/oauth2/:provider/redirect') ->desc('OAuth2 Redirect') ->groups(['api', 'account']) ->label('error', __DIR__.'/../../views/general/error.phtml') @@ -345,189 +356,194 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') ->param('code', '', function () { return new Text(1024); }, 'OAuth2 code.') ->param('state', '', function () { return new Text(2048); }, 'OAuth2 state params.', true) - ->action( - function ($provider, $code, $state) use ($response, $request, $user, $projectDB, $project, $audit, $oauthDefaultSuccess) { - $protocol = Config::getParam('protocol'); - $callback = $protocol.'://'.$request->getServer('HTTP_HOST').'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId(); - $defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => '']; - $validateURL = new URL(); + ->action(function ($provider, $code, $state, $request, $response, $project, $user, $projectDB, $audits) use ($oauthDefaultSuccess) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ + + $protocol = $request->getProtocol(); + $callback = $protocol.'://'.$request->getHostname().'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId(); + $defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => '']; + $validateURL = new URL(); - $appId = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Appid', ''); - $appSecret = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'); + $appId = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Appid', ''); + $appSecret = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'); - $appSecret = \json_decode($appSecret, true); + $appSecret = \json_decode($appSecret, true); - if (!empty($appSecret) && isset($appSecret['version'])) { - $key = $request->getServer('_APP_OPENSSL_KEY_V'.$appSecret['version']); - $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); + if (!empty($appSecret) && isset($appSecret['version'])) { + $key = App::getEnv('_APP_OPENSSL_KEY_V'.$appSecret['version']); + $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); + } + + $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); + + if (!\class_exists($classname)) { + throw new Exception('Provider is not supported', 501); + } + + $oauth2 = new $classname($appId, $appSecret, $callback); + + if (!empty($state)) { + try { + $state = \array_merge($defaultState, $oauth2->parseState($state)); + } catch (\Exception $exception) { + throw new Exception('Failed to parse login state params as passed from OAuth2 provider'); + } + } else { + $state = $defaultState; + } + + if (!$validateURL->isValid($state['success'])) { + throw new Exception('Invalid redirect URL for success login', 400); + } + + if (!empty($state['failure']) && !$validateURL->isValid($state['failure'])) { + throw new Exception('Invalid redirect URL for failure login', 400); + } + + $state['failure'] = null; + $accessToken = $oauth2->getAccessToken($code); + + if (empty($accessToken)) { + if (!empty($state['failure'])) { + $response->redirect($state['failure'], 301, 0); } - $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); + throw new Exception('Failed to obtain access token'); + } - if (!\class_exists($classname)) { - throw new Exception('Provider is not supported', 501); + $oauth2ID = $oauth2->getUserID($accessToken); + + if (empty($oauth2ID)) { + if (!empty($state['failure'])) { + $response->redirect($state['failure'], 301, 0); } - $oauth2 = new $classname($appId, $appSecret, $callback); + throw new Exception('Missing ID from OAuth2 provider', 400); + } - if (!empty($state)) { - try { - $state = \array_merge($defaultState, $oauth2->parseState($state)); - } catch (\Exception $exception) { - throw new Exception('Failed to parse login state params as passed from OAuth2 provider'); - } - } else { - $state = $defaultState; - } + $current = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret); - if (!$validateURL->isValid($state['success'])) { - throw new Exception('Invalid redirect URL for success login', 400); - } + if ($current) { + $projectDB->deleteDocument($current); //throw new Exception('User already logged in', 401); + } - if (!empty($state['failure']) && !$validateURL->isValid($state['failure'])) { - throw new Exception('Invalid redirect URL for failure login', 400); - } - - $state['failure'] = null; - $accessToken = $oauth2->getAccessToken($code); + $user = (empty($user->getId())) ? $projectDB->getCollectionFirst([ // Get user by provider id + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'oauth2'.\ucfirst($provider).'='.$oauth2ID, + ], + ]) : $user; - if (empty($accessToken)) { - if (!empty($state['failure'])) { - $response->redirect($state['failure'], 301, 0); - } + if (empty($user)) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email + $name = $oauth2->getUserName($accessToken); + $email = $oauth2->getUserEmail($accessToken); - throw new Exception('Failed to obtain access token'); - } - - $oauth2ID = $oauth2->getUserID($accessToken); - - if (empty($oauth2ID)) { - if (!empty($state['failure'])) { - $response->redirect($state['failure'], 301, 0); - } - - throw new Exception('Missing ID from OAuth2 provider', 400); - } - - $current = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret); - - if ($current) { - $projectDB->deleteDocument($current); //throw new Exception('User already logged in', 401); - } - - $user = (empty($user->getId())) ? $projectDB->getCollectionFirst([ // Get user by provider id + $user = $projectDB->getCollectionFirst([ // Get user by provider email address 'limit' => 1, 'filters' => [ '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'oauth2'.\ucfirst($provider).'='.$oauth2ID, + 'email='.$email, ], - ]) : $user; - - if (empty($user)) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email - $name = $oauth2->getUserName($accessToken); - $email = $oauth2->getUserEmail($accessToken); - - $user = $projectDB->getCollectionFirst([ // Get user by provider email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); - - if (!$user || empty($user->getId())) { // Last option -> create user alone, generate random password - Authorization::disable(); - - try { - $user = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_USERS, - '$permissions' => ['read' => ['*'], 'write' => ['user:{self}']], - 'email' => $email, - 'emailVerification' => true, - 'status' => Auth::USER_STATUS_ACTIVATED, // Email should already be authenticated by OAuth2 provider - 'password' => Auth::passwordHash(Auth::passwordGenerator()), - 'password-update' => \time(), - 'registration' => \time(), - 'reset' => false, - 'name' => $name, - ], ['email' => $email]); - } catch (Duplicate $th) { - throw new Exception('Account already exists', 409); - } - - Authorization::enable(); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - } - } - - // Create session token, verify user account and update OAuth2 ID and Access Token - - $secret = Auth::tokenGenerator(); - $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; - $session = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, - '$permissions' => ['read' => ['user:'.$user['$id']], 'write' => ['user:'.$user['$id']]], - 'type' => Auth::TOKEN_TYPE_LOGIN, - 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak - 'expire' => $expiry, - 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), - 'ip' => $request->getIP(), ]); - $user - ->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID) - ->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken) - ->setAttribute('status', Auth::USER_STATUS_ACTIVATED) - ->setAttribute('tokens', $session, Document::SET_TYPE_APPEND) - ; + if (!$user || empty($user->getId())) { // Last option -> create user alone, generate random password + Authorization::disable(); - Authorization::setRole('user:'.$user->getId()); + try { + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => ['read' => ['*'], 'write' => ['user:{self}']], + 'email' => $email, + 'emailVerification' => true, + 'status' => Auth::USER_STATUS_ACTIVATED, // Email should already be authenticated by OAuth2 provider + 'password' => Auth::passwordHash(Auth::passwordGenerator()), + 'password-update' => \time(), + 'registration' => \time(), + 'reset' => false, + 'name' => $name, + ], ['email' => $email]); + } catch (Duplicate $th) { + throw new Exception('Account already exists', 409); + } - $user = $projectDB->updateDocument($user->getArrayCopy()); + Authorization::enable(); - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } } + } - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.sessions.create') - ->setParam('resource', 'users/'.$user->getId()) - ->setParam('data', ['provider' => $provider]) - ; + // Create session token, verify user account and update OAuth2 ID and Access Token - if (!Config::getParam('domainVerification')) { - $response - ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) - ; - } - - // Add keys for non-web platforms - TODO - add verification phase to aviod session sniffing - if (parse_url($state['success'], PHP_URL_PATH) === $oauthDefaultSuccess) { - $state['success'] = URLParser::parse($state['success']); - $query = URLParser::parseQuery($state['success']['query']); - $query['project'] = $project->getId(); - $query['domain'] = COOKIE_DOMAIN; - $query['key'] = Auth::$cookieName; - $query['secret'] = Auth::encodeSession($user->getId(), $secret); - $state['success']['query'] = URLParser::unparseQuery($query); - $state['success'] = URLParser::unparse($state['success']); - } + $secret = Auth::tokenGenerator(); + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $session = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:'.$user['$id']], 'write' => ['user:'.$user['$id']]], + 'type' => Auth::TOKEN_TYPE_LOGIN, + 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak + 'expire' => $expiry, + 'userAgent' => $request->getUserAgent('UNKNOWN'), + 'ip' => $request->getIP(), + ]); + $user + ->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID) + ->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken) + ->setAttribute('status', Auth::USER_STATUS_ACTIVATED) + ->setAttribute('tokens', $session, Document::SET_TYPE_APPEND) + ; + + Authorization::setRole('user:'.$user->getId()); + + $user = $projectDB->updateDocument($user->getArrayCopy()); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.sessions.create') + ->setParam('resource', 'users/'.$user->getId()) + ->setParam('data', ['provider' => $provider]) + ; + + if (!Config::getParam('domainVerification')) { $response - ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - ->addHeader('Pragma', 'no-cache') - ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) - ->redirect($state['success']) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) ; } - ); + + // Add keys for non-web platforms - TODO - add verification phase to aviod session sniffing + if (parse_url($state['success'], PHP_URL_PATH) === $oauthDefaultSuccess) { + $state['success'] = URLParser::parse($state['success']); + $query = URLParser::parseQuery($state['success']['query']); + $query['project'] = $project->getId(); + $query['domain'] = Config::getParam('cookieDomain'); + $query['key'] = Auth::$cookieName; + $query['secret'] = Auth::encodeSession($user->getId(), $secret); + $state['success']['query'] = URLParser::unparseQuery($query); + $state['success'] = URLParser::unparse($state['success']); + } -$utopia->get('/v1/account') + $response + ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->addHeader('Pragma', 'no-cache') + ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->redirect($state['success']) + ; + }, ['request', 'response', 'project', 'user', 'projectDB', 'audits']); + +App::get('/v1/account') ->desc('Get Account') ->groups(['api', 'account']) ->label('scope', 'account') @@ -536,22 +552,23 @@ $utopia->get('/v1/account') ->label('sdk.method', 'get') ->label('sdk.description', '/docs/references/account/get.md') ->label('sdk.response', ['200' => 'user']) - ->action( - function () use ($response, &$user, $oauth2Keys) { - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'emailVerification', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); - } - ); + ->action(function ($response, $user) use ($oauth2Keys) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ -$utopia->get('/v1/account/prefs') + $response->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'email', + 'emailVerification', + 'registration', + 'name', + ], + $oauth2Keys + )), ['roles' => Authorization::getRoles()])); + }, ['response', 'user']); + +App::get('/v1/account/prefs') ->desc('Get Account Preferences') ->groups(['api', 'account']) ->label('scope', 'account') @@ -559,22 +576,23 @@ $utopia->get('/v1/account/prefs') ->label('sdk.namespace', 'account') ->label('sdk.method', 'getPrefs') ->label('sdk.description', '/docs/references/account/get-prefs.md') - ->action( - function () use ($response, $user) { - $prefs = $user->getAttribute('prefs', '{}'); + ->action(function ($response, $user) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ - try { - $prefs = \json_decode($prefs, true); - $prefs = ($prefs) ? $prefs : []; - } catch (\Exception $error) { - throw new Exception('Failed to parse prefs', 500); - } + $prefs = $user->getAttribute('prefs', '{}'); - $response->json($prefs); + try { + $prefs = \json_decode($prefs, true); + $prefs = ($prefs) ? $prefs : []; + } catch (\Exception $error) { + throw new Exception('Failed to parse prefs', 500); } - ); -$utopia->get('/v1/account/sessions') + $response->json($prefs); + }, ['response', 'user']); + +App::get('/v1/account/sessions') ->desc('Get Account Sessions') ->groups(['api', 'account']) ->label('scope', 'account') @@ -582,58 +600,60 @@ $utopia->get('/v1/account/sessions') ->label('sdk.namespace', 'account') ->label('sdk.method', 'getSessions') ->label('sdk.description', '/docs/references/account/get-sessions.md') - ->action( - function () use ($response, $user) { - $tokens = $user->getAttribute('tokens', []); - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $sessions = []; - $current = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_LOGIN, Auth::$secret); - $index = 0; - $countries = Locale::getText('countries'); + ->action(function ($response, $user, $locale, $geodb) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Locale\Locale $locale */ + /** @var GeoIp2\Database\Reader $geodb */ - foreach ($tokens as $token) { /* @var $token Document */ - if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) { - continue; - } + $tokens = $user->getAttribute('tokens', []); + $sessions = []; + $current = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_LOGIN, Auth::$secret); + $index = 0; + $countries = $locale->getText('countries'); - $userAgent = (!empty($token->getAttribute('userAgent'))) ? $token->getAttribute('userAgent') : 'UNKNOWN'; - - $dd = new DeviceDetector($userAgent); - - // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - // $dd->skipBotDetection(); - - $dd->parse(); - - $sessions[$index] = [ - '$id' => $token->getId(), - 'OS' => $dd->getOs(), - 'client' => $dd->getClient(), - 'device' => $dd->getDevice(), - 'brand' => $dd->getBrand(), - 'model' => $dd->getModel(), - 'ip' => $token->getAttribute('ip', ''), - 'geo' => [], - 'current' => ($current == $token->getId()) ? true : false, - ]; - - try { - $record = $reader->country($token->getAttribute('ip', '')); - $sessions[$index]['geo']['isoCode'] = \strtolower($record->country->isoCode); - $sessions[$index]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); - } catch (\Exception $e) { - $sessions[$index]['geo']['isoCode'] = '--'; - $sessions[$index]['geo']['country'] = Locale::getText('locale.country.unknown'); - } - - ++$index; + foreach ($tokens as $token) { /* @var $token Document */ + if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) { + continue; } - $response->json($sessions); - } - ); + $userAgent = (!empty($token->getAttribute('userAgent'))) ? $token->getAttribute('userAgent') : 'UNKNOWN'; -$utopia->get('/v1/account/logs') + $dd = new DeviceDetector($userAgent); + + // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + // $dd->skipBotDetection(); + + $dd->parse(); + + $sessions[$index] = [ + '$id' => $token->getId(), + 'OS' => $dd->getOs(), + 'client' => $dd->getClient(), + 'device' => $dd->getDevice(), + 'brand' => $dd->getBrand(), + 'model' => $dd->getModel(), + 'ip' => $token->getAttribute('ip', ''), + 'geo' => [], + 'current' => ($current == $token->getId()) ? true : false, + ]; + + try { + $record = $geodb->country($token->getAttribute('ip', '')); + $sessions[$index]['geo']['isoCode'] = \strtolower($record->country->isoCode); + $sessions[$index]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : $locale->getText('locale.country.unknown'); + } catch (\Exception $e) { + $sessions[$index]['geo']['isoCode'] = '--'; + $sessions[$index]['geo']['country'] = $locale->getText('locale.country.unknown'); + } + + ++$index; + } + + $response->json($sessions); + }, ['response', 'user', 'locale', 'geodb']); + +App::get('/v1/account/logs') ->desc('Get Account Logs') ->groups(['api', 'account']) ->label('scope', 'account') @@ -641,72 +661,75 @@ $utopia->get('/v1/account/logs') ->label('sdk.namespace', 'account') ->label('sdk.method', 'getLogs') ->label('sdk.description', '/docs/references/account/get-logs.md') - ->action( - function () use ($response, $register, $project, $user) { - $adapter = new AuditAdapter($register->get('db')); - $adapter->setNamespace('app_'.$project->getId()); + ->action(function ($response, $register, $project, $user, $locale, $geodb) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Locale\Locale $locale */ + /** @var GeoIp2\Database\Reader $geodb */ - $audit = new Audit($adapter); - $countries = Locale::getText('countries'); + $adapter = new AuditAdapter($register->get('db')); + $adapter->setNamespace('app_'.$project->getId()); - $logs = $audit->getLogsByUserAndActions($user->getId(), [ - 'account.create', - 'account.delete', - 'account.update.name', - 'account.update.email', - 'account.update.password', - 'account.update.prefs', - 'account.sessions.create', - 'account.sessions.delete', - 'account.recovery.create', - 'account.recovery.update', - 'account.verification.create', - 'account.verification.update', - 'teams.membership.create', - 'teams.membership.update', - 'teams.membership.delete', - ]); + $audit = new Audit($adapter); + $countries = $locale->getText('countries'); - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $output = []; + $logs = $audit->getLogsByUserAndActions($user->getId(), [ + 'account.create', + 'account.delete', + 'account.update.name', + 'account.update.email', + 'account.update.password', + 'account.update.prefs', + 'account.sessions.create', + 'account.sessions.delete', + 'account.recovery.create', + 'account.recovery.update', + 'account.verification.create', + 'account.verification.update', + 'teams.membership.create', + 'teams.membership.update', + 'teams.membership.delete', + ]); - foreach ($logs as $i => &$log) { - $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; + $output = []; - $dd = new DeviceDetector($log['userAgent']); + foreach ($logs as $i => &$log) { + $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; - $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + $dd = new DeviceDetector($log['userAgent']); - $dd->parse(); + $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - $output[$i] = [ - 'event' => $log['event'], - 'ip' => $log['ip'], - 'time' => \strtotime($log['time']), - 'OS' => $dd->getOs(), - 'client' => $dd->getClient(), - 'device' => $dd->getDevice(), - 'brand' => $dd->getBrand(), - 'model' => $dd->getModel(), - 'geo' => [], - ]; + $dd->parse(); - try { - $record = $reader->country($log['ip']); - $output[$i]['geo']['isoCode'] = \strtolower($record->country->isoCode); - $output[$i]['geo']['country'] = $record->country->name; - $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); - } catch (\Exception $e) { - $output[$i]['geo']['isoCode'] = '--'; - $output[$i]['geo']['country'] = Locale::getText('locale.country.unknown'); - } + $output[$i] = [ + 'event' => $log['event'], + 'ip' => $log['ip'], + 'time' => \strtotime($log['time']), + 'OS' => $dd->getOs(), + 'client' => $dd->getClient(), + 'device' => $dd->getDevice(), + 'brand' => $dd->getBrand(), + 'model' => $dd->getModel(), + 'geo' => [], + ]; + + try { + $record = $geodb->country($log['ip']); + $output[$i]['geo']['isoCode'] = \strtolower($record->country->isoCode); + $output[$i]['geo']['country'] = $record->country->name; + $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : $locale->getText('locale.country.unknown'); + } catch (\Exception $e) { + $output[$i]['geo']['isoCode'] = '--'; + $output[$i]['geo']['country'] = $locale->getText('locale.country.unknown'); } - - $response->json($output); } - ); -$utopia->patch('/v1/account/name') + $response->json($output); + }, ['response', 'register', 'project', 'user', 'locale', 'geodb']); + +App::patch('/v1/account/name') ->desc('Update Account Name') ->groups(['api', 'account']) ->label('webhook', 'account.update.name') @@ -716,35 +739,38 @@ $utopia->patch('/v1/account/name') ->label('sdk.method', 'updateName') ->label('sdk.description', '/docs/references/account/update-name.md') ->param('name', '', function () { return new Text(100); }, 'User name.') - ->action( - function ($name) use ($response, $user, $projectDB, $audit, $oauth2Keys) { - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'name' => $name, - ])); + ->action(function ($name, $response, $user, $projectDB, $audits) use ($oauth2Keys) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'name' => $name, + ])); - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.update.name') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); } - ); -$utopia->patch('/v1/account/password') + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.update.name') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $response->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'email', + 'registration', + 'name', + ], + $oauth2Keys + )), ['roles' => Authorization::getRoles()])); + }, ['response', 'user', 'projectDB', 'audits']); + +App::patch('/v1/account/password') ->desc('Update Account Password') ->groups(['api', 'account']) ->label('webhook', 'account.update.password') @@ -755,39 +781,42 @@ $utopia->patch('/v1/account/password') ->label('sdk.description', '/docs/references/account/update-password.md') ->param('password', '', function () { return new Password(); }, 'New user password. Must be between 6 to 32 chars.') ->param('oldPassword', '', function () { return new Password(); }, 'Old user password. Must be between 6 to 32 chars.') - ->action( - function ($password, $oldPassword) use ($response, $user, $projectDB, $audit, $oauth2Keys) { - if (!Auth::passwordVerify($oldPassword, $user->getAttribute('password'))) { // Double check user password - throw new Exception('Invalid credentials', 401); - } + ->action(function ($password, $oldPassword, $response, $user, $projectDB, $audits) use ($oauth2Keys) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'password' => Auth::passwordHash($password), - ])); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.update.password') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + if (!Auth::passwordVerify($oldPassword, $user->getAttribute('password'))) { // Double check user password + throw new Exception('Invalid credentials', 401); } - ); -$utopia->patch('/v1/account/email') + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'password' => Auth::passwordHash($password), + ])); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.update.password') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $response->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'email', + 'registration', + 'name', + ], + $oauth2Keys + )), ['roles' => Authorization::getRoles()])); + }, ['response', 'user', 'projectDB', 'audits']); + +App::patch('/v1/account/email') ->desc('Update Account Email') ->groups(['api', 'account']) ->label('webhook', 'account.update.email') @@ -798,54 +827,57 @@ $utopia->patch('/v1/account/email') ->label('sdk.description', '/docs/references/account/update-email.md') ->param('email', '', function () { return new Email(); }, 'User email.') ->param('password', '', function () { return new Password(); }, 'User password. Must be between 6 to 32 chars.') - ->action( - function ($email, $password) use ($response, $user, $projectDB, $audit, $oauth2Keys) { - if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password - throw new Exception('Invalid credentials', 401); - } + ->action(function ($email, $password, $response, $user, $projectDB, $audits) use ($oauth2Keys) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); - - if (!empty($profile)) { - throw new Exception('User already registered', 400); - } - - // TODO after this user needs to confirm mail again - - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'email' => $email, - 'emailVerification' => false, - ])); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.update.email') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password + throw new Exception('Invalid credentials', 401); } - ); -$utopia->patch('/v1/account/prefs') + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); + + if (!empty($profile)) { + throw new Exception('User already registered', 400); + } + + // TODO after this user needs to confirm mail again + + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'email' => $email, + 'emailVerification' => false, + ])); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.update.email') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $response->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'email', + 'registration', + 'name', + ], + $oauth2Keys + )), ['roles' => Authorization::getRoles()])); + }, ['response', 'user', 'projectDB', 'audits']); + +App::patch('/v1/account/prefs') ->desc('Update Account Preferences') ->groups(['api', 'account']) ->label('webhook', 'account.update.prefs') @@ -855,38 +887,41 @@ $utopia->patch('/v1/account/prefs') ->label('sdk.method', 'updatePrefs') ->param('prefs', '', function () { return new Assoc();}, 'Prefs key-value JSON object.') ->label('sdk.description', '/docs/references/account/update-prefs.md') - ->action( - function ($prefs) use ($response, $user, $projectDB, $audit) { - $old = \json_decode($user->getAttribute('prefs', '{}'), true); - $old = ($old) ? $old : []; + ->action(function ($prefs, $response, $user, $projectDB, $audits) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'prefs' => \json_encode(\array_merge($old, $prefs)), - ])); + $old = \json_decode($user->getAttribute('prefs', '{}'), true); + $old = ($old) ? $old : []; - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'prefs' => \json_encode(\array_merge($old, $prefs)), + ])); - $audit - ->setParam('event', 'account.update.prefs') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $prefs = $user->getAttribute('prefs', '{}'); - - try { - $prefs = \json_decode($prefs, true); - $prefs = ($prefs) ? $prefs : []; - } catch (\Exception $error) { - throw new Exception('Failed to parse prefs', 500); - } - - $response->json($prefs); + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); } - ); -$utopia->delete('/v1/account') + $audits + ->setParam('event', 'account.update.prefs') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $prefs = $user->getAttribute('prefs', '{}'); + + try { + $prefs = \json_decode($prefs, true); + $prefs = ($prefs) ? $prefs : []; + } catch (\Exception $error) { + throw new Exception('Failed to parse prefs', 500); + } + + $response->json($prefs); + }, ['response', 'user', 'projectDB', 'audits']); + +App::delete('/v1/account') ->desc('Delete Account') ->groups(['api', 'account']) ->label('webhook', 'account.delete') @@ -895,54 +930,59 @@ $utopia->delete('/v1/account') ->label('sdk.namespace', 'account') ->label('sdk.method', 'delete') ->label('sdk.description', '/docs/references/account/delete.md') - ->action( - function () use ($response, $user, $projectDB, $audit, $webhook) { - $protocol = Config::getParam('protocol'); - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'status' => Auth::USER_STATUS_BLOCKED, - ])); + ->action(function ($request, $response, $user, $projectDB, $audits, $webhooks) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Event\Event $webhooks */ - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } + $protocol = $request->getProtocol(); + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'status' => Auth::USER_STATUS_BLOCKED, + ])); - //TODO delete all tokens or only current session? - //TODO delete all user data according to GDPR. Make sure everything is backed up and backups are deleted later - /* - * Data to delete - * * Tokens - * * Memberships - */ + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.delete') - ->setParam('resource', 'users/'.$user->getId()) - ->setParam('data', $user->getArrayCopy()) - ; + //TODO delete all tokens or only current session? + //TODO delete all user data according to GDPR. Make sure everything is backed up and backups are deleted later + /* + * Data to delete + * * Tokens + * * Memberships + */ - $webhook - ->setParam('payload', [ - 'name' => $user->getAttribute('name', ''), - 'email' => $user->getAttribute('email', ''), - ]) - ; + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.delete') + ->setParam('resource', 'users/'.$user->getId()) + ->setParam('data', $user->getArrayCopy()) + ; - if (!Config::getParam('domainVerification')) { - $response - ->addHeader('X-Fallback-Cookies', \json_encode([])) - ; - } + $webhooks + ->setParam('payload', [ + 'name' => $user->getAttribute('name', ''), + 'email' => $user->getAttribute('email', ''), + ]) + ; + if (!Config::getParam('domainVerification')) { $response - ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) - ->noContent() + ->addHeader('X-Fallback-Cookies', \json_encode([])) ; } - ); -$utopia->delete('/v1/account/sessions/:sessionId') + $response + ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->noContent() + ; + }, ['request', 'response', 'user', 'projectDB', 'audits', 'webhooks']); + +App::delete('/v1/account/sessions/:sessionId') ->desc('Delete Account Session') ->groups(['api', 'account']) ->label('scope', 'account') @@ -953,82 +993,34 @@ $utopia->delete('/v1/account/sessions/:sessionId') ->label('sdk.description', '/docs/references/account/delete-session.md') ->label('abuse-limit', 100) ->param('sessionId', null, function () { return new UID(); }, 'Session unique ID. Use the string \'current\' to delete the current device session.') - ->action( - function ($sessionId) use ($response, $user, $projectDB, $webhook, $audit) { - $protocol = Config::getParam('protocol'); - $sessionId = ($sessionId === 'current') - ? Auth::tokenVerify($user->getAttribute('tokens'), Auth::TOKEN_TYPE_LOGIN, Auth::$secret) - : $sessionId; - - $tokens = $user->getAttribute('tokens', []); + ->action(function ($sessionId, $request, $response, $user, $projectDB, $audits, $webhooks) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Event\Event $webhooks */ - foreach ($tokens as $token) { /* @var $token Document */ - if (($sessionId == $token->getId()) && Auth::TOKEN_TYPE_LOGIN == $token->getAttribute('type')) { - if (!$projectDB->deleteDocument($token->getId())) { - throw new Exception('Failed to remove token from DB', 500); - } + $protocol = $request->getProtocol(); + $sessionId = ($sessionId === 'current') + ? Auth::tokenVerify($user->getAttribute('tokens'), Auth::TOKEN_TYPE_LOGIN, Auth::$secret) + : $sessionId; + + $tokens = $user->getAttribute('tokens', []); - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.sessions.delete') - ->setParam('resource', '/user/'.$user->getId()) - ; - - $webhook - ->setParam('payload', [ - 'name' => $user->getAttribute('name', ''), - 'email' => $user->getAttribute('email', ''), - ]) - ; - - if (!Config::getParam('domainVerification')) { - $response - ->addHeader('X-Fallback-Cookies', \json_encode([])) - ; - } - - if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too - $response - ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) - ; - } - - return $response->noContent(); - } - } - - throw new Exception('Session not found', 404); - } - ); - -$utopia->delete('/v1/account/sessions') - ->desc('Delete All Account Sessions') - ->groups(['api', 'account']) - ->label('scope', 'account') - ->label('webhook', 'account.sessions.delete') - ->label('sdk.platform', [APP_PLATFORM_CLIENT]) - ->label('sdk.namespace', 'account') - ->label('sdk.method', 'deleteSessions') - ->label('sdk.description', '/docs/references/account/delete-sessions.md') - ->label('abuse-limit', 100) - ->action( - function () use ($response, $user, $projectDB, $audit, $webhook) { - $protocol = Config::getParam('protocol'); - $tokens = $user->getAttribute('tokens', []); - - foreach ($tokens as $token) { /* @var $token Document */ + foreach ($tokens as $token) { /* @var $token Document */ + if (($sessionId == $token->getId()) && Auth::TOKEN_TYPE_LOGIN == $token->getAttribute('type')) { if (!$projectDB->deleteDocument($token->getId())) { throw new Exception('Failed to remove token from DB', 500); } - $audit + $audits ->setParam('userId', $user->getId()) ->setParam('event', 'account.sessions.delete') ->setParam('resource', '/user/'.$user->getId()) ; - $webhook + $webhooks ->setParam('payload', [ 'name' => $user->getAttribute('name', ''), 'email' => $user->getAttribute('email', ''), @@ -1043,17 +1035,75 @@ $utopia->delete('/v1/account/sessions') if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too $response - ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) + ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; } + + return $response->noContent(); + } + } + + throw new Exception('Session not found', 404); + }, ['request', 'response', 'user', 'projectDB', 'audits', 'webhooks']); + +App::delete('/v1/account/sessions') + ->desc('Delete All Account Sessions') + ->groups(['api', 'account']) + ->label('scope', 'account') + ->label('webhook', 'account.sessions.delete') + ->label('sdk.platform', [APP_PLATFORM_CLIENT]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'deleteSessions') + ->label('sdk.description', '/docs/references/account/delete-sessions.md') + ->label('abuse-limit', 100) + ->action(function ($request, $response, $user, $projectDB, $audits, $webhooks) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Event\Event $webhooks */ + + $protocol = $request->getProtocol(); + $tokens = $user->getAttribute('tokens', []); + + foreach ($tokens as $token) { /* @var $token Document */ + if (!$projectDB->deleteDocument($token->getId())) { + throw new Exception('Failed to remove token from DB', 500); } - $response->noContent(); - } - ); + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.sessions.delete') + ->setParam('resource', '/user/'.$user->getId()) + ; -$utopia->post('/v1/account/recovery') + $webhooks + ->setParam('payload', [ + 'name' => $user->getAttribute('name', ''), + 'email' => $user->getAttribute('email', ''), + ]) + ; + + if (!Config::getParam('domainVerification')) { + $response + ->addHeader('X-Fallback-Cookies', \json_encode([])) + ; + } + + if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + $response + ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ; + } + } + + $response->noContent(); + }, ['request', 'response', 'user', 'projectDB', 'audits', 'webhooks']); + +App::post('/v1/account/recovery') ->desc('Create Password Recovery') ->groups(['api', 'account']) ->label('scope', 'public') @@ -1064,95 +1114,102 @@ $utopia->post('/v1/account/recovery') ->label('abuse-limit', 10) ->label('abuse-key', 'url:{url},email:{param-email}') ->param('email', '', function () { return new Email(); }, 'User email.') - ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.') - ->action( - function ($email, $url) use ($request, $response, $projectDB, $mail, $audit, $project) { - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); + ->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) + ->action(function ($email, $url, $request, $response, $projectDB, $project, $locale, $mails, $audits) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Database\Document $project */ + /** @var Utopia\Locale\Locale $locale */ + /** @var Appwrite\Event\Event $mails */ + /** @var Appwrite\Event\Event $audits */ - if (empty($profile)) { - throw new Exception('User not found', 404); // TODO maybe hide this - } + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); - $secret = Auth::tokenGenerator(); - $recovery = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, - '$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]], - 'type' => Auth::TOKEN_TYPE_RECOVERY, - 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak - 'expire' => \time() + Auth::TOKEN_EXPIRATION_RECOVERY, - 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), - 'ip' => $request->getIP(), - ]); - - Authorization::setRole('user:'.$profile->getId()); - - $recovery = $projectDB->createDocument($recovery->getArrayCopy()); - - if (false === $recovery) { - throw new Exception('Failed saving recovery to DB', 500); - } - - $profile->setAttribute('tokens', $recovery, Document::SET_TYPE_APPEND); - - $profile = $projectDB->updateDocument($profile->getArrayCopy()); - - if (false === $profile) { - throw new Exception('Failed to save user to DB', 500); - } - - $url = Template::parseURL($url); - $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $profile->getId(), 'secret' => $secret]); - $url = Template::unParseURL($url); - - $body = new Template(__DIR__.'/../../config/locales/templates/_base.tpl'); - $content = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('account.emails.recovery.body')); - $cta = new Template(__DIR__.'/../../config/locales/templates/_cta.tpl'); - - $body - ->setParam('{{content}}', $content->render()) - ->setParam('{{cta}}', $cta->render()) - ->setParam('{{title}}', Locale::getText('account.emails.recovery.title')) - ->setParam('{{direction}}', Locale::getText('settings.direction')) - ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) - ->setParam('{{name}}', $profile->getAttribute('name')) - ->setParam('{{redirect}}', $url) - ->setParam('{{bg-body}}', '#f6f6f6') - ->setParam('{{bg-content}}', '#ffffff') - ->setParam('{{bg-cta}}', '#3498db') - ->setParam('{{bg-cta-hover}}', '#34495e') - ->setParam('{{text-content}}', '#000000') - ->setParam('{{text-cta}}', '#ffffff') - ; - - $mail - ->setParam('event', 'account.recovery.create') - ->setParam('recipient', $profile->getAttribute('email', '')) - ->setParam('name', $profile->getAttribute('name', '')) - ->setParam('subject', Locale::getText('account.emails.recovery.title')) - ->setParam('body', $body->render()) - ->trigger(); - ; - - $audit - ->setParam('userId', $profile->getId()) - ->setParam('event', 'account.recovery.create') - ->setParam('resource', 'users/'.$profile->getId()) - ; - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($recovery->getArrayCopy(['$id', 'type', 'expire'])) - ; + if (empty($profile)) { + throw new Exception('User not found', 404); // TODO maybe hide this } - ); -$utopia->put('/v1/account/recovery') + $secret = Auth::tokenGenerator(); + $recovery = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]], + 'type' => Auth::TOKEN_TYPE_RECOVERY, + 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak + 'expire' => \time() + Auth::TOKEN_EXPIRATION_RECOVERY, + 'userAgent' => $request->getUserAgent('UNKNOWN'), + 'ip' => $request->getIP(), + ]); + + Authorization::setRole('user:'.$profile->getId()); + + $recovery = $projectDB->createDocument($recovery->getArrayCopy()); + + if (false === $recovery) { + throw new Exception('Failed saving recovery to DB', 500); + } + + $profile->setAttribute('tokens', $recovery, Document::SET_TYPE_APPEND); + + $profile = $projectDB->updateDocument($profile->getArrayCopy()); + + if (false === $profile) { + throw new Exception('Failed to save user to DB', 500); + } + + $url = Template::parseURL($url); + $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $profile->getId(), 'secret' => $secret]); + $url = Template::unParseURL($url); + + $body = new Template(__DIR__.'/../../config/locale/templates/email-base.tpl'); + $content = new Template(__DIR__.'/../../config/locale/translations/templates/'.$locale->getText('account.emails.recovery.body')); + $cta = new Template(__DIR__.'/../../config/locale/templates/email-cta.tpl'); + + $body + ->setParam('{{content}}', $content->render()) + ->setParam('{{cta}}', $cta->render()) + ->setParam('{{title}}', $locale->getText('account.emails.recovery.title')) + ->setParam('{{direction}}', $locale->getText('settings.direction')) + ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) + ->setParam('{{name}}', $profile->getAttribute('name')) + ->setParam('{{redirect}}', $url) + ->setParam('{{bg-body}}', '#f6f6f6') + ->setParam('{{bg-content}}', '#ffffff') + ->setParam('{{bg-cta}}', '#3498db') + ->setParam('{{bg-cta-hover}}', '#34495e') + ->setParam('{{text-content}}', '#000000') + ->setParam('{{text-cta}}', '#ffffff') + ; + + $mails + ->setParam('event', 'account.recovery.create') + ->setParam('from', ($project->getId() === 'console') ? '' : \sprintf($locale->getText('account.emails.team'), $project->getAttribute('name'))) + ->setParam('recipient', $profile->getAttribute('email', '')) + ->setParam('name', $profile->getAttribute('name', '')) + ->setParam('subject', $locale->getText('account.emails.recovery.title')) + ->setParam('body', $body->render()) + ->trigger(); + ; + + $audits + ->setParam('userId', $profile->getId()) + ->setParam('event', 'account.recovery.create') + ->setParam('resource', 'users/'.$profile->getId()) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($recovery->getArrayCopy(['$id', 'type', 'expire'])) + ; + }, ['request', 'response', 'projectDB', 'project', 'locale', 'mails', 'audits']); + +App::put('/v1/account/recovery') ->desc('Complete Password Recovery') ->groups(['api', 'account']) ->label('scope', 'public') @@ -1166,63 +1223,65 @@ $utopia->put('/v1/account/recovery') ->param('secret', '', function () { return new Text(256); }, 'Valid reset token.') ->param('password', '', function () { return new Password(); }, 'New password. Must be between 6 to 32 chars.') ->param('passwordAgain', '', function () {return new Password(); }, 'New password again. Must be between 6 to 32 chars.') - ->action( - function ($userId, $secret, $password, $passwordAgain) use ($response, $projectDB, $audit) { - if ($password !== $passwordAgain) { - throw new Exception('Passwords must match', 400); - } - - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - '$id='.$userId, - ], - ]); - - if (empty($profile)) { - throw new Exception('User not found', 404); // TODO maybe hide this - } - - $recovery = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_RECOVERY, $secret); - - if (!$recovery) { - throw new Exception('Invalid recovery token', 401); - } - - Authorization::setRole('user:'.$profile->getId()); - - $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ - 'password' => Auth::passwordHash($password), - 'password-update' => \time(), - 'emailVerification' => true, - ])); - - if (false === $profile) { - throw new Exception('Failed saving user to DB', 500); - } - - /** - * We act like we're updating and validating - * the recovery token but actually we don't need it anymore. - */ - if (!$projectDB->deleteDocument($recovery)) { - throw new Exception('Failed to remove recovery from DB', 500); - } - - $audit - ->setParam('userId', $profile->getId()) - ->setParam('event', 'account.recovery.update') - ->setParam('resource', 'users/'.$profile->getId()) - ; - - $recovery = $profile->search('$id', $recovery, $profile->getAttribute('tokens', [])); - - $response->json($recovery->getArrayCopy(['$id', 'type', 'expire'])); + ->action(function ($userId, $secret, $password, $passwordAgain, $response, $projectDB, $audits) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ + + if ($password !== $passwordAgain) { + throw new Exception('Passwords must match', 400); } - ); -$utopia->post('/v1/account/verification') + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + '$id='.$userId, + ], + ]); + + if (empty($profile)) { + throw new Exception('User not found', 404); // TODO maybe hide this + } + + $recovery = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_RECOVERY, $secret); + + if (!$recovery) { + throw new Exception('Invalid recovery token', 401); + } + + Authorization::setRole('user:'.$profile->getId()); + + $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ + 'password' => Auth::passwordHash($password), + 'password-update' => \time(), + 'emailVerification' => true, + ])); + + if (false === $profile) { + throw new Exception('Failed saving user to DB', 500); + } + + /** + * We act like we're updating and validating + * the recovery token but actually we don't need it anymore. + */ + if (!$projectDB->deleteDocument($recovery)) { + throw new Exception('Failed to remove recovery from DB', 500); + } + + $audits + ->setParam('userId', $profile->getId()) + ->setParam('event', 'account.recovery.update') + ->setParam('resource', 'users/'.$profile->getId()) + ; + + $recovery = $profile->search('$id', $recovery, $profile->getAttribute('tokens', [])); + + $response->json($recovery->getArrayCopy(['$id', 'type', 'expire'])); + }, ['response', 'projectDB', 'audits']); + +App::post('/v1/account/verification') ->desc('Create Email Verification') ->groups(['api', 'account']) ->label('scope', 'account') @@ -1232,84 +1291,92 @@ $utopia->post('/v1/account/verification') ->label('sdk.description', '/docs/references/account/create-verification.md') ->label('abuse-limit', 10) ->label('abuse-key', 'url:{url},email:{param-email}') - ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.') // TODO add built-in confirm page - ->action( - function ($url) use ($request, $response, $mail, $user, $project, $projectDB, $audit) { - $verificationSecret = Auth::tokenGenerator(); + ->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add built-in confirm page + ->action(function ($url, $request, $response, $project, $user, $projectDB, $locale, $audits, $mails) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Locale\Locale $locale */ + /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Event\Event $mails */ + + $verificationSecret = Auth::tokenGenerator(); + + $verification = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]], + 'type' => Auth::TOKEN_TYPE_VERIFICATION, + 'secret' => Auth::hash($verificationSecret), // On way hash encryption to protect DB leak + 'expire' => \time() + Auth::TOKEN_EXPIRATION_CONFIRM, + 'userAgent' => $request->getUserAgent('UNKNOWN'), + 'ip' => $request->getIP(), + ]); - $verification = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, - '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]], - 'type' => Auth::TOKEN_TYPE_VERIFICATION, - 'secret' => Auth::hash($verificationSecret), // On way hash encryption to protect DB leak - 'expire' => \time() + Auth::TOKEN_EXPIRATION_CONFIRM, - 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), - 'ip' => $request->getIP(), - ]); - - Authorization::setRole('user:'.$user->getId()); + Authorization::setRole('user:'.$user->getId()); - $verification = $projectDB->createDocument($verification->getArrayCopy()); + $verification = $projectDB->createDocument($verification->getArrayCopy()); - if (false === $verification) { - throw new Exception('Failed saving verification to DB', 500); - } - - $user->setAttribute('tokens', $verification, Document::SET_TYPE_APPEND); - - $user = $projectDB->updateDocument($user->getArrayCopy()); - - if (false === $user) { - throw new Exception('Failed to save user to DB', 500); - } - - $url = Template::parseURL($url); - $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $verificationSecret]); - $url = Template::unParseURL($url); - - $body = new Template(__DIR__.'/../../config/locales/templates/_base.tpl'); - $content = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('account.emails.verification.body')); - $cta = new Template(__DIR__.'/../../config/locales/templates/_cta.tpl'); - - $body - ->setParam('{{content}}', $content->render()) - ->setParam('{{cta}}', $cta->render()) - ->setParam('{{title}}', Locale::getText('account.emails.verification.title')) - ->setParam('{{direction}}', Locale::getText('settings.direction')) - ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) - ->setParam('{{name}}', $user->getAttribute('name')) - ->setParam('{{redirect}}', $url) - ->setParam('{{bg-body}}', '#f6f6f6') - ->setParam('{{bg-content}}', '#ffffff') - ->setParam('{{bg-cta}}', '#3498db') - ->setParam('{{bg-cta-hover}}', '#34495e') - ->setParam('{{text-content}}', '#000000') - ->setParam('{{text-cta}}', '#ffffff') - ; - - $mail - ->setParam('event', 'account.verification.create') - ->setParam('recipient', $user->getAttribute('email')) - ->setParam('name', $user->getAttribute('name')) - ->setParam('subject', Locale::getText('account.emails.verification.title')) - ->setParam('body', $body->render()) - ->trigger() - ; - - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'account.verification.create') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($verification->getArrayCopy(['$id', 'type', 'expire'])) - ; + if (false === $verification) { + throw new Exception('Failed saving verification to DB', 500); } - ); -$utopia->put('/v1/account/verification') + $user->setAttribute('tokens', $verification, Document::SET_TYPE_APPEND); + + $user = $projectDB->updateDocument($user->getArrayCopy()); + + if (false === $user) { + throw new Exception('Failed to save user to DB', 500); + } + + $url = Template::parseURL($url); + $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $verificationSecret]); + $url = Template::unParseURL($url); + + $body = new Template(__DIR__.'/../../config/locale/templates/email-base.tpl'); + $content = new Template(__DIR__.'/../../config/locale/translations/templates/'.$locale->getText('account.emails.verification.body')); + $cta = new Template(__DIR__.'/../../config/locale/templates/email-cta.tpl'); + + $body + ->setParam('{{content}}', $content->render()) + ->setParam('{{cta}}', $cta->render()) + ->setParam('{{title}}', $locale->getText('account.emails.verification.title')) + ->setParam('{{direction}}', $locale->getText('settings.direction')) + ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) + ->setParam('{{name}}', $user->getAttribute('name')) + ->setParam('{{redirect}}', $url) + ->setParam('{{bg-body}}', '#f6f6f6') + ->setParam('{{bg-content}}', '#ffffff') + ->setParam('{{bg-cta}}', '#3498db') + ->setParam('{{bg-cta-hover}}', '#34495e') + ->setParam('{{text-content}}', '#000000') + ->setParam('{{text-cta}}', '#ffffff') + ; + + $mails + ->setParam('event', 'account.verification.create') + ->setParam('from', ($project->getId() === 'console') ? '' : \sprintf($locale->getText('account.emails.team'), $project->getAttribute('name'))) + ->setParam('recipient', $user->getAttribute('email')) + ->setParam('name', $user->getAttribute('name')) + ->setParam('subject', $locale->getText('account.emails.verification.title')) + ->setParam('body', $body->render()) + ->trigger() + ; + + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.verification.create') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($verification->getArrayCopy(['$id', 'type', 'expire'])) + ; + }, ['request', 'response', 'project', 'user', 'projectDB', 'locale', 'audits', 'mails']); + +App::put('/v1/account/verification') ->desc('Complete Email Verification') ->groups(['api', 'account']) ->label('scope', 'public') @@ -1321,52 +1388,55 @@ $utopia->put('/v1/account/verification') ->label('abuse-key', 'url:{url},userId:{param-userId}') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') ->param('secret', '', function () { return new Text(256); }, 'Valid verification token.') - ->action( - function ($userId, $secret) use ($response, $user, $projectDB, $audit) { - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - '$id='.$userId, - ], - ]); + ->action(function ($userId, $secret, $response, $user, $projectDB, $audits) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ - if (empty($profile)) { - throw new Exception('User not found', 404); // TODO maybe hide this - } + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + '$id='.$userId, + ], + ]); - $verification = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_VERIFICATION, $secret); - - if (!$verification) { - throw new Exception('Invalid verification token', 401); - } - - Authorization::setRole('user:'.$profile->getId()); - - $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ - 'emailVerification' => true, - ])); - - if (false === $profile) { - throw new Exception('Failed saving user to DB', 500); - } - - /** - * We act like we're updating and validating - * the verification token but actually we don't need it anymore. - */ - if (!$projectDB->deleteDocument($verification)) { - throw new Exception('Failed to remove verification from DB', 500); - } - - $audit - ->setParam('userId', $profile->getId()) - ->setParam('event', 'account.verification.update') - ->setParam('resource', 'users/'.$user->getId()) - ; - - $verification = $profile->search('$id', $verification, $profile->getAttribute('tokens', [])); - - $response->json($verification->getArrayCopy(['$id', 'type', 'expire'])); + if (empty($profile)) { + throw new Exception('User not found', 404); // TODO maybe hide this } - ); \ No newline at end of file + + $verification = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_VERIFICATION, $secret); + + if (!$verification) { + throw new Exception('Invalid verification token', 401); + } + + Authorization::setRole('user:'.$profile->getId()); + + $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ + 'emailVerification' => true, + ])); + + if (false === $profile) { + throw new Exception('Failed saving user to DB', 500); + } + + /** + * We act like we're updating and validating + * the verification token but actually we don't need it anymore. + */ + if (!$projectDB->deleteDocument($verification)) { + throw new Exception('Failed to remove verification from DB', 500); + } + + $audits + ->setParam('userId', $profile->getId()) + ->setParam('event', 'account.verification.update') + ->setParam('resource', 'users/'.$user->getId()) + ; + + $verification = $profile->search('$id', $verification, $profile->getAttribute('tokens', [])); + + $response->json($verification->getArrayCopy(['$id', 'type', 'expire'])); + }, ['response', 'user', 'projectDB', 'audits']); \ No newline at end of file diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 9e888f7f7d..fb8055870c 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -1,7 +1,6 @@ include __DIR__.'/../../config/avatars/browsers.php', - 'credit-cards' => include __DIR__.'/../../config/avatars/credit-cards.php', - 'flags' => include __DIR__.'/../../config/avatars/flags.php', -]; +$avatarCallback = function ($type, $code, $width, $height, $quality, $response) { + /** @var Utopia\Response $response */ -$avatarCallback = function ($type, $code, $width, $height, $quality) use ($types, $response) { $code = \strtolower($code); $type = \strtolower($type); + $set = Config::getParam('avatar-'.$type, []); - if (!\array_key_exists($type, $types)) { + if (empty($set)) { throw new Exception('Avatar set not found', 404); } - if (!\array_key_exists($code, $types[$type])) { + if (!\array_key_exists($code, $set)) { throw new Exception('Avatar not found', 404); } @@ -44,7 +40,7 @@ $avatarCallback = function ($type, $code, $width, $height, $quality) use ($types $output = 'png'; $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache $key = \md5('/v1/avatars/:type/:code-'.$code.$width.$height.$quality.$output); - $path = $types[$type][$code]; + $path = $set[$code]; $type = 'png'; if (!\is_readable($path)) { @@ -57,11 +53,11 @@ $avatarCallback = function ($type, $code, $width, $height, $quality) use ($types if ($data) { //$output = (empty($output)) ? $type : $output; - $response + return $response ->setContentType('image/png') ->addHeader('Expires', $date) ->addHeader('X-Appwrite-Cache', 'hit') - ->send($data, 0) + ->send($data) ; } @@ -71,403 +67,397 @@ $avatarCallback = function ($type, $code, $width, $height, $quality) use ($types $output = (empty($output)) ? $type : $output; + $data = $resize->output($output, $quality); + + $cache->save($key, $data); + $response ->setContentType('image/png') ->addHeader('Expires', $date) ->addHeader('X-Appwrite-Cache', 'miss') - ->send('', null) + ->send($data, null); ; - $data = $resize->output($output, $quality); - - $cache->save($key, $data); - - echo $data; - unset($resize); }; -$utopia->get('/v1/avatars/credit-cards/:code') +App::get('/v1/avatars/credit-cards/:code') ->desc('Get Credit Card Icon') ->groups(['api', 'avatars']) - ->param('code', '', function () use ($types) { return new WhiteList(\array_keys($types['credit-cards'])); }, 'Credit Card Code. Possible values: '.\implode(', ', \array_keys($types['credit-cards'])).'.') - ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getCreditCard') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-credit-card.md') - ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { - return $avatarCallback('credit-cards', $code, $width, $height, $quality); - }); - -$utopia->get('/v1/avatars/browsers/:code') - ->desc('Get Browser Icon') - ->groups(['api', 'avatars']) - ->param('code', '', function () use ($types) { return new WhiteList(\array_keys($types['browsers'])); }, 'Browser Code.') + ->param('code', '', function () { return new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))); }, 'Credit Card Code. Possible values: '.\implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))).'.') ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) + ->action(function ($code, $width, $height, $quality, $response) use ($avatarCallback) { + return $avatarCallback('credit-cards', $code, $width, $height, $quality, $response); + }, ['response']); + +App::get('/v1/avatars/browsers/:code') + ->desc('Get Browser Icon') + ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getBrowser') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-browser.md') - ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { - return $avatarCallback('browsers', $code, $width, $height, $quality); - }); - -$utopia->get('/v1/avatars/flags/:code') - ->desc('Get Country Flag') - ->groups(['api', 'avatars']) - ->param('code', '', function () use ($types) { return new WhiteList(\array_keys($types['flags'])); }, 'Country Code. ISO Alpha-2 country code format.') + ->param('code', '', function () { return new WhiteList(\array_keys(Config::getParam('avatar-browsers'))); }, 'Browser Code.') ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) + ->action(function ($code, $width, $height, $quality, $response) use ($avatarCallback) { + return $avatarCallback('browsers', $code, $width, $height, $quality, $response); + }, ['response']); + +App::get('/v1/avatars/flags/:code') + ->desc('Get Country Flag') + ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getFlag') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-flag.md') - ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { - return $avatarCallback('flags', $code, $width, $height, $quality); - }); + ->param('code', '', function () { return new WhiteList(\array_keys(Config::getParam('avatar-flags'))); }, 'Country Code. ISO Alpha-2 country code format.') + ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) + ->action(function ($code, $width, $height, $quality, $response) use ($avatarCallback) { + return $avatarCallback('flags', $code, $width, $height, $quality, $response); + }, ['response']); -$utopia->get('/v1/avatars/image') +App::get('/v1/avatars/image') ->desc('Get Image from URL') ->groups(['api', 'avatars']) - ->param('url', '', function () { return new URL(); }, 'Image URL which you want to crop.') - ->param('width', 400, function () { return new Range(0, 2000); }, 'Resize preview image width, Pass an integer between 0 to 2000.', true) - ->param('height', 400, function () { return new Range(0, 2000); }, 'Resize preview image height, Pass an integer between 0 to 2000.', true) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getImage') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-image.md') - ->action( - function ($url, $width, $height) use ($response) { - $quality = 80; - $output = 'png'; - $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache - $key = \md5('/v2/avatars/images-'.$url.'-'.$width.'/'.$height.'/'.$quality); - $type = 'png'; - $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-0')); // Limit file number or size - $data = $cache->load($key, 60 * 60 * 24 * 7 /* 1 week */); + ->param('url', '', function () { return new URL(); }, 'Image URL which you want to crop.') + ->param('width', 400, function () { return new Range(0, 2000); }, 'Resize preview image width, Pass an integer between 0 to 2000.', true) + ->param('height', 400, function () { return new Range(0, 2000); }, 'Resize preview image height, Pass an integer between 0 to 2000.', true) + ->action(function ($url, $width, $height, $response) { + /** @var Utopia\Response $response */ - if ($data) { - $response - ->setContentType('image/png') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'hit') - ->send($data, 0) - ; - } + $quality = 80; + $output = 'png'; + $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache + $key = \md5('/v2/avatars/images-'.$url.'-'.$width.'/'.$height.'/'.$quality); + $type = 'png'; + $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-0')); // Limit file number or size + $data = $cache->load($key, 60 * 60 * 24 * 7 /* 1 week */); - if (!\extension_loaded('imagick')) { - throw new Exception('Imagick extension is missing', 500); - } - - $fetch = @\file_get_contents($url, false); - - if (!$fetch) { - throw new Exception('Image not found', 404); - } - - try { - $resize = new Resize($fetch); - } catch (\Exception $exception) { - throw new Exception('Unable to parse image', 500); - } - - $resize->crop((int) $width, (int) $height); - - $output = (empty($output)) ? $type : $output; - - $response + if ($data) { + return $response ->setContentType('image/png') ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send('', null) + ->addHeader('X-Appwrite-Cache', 'hit') + ->send($data) ; - - $data = $resize->output($output, $quality); - - $cache->save($key, $data); - - echo $data; - - unset($resize); } - ); -$utopia->get('/v1/avatars/favicon') + if (!\extension_loaded('imagick')) { + throw new Exception('Imagick extension is missing', 500); + } + + $fetch = @\file_get_contents($url, false); + + if (!$fetch) { + throw new Exception('Image not found', 404); + } + + try { + $resize = new Resize($fetch); + } catch (\Exception $exception) { + throw new Exception('Unable to parse image', 500); + } + + $resize->crop((int) $width, (int) $height); + + $output = (empty($output)) ? $type : $output; + + $data = $resize->output($output, $quality); + + $cache->save($key, $data); + + $response + ->setContentType('image/png') + ->addHeader('Expires', $date) + ->addHeader('X-Appwrite-Cache', 'miss') + ->send($data); + ; + + unset($resize); + }, ['response']); + +App::get('/v1/avatars/favicon') ->desc('Get Favicon') ->groups(['api', 'avatars']) - ->param('url', '', function () { return new URL(); }, 'Website URL which you want to fetch the favicon from.') ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getFavicon') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-favicon.md') - ->action( - function ($url) use ($response, $request) { - $width = 56; - $height = 56; - $quality = 80; - $output = 'png'; - $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache - $key = \md5('/v2/avatars/favicon-'.$url); - $type = 'png'; - $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-0')); // Limit file number or size - $data = $cache->load($key, 60 * 60 * 24 * 30 * 3 /* 3 months */); + ->param('url', '', function () { return new URL(); }, 'Website URL which you want to fetch the favicon from.') + ->action(function ($url, $response) { + /** @var Utopia\Response $response */ - if ($data) { - $response - ->setContentType('image/png') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'hit') - ->send($data, 0) - ; - } + $width = 56; + $height = 56; + $quality = 80; + $output = 'png'; + $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache + $key = \md5('/v2/avatars/favicon-'.$url); + $type = 'png'; + $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-0')); // Limit file number or size + $data = $cache->load($key, 60 * 60 * 24 * 30 * 3 /* 3 months */); - if (!\extension_loaded('imagick')) { - throw new Exception('Imagick extension is missing', 500); - } - - $curl = \curl_init(); - - \curl_setopt_array($curl, [ - CURLOPT_RETURNTRANSFER => 1, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 3, - CURLOPT_URL => $url, - CURLOPT_USERAGENT => \sprintf(APP_USERAGENT, - Config::getParam('version'), - $request->getServer('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) - ), - ]); - - $html = \curl_exec($curl); - - \curl_close($curl); - - if (!$html) { - throw new Exception('Failed to fetch remote URL', 404); - } - - $doc = new DOMDocument(); - $doc->strictErrorChecking = false; - @$doc->loadHTML($html); - - $links = $doc->getElementsByTagName('link'); - $outputHref = ''; - $outputExt = ''; - $space = 0; - - foreach ($links as $link) { /* @var $link DOMElement */ - $href = $link->getAttribute('href'); - $rel = $link->getAttribute('rel'); - $sizes = $link->getAttribute('sizes'); - $absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href))); - - switch (\strtolower($rel)) { - case 'icon': - case 'shortcut icon': - //case 'apple-touch-icon': - $ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); - - switch ($ext) { - case 'ico': - case 'png': - case 'jpg': - case 'jpeg': - $size = \explode('x', \strtolower($sizes)); - - $sizeWidth = (isset($size[0])) ? (int) $size[0] : 0; - $sizeHeight = (isset($size[1])) ? (int) $size[1] : 0; - - if (($sizeWidth * $sizeHeight) >= $space) { - $space = $sizeWidth * $sizeHeight; - $outputHref = $absolute; - $outputExt = $ext; - } - - break; - } - - break; - } - } - - if (empty($outputHref) || empty($outputExt)) { - $default = \parse_url($url); - - $outputHref = $default['scheme'].'://'.$default['host'].'/favicon.ico'; - $outputExt = 'ico'; - } - - if ('ico' == $outputExt) { // Skip crop, Imagick isn\'t supporting icon files - $data = @\file_get_contents($outputHref, false); - - if (empty($data) || (\mb_substr($data, 0, 5) === 'save($key, $data); - - $response - ->setContentType('image/x-icon') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send($data, 0) - ; - } - - $fetch = @\file_get_contents($outputHref, false); - - if (!$fetch) { - throw new Exception('Icon not found', 404); - } - - $resize = new Resize($fetch); - - $resize->crop((int) $width, (int) $height); - - $output = (empty($output)) ? $type : $output; - - $response + if ($data) { + return $response ->setContentType('image/png') ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send('', null) + ->addHeader('X-Appwrite-Cache', 'hit') + ->send($data) ; + } - $data = $resize->output($output, $quality); + if (!\extension_loaded('imagick')) { + throw new Exception('Imagick extension is missing', 500); + } + + $curl = \curl_init(); + + \curl_setopt_array($curl, [ + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + CURLOPT_URL => $url, + CURLOPT_USERAGENT => \sprintf(APP_USERAGENT, + App::getEnv('_APP_VERSION', 'UNKNOWN'), + App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) + ), + ]); + + $html = \curl_exec($curl); + + \curl_close($curl); + + if (!$html) { + throw new Exception('Failed to fetch remote URL', 404); + } + + $doc = new DOMDocument(); + $doc->strictErrorChecking = false; + @$doc->loadHTML($html); + + $links = $doc->getElementsByTagName('link'); + $outputHref = ''; + $outputExt = ''; + $space = 0; + + foreach ($links as $link) { /* @var $link DOMElement */ + $href = $link->getAttribute('href'); + $rel = $link->getAttribute('rel'); + $sizes = $link->getAttribute('sizes'); + $absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href))); + + switch (\strtolower($rel)) { + case 'icon': + case 'shortcut icon': + //case 'apple-touch-icon': + $ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); + + switch ($ext) { + case 'ico': + case 'png': + case 'jpg': + case 'jpeg': + $size = \explode('x', \strtolower($sizes)); + + $sizeWidth = (isset($size[0])) ? (int) $size[0] : 0; + $sizeHeight = (isset($size[1])) ? (int) $size[1] : 0; + + if (($sizeWidth * $sizeHeight) >= $space) { + $space = $sizeWidth * $sizeHeight; + $outputHref = $absolute; + $outputExt = $ext; + } + + break; + } + + break; + } + } + + if (empty($outputHref) || empty($outputExt)) { + $default = \parse_url($url); + + $outputHref = $default['scheme'].'://'.$default['host'].'/favicon.ico'; + $outputExt = 'ico'; + } + + if ('ico' == $outputExt) { // Skip crop, Imagick isn\'t supporting icon files + $data = @\file_get_contents($outputHref, false); + + if (empty($data) || (\mb_substr($data, 0, 5) === 'save($key, $data); - echo $data; - - unset($resize); + return $response + ->setContentType('image/x-icon') + ->addHeader('Expires', $date) + ->addHeader('X-Appwrite-Cache', 'miss') + ->send($data) + ; } - ); -$utopia->get('/v1/avatars/qr') + $fetch = @\file_get_contents($outputHref, false); + + if (!$fetch) { + throw new Exception('Icon not found', 404); + } + + $resize = new Resize($fetch); + + $resize->crop((int) $width, (int) $height); + + $output = (empty($output)) ? $type : $output; + + $data = $resize->output($output, $quality); + + $cache->save($key, $data); + + $response + ->setContentType('image/png') + ->addHeader('Expires', $date) + ->addHeader('X-Appwrite-Cache', 'miss') + ->send($data); + + unset($resize); + }, ['response']); + +App::get('/v1/avatars/qr') ->desc('Get QR Code') ->groups(['api', 'avatars']) - ->param('text', '', function () { return new Text(512); }, 'Plain text to be converted to QR code image.') - ->param('size', 400, function () { return new Range(0, 1000); }, 'QR code size. Pass an integer between 0 to 1000. Defaults to 400.', true) - ->param('margin', 1, function () { return new Range(0, 10); }, 'Margin from edge. Pass an integer between 0 to 10. Defaults to 1.', true) - ->param('download', false, function () { return new Boolean(true); }, 'Return resulting image with \'Content-Disposition: attachment \' headers for the browser to start downloading it. Pass 0 for no header, or 1 for otherwise. Default value is set to 0.', true) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getQR') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-qr.md') - ->action( - function ($text, $size, $margin, $download) use ($response) { - $download = ($download === '1' || $download === 'true' || $download === 1 || $download === true); + ->param('text', '', function () { return new Text(512); }, 'Plain text to be converted to QR code image.') + ->param('size', 400, function () { return new Range(0, 1000); }, 'QR code size. Pass an integer between 0 to 1000. Defaults to 400.', true) + ->param('margin', 1, function () { return new Range(0, 10); }, 'Margin from edge. Pass an integer between 0 to 10. Defaults to 1.', true) + ->param('download', false, function () { return new Boolean(true); }, 'Return resulting image with \'Content-Disposition: attachment \' headers for the browser to start downloading it. Pass 0 for no header, or 1 for otherwise. Default value is set to 0.', true) + ->action(function ($text, $size, $margin, $download, $response) { + /** @var Utopia\Response $response */ - $renderer = new ImageRenderer( - new RendererStyle($size, $margin), - new ImagickImageBackEnd('png', 100) - ); + $download = ($download === '1' || $download === 'true' || $download === 1 || $download === true); - $writer = new Writer($renderer); + $renderer = new ImageRenderer( + new RendererStyle($size, $margin), + new ImagickImageBackEnd('png', 100) + ); - if ($download) { - $response->addHeader('Content-Disposition', 'attachment; filename="qr.png"'); - } + $writer = new Writer($renderer); - $response - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache - ->setContentType('image/png') - ->send($writer->writeString($text)) - ; + if ($download) { + $response->addHeader('Content-Disposition', 'attachment; filename="qr.png"'); } - ); -$utopia->get('/v1/avatars/initials') + $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->setContentType('image/png') + ->send($writer->writeString($text)) + ; + }, ['response']); + +App::get('/v1/avatars/initials') ->desc('Get User Initials') ->groups(['api', 'avatars']) - ->param('name', '', function () { return new Text(512); }, 'Full Name. When empty, current user name or email will be used.', true) - ->param('width', 500, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 500, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('color', '', function () { return new HexColor(); }, 'Changes text color. By default a random color will be picked and stay will persistent to the given name.', true) - ->param('background', '', function () { return new HexColor(); }, 'Changes background color. By default a random color will be picked and stay will persistent to the given name.', true) ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getInitials') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-initials.md') - ->action( - function ($name, $width, $height, $color, $background) use ($response, $user) { - $themes = [ - ['color' => '#27005e', 'background' => '#e1d2f6'], // VIOLET - ['color' => '#5e2700', 'background' => '#f3d9c6'], // ORANGE - ['color' => '#006128', 'background' => '#c9f3c6'], // GREEN - ['color' => '#580061', 'background' => '#f2d1f5'], // FUSCHIA - ['color' => '#00365d', 'background' => '#c6e1f3'], // BLUE - ['color' => '#00075c', 'background' => '#d2d5f6'], // INDIGO - ['color' => '#610038', 'background' => '#f5d1e6'], // PINK - ['color' => '#386100', 'background' => '#dcf1bd'], // LIME - ['color' => '#615800', 'background' => '#f1ecba'], // YELLOW - ['color' => '#610008', 'background' => '#f6d2d5'] // RED - ]; + ->param('name', '', function () { return new Text(512); }, 'Full Name. When empty, current user name or email will be used.', true) + ->param('width', 500, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 500, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('color', '', function () { return new HexColor(); }, 'Changes text color. By default a random color will be picked and stay will persistent to the given name.', true) + ->param('background', '', function () { return new HexColor(); }, 'Changes background color. By default a random color will be picked and stay will persistent to the given name.', true) + ->action(function ($name, $width, $height, $color, $background, $response, $user) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ - $rand = \rand(0, \count($themes)-1); + $themes = [ + ['color' => '#27005e', 'background' => '#e1d2f6'], // VIOLET + ['color' => '#5e2700', 'background' => '#f3d9c6'], // ORANGE + ['color' => '#006128', 'background' => '#c9f3c6'], // GREEN + ['color' => '#580061', 'background' => '#f2d1f5'], // FUSCHIA + ['color' => '#00365d', 'background' => '#c6e1f3'], // BLUE + ['color' => '#00075c', 'background' => '#d2d5f6'], // INDIGO + ['color' => '#610038', 'background' => '#f5d1e6'], // PINK + ['color' => '#386100', 'background' => '#dcf1bd'], // LIME + ['color' => '#615800', 'background' => '#f1ecba'], // YELLOW + ['color' => '#610008', 'background' => '#f6d2d5'] // RED + ]; - $name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', '')); - $words = \explode(' ', \strtoupper($name)); - $initials = null; - $code = 0; + $rand = \rand(0, \count($themes)-1); - foreach ($words as $key => $w) { - $initials .= (isset($w[0])) ? $w[0] : ''; - $code += (isset($w[0])) ? \ord($w[0]) : 0; + $name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', '')); + $words = \explode(' ', \strtoupper($name)); + $initials = null; + $code = 0; - if ($key == 1) { - break; - } + foreach ($words as $key => $w) { + $initials .= (isset($w[0])) ? $w[0] : ''; + $code += (isset($w[0])) ? \ord($w[0]) : 0; + + if ($key == 1) { + break; } - - $length = \count($words); - $rand = \substr($code,-1); - $background = (!empty($background)) ? '#'.$background : $themes[$rand]['background']; - $color = (!empty($color)) ? '#'.$color : $themes[$rand]['color']; - - $image = new \Imagick(); - $draw = new \ImagickDraw(); - $fontSize = \min($width, $height) / 2; - - $draw->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-500.ttf"); - $image->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-500.ttf"); - - $draw->setFillColor(new \ImagickPixel($color)); - $draw->setFontSize($fontSize); - - $draw->setTextAlignment(\Imagick::ALIGN_CENTER); - $draw->annotation($width / 1.97, ($height / 2) + ($fontSize / 3), $initials); - - $image->newImage($width, $height, $background); - $image->setImageFormat("png"); - $image->drawImage($draw); - - //$image->setImageCompressionQuality(9 - round(($quality / 100) * 9)); - - $response - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache - ->setContentType('image/png') - ->send($image->getImageBlob()) - ; } - ); \ No newline at end of file + + $length = \count($words); + $rand = \substr($code,-1); + $background = (!empty($background)) ? '#'.$background : $themes[$rand]['background']; + $color = (!empty($color)) ? '#'.$color : $themes[$rand]['color']; + + $image = new \Imagick(); + $draw = new \ImagickDraw(); + $fontSize = \min($width, $height) / 2; + + $draw->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-500.ttf"); + $image->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-500.ttf"); + + $draw->setFillColor(new \ImagickPixel($color)); + $draw->setFontSize($fontSize); + + $draw->setTextAlignment(\Imagick::ALIGN_CENTER); + $draw->annotation($width / 1.97, ($height / 2) + ($fontSize / 3), $initials); + + $image->newImage($width, $height, $background); + $image->setImageFormat("png"); + $image->drawImage($draw); + + //$image->setImageCompressionQuality(9 - round(($quality / 100) * 9)); + + $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->setContentType('image/png') + ->send($image->getImageBlob()) + ; + }, ['response', 'user']); \ No newline at end of file diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index dadf7e9576..548905edae 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -1,11 +1,8 @@ post('/v1/database/collections') +App::post('/v1/database/collections') ->desc('Create Collection') ->groups(['api', 'database']) ->label('webhook', 'database.collections.create') @@ -39,69 +33,72 @@ $utopia->post('/v1/database/collections') ->param('name', '', function () { return new Text(256); }, 'Collection name.') ->param('read', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->param('rules', [], function () use ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.') - ->action( - function ($name, $read, $write, $rules) use ($response, $projectDB, $webhook, $audit) { - $parsedRules = []; + ->param('rules', [], function ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', false, ['projectDB']) + ->action(function ($name, $read, $write, $rules, $response, $projectDB, $webhooks, $audits) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ - foreach ($rules as &$rule) { - $parsedRules[] = \array_merge([ - '$collection' => Database::SYSTEM_COLLECTION_RULES, - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - ], $rule); - } + $parsedRules = []; - try { - $data = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, - 'name' => $name, - 'dateCreated' => \time(), - 'dateUpdated' => \time(), - 'structure' => true, - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - 'rules' => $parsedRules, - ]); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB', 500); - } - - if (false === $data) { - throw new Exception('Failed saving collection to DB', 500); - } - - $data = $data->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.collections.create') - ->setParam('resource', 'database/collection/'.$data['$id']) - ->setParam('data', $data) - ; - - /* - * View - */ - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($data) - ; + foreach ($rules as &$rule) { + $parsedRules[] = \array_merge([ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + ], $rule); } - ); -$utopia->get('/v1/database/collections') + try { + $data = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, + 'name' => $name, + 'dateCreated' => \time(), + 'dateUpdated' => \time(), + 'structure' => true, + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + 'rules' => $parsedRules, + ]); + } catch (AuthorizationException $exception) { + throw new Exception('Unauthorized action', 401); + } catch (StructureException $exception) { + throw new Exception('Bad structure. '.$exception->getMessage(), 400); + } catch (\Exception $exception) { + throw new Exception('Failed saving document to DB', 500); + } + + if (false === $data) { + throw new Exception('Failed saving collection to DB', 500); + } + + $data = $data->getArrayCopy(); + + $webhooks + ->setParam('payload', $data) + ; + + $audits + ->setParam('event', 'database.collections.create') + ->setParam('resource', 'database/collection/'.$data['$id']) + ->setParam('data', $data) + ; + + /* + * View + */ + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($data) + ; + }, ['response', 'projectDB', 'webhooks', 'audits']); + +App::get('/v1/database/collections') ->desc('List Collections') ->groups(['api', 'database']) ->label('scope', 'collections.read') @@ -113,44 +110,26 @@ $utopia->get('/v1/database/collections') ->param('limit', 25, function () { return new Range(0, 100); }, 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) ->param('offset', 0, function () { return new Range(0, 40000); }, 'Results offset. The default value is 0. Use this param to manage pagination.', true) ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) - ->action( - function ($search, $limit, $offset, $orderType) use ($response, $projectDB) { - /*$vl = new Structure($projectDB); + ->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - var_dump($vl->isValid(new Document([ - '$collection' => Database::SYSTEM_COLLECTION_RULES, - '$permissions' => [ - 'read' => ['*'], - 'write' => ['*'], - ], - 'label' => 'Platforms', - 'key' => 'platforms', - 'type' => 'document', - 'default' => [], - 'required' => false, - 'array' => true, - 'options' => [Database::SYSTEM_COLLECTION_PLATFORMS], - ]))); + $results = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'name', + 'orderType' => $orderType, + 'orderCast' => 'string', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_COLLECTIONS, + ], + ]); - var_dump($vl->getDescription());*/ + $response->json(['sum' => $projectDB->getSum(), 'collections' => $results]); + }, ['response', 'projectDB']); - $results = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderField' => 'name', - 'orderType' => $orderType, - 'orderCast' => 'string', - 'search' => $search, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_COLLECTIONS, - ], - ]); - - $response->json(['sum' => $projectDB->getSum(), 'collections' => $results]); - } - ); - -$utopia->get('/v1/database/collections/:collectionId') +App::get('/v1/database/collections/:collectionId') ->desc('Get Collection') ->groups(['api', 'database']) ->label('scope', 'collections.read') @@ -159,19 +138,20 @@ $utopia->get('/v1/database/collections/:collectionId') ->label('sdk.method', 'getCollection') ->label('sdk.description', '/docs/references/database/get-collection.md') ->param('collectionId', '', function () { return new UID(); }, 'Collection unique ID.') - ->action( - function ($collectionId) use ($response, $projectDB) { - $collection = $projectDB->getDocument($collectionId, false); + ->action(function ($collectionId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + + $collection = $projectDB->getDocument($collectionId, false); - if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } - - $response->json($collection->getArrayCopy()); + if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); } - ); -// $utopia->get('/v1/database/collections/:collectionId/logs') + $response->json($collection->getArrayCopy()); + }, ['response', 'projectDB']); + +// App::get('/v1/database/collections/:collectionId/logs') // ->desc('Get Collection Logs') // ->groups(['api', 'database']) // ->label('scope', 'collections.read') @@ -236,7 +216,7 @@ $utopia->get('/v1/database/collections/:collectionId') // } // ); -$utopia->put('/v1/database/collections/:collectionId') +App::put('/v1/database/collections/:collectionId') ->desc('Update Collection') ->groups(['api', 'database']) ->label('scope', 'collections.write') @@ -249,67 +229,70 @@ $utopia->put('/v1/database/collections/:collectionId') ->param('name', null, function () { return new Text(256); }, 'Collection name.') ->param('read', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions(/docs/permissions) and get a full list of available permissions.') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->param('rules', [], function () use ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', true) - ->action( - function ($collectionId, $name, $read, $write, $rules) use ($response, $projectDB, $webhook, $audit) { - $collection = $projectDB->getDocument($collectionId, false); + ->param('rules', [], function ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', true, ['projectDB']) + ->action(function ($collectionId, $name, $read, $write, $rules, $response, $projectDB, $webhooks, $audits) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ - if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } + $collection = $projectDB->getDocument($collectionId, false); - $parsedRules = []; - - foreach ($rules as &$rule) { - $parsedRules[] = \array_merge([ - '$collection' => Database::SYSTEM_COLLECTION_RULES, - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - ], $rule); - } - - try { - $collection = $projectDB->updateDocument(\array_merge($collection->getArrayCopy(), [ - 'name' => $name, - 'structure' => true, - 'dateUpdated' => \time(), - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - 'rules' => $parsedRules, - ])); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB', 500); - } - - if (false === $collection) { - throw new Exception('Failed saving collection to DB', 500); - } - - $data = $collection->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.collections.update') - ->setParam('resource', 'database/collections/'.$data['$id']) - ->setParam('data', $data) - ; - - $response->json($collection->getArrayCopy()); + if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); } - ); -$utopia->delete('/v1/database/collections/:collectionId') + $parsedRules = []; + + foreach ($rules as &$rule) { + $parsedRules[] = \array_merge([ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + ], $rule); + } + + try { + $collection = $projectDB->updateDocument(\array_merge($collection->getArrayCopy(), [ + 'name' => $name, + 'structure' => true, + 'dateUpdated' => \time(), + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + 'rules' => $parsedRules, + ])); + } catch (AuthorizationException $exception) { + throw new Exception('Unauthorized action', 401); + } catch (StructureException $exception) { + throw new Exception('Bad structure. '.$exception->getMessage(), 400); + } catch (\Exception $exception) { + throw new Exception('Failed saving document to DB', 500); + } + + if (false === $collection) { + throw new Exception('Failed saving collection to DB', 500); + } + + $data = $collection->getArrayCopy(); + + $webhooks + ->setParam('payload', $data) + ; + + $audits + ->setParam('event', 'database.collections.update') + ->setParam('resource', 'database/collections/'.$data['$id']) + ->setParam('data', $data) + ; + + $response->json($collection->getArrayCopy()); + }, ['response', 'projectDB', 'webhooks', 'audits']); + +App::delete('/v1/database/collections/:collectionId') ->desc('Delete Collection') ->groups(['api', 'database']) ->label('scope', 'collections.write') @@ -319,35 +302,38 @@ $utopia->delete('/v1/database/collections/:collectionId') ->label('sdk.method', 'deleteCollection') ->label('sdk.description', '/docs/references/database/delete-collection.md') ->param('collectionId', '', function () { return new UID(); }, 'Collection unique ID.') - ->action( - function ($collectionId) use ($response, $projectDB, $webhook, $audit) { - $collection = $projectDB->getDocument($collectionId, false); + ->action(function ($collectionId, $response, $projectDB, $webhooks, $audits) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ - if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } + $collection = $projectDB->getDocument($collectionId, false); - if (!$projectDB->deleteDocument($collectionId)) { - throw new Exception('Failed to remove collection from DB', 500); - } - - $data = $collection->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.collections.delete') - ->setParam('resource', 'database/collections/'.$data['$id']) - ->setParam('data', $data) - ; - - $response->noContent(); + if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); } - ); -$utopia->post('/v1/database/collections/:collectionId/documents') + if (!$projectDB->deleteDocument($collectionId)) { + throw new Exception('Failed to remove collection from DB', 500); + } + + $data = $collection->getArrayCopy(); + + $webhooks + ->setParam('payload', $data) + ; + + $audits + ->setParam('event', 'database.collections.delete') + ->setParam('resource', 'database/collections/'.$data['$id']) + ->setParam('data', $data) + ; + + $response->noContent(); + }, ['response', 'projectDB', 'webhooks', 'audits']); + +App::post('/v1/database/collections/:collectionId/documents') ->desc('Create Document') ->groups(['api', 'database']) ->label('webhook', 'database.documents.create') @@ -363,111 +349,115 @@ $utopia->post('/v1/database/collections/:collectionId/documents') ->param('parentDocument', '', function () { return new UID(); }, 'Parent document unique ID. Use when you want your new document to be a child of a parent document.', true) ->param('parentProperty', '', function () { return new Key(); }, 'Parent document property name. Use when you want your new document to be a child of a parent document.', true) ->param('parentPropertyType', Document::SET_TYPE_ASSIGN, function () { return new WhiteList([Document::SET_TYPE_ASSIGN, Document::SET_TYPE_APPEND, Document::SET_TYPE_PREPEND]); }, 'Parent document property connection type. You can set this value to **assign**, **append** or **prepend**, default value is assign. Use when you want your new document to be a child of a parent document.', true) - ->action( - function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType) use ($response, $projectDB, $webhook, $audit) { - $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array + ->action(function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType, $response, $projectDB, $webhooks, $audits) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ + + $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array - if (empty($data)) { - throw new Exception('Missing payload', 400); + if (empty($data)) { + throw new Exception('Missing payload', 400); + } + + if (isset($data['$id'])) { + throw new Exception('$id is not allowed for creating new documents, try update instead', 400); + } + + $collection = $projectDB->getDocument($collectionId, false); + + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); + } + + $data['$collection'] = $collectionId; // Adding this param to make API easier for developers + $data['$permissions'] = [ + 'read' => $read, + 'write' => $write, + ]; + + // Read parent document + validate not 404 + validate read / write permission like patch method + // Add payload to parent document property + if ((!empty($parentDocument)) && (!empty($parentProperty))) { + $parentDocument = $projectDB->getDocument($parentDocument, false); + + if (empty($parentDocument->getArrayCopy())) { // Check empty + throw new Exception('No parent document found', 404); } - if (isset($data['$id'])) { - throw new Exception('$id is not allowed for creating new documents, try update instead', 400); - } - - $collection = $projectDB->getDocument($collectionId, false); - - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } - - $data['$collection'] = $collectionId; // Adding this param to make API easier for developers - $data['$permissions'] = [ - 'read' => $read, - 'write' => $write, - ]; - - // Read parent document + validate not 404 + validate read / write permission like patch method - // Add payload to parent document property - if ((!empty($parentDocument)) && (!empty($parentProperty))) { - $parentDocument = $projectDB->getDocument($parentDocument, false); - - if (empty($parentDocument->getArrayCopy())) { // Check empty - throw new Exception('No parent document found', 404); - } - - /* - * 1. Check child has valid structure, - * 2. Check user have write permission for parent document - * 3. Assign parent data (including child) to $data - * 4. Validate the combined result has valid structure (inside $projectDB->createDocument method) - */ - - $new = new Document($data); - - $structure = new Structure($projectDB); - - if (!$structure->isValid($new)) { - throw new Exception('Invalid data structure: '.$structure->getDescription(), 400); - } - - $authorization = new Authorization($parentDocument, 'write'); - - if (!$authorization->isValid($new->getPermissions())) { - throw new Exception('Unauthorized action', 401); - } - - $parentDocument - ->setAttribute($parentProperty, $data, $parentPropertyType); - - $data = $parentDocument->getArrayCopy(); - } - - /** - * Set default collection values - */ - foreach ($collection->getAttribute('rules') as $key => $rule) { - $key = (isset($rule['key'])) ? $rule['key'] : ''; - $default = (isset($rule['default'])) ? $rule['default'] : null; - - if (!isset($data[$key])) { - $data[$key] = $default; - } - } - - try { - $data = $projectDB->createDocument($data); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB'.$exception->getMessage(), 500); - } - - $data = $data->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.documents.create') - ->setParam('resource', 'database/document/'.$data['$id']) - ->setParam('data', $data) - ; - /* - * View - */ - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($data) - ; - } - ); + * 1. Check child has valid structure, + * 2. Check user have write permission for parent document + * 3. Assign parent data (including child) to $data + * 4. Validate the combined result has valid structure (inside $projectDB->createDocument method) + */ -$utopia->get('/v1/database/collections/:collectionId/documents') + $new = new Document($data); + + $structure = new Structure($projectDB); + + if (!$structure->isValid($new)) { + throw new Exception('Invalid data structure: '.$structure->getDescription(), 400); + } + + $authorization = new Authorization($parentDocument, 'write'); + + if (!$authorization->isValid($new->getPermissions())) { + throw new Exception('Unauthorized action', 401); + } + + $parentDocument + ->setAttribute($parentProperty, $data, $parentPropertyType); + + $data = $parentDocument->getArrayCopy(); + $collection = $projectDB->getDocument($parentDocument->getCollection(), false); + } + + /** + * Set default collection values + */ + foreach ($collection->getAttribute('rules') as $key => $rule) { + $key = (isset($rule['key'])) ? $rule['key'] : ''; + $default = (isset($rule['default'])) ? $rule['default'] : null; + + if (!isset($data[$key])) { + $data[$key] = $default; + } + } + + try { + $data = $projectDB->createDocument($data); + } catch (AuthorizationException $exception) { + throw new Exception('Unauthorized action', 401); + } catch (StructureException $exception) { + throw new Exception('Bad structure. '.$exception->getMessage(), 400); + } catch (\Exception $exception) { + throw new Exception('Failed saving document to DB'.$exception->getMessage(), 500); + } + + $data = $data->getArrayCopy(); + + $webhooks + ->setParam('payload', $data) + ; + + $audits + ->setParam('event', 'database.documents.create') + ->setParam('resource', 'database/document/'.$data['$id']) + ->setParam('data', $data) + ; + + /* + * View + */ + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($data) + ; + }, ['response', 'projectDB', 'webhooks', 'audits']); + +App::get('/v1/database/collections/:collectionId/documents') ->desc('List Documents') ->groups(['api', 'database']) ->label('scope', 'documents.read') @@ -477,57 +467,58 @@ $utopia->get('/v1/database/collections/:collectionId/documents') ->label('sdk.description', '/docs/references/database/list-documents.md') ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('filters', [], function () { return new ArrayList(new Text(128)); }, 'Array of filter strings. Each filter is constructed from a key name, comparison operator (=, !=, >, <, <=, >=) and a value. You can also use a dot (.) separator in attribute names to filter by child document attributes. Examples: \'name=John Doe\' or \'category.$id>=5bed2d152c362\'.', true) + ->param('limit', 25, function () { return new Range(0, 1000); }, 'Maximum number of documents to return in response. Use this value to manage pagination.', true) ->param('offset', 0, function () { return new Range(0, 900000000); }, 'Offset value. Use this value to manage pagination.', true) - ->param('limit', 50, function () { return new Range(0, 1000); }, 'Maximum number of documents to return in response. Use this value to manage pagination.', true) ->param('orderField', '$id', function () { return new Text(128); }, 'Document field that results will be sorted by.', true) ->param('orderType', 'ASC', function () { return new WhiteList(array('DESC', 'ASC')); }, 'Order direction. Possible values are DESC for descending order, or ASC for ascending order.', true) ->param('orderCast', 'string', function () { return new WhiteList(array('int', 'string', 'date', 'time', 'datetime')); }, 'Order field type casting. Possible values are int, string, date, time or datetime. The database will attempt to cast the order field to the value you pass here. The default value is a string.', true) ->param('search', '', function () { return new Text(256); }, 'Search query. Enter any free text search. The database will try to find a match against all document attributes and children.', true) - ->action( - function ($collectionId, $filters, $offset, $limit, $orderField, $orderType, $orderCast, $search) use ($response, $projectDB, $utopia) { - $collection = $projectDB->getDocument($collectionId, false); + ->action(function ($collectionId, $filters, $limit, $offset, $orderField, $orderType, $orderCast, $search, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } + $collection = $projectDB->getDocument($collectionId, false); - $list = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderField' => $orderField, - 'orderType' => $orderType, - 'orderCast' => $orderCast, - 'search' => $search, - 'filters' => \array_merge($filters, [ - '$collection='.$collectionId, - ]), - ]); - - if ($utopia->isDevelopment()) { - $collection - ->setAttribute('debug', $projectDB->getDebug()) - ->setAttribute('limit', $limit) - ->setAttribute('offset', $offset) - ->setAttribute('orderField', $orderField) - ->setAttribute('orderType', $orderType) - ->setAttribute('orderCast', $orderCast) - ->setAttribute('filters', $filters) - ; - } - - $collection - ->setAttribute('sum', $projectDB->getSum()) - ->setAttribute('documents', $list) - ; - - /* - * View - */ - $response->json($collection->getArrayCopy(/*['$id', '$collection', 'name', 'documents']*/[], ['rules'])); + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); } - ); -$utopia->get('/v1/database/collections/:collectionId/documents/:documentId') + $list = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => $orderField, + 'orderType' => $orderType, + 'orderCast' => $orderCast, + 'search' => $search, + 'filters' => \array_merge($filters, [ + '$collection='.$collectionId, + ]), + ]); + + if (App::isDevelopment()) { + $collection + ->setAttribute('debug', $projectDB->getDebug()) + ->setAttribute('limit', $limit) + ->setAttribute('offset', $offset) + ->setAttribute('orderField', $orderField) + ->setAttribute('orderType', $orderType) + ->setAttribute('orderCast', $orderCast) + ->setAttribute('filters', $filters) + ; + } + + $collection + ->setAttribute('sum', $projectDB->getSum()) + ->setAttribute('documents', $list) + ; + + /* + * View + */ + $response->json($collection->getArrayCopy(/*['$id', '$collection', 'name', 'documents']*/[], ['rules'])); + }, ['response', 'projectDB']); + +App::get('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Get Document') ->groups(['api', 'database']) ->label('scope', 'documents.read') @@ -537,43 +528,45 @@ $utopia->get('/v1/database/collections/:collectionId/documents/:documentId') ->label('sdk.description', '/docs/references/database/get-document.md') ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('documentId', null, function () { return new UID(); }, 'Document unique ID.') - ->action( - function ($collectionId, $documentId) use ($response, $request, $projectDB) { - $document = $projectDB->getDocument($documentId, false); - $collection = $projectDB->getDocument($collectionId, false); + ->action(function ($collectionId, $documentId, $request, $response, $projectDB) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($document->getArrayCopy()) || $document->getCollection() != $collection->getId()) { // Check empty + $document = $projectDB->getDocument($documentId, false); + $collection = $projectDB->getDocument($collectionId, false); + + if (empty($document->getArrayCopy()) || $document->getCollection() != $collection->getId()) { // Check empty + throw new Exception('No document found', 404); + } + + $output = $document->getArrayCopy(); + + $paths = \explode('/', $request->getParam('q', '')); + $paths = \array_slice($paths, 7, \count($paths)); + + if (\count($paths) > 0) { + if (\count($paths) % 2 == 1) { + $output = $document->getAttribute(\implode('.', $paths)); + } else { + $id = (int) \array_pop($paths); + $output = $document->search('$id', $id, $document->getAttribute(\implode('.', $paths))); + } + + $output = ($output instanceof Document) ? $output->getArrayCopy() : $output; + + if (!\is_array($output)) { throw new Exception('No document found', 404); } - - $output = $document->getArrayCopy(); - - $paths = \explode('/', $request->getParam('q', '')); - $paths = \array_slice($paths, 7, \count($paths)); - - if (\count($paths) > 0) { - if (\count($paths) % 2 == 1) { - $output = $document->getAttribute(\implode('.', $paths)); - } else { - $id = (int) \array_pop($paths); - $output = $document->search('$id', $id, $document->getAttribute(\implode('.', $paths))); - } - - $output = ($output instanceof Document) ? $output->getArrayCopy() : $output; - - if (!\is_array($output)) { - throw new Exception('No document found', 404); - } - } - - /* - * View - */ - $response->json($output); } - ); -$utopia->patch('/v1/database/collections/:collectionId/documents/:documentId') + /* + * View + */ + $response->json($output); + }, ['request', 'response', 'projectDB']); + +App::patch('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Update Document') ->groups(['api', 'database']) ->label('webhook', 'database.documents.update') @@ -587,73 +580,76 @@ $utopia->patch('/v1/database/collections/:collectionId/documents/:documentId') ->param('data', [], function () { return new JSON(); }, 'Document data as JSON object.') ->param('read', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->action( - function ($collectionId, $documentId, $data, $read, $write) use ($response, $projectDB, $webhook, $audit) { - $collection = $projectDB->getDocument($collectionId, false); - $document = $projectDB->getDocument($documentId, false); + ->action(function ($collectionId, $documentId, $data, $read, $write, $response, $projectDB, $webhooks, $audits) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ - $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array + $collection = $projectDB->getDocument($collectionId, false); + $document = $projectDB->getDocument($documentId, false); - if (!\is_array($data)) { - throw new Exception('Data param should be a valid JSON', 400); - } + $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } - - if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty - throw new Exception('No document found', 404); - } - - //TODO check merge read write permissions - - if (!empty($read)) { // Overwrite permissions only when passed - $data['$permissions']['read'] = $read; - } - - if (!empty($write)) { // Overwrite permissions only when passed - $data['$permissions']['write'] = $write; - } - - $data = \array_merge($document->getArrayCopy(), $data); - - $data['$collection'] = $collection->getId(); // Make sure user don't switch collectionID - $data['$id'] = $document->getId(); // Make sure user don't switch document unique ID - - if (empty($data)) { - throw new Exception('Missing payload', 400); - } - try { - $data = $projectDB->updateDocument($data); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB', 500); - } - - $data = $data->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.documents.update') - ->setParam('resource', 'database/document/'.$data['$id']) - ->setParam('data', $data) - ; - - /* - * View - */ - $response->json($data); + if (!\is_array($data)) { + throw new Exception('Data param should be a valid JSON', 400); } - ); -$utopia->delete('/v1/database/collections/:collectionId/documents/:documentId') + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); + } + + if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty + throw new Exception('No document found', 404); + } + + //TODO check merge read write permissions + + if (!empty($read)) { // Overwrite permissions only when passed + $data['$permissions']['read'] = $read; + } + + if (!empty($write)) { // Overwrite permissions only when passed + $data['$permissions']['write'] = $write; + } + + $data = \array_merge($document->getArrayCopy(), $data); + + $data['$collection'] = $collection->getId(); // Make sure user don't switch collectionID + $data['$id'] = $document->getId(); // Make sure user don't switch document unique ID + + if (empty($data)) { + throw new Exception('Missing payload', 400); + } + try { + $data = $projectDB->updateDocument($data); + } catch (AuthorizationException $exception) { + throw new Exception('Unauthorized action', 401); + } catch (StructureException $exception) { + throw new Exception('Bad structure. '.$exception->getMessage(), 400); + } catch (\Exception $exception) { + throw new Exception('Failed saving document to DB', 500); + } + + $data = $data->getArrayCopy(); + + $webhooks + ->setParam('payload', $data) + ; + + $audits + ->setParam('event', 'database.documents.update') + ->setParam('resource', 'database/document/'.$data['$id']) + ->setParam('data', $data) + ; + + /* + * View + */ + $response->json($data); + }, ['response', 'projectDB', 'webhooks', 'audits']); + +App::delete('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Delete Document') ->groups(['api', 'database']) ->label('scope', 'documents.write') @@ -664,41 +660,44 @@ $utopia->delete('/v1/database/collections/:collectionId/documents/:documentId') ->label('sdk.description', '/docs/references/database/delete-document.md') ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('documentId', null, function () { return new UID(); }, 'Document unique ID.') - ->action( - function ($collectionId, $documentId) use ($response, $projectDB, $audit, $webhook) { - $collection = $projectDB->getDocument($collectionId, false); - $document = $projectDB->getDocument($documentId, false); + ->action(function ($collectionId, $documentId, $response, $projectDB, $webhooks, $audits) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ - if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty - throw new Exception('No document found', 404); - } + $collection = $projectDB->getDocument($collectionId, false); + $document = $projectDB->getDocument($documentId, false); - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } - - try { - $projectDB->deleteDocument($documentId); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed to remove document from DB', 500); - } - - $data = $document->getArrayCopy(); - - $webhook - ->setParam('payload', $data) - ; - - $audit - ->setParam('event', 'database.documents.delete') - ->setParam('resource', 'database/document/'.$data['$id']) - ->setParam('data', $data) // Audit document in case of malicious or disastrous action - ; - - $response->noContent(); + if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty + throw new Exception('No document found', 404); } - ); + + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + throw new Exception('Collection not found', 404); + } + + try { + $projectDB->deleteDocument($documentId); + } catch (AuthorizationException $exception) { + throw new Exception('Unauthorized action', 401); + } catch (StructureException $exception) { + throw new Exception('Bad structure. '.$exception->getMessage(), 400); + } catch (\Exception $exception) { + throw new Exception('Failed to remove document from DB', 500); + } + + $data = $document->getArrayCopy(); + + $webhooks + ->setParam('payload', $data) + ; + + $audits + ->setParam('event', 'database.documents.delete') + ->setParam('resource', 'database/document/'.$data['$id']) + ->setParam('data', $data) // Audit document in case of malicious or disastrous action + ; + + $response->noContent(); + }, ['response', 'projectDB', 'webhooks', 'audits']); \ No newline at end of file diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php new file mode 100644 index 0000000000..6118fa6dcc --- /dev/null +++ b/app/controllers/api/functions.php @@ -0,0 +1,539 @@ +groups(['api', 'functions']) + ->desc('Create Function') + ->label('scope', 'functions.write') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'create') + ->label('sdk.description', '/docs/references/functions/create-function.md') + ->param('name', '', function () { return new Text(128); }, 'Function name.') + ->param('env', '', function () { return new WhiteList(array_keys(Config::getParam('environments'))); }, 'Execution enviornment.') + ->param('vars', [], function () { return new Assoc();}, 'Key-value JSON object.', true) + ->param('events', [], function () { return new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)); }, 'Events list.', true) + ->param('schedule', '', function () { return new Cron(); }, 'Schedule CRON syntax.', true) + ->param('timeout', 15, function () { return new Range(1, 900); }, 'Function maximum execution time in seconds.', true) + ->action(function ($name, $env, $vars, $events, $schedule, $timeout, $response, $projectDB) { + $function = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_FUNCTIONS, + '$permissions' => [ + 'read' => [], + 'write' => [], + ], + 'dateCreated' => time(), + 'dateUpdated' => time(), + 'status' => 'paused', + 'name' => $name, + 'env' => $env, + 'tag' => '', + 'vars' => $vars, + 'events' => $events, + 'schedule' => $schedule, + 'previous' => null, + 'next' => null, + 'timeout' => $timeout, + ]); + + if (false === $function) { + throw new Exception('Failed saving function to DB', 500); + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($function->getArrayCopy()) + ; + }, ['response', 'projectDB']); + +App::get('/v1/functions') + ->groups(['api', 'functions']) + ->desc('List Functions') + ->label('scope', 'functions.read') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'list') + ->label('sdk.description', '/docs/references/functions/list-functions.md') + ->param('search', '', function () { return new Text(256); }, 'Search term to filter your list results.', true) + ->param('limit', 25, function () { return new Range(0, 100); }, 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) + ->param('offset', 0, function () { return new Range(0, 2000); }, 'Results offset. The default value is 0. Use this param to manage pagination.', true) + ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) + ->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) { + $results = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'dateCreated', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_FUNCTIONS, + ], + ]); + + $response->json(['sum' => $projectDB->getSum(), 'functions' => $results]); + }, ['response', 'projectDB']); + +App::get('/v1/functions/:functionId') + ->groups(['api', 'functions']) + ->desc('Get Function') + ->label('scope', 'functions.read') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'get') + ->label('sdk.description', '/docs/references/functions/get-function.md') + ->param('functionId', '', function () { return new UID(); }, 'Function unique ID.') + ->action(function ($functionId, $response, $projectDB) { + $function = $projectDB->getDocument($functionId); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('function not found', 404); + } + + $response->json($function->getArrayCopy()); + }, ['response', 'projectDB']); + +App::put('/v1/functions/:functionId') + ->groups(['api', 'functions']) + ->desc('Update Function') + ->label('scope', 'functions.write') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'update') + ->label('sdk.description', '/docs/references/functions/update-function.md') + ->param('functionId', '', function () { return new UID(); }, 'Function unique ID.') + ->param('name', '', function () { return new Text(128); }, 'Function name.') + ->param('vars', [], function () { return new Assoc();}, 'Key-value JSON object.', true) + ->param('events', [], function () { return new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)); }, 'Events list.', true) + ->param('schedule', '', function () { return new Cron(); }, 'Schedule CRON syntax.', true) + ->param('timeout', 15, function () { return new Range(1, 900); }, 'Function maximum execution time in seconds.', true) + ->action(function ($functionId, $name, $vars, $events, $schedule, $timeout, $response, $projectDB) { + $function = $projectDB->getDocument($functionId); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + $cron = (!empty($function->getAttribute('tag', null)) && !empty($schedule)) ? CronExpression::factory($schedule) : null; + $next = (!empty($function->getAttribute('tag', null)) && !empty($schedule)) ? $cron->getNextRunDate()->format('U') : null; + + $function = $projectDB->updateDocument(array_merge($function->getArrayCopy(), [ + 'dateUpdated' => time(), + 'name' => $name, + 'vars' => $vars, + 'events' => $events, + 'schedule' => $schedule, + 'previous' => null, + 'next' => $next, + 'timeout' => $timeout, + ])); + + if (false === $function) { + throw new Exception('Failed saving function to DB', 500); + } + + $response->json($function->getArrayCopy()); + }, ['response', 'projectDB']); + +App::patch('/v1/functions/:functionId/tag') + ->groups(['api', 'functions']) + ->desc('Update Function Tag') + ->label('scope', 'functions.write') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'updateTag') + ->label('sdk.description', '/docs/references/functions/update-tag.md') + ->param('functionId', '', function () { return new UID(); }, 'Function unique ID.') + ->param('tag', '', function () { return new UID(); }, 'Tag unique ID.') + ->action(function ($functionId, $tag, $response, $projectDB) { + $function = $projectDB->getDocument($functionId); + $tag = $projectDB->getDocument($tag); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + if (empty($tag->getId()) || Database::SYSTEM_COLLECTION_TAGS != $tag->getCollection()) { + throw new Exception('Tag not found', 404); + } + + $schedule = $function->getAttribute('schedule', ''); + $cron = (!empty($function->getAttribute('tag')&& !empty($schedule))) ? CronExpression::factory($schedule) : null; + $next = (!empty($function->getAttribute('tag')&& !empty($schedule))) ? $cron->getNextRunDate()->format('U') : null; + + $function = $projectDB->updateDocument(array_merge($function->getArrayCopy(), [ + 'tag' => $tag->getId(), + 'next' => $next, + ])); + + if (false === $function) { + throw new Exception('Failed saving function to DB', 500); + } + + $response->json($function->getArrayCopy()); + }, ['response', 'projectDB']); + +App::delete('/v1/functions/:functionId') + ->groups(['api', 'functions']) + ->desc('Delete Function') + ->label('scope', 'functions.write') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'delete') + ->label('sdk.description', '/docs/references/functions/delete-function.md') + ->param('functionId', '', function () { return new UID(); }, 'Function unique ID.') + ->action(function ($functionId, $response, $projectDB) { + $function = $projectDB->getDocument($functionId); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + if (!$projectDB->deleteDocument($function->getId())) { + throw new Exception('Failed to remove function from DB', 500); + } + + $response->noContent(); + }, ['response', 'projectDB']); + +App::post('/v1/functions/:functionId/tags') + ->groups(['api', 'functions']) + ->desc('Create Tag') + ->label('scope', 'functions.write') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'createTag') + ->label('sdk.description', '/docs/references/functions/create-tag.md') + ->param('functionId', '', function () { return new UID(); }, 'Function unique ID.') + ->param('command', '', function () { return new Text('1028'); }, 'Code execution command.') + ->param('code', [], function () { return new File(); }, 'Gzip file containing your code.', false) + // ->param('code', '', function () { return new Text(128); }, 'Code package. Use the '.APP_NAME.' code packager to create a deployable package file.') + ->action(function ($functionId, $command, $code, $request, $response, $projectDB, $usage) { + $function = $projectDB->getDocument($functionId); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + $file = $request->getFiles('code'); + $device = Storage::getDevice('functions'); + $fileType = new FileType([FileType::FILE_TYPE_GZIP]); + $fileSize = new FileSize(App::getEnv('_APP_STORAGE_LIMIT', 0)); + $upload = new Upload(); + + if (empty($file)) { + throw new Exception('No file sent', 400); + } + + // Make sure we handle a single file and multiple files the same way + $file['name'] = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name']; + $file['tmp_name'] = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name']; + $file['size'] = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; + + // Check if file type is allowed (feature for project settings?) + // if (!$fileType->isValid($file['tmp_name'])) { + // throw new Exception('File type not allowed', 400); + // } + + if (!$fileSize->isValid($file['size'])) { // Check if file size is exceeding allowed limit + throw new Exception('File size not allowed', 400); + } + + if (!$upload->isValid($file['tmp_name'])) { + throw new Exception('Invalid file', 403); + } + + // Save to storage + $size = $device->getFileSize($file['tmp_name']); + $path = $device->getPath(\uniqid().'.'.\pathinfo($file['name'], PATHINFO_EXTENSION)); + + if (!$device->upload($file['tmp_name'], $path)) { // TODO deprecate 'upload' and replace with 'move' + throw new Exception('Failed moving file', 500); + } + + $tag = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_TAGS, + '$permissions' => [ + 'read' => [], + 'write' => [], + ], + 'dateCreated' => time(), + 'functionId' => $function->getId(), + 'command' => $command, + 'codePath' => $path, + 'codeSize' => $size, + ]); + + if (false === $tag) { + throw new Exception('Failed saving tag to DB', 500); + } + + $usage + ->setParam('storage', $tag->getAttribute('codeSize', 0)) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($tag->getArrayCopy()) + ; + }, ['request', 'response', 'projectDB', 'usage']); + +App::get('/v1/functions/:functionId/tags') + ->groups(['api', 'functions']) + ->desc('List Tags') + ->label('scope', 'functions.read') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'listTags') + ->label('sdk.description', '/docs/references/functions/list-tags.md') + ->param('functionId', '', function () { return new UID(); }, 'Function unique ID.') + ->param('search', '', function () { return new Text(256); }, 'Search term to filter your list results.', true) + ->param('limit', 25, function () { return new Range(0, 100); }, 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) + ->param('offset', 0, function () { return new Range(0, 2000); }, 'Results offset. The default value is 0. Use this param to manage pagination.', true) + ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) + ->action(function ($functionId, $search, $limit, $offset, $orderType, $response, $projectDB) { + $function = $projectDB->getDocument($functionId); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + $results = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'dateCreated', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_TAGS, + 'functionId='.$function->getId(), + ], + ]); + + $response->json(['sum' => $projectDB->getSum(), 'tags' => $results]); + }, ['response', 'projectDB']); + +App::get('/v1/functions/:functionId/tags/:tagId') + ->groups(['api', 'functions']) + ->desc('Get Tag') + ->label('scope', 'functions.read') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'getTag') + ->label('sdk.description', '/docs/references/functions/get-tag.md') + ->param('functionId', '', function () { return new UID(); }, 'Function unique ID.') + ->param('tagId', '', function () { return new UID(); }, 'Tag unique ID.') + ->action(function ($functionId, $tagId, $response, $projectDB) { + $function = $projectDB->getDocument($functionId); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + $tag = $projectDB->getDocument($tagId); + + if($tag->getAttribute('functionId') !== $function->getId()) { + throw new Exception('Tag not found', 404); + } + + if (empty($tag->getId()) || Database::SYSTEM_COLLECTION_TAGS != $tag->getCollection()) { + throw new Exception('Tag not found', 404); + } + + $response->json($tag->getArrayCopy()); + }, ['response', 'projectDB']); + +App::delete('/v1/functions/:functionId/tags/:tagId') + ->groups(['api', 'functions']) + ->desc('Delete Tag') + ->label('scope', 'functions.write') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'deleteTag') + ->label('sdk.description', '/docs/references/functions/delete-tag.md') + ->param('functionId', '', function () { return new UID(); }, 'Function unique ID.') + ->param('tagId', '', function () { return new UID(); }, 'Tag unique ID.') + ->action(function ($functionId, $tagId, $response, $projectDB, $usage) { + $function = $projectDB->getDocument($functionId); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + $tag = $projectDB->getDocument($tagId); + + if($tag->getAttribute('functionId') !== $function->getId()) { + throw new Exception('Tag not found', 404); + } + + if (empty($tag->getId()) || Database::SYSTEM_COLLECTION_TAGS != $tag->getCollection()) { + throw new Exception('Tag not found', 404); + } + + $device = Storage::getDevice('functions'); + + if ($device->delete($tag->getAttribute('codePath', ''))) { + if (!$projectDB->deleteDocument($tag->getId())) { + throw new Exception('Failed to remove tag from DB', 500); + } + } + + $usage + ->setParam('storage', $tag->getAttribute('codeSize', 0) * -1) + ; + + $response->noContent(); + }, ['response', 'projectDB', 'usage']); + +App::post('/v1/functions/:functionId/executions') + ->groups(['api', 'functions']) + ->desc('Create Execution') + ->label('scope', 'functions.write') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'createExecution') + ->label('sdk.description', '/docs/references/functions/create-execution.md') + ->param('functionId', '', function () { return new UID(); }, 'Function unique ID.') + ->param('async', 1, function () { return new Range(0, 1); }, 'Execute code asynchronously. Pass 1 for true, 0 for false. Default value is 1.', true) + ->action(function ($functionId, $async, $response, $project, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $projectDB */ + + $function = $projectDB->getDocument($functionId); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + $tag = $projectDB->getDocument($function->getAttribute('tag')); + + if($tag->getAttribute('functionId') !== $function->getId()) { + throw new Exception('Tag not found. Deploy tag before trying to execute a function', 404); + } + + if (empty($tag->getId()) || Database::SYSTEM_COLLECTION_TAGS != $tag->getCollection()) { + throw new Exception('Tag not found. Deploy tag before trying to execute a function', 404); + } + + $execution = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_EXECUTIONS, + '$permissions' => [ + 'read' => [], + 'write' => [], + ], + 'dateCreated' => time(), + 'functionId' => $function->getId(), + 'status' => 'waiting', // waiting / processing / completed / failed + 'exitCode' => 0, + 'stdout' => '', + 'stderr' => '', + 'time' => 0, + ]); + + if (false === $execution) { + throw new Exception('Failed saving execution to DB', 500); + } + + if((bool)$async) { + // Issue a TLS certificate when domain is verified + Resque::enqueue('v1-functions', 'FunctionsV1', [ + 'projectId' => $project->getId(), + 'functionId' => $function->getId(), + 'executionId' => $execution->getId(), + 'functionTag' => $tag->getId(), + 'functionTrigger' => 'API', + ]); + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($execution->getArrayCopy()) + ; + }, ['response', 'project', 'projectDB']); + +App::get('/v1/functions/:functionId/executions') + ->groups(['api', 'functions']) + ->desc('List Executions') + ->label('scope', 'functions.read') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'listExecutions') + ->label('sdk.description', '/docs/references/functions/list-executions.md') + ->param('functionId', '', function () { return new UID(); }, 'Function unique ID.') + ->param('search', '', function () { return new Text(256); }, 'Search term to filter your list results.', true) + ->param('limit', 25, function () { return new Range(0, 100); }, 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) + ->param('offset', 0, function () { return new Range(0, 2000); }, 'Results offset. The default value is 0. Use this param to manage pagination.', true) + ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) + ->action(function ($functionId, $search, $limit, $offset, $orderType, $response, $projectDB) { + $function = $projectDB->getDocument($functionId); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + $results = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'dateCreated', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_EXECUTIONS, + 'functionId='.$function->getId(), + ], + ]); + + $response->json(['sum' => $projectDB->getSum(), 'executions' => $results]); + }, ['response', 'projectDB']); + +App::get('/v1/functions/:functionId/executions/:executionId') + ->groups(['api', 'functions']) + ->desc('Get Execution') + ->label('scope', 'functions.read') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'getExecution') + ->label('sdk.description', '/docs/references/functions/get-execution.md') + ->param('functionId', '', function () { return new UID(); }, 'Function unique ID.') + ->param('executionId', '', function () { return new UID(); }, 'Execution unique ID.') + ->action(function ($functionId, $executionId, $response, $projectDB) { + $function = $projectDB->getDocument($functionId); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + $execution = $projectDB->getDocument($executionId); + + if($execution->getAttribute('functionId') !== $function->getId()) { + throw new Exception('Execution not found', 404); + } + + if (empty($execution->getId()) || Database::SYSTEM_COLLECTION_EXECUTIONS != $execution->getCollection()) { + throw new Exception('Execution not found', 404); + } + + $response->json($execution->getArrayCopy()); + }, ['response', 'projectDB']); \ No newline at end of file diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index 0eecb23e15..cf72731c6a 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -1,6 +1,6 @@ post('/v1/graphql') +App::post('/v1/graphql') ->desc('GraphQL Endpoint') ->groups(['api', 'graphql']) ->label('scope', 'public') diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 76727563ae..b8955a1989 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -1,13 +1,12 @@ get('/v1/health') +App::get('/v1/health') ->desc('Get HTTP') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -15,23 +14,23 @@ $utopia->get('/v1/health') ->label('sdk.namespace', 'health') ->label('sdk.method', 'get') ->label('sdk.description', '/docs/references/health/get.md') - ->action( - function () use ($response) { - $response->json(['status' => 'OK']); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/health/version') + $response->json(['status' => 'OK']); + }, ['response']); + +App::get('/v1/health/version') ->desc('Get Version') ->groups(['api', 'health']) ->label('scope', 'public') - ->action( - function () use ($response) { - $response->json(['version' => APP_VERSION_STABLE]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/health/db') + $response->json(['version' => APP_VERSION_STABLE]); + }, ['response']); + +App::get('/v1/health/db') ->desc('Get DB') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -39,15 +38,16 @@ $utopia->get('/v1/health/db') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getDB') ->label('sdk.description', '/docs/references/health/get-db.md') - ->action( - function () use ($response, $register) { - $register->get('db'); /* @var $db PDO */ + ->action(function ($response, $register) { + /** @var Utopia\Response $response */ + /** @var Utopia\Registry\Registry $register */ - $response->json(['status' => 'OK']); - } - ); + $register->get('db'); /* @var $db PDO */ -$utopia->get('/v1/health/cache') + $response->json(['status' => 'OK']); + }, ['response', 'register']); + +App::get('/v1/health/cache') ->desc('Get Cache') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -55,15 +55,15 @@ $utopia->get('/v1/health/cache') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getCache') ->label('sdk.description', '/docs/references/health/get-cache.md') - ->action( - function () use ($response, $register) { - $register->get('cache'); /* @var $cache Predis\Client */ + ->action(function ($response, $register) { + /** @var Utopia\Response $response */ + /** @var Utopia\Registry\Registry $register */ + $register->get('cache'); /* @var $cache Predis\Client */ - $response->json(['status' => 'OK']); - } - ); + $response->json(['status' => 'OK']); + }, ['response']); -$utopia->get('/v1/health/time') +App::get('/v1/health/time') ->desc('Get Time') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -71,47 +71,47 @@ $utopia->get('/v1/health/time') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getTime') ->label('sdk.description', '/docs/references/health/get-time.md') - ->action( - function () use ($response) { - /* - * Code from: @see https://www.beliefmedia.com.au/query-ntp-time-server - */ - $host = 'time.google.com'; // https://developers.google.com/time/ - $gap = 60; // Allow [X] seconds gap + ->action(function ($response) { + /** @var Utopia\Response $response */ - /* Create a socket and connect to NTP server */ - $sock = \socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + /* + * Code from: @see https://www.beliefmedia.com.au/query-ntp-time-server + */ + $host = 'time.google.com'; // https://developers.google.com/time/ + $gap = 60; // Allow [X] seconds gap - \socket_connect($sock, $host, 123); + /* Create a socket and connect to NTP server */ + $sock = \socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); - /* Send request */ - $msg = "\010".\str_repeat("\0", 47); + \socket_connect($sock, $host, 123); - \socket_send($sock, $msg, \strlen($msg), 0); + /* Send request */ + $msg = "\010".\str_repeat("\0", 47); - /* Receive response and close socket */ - \socket_recv($sock, $recv, 48, MSG_WAITALL); - \socket_close($sock); + \socket_send($sock, $msg, \strlen($msg), 0); - /* Interpret response */ - $data = \unpack('N12', $recv); - $timestamp = \sprintf('%u', $data[9]); + /* Receive response and close socket */ + \socket_recv($sock, $recv, 48, MSG_WAITALL); + \socket_close($sock); - /* NTP is number of seconds since 0000 UT on 1 January 1900 - Unix time is seconds since 0000 UT on 1 January 1970 */ - $timestamp -= 2208988800; + /* Interpret response */ + $data = \unpack('N12', $recv); + $timestamp = \sprintf('%u', $data[9]); - $diff = ($timestamp - \time()); + /* NTP is number of seconds since 0000 UT on 1 January 1900 + Unix time is seconds since 0000 UT on 1 January 1970 */ + $timestamp -= 2208988800; - if ($diff > $gap || $diff < ($gap * -1)) { - throw new Exception('Server time gaps detected'); - } + $diff = ($timestamp - \time()); - $response->json(['remote' => $timestamp, 'local' => \time(), 'diff' => $diff]); + if ($diff > $gap || $diff < ($gap * -1)) { + throw new Exception('Server time gaps detected'); } - ); -$utopia->get('/v1/health/queue/webhooks') + $response->json(['remote' => $timestamp, 'local' => \time(), 'diff' => $diff]); + }, ['response']); + +App::get('/v1/health/queue/webhooks') ->desc('Get Webhooks Queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -119,13 +119,13 @@ $utopia->get('/v1/health/queue/webhooks') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueWebhooks') ->label('sdk.description', '/docs/references/health/get-queue-webhooks.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-webhooks')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/health/queue/tasks') + $response->json(['size' => Resque::size('v1-webhooks')]); + }, ['response']); + +App::get('/v1/health/queue/tasks') ->desc('Get Tasks Queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -133,13 +133,13 @@ $utopia->get('/v1/health/queue/tasks') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueTasks') ->label('sdk.description', '/docs/references/health/get-queue-tasks.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-tasks')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/health/queue/logs') + $response->json(['size' => Resque::size('v1-tasks')]); + }, ['response']); + +App::get('/v1/health/queue/logs') ->desc('Get Logs Queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -147,13 +147,13 @@ $utopia->get('/v1/health/queue/logs') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueLogs') ->label('sdk.description', '/docs/references/health/get-queue-logs.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-audit')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/health/queue/usage') + $response->json(['size' => Resque::size('v1-audit')]); + }, ['response']); + +App::get('/v1/health/queue/usage') ->desc('Get Usage Queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -161,13 +161,13 @@ $utopia->get('/v1/health/queue/usage') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueUsage') ->label('sdk.description', '/docs/references/health/get-queue-usage.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-usage')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/health/queue/certificates') + $response->json(['size' => Resque::size('v1-usage')]); + }, ['response']); + +App::get('/v1/health/queue/certificates') ->desc('Get Certificate Queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -175,13 +175,13 @@ $utopia->get('/v1/health/queue/certificates') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueCertificates') ->label('sdk.description', '/docs/references/health/get-queue-certificates.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-certificates')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/health/queue/functions') + $response->json(['size' => Resque::size('v1-certificates')]); + }, ['response']); + +App::get('/v1/health/queue/functions') ->desc('Get Functions Queue') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -189,13 +189,13 @@ $utopia->get('/v1/health/queue/functions') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getQueueFunctions') ->label('sdk.description', '/docs/references/health/get-queue-functions.md') - ->action( - function () use ($response) { - $response->json(['size' => Resque::size('v1-functions')]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/health/storage/local') + $response->json(['size' => Resque::size('v1-functions')]); + }, ['response']); + +App::get('/v1/health/storage/local') ->desc('Get Local Storage') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -203,30 +203,30 @@ $utopia->get('/v1/health/storage/local') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getStorageLocal') ->label('sdk.description', '/docs/references/health/get-storage-local.md') - ->action( - function () use ($response) { - foreach ([ - 'Uploads' => APP_STORAGE_UPLOADS, - 'Cache' => APP_STORAGE_CACHE, - 'Config' => APP_STORAGE_CONFIG, - 'Certs' => APP_STORAGE_CERTIFICATES - ] as $key => $volume) { - $device = new Local($volume); + ->action(function ($response) { + /** @var Utopia\Response $response */ - if (!\is_readable($device->getRoot())) { - throw new Exception('Device '.$key.' dir is not readable'); - } + foreach ([ + 'Uploads' => APP_STORAGE_UPLOADS, + 'Cache' => APP_STORAGE_CACHE, + 'Config' => APP_STORAGE_CONFIG, + 'Certs' => APP_STORAGE_CERTIFICATES + ] as $key => $volume) { + $device = new Local($volume); - if (!\is_writable($device->getRoot())) { - throw new Exception('Device '.$key.' dir is not writable'); - } + if (!\is_readable($device->getRoot())) { + throw new Exception('Device '.$key.' dir is not readable'); } - $response->json(['status' => 'OK']); + if (!\is_writable($device->getRoot())) { + throw new Exception('Device '.$key.' dir is not writable'); + } } - ); -$utopia->get('/v1/health/anti-virus') + $response->json(['status' => 'OK']); + }, ['response']); + +App::get('/v1/health/anti-virus') ->desc('Get Anti virus') ->groups(['api', 'health']) ->label('scope', 'health.read') @@ -234,22 +234,22 @@ $utopia->get('/v1/health/anti-virus') ->label('sdk.namespace', 'health') ->label('sdk.method', 'getAntiVirus') ->label('sdk.description', '/docs/references/health/get-storage-anti-virus.md') - ->action( - function () use ($request, $response) { - if ($request->getServer('_APP_STORAGE_ANTIVIRUS') === 'disabled') { // Check if scans are enabled - throw new Exception('Anitvirus is disabled'); - } + ->action(function ($response) { + /** @var Utopia\Response $response */ - $antiVirus = new Network('clamav', 3310); - - $response->json([ - 'status' => (@$antiVirus->ping()) ? 'online' : 'offline', - 'version' => @$antiVirus->version(), - ]); + if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'disabled') { // Check if scans are enabled + throw new Exception('Anitvirus is disabled'); } - ); -$utopia->get('/v1/health/stats') // Currently only used internally + $antiVirus = new Network('clamav', 3310); + + $response->json([ + 'status' => (@$antiVirus->ping()) ? 'online' : 'offline', + 'version' => @$antiVirus->version(), + ]); + }, ['response']); + +App::get('/v1/health/stats') // Currently only used internally ->desc('Get System Stats') ->groups(['api', 'health']) ->label('scope', 'god') @@ -257,34 +257,35 @@ $utopia->get('/v1/health/stats') // Currently only used internally // ->label('sdk.namespace', 'health') // ->label('sdk.method', 'getStats') ->label('docs', false) - ->action( - function () use ($response, $register) { - $device = Storage::getDevice('local'); - $cache = $register->get('cache'); + ->action(function ($response, $register) { + /** @var Utopia\Response $response */ + /** @var Utopia\Registry\Registry $register */ - $cacheStats = $cache->info(); + $device = Storage::getDevice('files'); + $cache = $register->get('cache'); - $response - ->json([ - 'server' => [ - 'name' => 'nginx', - 'version' => \shell_exec('nginx -v 2>&1'), - ], - 'storage' => [ - 'used' => Storage::human($device->getDirectorySize($device->getRoot().'/')), - 'partitionTotal' => Storage::human($device->getPartitionTotalSpace()), - 'partitionFree' => Storage::human($device->getPartitionFreeSpace()), - ], - 'cache' => [ - 'uptime' => (isset($cacheStats['uptime_in_seconds'])) ? $cacheStats['uptime_in_seconds'] : 0, - 'clients' => (isset($cacheStats['connected_clients'])) ? $cacheStats['connected_clients'] : 0, - 'hits' => (isset($cacheStats['keyspace_hits'])) ? $cacheStats['keyspace_hits'] : 0, - 'misses' => (isset($cacheStats['keyspace_misses'])) ? $cacheStats['keyspace_misses'] : 0, - 'memory_used' => (isset($cacheStats['used_memory'])) ? $cacheStats['used_memory'] : 0, - 'memory_used_human' => (isset($cacheStats['used_memory_human'])) ? $cacheStats['used_memory_human'] : 0, - 'memory_used_peak' => (isset($cacheStats['used_memory_peak'])) ? $cacheStats['used_memory_peak'] : 0, - 'memory_used_peak_human' => (isset($cacheStats['used_memory_peak_human'])) ? $cacheStats['used_memory_peak_human'] : 0, - ], - ]); - } - ); + $cacheStats = $cache->info(); + + $response + ->json([ + 'server' => [ + 'name' => 'nginx', + 'version' => \shell_exec('nginx -v 2>&1'), + ], + 'storage' => [ + 'used' => Storage::human($device->getDirectorySize($device->getRoot().'/')), + 'partitionTotal' => Storage::human($device->getPartitionTotalSpace()), + 'partitionFree' => Storage::human($device->getPartitionFreeSpace()), + ], + 'cache' => [ + 'uptime' => (isset($cacheStats['uptime_in_seconds'])) ? $cacheStats['uptime_in_seconds'] : 0, + 'clients' => (isset($cacheStats['connected_clients'])) ? $cacheStats['connected_clients'] : 0, + 'hits' => (isset($cacheStats['keyspace_hits'])) ? $cacheStats['keyspace_hits'] : 0, + 'misses' => (isset($cacheStats['keyspace_misses'])) ? $cacheStats['keyspace_misses'] : 0, + 'memory_used' => (isset($cacheStats['used_memory'])) ? $cacheStats['used_memory'] : 0, + 'memory_used_human' => (isset($cacheStats['used_memory_human'])) ? $cacheStats['used_memory_human'] : 0, + 'memory_used_peak' => (isset($cacheStats['used_memory_peak'])) ? $cacheStats['used_memory_peak'] : 0, + 'memory_used_peak_human' => (isset($cacheStats['used_memory_peak_human'])) ? $cacheStats['used_memory_peak_human'] : 0, + ], + ]); + }, ['response', 'register']); diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php index 148ee3e8e9..8431e00f6a 100644 --- a/app/controllers/api/locale.php +++ b/app/controllers/api/locale.php @@ -1,12 +1,9 @@ get('/v1/locale') +App::get('/v1/locale') ->desc('Get User Locale') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -14,58 +11,60 @@ $utopia->get('/v1/locale') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'get') ->label('sdk.description', '/docs/references/locale/get-locale.md') - ->action( - function () use ($response, $request, $utopia) { - $eu = include __DIR__.'/../../config/eu.php'; - $currencies = include __DIR__.'/../../config/currencies.php'; - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $output = []; - $ip = $request->getIP(); - $time = (60 * 60 * 24 * 45); // 45 days cache - $countries = Locale::getText('countries'); - $continents = Locale::getText('continents'); + ->action(function ($request, $response, $locale, $geodb) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ + /** @var GeoIp2\Database\Reader $geodb */ - if (App::MODE_TYPE_PRODUCTION !== $utopia->getMode()) { - $ip = '79.177.241.94'; - } + $eu = Config::getParam('locale-eu'); + $currencies = Config::getParam('locale-currencies'); + $output = []; + $ip = $request->getIP(); + $time = (60 * 60 * 24 * 45); // 45 days cache + $countries = $locale->getText('countries'); + $continents = $locale->getText('continents'); - $output['ip'] = $ip; - - $currency = null; - - try { - $record = $reader->country($ip); - $output['countryCode'] = $record->country->isoCode; - $output['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); - //$output['countryTimeZone'] = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $record->country->isoCode); - $output['continent'] = (isset($continents[$record->continent->code])) ? $continents[$record->continent->code] : Locale::getText('locale.country.unknown'); - $output['continentCode'] = $record->continent->code; - $output['eu'] = (\in_array($record->country->isoCode, $eu)) ? true : false; - - foreach ($currencies as $code => $element) { - if (isset($element['locations']) && isset($element['code']) && \in_array($record->country->isoCode, $element['locations'])) { - $currency = $element['code']; - } - } - - $output['currency'] = $currency; - } catch (\Exception $e) { - $output['countryCode'] = '--'; - $output['country'] = Locale::getText('locale.country.unknown'); - $output['continent'] = Locale::getText('locale.country.unknown'); - $output['continentCode'] = '--'; - $output['eu'] = false; - $output['currency'] = $currency; - } - - $response - ->addHeader('Cache-Control', 'public, max-age='.$time) - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache - ->json($output); + if (!App::isProduction()) { + $ip = '79.177.241.94'; } - ); -$utopia->get('/v1/locale/countries') + $output['ip'] = $ip; + + $currency = null; + + try { + $record = $geodb->country($ip); + $output['countryCode'] = $record->country->isoCode; + $output['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : $locale->getText('locale.country.unknown'); + //$output['countryTimeZone'] = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $record->country->isoCode); + $output['continent'] = (isset($continents[$record->continent->code])) ? $continents[$record->continent->code] : $locale->getText('locale.country.unknown'); + $output['continentCode'] = $record->continent->code; + $output['eu'] = (\in_array($record->country->isoCode, $eu)) ? true : false; + + foreach ($currencies as $code => $element) { + if (isset($element['locations']) && isset($element['code']) && \in_array($record->country->isoCode, $element['locations'])) { + $currency = $element['code']; + } + } + + $output['currency'] = $currency; + } catch (\Exception $e) { + $output['countryCode'] = '--'; + $output['country'] = $locale->getText('locale.country.unknown'); + $output['continent'] = $locale->getText('locale.country.unknown'); + $output['continentCode'] = '--'; + $output['eu'] = false; + $output['currency'] = $currency; + } + + $response + ->addHeader('Cache-Control', 'public, max-age='.$time) + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache + ->json($output); + }, ['request', 'response', 'locale', 'geodb']); + +App::get('/v1/locale/countries') ->desc('List Countries') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -73,17 +72,18 @@ $utopia->get('/v1/locale/countries') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getCountries') ->label('sdk.description', '/docs/references/locale/get-countries.md') - ->action( - function () use ($response) { - $list = Locale::getText('countries'); /* @var $list array */ + ->action(function ($response, $locale) { + /** @var Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ - \asort($list); + $list = $locale->getText('countries'); /* @var $list array */ - $response->json($list); - } - ); + \asort($list); -$utopia->get('/v1/locale/countries/eu') + $response->json($list); + }, ['response', 'locale']); + +App::get('/v1/locale/countries/eu') ->desc('List EU Countries') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -91,25 +91,26 @@ $utopia->get('/v1/locale/countries/eu') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getCountriesEU') ->label('sdk.description', '/docs/references/locale/get-countries-eu.md') - ->action( - function () use ($response) { - $countries = Locale::getText('countries'); /* @var $countries array */ - $eu = include __DIR__.'/../../config/eu.php'; - $list = []; + ->action(function ($response, $locale) { + /** @var Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ - foreach ($eu as $code) { - if (\array_key_exists($code, $countries)) { - $list[$code] = $countries[$code]; - } + $countries = $locale->getText('countries'); /* @var $countries array */ + $eu = Config::getParam('locale-eu'); + $list = []; + + foreach ($eu as $code) { + if (\array_key_exists($code, $countries)) { + $list[$code] = $countries[$code]; } - - \asort($list); - - $response->json($list); } - ); -$utopia->get('/v1/locale/countries/phones') + \asort($list); + + $response->json($list); + }, ['response', 'locale']); + +App::get('/v1/locale/countries/phones') ->desc('List Countries Phone Codes') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -117,25 +118,26 @@ $utopia->get('/v1/locale/countries/phones') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getCountriesPhones') ->label('sdk.description', '/docs/references/locale/get-countries-phones.md') - ->action( - function () use ($response) { - $list = include __DIR__.'/../../config/phones.php'; /* @var $list array */ + ->action(function ($response, $locale) { + /** @var Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ - $countries = Locale::getText('countries'); /* @var $countries array */ + $list = Config::getParam('locale-phones'); /* @var $list array */ - foreach ($list as $code => $name) { - if (\array_key_exists($code, $countries)) { - $list[$code] = '+'.$list[$code]; - } + $countries = $locale->getText('countries'); /* @var $countries array */ + + foreach ($list as $code => $name) { + if (\array_key_exists($code, $countries)) { + $list[$code] = '+'.$list[$code]; } - - \asort($list); - - $response->json($list); } - ); -$utopia->get('/v1/locale/continents') + \asort($list); + + $response->json($list); + }, ['response', 'locale']); + +App::get('/v1/locale/continents') ->desc('List Continents') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -143,18 +145,18 @@ $utopia->get('/v1/locale/continents') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getContinents') ->label('sdk.description', '/docs/references/locale/get-continents.md') - ->action( - function () use ($response) { - $list = Locale::getText('continents'); /* @var $list array */ + ->action(function ($response, $locale) { + /** @var Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ - \asort($list); + $list = $locale->getText('continents'); /* @var $list array */ - $response->json($list); - } - ); + \asort($list); + $response->json($list); + }, ['response', 'locale']); -$utopia->get('/v1/locale/currencies') +App::get('/v1/locale/currencies') ->desc('List Currencies') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -162,16 +164,16 @@ $utopia->get('/v1/locale/currencies') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getCurrencies') ->label('sdk.description', '/docs/references/locale/get-currencies.md') - ->action( - function () use ($response) { - $currencies = include __DIR__.'/../../config/currencies.php'; + ->action(function ($response) { + /** @var Utopia\Response $response */ - $response->json($currencies); - } - ); + $currencies = Config::getParam('locale-currencies'); + + $response->json($currencies); + }, ['response']); -$utopia->get('/v1/locale/languages') +App::get('/v1/locale/languages') ->desc('List Languages') ->groups(['api', 'locale']) ->label('scope', 'locale.read') @@ -179,10 +181,10 @@ $utopia->get('/v1/locale/languages') ->label('sdk.namespace', 'locale') ->label('sdk.method', 'getLanguages') ->label('sdk.description', '/docs/references/locale/get-languages.md') - ->action( - function () use ($response) { - $languages = include __DIR__.'/../../config/languages.php'; + ->action(function ($response) { + /** @var Utopia\Response $response */ - $response->json($languages); - } - ); \ No newline at end of file + $languages = Config::getParam('locale-languages'); + + $response->json($languages); + }, ['response']); \ No newline at end of file diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index cf81df66ba..5da7be03f3 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1,7 +1,6 @@ post('/v1/projects') +App::post('/v1/projects') ->desc('Create Project') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -40,115 +38,123 @@ $utopia->post('/v1/projects') ->param('legalCity', '', function () { return new Text(256); }, 'Project legal City.', true) ->param('legalAddress', '', function () { return new Text(256); }, 'Project legal Address.', true) ->param('legalTaxId', '', function () { return new Text(256); }, 'Project legal Tax ID.', true) - ->action( - function ($name, $teamId, $description, $logo, $url, $legalName, $legalCountry, $legalState, $legalCity, $legalAddress, $legalTaxId) use ($response, $user, $consoleDB, $projectDB) { - $team = $projectDB->getDocument($teamId); + ->action(function ($name, $teamId, $description, $logo, $url, $legalName, $legalCountry, $legalState, $legalCity, $legalAddress, $legalTaxId, $response, $consoleDB, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $team = $projectDB->getDocument($teamId); - $project = $consoleDB->createDocument( - [ - '$collection' => Database::SYSTEM_COLLECTION_PROJECTS, - '$permissions' => [ - 'read' => ['team:'.$teamId], - 'write' => ['team:'.$teamId.'/owner', 'team:'.$teamId.'/developer'], - ], - 'name' => $name, - 'description' => $description, - 'logo' => $logo, - 'url' => $url, - 'legalName' => $legalName, - 'legalCountry' => $legalCountry, - 'legalState' => $legalState, - 'legalCity' => $legalCity, - 'legalAddress' => $legalAddress, - 'legalTaxId' => $legalTaxId, - 'teamId' => $team->getId(), - 'platforms' => [], - 'webhooks' => [], - 'keys' => [], - 'tasks' => [], - ] - ); - - if (false === $project) { - throw new Exception('Failed saving project to DB', 500); - } - - $consoleDB->createNamespace($project->getId()); - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($project->getArrayCopy()) - ; + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); } - ); -$utopia->get('/v1/projects') + $project = $consoleDB->createDocument( + [ + '$collection' => Database::SYSTEM_COLLECTION_PROJECTS, + '$permissions' => [ + 'read' => ['team:'.$teamId], + 'write' => ['team:'.$teamId.'/owner', 'team:'.$teamId.'/developer'], + ], + 'name' => $name, + 'description' => $description, + 'logo' => $logo, + 'url' => $url, + 'legalName' => $legalName, + 'legalCountry' => $legalCountry, + 'legalState' => $legalState, + 'legalCity' => $legalCity, + 'legalAddress' => $legalAddress, + 'legalTaxId' => $legalTaxId, + 'teamId' => $team->getId(), + 'platforms' => [], + 'webhooks' => [], + 'keys' => [], + 'tasks' => [], + ] + ); + + if (false === $project) { + throw new Exception('Failed saving project to DB', 500); + } + + $consoleDB->createNamespace($project->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($project->getArrayCopy()) + ; + }, ['response', 'consoleDB', 'projectDB']); + +App::get('/v1/projects') ->desc('List Projects') ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'list') - ->action( - function () use ($request, $response, $consoleDB) { - $results = $consoleDB->getCollection([ - 'limit' => 20, - 'offset' => 0, - 'orderField' => 'name', - 'orderType' => 'ASC', - 'orderCast' => 'string', - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_PROJECTS, - ], - ]); + ->param('search', '', function () { return new Text(256); }, 'Search term to filter your list results.', true) + ->param('limit', 25, function () { return new Range(0, 100); }, 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) + ->param('offset', 0, function () { return new Range(0, 2000); }, 'Results offset. The default value is 0. Use this param to manage pagination.', true) + ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) + ->action(function ($search, $limit, $offset, $orderType, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - foreach ($results as $project) { - foreach (Config::getParam('providers') as $provider => $node) { - $secret = \json_decode($project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'), true); + $results = $consoleDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'registration', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_PROJECTS, + ], + ]); + foreach ($results as $project) { + foreach (Config::getParam('providers') as $provider => $node) { + $secret = \json_decode($project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'), true); - if (!empty($secret) && isset($secret['version'])) { - $key = $request->getServer('_APP_OPENSSL_KEY_V'.$secret['version']); - $project->setAttribute('usersOauth2'.\ucfirst($provider).'Secret', OpenSSL::decrypt($secret['data'], $secret['method'], $key, 0, \hex2bin($secret['iv']), \hex2bin($secret['tag']))); - } + if (!empty($secret) && isset($secret['version'])) { + $key = App::getEnv('_APP_OPENSSL_KEY_V'.$secret['version']); + $project->setAttribute('usersOauth2'.\ucfirst($provider).'Secret', OpenSSL::decrypt($secret['data'], $secret['method'], $key, 0, \hex2bin($secret['iv']), \hex2bin($secret['tag']))); } } - - $response->json($results); } - ); -$utopia->get('/v1/projects/:projectId') + $response->json(['sum' => $consoleDB->getSum(), 'projects' => $results]); + }, ['response', 'consoleDB']); + +App::get('/v1/projects/:projectId') ->desc('Get Project') ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'get') ->param('projectId', '', function () { return new UID(); }, 'Project unique ID.') - ->action( - function ($projectId) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - foreach (Config::getParam('providers') as $provider => $node) { - $secret = \json_decode($project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'), true); - - if (!empty($secret) && isset($secret['version'])) { - $key = $request->getServer('_APP_OPENSSL_KEY_V'.$secret['version']); - $project->setAttribute('usersOauth2'.\ucfirst($provider).'Secret', OpenSSL::decrypt($secret['data'], $secret['method'], $key, 0, \hex2bin($secret['iv']), \hex2bin($secret['tag']))); - } - } - - $response->json($project->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->get('/v1/projects/:projectId/usage') + foreach (Config::getParam('providers') as $provider => $node) { + $secret = \json_decode($project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'), true); + + if (!empty($secret) && isset($secret['version'])) { + $key = App::getEnv('_APP_OPENSSL_KEY_V'.$secret['version']); + $project->setAttribute('usersOauth2'.\ucfirst($provider).'Secret', OpenSSL::decrypt($secret['data'], $secret['method'], $key, 0, \hex2bin($secret['iv']), \hex2bin($secret['tag']))); + } + } + + $response->json($project->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::get('/v1/projects/:projectId/usage') ->desc('Get Project') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -156,161 +162,173 @@ $utopia->get('/v1/projects/:projectId/usage') ->label('sdk.method', 'getUsage') ->param('projectId', '', function () { return new UID(); }, 'Project unique ID.') ->param('range', 'last30', function () { return new WhiteList(['daily', 'monthly', 'last30', 'last90']); }, 'Date range.', true) - ->action( - function ($projectId, $range) use ($response, $consoleDB, $projectDB, $register) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $range, $response, $consoleDB, $projectDB, $register) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Registry\Registry $register */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); + $project = $consoleDB->getDocument($projectId); + + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); + } + + $period = [ + 'daily' => [ + 'start' => DateTime::createFromFormat('U', \strtotime('today')), + 'end' => DateTime::createFromFormat('U', \strtotime('tomorrow')), + 'group' => '1m', + ], + 'monthly' => [ + 'start' => DateTime::createFromFormat('U', \strtotime('midnight first day of this month')), + 'end' => DateTime::createFromFormat('U', \strtotime('midnight last day of this month')), + 'group' => '1d', + ], + 'last30' => [ + 'start' => DateTime::createFromFormat('U', \strtotime('-30 days')), + 'end' => DateTime::createFromFormat('U', \strtotime('tomorrow')), + 'group' => '1d', + ], + 'last90' => [ + 'start' => DateTime::createFromFormat('U', \strtotime('-90 days')), + 'end' => DateTime::createFromFormat('U', \strtotime('today')), + 'group' => '1d', + ], + // 'yearly' => [ + // 'start' => DateTime::createFromFormat('U', strtotime('midnight first day of january')), + // 'end' => DateTime::createFromFormat('U', strtotime('midnight last day of december')), + // 'group' => '4w', + // ], + ]; + + $client = $register->get('influxdb'); + + $requests = []; + $network = []; + + if ($client) { + $start = $period[$range]['start']->format(DateTime::RFC3339); + $end = $period[$range]['end']->format(DateTime::RFC3339); + $database = $client->selectDB('telegraf'); + + // Requests + $result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_requests_all" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' GROUP BY time('.$period[$range]['group'].') FILL(null)'); + $points = $result->getPoints(); + + foreach ($points as $point) { + $requests[] = [ + 'value' => (!empty($point['value'])) ? $point['value'] : 0, + 'date' => \strtotime($point['time']), + ]; } - $period = [ - 'daily' => [ - 'start' => DateTime::createFromFormat('U', \strtotime('today')), - 'end' => DateTime::createFromFormat('U', \strtotime('tomorrow')), - 'group' => '1m', - ], - 'monthly' => [ - 'start' => DateTime::createFromFormat('U', \strtotime('midnight first day of this month')), - 'end' => DateTime::createFromFormat('U', \strtotime('midnight last day of this month')), - 'group' => '1d', - ], - 'last30' => [ - 'start' => DateTime::createFromFormat('U', \strtotime('-30 days')), - 'end' => DateTime::createFromFormat('U', \strtotime('tomorrow')), - 'group' => '1d', - ], - 'last90' => [ - 'start' => DateTime::createFromFormat('U', \strtotime('-90 days')), - 'end' => DateTime::createFromFormat('U', \strtotime('today')), - 'group' => '1d', - ], - // 'yearly' => [ - // 'start' => DateTime::createFromFormat('U', strtotime('midnight first day of january')), - // 'end' => DateTime::createFromFormat('U', strtotime('midnight last day of december')), - // 'group' => '4w', - // ], - ]; + // Network + $result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_network_all" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' GROUP BY time('.$period[$range]['group'].') FILL(null)'); + $points = $result->getPoints(); - $client = $register->get('influxdb'); - - $requests = []; - $network = []; - - if ($client) { - $start = $period[$range]['start']->format(DateTime::RFC3339); - $end = $period[$range]['end']->format(DateTime::RFC3339); - $database = $client->selectDB('telegraf'); - - // Requests - $result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_requests_all" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' GROUP BY time('.$period[$range]['group'].') FILL(null)'); - $points = $result->getPoints(); - - foreach ($points as $point) { - $requests[] = [ - 'value' => (!empty($point['value'])) ? $point['value'] : 0, - 'date' => \strtotime($point['time']), - ]; - } - - // Network - $result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_network_all" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' GROUP BY time('.$period[$range]['group'].') FILL(null)'); - $points = $result->getPoints(); - - foreach ($points as $point) { - $network[] = [ - 'value' => (!empty($point['value'])) ? $point['value'] : 0, - 'date' => \strtotime($point['time']), - ]; - } + foreach ($points as $point) { + $network[] = [ + 'value' => (!empty($point['value'])) ? $point['value'] : 0, + 'date' => \strtotime($point['time']), + ]; } + } - // Users + // Users - $projectDB->getCollection([ + $projectDB->getCollection([ + 'limit' => 0, + 'offset' => 0, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + ], + ]); + + $usersTotal = $projectDB->getSum(); + + // Documents + + $collections = $projectDB->getCollection([ + 'limit' => 100, + 'offset' => 0, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_COLLECTIONS, + ], + ]); + + $collectionsTotal = $projectDB->getSum(); + + $documents = []; + + foreach ($collections as $collection) { + $result = $projectDB->getCollection([ 'limit' => 0, 'offset' => 0, 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, + '$collection='.$collection['$id'], ], ]); - $usersTotal = $projectDB->getSum(); - - // Documents - - $collections = $projectDB->getCollection([ - 'limit' => 100, - 'offset' => 0, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_COLLECTIONS, - ], - ]); - - $collectionsTotal = $projectDB->getSum(); - - $documents = []; - - foreach ($collections as $collection) { - $result = $projectDB->getCollection([ - 'limit' => 0, - 'offset' => 0, - 'filters' => [ - '$collection='.$collection['$id'], - ], - ]); - - $documents[] = ['name' => $collection['name'], 'total' => $projectDB->getSum()]; - } - - // Tasks - $tasksTotal = \count($project->getAttribute('tasks', [])); - - $response->json([ - 'requests' => [ - 'data' => $requests, - 'total' => \array_sum(\array_map(function ($item) { - return $item['value']; - }, $requests)), - ], - 'network' => [ - 'data' => $network, - 'total' => \array_sum(\array_map(function ($item) { - return $item['value']; - }, $network)), - ], - 'collections' => [ - 'data' => $collections, - 'total' => $collectionsTotal, - ], - 'documents' => [ - 'data' => $documents, - 'total' => \array_sum(\array_map(function ($item) { - return $item['total']; - }, $documents)), - ], - 'users' => [ - 'data' => [], - 'total' => $usersTotal, - ], - 'tasks' => [ - 'data' => [], - 'total' => $tasksTotal, - ], - 'storage' => [ - 'total' => $projectDB->getCount( - [ - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_FILES, - ], - ] - ), - ], - ]); + $documents[] = ['name' => $collection['name'], 'total' => $projectDB->getSum()]; } - ); -$utopia->patch('/v1/projects/:projectId') + // Tasks + $tasksTotal = \count($project->getAttribute('tasks', [])); + + $response->json([ + 'requests' => [ + 'data' => $requests, + 'total' => \array_sum(\array_map(function ($item) { + return $item['value']; + }, $requests)), + ], + 'network' => [ + 'data' => \array_map(function ($value) {return ['value' => \round($value['value'] / 1000000, 2), 'date' => $value['date']];}, $network), // convert bytes to mb + 'total' => \array_sum(\array_map(function ($item) { + return $item['value']; + }, $network)), + ], + 'collections' => [ + 'data' => $collections, + 'total' => $collectionsTotal, + ], + 'documents' => [ + 'data' => $documents, + 'total' => \array_sum(\array_map(function ($item) { + return $item['total']; + }, $documents)), + ], + 'users' => [ + 'data' => [], + 'total' => $usersTotal, + ], + 'tasks' => [ + 'data' => [], + 'total' => $tasksTotal, + ], + 'storage' => [ + 'total' => $projectDB->getCount( + [ + 'attribute' => 'sizeOriginal', + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_FILES, + ], + ] + ) + + $projectDB->getCount( + [ + 'attribute' => 'codeSize', + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_TAGS, + ], + ] + ), + ], + ]); + }, ['response', 'consoleDB', 'projectDB', 'register']); + +App::patch('/v1/projects/:projectId') ->desc('Update Project') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -327,36 +345,37 @@ $utopia->patch('/v1/projects/:projectId') ->param('legalCity', '', function () { return new Text(256); }, 'Project legal city.', true) ->param('legalAddress', '', function () { return new Text(256); }, 'Project legal address.', true) ->param('legalTaxId', '', function () { return new Text(256); }, 'Project legal tax ID.', true) - ->action( - function ($projectId, $name, $description, $logo, $url, $legalName, $legalCountry, $legalState, $legalCity, $legalAddress, $legalTaxId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $name, $description, $logo, $url, $legalName, $legalCountry, $legalState, $legalCity, $legalAddress, $legalTaxId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $project = $consoleDB->updateDocument(\array_merge($project->getArrayCopy(), [ - 'name' => $name, - 'description' => $description, - 'logo' => $logo, - 'url' => $url, - 'legalName' => $legalName, - 'legalCountry' => $legalCountry, - 'legalState' => $legalState, - 'legalCity' => $legalCity, - 'legalAddress' => $legalAddress, - 'legalTaxId' => $legalTaxId, - ])); - - if (false === $project) { - throw new Exception('Failed saving project to DB', 500); - } - - $response->json($project->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->patch('/v1/projects/:projectId/oauth2') + $project = $consoleDB->updateDocument(\array_merge($project->getArrayCopy(), [ + 'name' => $name, + 'description' => $description, + 'logo' => $logo, + 'url' => $url, + 'legalName' => $legalName, + 'legalCountry' => $legalCountry, + 'legalState' => $legalState, + 'legalCity' => $legalCity, + 'legalAddress' => $legalAddress, + 'legalTaxId' => $legalTaxId, + ])); + + if (false === $project) { + throw new Exception('Failed saving project to DB', 500); + } + + $response->json($project->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::patch('/v1/projects/:projectId/oauth2') ->desc('Update Project OAuth2') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -366,39 +385,40 @@ $utopia->patch('/v1/projects/:projectId/oauth2') ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'Provider Name', false) ->param('appId', '', function () { return new Text(256); }, 'Provider app ID.', true) ->param('secret', '', function () { return new text(512); }, 'Provider secret key.', true) - ->action( - function ($projectId, $provider, $appId, $secret) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $provider, $appId, $secret, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $key = $request->getServer('_APP_OPENSSL_KEY_V1'); - $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); - $tag = null; - $secret = \json_encode([ - 'data' => OpenSSL::encrypt($secret, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), - 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => \bin2hex($iv), - 'tag' => \bin2hex($tag), - 'version' => '1', - ]); - - $project = $consoleDB->updateDocument(\array_merge($project->getArrayCopy(), [ - 'usersOauth2'.\ucfirst($provider).'Appid' => $appId, - 'usersOauth2'.\ucfirst($provider).'Secret' => $secret, - ])); - - if (false === $project) { - throw new Exception('Failed saving project to DB', 500); - } - - $response->json($project->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->delete('/v1/projects/:projectId') + $key = App::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $tag = null; + $secret = \json_encode([ + 'data' => OpenSSL::encrypt($secret, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), + 'method' => OpenSSL::CIPHER_AES_128_GCM, + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), + 'version' => '1', + ]); + + $project = $consoleDB->updateDocument(\array_merge($project->getArrayCopy(), [ + 'usersOauth2'.\ucfirst($provider).'Appid' => $appId, + 'usersOauth2'.\ucfirst($provider).'Secret' => $secret, + ])); + + if (false === $project) { + throw new Exception('Failed saving project to DB', 500); + } + + $response->json($project->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::delete('/v1/projects/:projectId') ->desc('Delete Project') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -406,45 +426,48 @@ $utopia->delete('/v1/projects/:projectId') ->label('sdk.method', 'delete') ->param('projectId', '', function () { return new UID(); }, 'Project unique ID.') ->param('password', '', function () { return new UID(); }, 'Your user password for confirmation. Must be between 6 to 32 chars.') - ->action( - function ($projectId, $password) use ($response, $consoleDB, $user, $deletes) { - if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password - throw new Exception('Invalid credentials', 401); - } + ->action(function ($projectId, $password, $response, $user, $consoleDB, $deletes) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $consoleDB */ + /** @var Appwrite\Event\Event $deletes */ - $project = $consoleDB->getDocument($projectId); + if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password + throw new Exception('Invalid credentials', 401); + } - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $deletes->setParam('document', $project->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); + } - foreach (['keys', 'webhooks', 'tasks', 'platforms', 'domains'] as $key) { // Delete all children (keys, webhooks, tasks [stop tasks?], platforms) - $list = $project->getAttribute('webhooks', []); + $deletes->setParam('document', $project->getArrayCopy()); - foreach ($list as $document) { /* @var $document Document */ - if (!$consoleDB->deleteDocument($projectId)) { - throw new Exception('Failed to remove project document ('.$key.')] from DB', 500); - } + foreach (['keys', 'webhooks', 'tasks', 'platforms', 'domains'] as $key) { // Delete all children (keys, webhooks, tasks [stop tasks?], platforms) + $list = $project->getAttribute('webhooks', []); + + foreach ($list as $document) { /* @var $document Document */ + if (!$consoleDB->deleteDocument($projectId)) { + throw new Exception('Failed to remove project document ('.$key.')] from DB', 500); } } - - if (!$consoleDB->deleteDocument($project->getAttribute('teamId', null))) { - throw new Exception('Failed to remove project team from DB', 500); - } - - if (!$consoleDB->deleteDocument($projectId)) { - throw new Exception('Failed to remove project from DB', 500); - } - - $response->noContent(); } - ); + + if (!$consoleDB->deleteDocument($project->getAttribute('teamId', null))) { + throw new Exception('Failed to remove project team from DB', 500); + } + + if (!$consoleDB->deleteDocument($projectId)) { + throw new Exception('Failed to remove project from DB', 500); + } + + $response->noContent(); + }, ['response', 'user', 'consoleDB', 'deletes']); // Webhooks -$utopia->post('/v1/projects/:projectId/webhooks') +App::post('/v1/projects/:projectId/webhooks') ->desc('Create Webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -452,98 +475,100 @@ $utopia->post('/v1/projects/:projectId/webhooks') ->label('sdk.method', 'createWebhook') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('name', null, function () { return new Text(256); }, 'Webhook name.') - ->param('events', null, function () { return new ArrayList(new Text(256)); }, 'Webhook events list.') - ->param('url', null, function () { return new Text(2000); }, 'Webhook URL.') + ->param('events', null, function () { return new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)); }, 'Events list.') + ->param('url', null, function () { return new URL(); }, 'Webhook URL.') ->param('security', false, function () { return new Boolean(true); }, 'Certificate verification, false for disabled or true for enabled.') ->param('httpUser', '', function () { return new Text(256); }, 'Webhook HTTP user.', true) ->param('httpPass', '', function () { return new Text(256); }, 'Webhook HTTP password.', true) - ->action( - function ($projectId, $name, $events, $url, $security, $httpUser, $httpPass) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $name, $events, $url, $security, $httpUser, $httpPass, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); - $key = $request->getServer('_APP_OPENSSL_KEY_V1'); - $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); - $tag = null; - $httpPass = \json_encode([ - 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), - 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => \bin2hex($iv), - 'tag' => \bin2hex($tag), - 'version' => '1', - ]); - - $webhook = $consoleDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_WEBHOOKS, - '$permissions' => [ - 'read' => ['team:'.$project->getAttribute('teamId', null)], - 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], - ], - 'name' => $name, - 'events' => $events, - 'url' => $url, - 'security' => (int) $security, - 'httpUser' => $httpUser, - 'httpPass' => $httpPass, - ]); - - if (false === $webhook) { - throw new Exception('Failed saving webhook to DB', 500); - } - - $project->setAttribute('webhooks', $webhook, Document::SET_TYPE_APPEND); - - $project = $consoleDB->updateDocument($project->getArrayCopy()); - - if (false === $project) { - throw new Exception('Failed saving project to DB', 500); - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($webhook->getArrayCopy()) - ; + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->get('/v1/projects/:projectId/webhooks') + $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); + $key = App::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $tag = null; + $httpPass = \json_encode([ + 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), + 'method' => OpenSSL::CIPHER_AES_128_GCM, + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), + 'version' => '1', + ]); + + $webhook = $consoleDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_WEBHOOKS, + '$permissions' => [ + 'read' => ['team:'.$project->getAttribute('teamId', null)], + 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], + ], + 'name' => $name, + 'events' => $events, + 'url' => $url, + 'security' => $security, + 'httpUser' => $httpUser, + 'httpPass' => $httpPass, + ]); + + if (false === $webhook) { + throw new Exception('Failed saving webhook to DB', 500); + } + + $project->setAttribute('webhooks', $webhook, Document::SET_TYPE_APPEND); + + $project = $consoleDB->updateDocument($project->getArrayCopy()); + + if (false === $project) { + throw new Exception('Failed saving project to DB', 500); + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($webhook->getArrayCopy()) + ; + }, ['response', 'consoleDB']); + +App::get('/v1/projects/:projectId/webhooks') ->desc('List Webhooks') ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listWebhooks') ->param('projectId', '', function () { return new UID(); }, 'Project unique ID.') - ->action( - function ($projectId) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $webhooks = $project->getAttribute('webhooks', []); - - foreach ($webhooks as $webhook) { /* @var $webhook Document */ - $httpPass = \json_decode($webhook->getAttribute('httpPass', '{}'), true); - - if (empty($httpPass) || !isset($httpPass['version'])) { - continue; - } - - $key = $request->getServer('_APP_OPENSSL_KEY_V'.$httpPass['version']); - - $webhook->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); - } - - $response->json($webhooks); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->get('/v1/projects/:projectId/webhooks/:webhookId') + $webhooks = $project->getAttribute('webhooks', []); + + foreach ($webhooks as $webhook) { /* @var $webhook Document */ + $httpPass = \json_decode($webhook->getAttribute('httpPass', '{}'), true); + + if (empty($httpPass) || !isset($httpPass['version'])) { + continue; + } + + $key = App::getEnv('_APP_OPENSSL_KEY_V'.$httpPass['version']); + + $webhook->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); + } + + $response->json($webhooks); + }, ['response', 'consoleDB']); + +App::get('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Get Webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -551,33 +576,33 @@ $utopia->get('/v1/projects/:projectId/webhooks/:webhookId') ->label('sdk.method', 'getWebhook') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('webhookId', null, function () { return new UID(); }, 'Webhook unique ID.') - ->action( - function ($projectId, $webhookId) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $webhookId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $webhook = $project->search('$id', $webhookId, $project->getAttribute('webhooks', [])); - - if (empty($webhook) || !$webhook instanceof Document) { - throw new Exception('Webhook not found', 404); - } - - $httpPass = \json_decode($webhook->getAttribute('httpPass', '{}'), true); - - if (!empty($httpPass) && isset($httpPass['version'])) { - $key = $request->getServer('_APP_OPENSSL_KEY_V'.$httpPass['version']); - $webhook->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); - } - - $response->json($webhook->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); + $webhook = $project->search('$id', $webhookId, $project->getAttribute('webhooks', [])); -$utopia->put('/v1/projects/:projectId/webhooks/:webhookId') + if (empty($webhook) || !$webhook instanceof Document) { + throw new Exception('Webhook not found', 404); + } + + $httpPass = \json_decode($webhook->getAttribute('httpPass', '{}'), true); + + if (!empty($httpPass) && isset($httpPass['version'])) { + $key = App::getEnv('_APP_OPENSSL_KEY_V'.$httpPass['version']); + $webhook->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); + } + + $response->json($webhook->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::put('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Update Webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -586,54 +611,55 @@ $utopia->put('/v1/projects/:projectId/webhooks/:webhookId') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('webhookId', null, function () { return new UID(); }, 'Webhook unique ID.') ->param('name', null, function () { return new Text(256); }, 'Webhook name.') - ->param('events', null, function () { return new ArrayList(new Text(256)); }, 'Webhook events list.') - ->param('url', null, function () { return new Text(2000); }, 'Webhook URL.') + ->param('events', null, function () { return new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)); }, 'Events list.') + ->param('url', null, function () { return new URL(); }, 'Webhook URL.') ->param('security', false, function () { return new Boolean(true); }, 'Certificate verification, false for disabled or true for enabled.') ->param('httpUser', '', function () { return new Text(256); }, 'Webhook HTTP user.', true) ->param('httpPass', '', function () { return new Text(256); }, 'Webhook HTTP password.', true) - ->action( - function ($projectId, $webhookId, $name, $events, $url, $security, $httpUser, $httpPass) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $webhookId, $name, $events, $url, $security, $httpUser, $httpPass, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); - $key = $request->getServer('_APP_OPENSSL_KEY_V1'); - $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); - $tag = null; - $httpPass = \json_encode([ - 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), - 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => \bin2hex($iv), - 'tag' => \bin2hex($tag), - 'version' => '1', - ]); - - $webhook = $project->search('$id', $webhookId, $project->getAttribute('webhooks', [])); - - if (empty($webhook) || !$webhook instanceof Document) { - throw new Exception('Webhook not found', 404); - } - - $webhook - ->setAttribute('name', $name) - ->setAttribute('events', $events) - ->setAttribute('url', $url) - ->setAttribute('security', (int) $security) - ->setAttribute('httpUser', $httpUser) - ->setAttribute('httpPass', $httpPass) - ; - - if (false === $consoleDB->updateDocument($webhook->getArrayCopy())) { - throw new Exception('Failed saving webhook to DB', 500); - } - - $response->json($webhook->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->delete('/v1/projects/:projectId/webhooks/:webhookId') + $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); + $key = App::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $tag = null; + $httpPass = \json_encode([ + 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), + 'method' => OpenSSL::CIPHER_AES_128_GCM, + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), + 'version' => '1', + ]); + + $webhook = $project->search('$id', $webhookId, $project->getAttribute('webhooks', [])); + + if (empty($webhook) || !$webhook instanceof Document) { + throw new Exception('Webhook not found', 404); + } + + $webhook + ->setAttribute('name', $name) + ->setAttribute('events', $events) + ->setAttribute('url', $url) + ->setAttribute('security', $security) + ->setAttribute('httpUser', $httpUser) + ->setAttribute('httpPass', $httpPass) + ; + + if (false === $consoleDB->updateDocument($webhook->getArrayCopy())) { + throw new Exception('Failed saving webhook to DB', 500); + } + + $response->json($webhook->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::delete('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Delete Webhook') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -641,31 +667,32 @@ $utopia->delete('/v1/projects/:projectId/webhooks/:webhookId') ->label('sdk.method', 'deleteWebhook') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('webhookId', null, function () { return new UID(); }, 'Webhook unique ID.') - ->action( - function ($projectId, $webhookId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $webhookId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $webhook = $project->search('$id', $webhookId, $project->getAttribute('webhooks', [])); - - if (empty($webhook) || !$webhook instanceof Document) { - throw new Exception('Webhook not found', 404); - } - - if (!$consoleDB->deleteDocument($webhook->getId())) { - throw new Exception('Failed to remove webhook from DB', 500); - } - - $response->noContent(); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); + + $webhook = $project->search('$id', $webhookId, $project->getAttribute('webhooks', [])); + + if (empty($webhook) || !$webhook instanceof Document) { + throw new Exception('Webhook not found', 404); + } + + if (!$consoleDB->deleteDocument($webhook->getId())) { + throw new Exception('Failed to remove webhook from DB', 500); + } + + $response->noContent(); + }, ['response', 'consoleDB']); // Keys -$utopia->post('/v1/projects/:projectId/keys') +App::post('/v1/projects/:projectId/keys') ->desc('Create Key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -673,65 +700,67 @@ $utopia->post('/v1/projects/:projectId/keys') ->label('sdk.method', 'createKey') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('name', null, function () { return new Text(256); }, 'Key name.') - ->param('scopes', null, function () use ($scopes) { return new ArrayList(new WhiteList($scopes)); }, 'Key scopes list.') - ->action( - function ($projectId, $name, $scopes) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->param('scopes', null, function () { return new ArrayList(new WhiteList(Config::getParam('scopes'))); }, 'Key scopes list.') + ->action(function ($projectId, $name, $scopes, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $key = $consoleDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_KEYS, - '$permissions' => [ - 'read' => ['team:'.$project->getAttribute('teamId', null)], - 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], - ], - 'name' => $name, - 'scopes' => $scopes, - 'secret' => \bin2hex(\random_bytes(128)), - ]); - - if (false === $key) { - throw new Exception('Failed saving key to DB', 500); - } - - $project->setAttribute('keys', $key, Document::SET_TYPE_APPEND); - - $project = $consoleDB->updateDocument($project->getArrayCopy()); - - if (false === $project) { - throw new Exception('Failed saving project to DB', 500); - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($key->getArrayCopy()) - ; + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->get('/v1/projects/:projectId/keys') + $key = $consoleDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_KEYS, + '$permissions' => [ + 'read' => ['team:'.$project->getAttribute('teamId', null)], + 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], + ], + 'name' => $name, + 'scopes' => $scopes, + 'secret' => \bin2hex(\random_bytes(128)), + ]); + + if (false === $key) { + throw new Exception('Failed saving key to DB', 500); + } + + $project->setAttribute('keys', $key, Document::SET_TYPE_APPEND); + + $project = $consoleDB->updateDocument($project->getArrayCopy()); + + if (false === $project) { + throw new Exception('Failed saving project to DB', 500); + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($key->getArrayCopy()) + ; + }, ['response', 'consoleDB']); + +App::get('/v1/projects/:projectId/keys') ->desc('List Keys') ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listKeys') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') - ->action( - function ($projectId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ + + $project = $consoleDB->getDocument($projectId); - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } - - $response->json($project->getAttribute('keys', [])); //FIXME make sure array objects return correctly + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->get('/v1/projects/:projectId/keys/:keyId') + $response->json($project->getAttribute('keys', [])); //FIXME make sure array objects return correctly + }, ['response', 'consoleDB']); + +App::get('/v1/projects/:projectId/keys/:keyId') ->desc('Get Key') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -739,25 +768,23 @@ $utopia->get('/v1/projects/:projectId/keys/:keyId') ->label('sdk.method', 'getKey') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('keyId', null, function () { return new UID(); }, 'Key unique ID.') - ->action( - function ($projectId, $keyId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $keyId, $response, $consoleDB) { + $project = $consoleDB->getDocument($projectId); - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } - - $key = $project->search('$id', $keyId, $project->getAttribute('keys', [])); - - if (empty($key) || !$key instanceof Document) { - throw new Exception('Key not found', 404); - } - - $response->json($key->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->put('/v1/projects/:projectId/keys/:keyId') + $key = $project->search('$id', $keyId, $project->getAttribute('keys', [])); + + if (empty($key) || !$key instanceof Document) { + throw new Exception('Key not found', 404); + } + + $response->json($key->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::put('/v1/projects/:projectId/keys/:keyId') ->desc('Update Key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -766,35 +793,36 @@ $utopia->put('/v1/projects/:projectId/keys/:keyId') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('keyId', null, function () { return new UID(); }, 'Key unique ID.') ->param('name', null, function () { return new Text(256); }, 'Key name.') - ->param('scopes', null, function () use ($scopes) { return new ArrayList(new WhiteList($scopes)); }, 'Key scopes list') - ->action( - function ($projectId, $keyId, $name, $scopes) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->param('scopes', null, function () { return new ArrayList(new WhiteList(Config::getParam('scopes'))); }, 'Key scopes list') + ->action(function ($projectId, $keyId, $name, $scopes, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $key = $project->search('$id', $keyId, $project->getAttribute('keys', [])); - - if (empty($key) || !$key instanceof Document) { - throw new Exception('Key not found', 404); - } - - $key - ->setAttribute('name', $name) - ->setAttribute('scopes', $scopes) - ; - - if (false === $consoleDB->updateDocument($key->getArrayCopy())) { - throw new Exception('Failed saving key to DB', 500); - } - - $response->json($key->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->delete('/v1/projects/:projectId/keys/:keyId') + $key = $project->search('$id', $keyId, $project->getAttribute('keys', [])); + + if (empty($key) || !$key instanceof Document) { + throw new Exception('Key not found', 404); + } + + $key + ->setAttribute('name', $name) + ->setAttribute('scopes', $scopes) + ; + + if (false === $consoleDB->updateDocument($key->getArrayCopy())) { + throw new Exception('Failed saving key to DB', 500); + } + + $response->json($key->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::delete('/v1/projects/:projectId/keys/:keyId') ->desc('Delete Key') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -802,31 +830,32 @@ $utopia->delete('/v1/projects/:projectId/keys/:keyId') ->label('sdk.method', 'deleteKey') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('keyId', null, function () { return new UID(); }, 'Key unique ID.') - ->action( - function ($projectId, $keyId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $keyId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $key = $project->search('$id', $keyId, $project->getAttribute('keys', [])); - - if (empty($key) || !$key instanceof Document) { - throw new Exception('Key not found', 404); - } - - if (!$consoleDB->deleteDocument($key->getId())) { - throw new Exception('Failed to remove key from DB', 500); - } - - $response->noContent(); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); + + $key = $project->search('$id', $keyId, $project->getAttribute('keys', [])); + + if (empty($key) || !$key instanceof Document) { + throw new Exception('Key not found', 404); + } + + if (!$consoleDB->deleteDocument($key->getId())) { + throw new Exception('Failed to remove key from DB', 500); + } + + $response->noContent(); + }, ['response', 'consoleDB']); // Tasks -$utopia->post('/v1/projects/:projectId/tasks') +App::post('/v1/projects/:projectId/tasks') ->desc('Create Task') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -836,113 +865,116 @@ $utopia->post('/v1/projects/:projectId/tasks') ->param('name', null, function () { return new Text(256); }, 'Task name.') ->param('status', null, function () { return new WhiteList(['play', 'pause']); }, 'Task status.') ->param('schedule', null, function () { return new Cron(); }, 'Task schedule CRON syntax.') - ->param('security', false, function () { return new Boolean(true); }, 'Certificate verification, false for disabled or true for enabled.') ->param('httpMethod', '', function () { return new WhiteList(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT']); }, 'Task HTTP method.') + ->param('security', false, function () { return new Boolean(true); }, 'Certificate verification, false for disabled or true for enabled.') + ->param('httpMethod', '', function () { return new WhiteList(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT']); }, 'Task HTTP method.') ->param('httpUrl', '', function () { return new URL(); }, 'Task HTTP URL') ->param('httpHeaders', null, function () { return new ArrayList(new Text(256)); }, 'Task HTTP headers list.', true) ->param('httpUser', '', function () { return new Text(256); }, 'Task HTTP user.', true) ->param('httpPass', '', function () { return new Text(256); }, 'Task HTTP password.', true) - ->action( - function ($projectId, $name, $status, $schedule, $security, $httpMethod, $httpUrl, $httpHeaders, $httpUser, $httpPass) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $name, $status, $schedule, $security, $httpMethod, $httpUrl, $httpHeaders, $httpUser, $httpPass, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $cron = CronExpression::factory($schedule); - $next = ($status == 'play') ? $cron->getNextRunDate()->format('U') : null; - - $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); - $key = $request->getServer('_APP_OPENSSL_KEY_V1'); - $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); - $tag = null; - $httpPass = \json_encode([ - 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), - 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => \bin2hex($iv), - 'tag' => \bin2hex($tag), - 'version' => '1', - ]); - - $task = $consoleDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_TASKS, - '$permissions' => [ - 'read' => ['team:'.$project->getAttribute('teamId', null)], - 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], - ], - 'name' => $name, - 'status' => $status, - 'schedule' => $schedule, - 'updated' => \time(), - 'previous' => null, - 'next' => $next, - 'security' => (int) $security, - 'httpMethod' => $httpMethod, - 'httpUrl' => $httpUrl, - 'httpHeaders' => $httpHeaders, - 'httpUser' => $httpUser, - 'httpPass' => $httpPass, - 'log' => '{}', - 'failures' => 0, - ]); - - if (false === $task) { - throw new Exception('Failed saving tasks to DB', 500); - } - - $project->setAttribute('tasks', $task, Document::SET_TYPE_APPEND); - - $project = $consoleDB->updateDocument($project->getArrayCopy()); - - if (false === $project) { - throw new Exception('Failed saving project to DB', 500); - } - - if ($next) { - ResqueScheduler::enqueueAt($next, 'v1-tasks', 'TasksV1', $task->getArrayCopy()); - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($task->getArrayCopy()) - ; + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->get('/v1/projects/:projectId/tasks') + $cron = CronExpression::factory($schedule); + $next = ($status == 'play') ? $cron->getNextRunDate()->format('U') : null; + + $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); + $key = App::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $tag = null; + $httpPass = \json_encode([ + 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), + 'method' => OpenSSL::CIPHER_AES_128_GCM, + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), + 'version' => '1', + ]); + + $task = $consoleDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_TASKS, + '$permissions' => [ + 'read' => ['team:'.$project->getAttribute('teamId', null)], + 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], + ], + 'name' => $name, + 'status' => $status, + 'schedule' => $schedule, + 'updated' => \time(), + 'previous' => null, + 'next' => $next, + 'security' => $security, + 'httpMethod' => $httpMethod, + 'httpUrl' => $httpUrl, + 'httpHeaders' => $httpHeaders, + 'httpUser' => $httpUser, + 'httpPass' => $httpPass, + 'log' => '{}', + 'failures' => 0, + ]); + + if (false === $task) { + throw new Exception('Failed saving tasks to DB', 500); + } + + $project->setAttribute('tasks', $task, Document::SET_TYPE_APPEND); + + $project = $consoleDB->updateDocument($project->getArrayCopy()); + + if (false === $project) { + throw new Exception('Failed saving project to DB', 500); + } + + if ($next) { + ResqueScheduler::enqueueAt($next, 'v1-tasks', 'TasksV1', $task->getArrayCopy()); + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($task->getArrayCopy()) + ; + }, ['response', 'consoleDB']); + +App::get('/v1/projects/:projectId/tasks') ->desc('List Tasks') ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listTasks') ->param('projectId', '', function () { return new UID(); }, 'Project unique ID.') - ->action( - function ($projectId) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $tasks = $project->getAttribute('tasks', []); - - foreach ($tasks as $task) { /* @var $task Document */ - $httpPass = \json_decode($task->getAttribute('httpPass', '{}'), true); - - if (empty($httpPass) || !isset($httpPass['version'])) { - continue; - } - - $key = $request->getServer('_APP_OPENSSL_KEY_V'.$httpPass['version']); - - $task->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); - } - - $response->json($tasks); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->get('/v1/projects/:projectId/tasks/:taskId') + $tasks = $project->getAttribute('tasks', []); + + foreach ($tasks as $task) { /* @var $task Document */ + $httpPass = \json_decode($task->getAttribute('httpPass', '{}'), true); + + if (empty($httpPass) || !isset($httpPass['version'])) { + continue; + } + + $key = App::getEnv('_APP_OPENSSL_KEY_V'.$httpPass['version']); + + $task->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); + } + + $response->json($tasks); + }, ['response', 'consoleDB']); + +App::get('/v1/projects/:projectId/tasks/:taskId') ->desc('Get Task') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -950,32 +982,33 @@ $utopia->get('/v1/projects/:projectId/tasks/:taskId') ->label('sdk.method', 'getTask') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('taskId', null, function () { return new UID(); }, 'Task unique ID.') - ->action( - function ($projectId, $taskId) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $taskId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $task = $project->search('$id', $taskId, $project->getAttribute('tasks', [])); - - if (empty($task) || !$task instanceof Document) { - throw new Exception('Task not found', 404); - } - - $httpPass = \json_decode($task->getAttribute('httpPass', '{}'), true); - - if (!empty($httpPass) && isset($httpPass['version'])) { - $key = $request->getServer('_APP_OPENSSL_KEY_V'.$httpPass['version']); - $task->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); - } - - $response->json($task->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->put('/v1/projects/:projectId/tasks/:taskId') + $task = $project->search('$id', $taskId, $project->getAttribute('tasks', [])); + + if (empty($task) || !$task instanceof Document) { + throw new Exception('Task not found', 404); + } + + $httpPass = \json_decode($task->getAttribute('httpPass', '{}'), true); + + if (!empty($httpPass) && isset($httpPass['version'])) { + $key = App::getEnv('_APP_OPENSSL_KEY_V'.$httpPass['version']); + $task->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); + } + + $response->json($task->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::put('/v1/projects/:projectId/tasks/:taskId') ->desc('Update Task') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -992,62 +1025,63 @@ $utopia->put('/v1/projects/:projectId/tasks/:taskId') ->param('httpHeaders', null, function () { return new ArrayList(new Text(256)); }, 'Task HTTP headers list.', true) ->param('httpUser', '', function () { return new Text(256); }, 'Task HTTP user.', true) ->param('httpPass', '', function () { return new Text(256); }, 'Task HTTP password.', true) - ->action( - function ($projectId, $taskId, $name, $status, $schedule, $security, $httpMethod, $httpUrl, $httpHeaders, $httpUser, $httpPass) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $taskId, $name, $status, $schedule, $security, $httpMethod, $httpUrl, $httpHeaders, $httpUser, $httpPass, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $task = $project->search('$id', $taskId, $project->getAttribute('tasks', [])); - - if (empty($task) || !$task instanceof Document) { - throw new Exception('Task not found', 404); - } - - $cron = CronExpression::factory($schedule); - $next = ($status == 'play') ? $cron->getNextRunDate()->format('U') : null; - - $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); - $key = $request->getServer('_APP_OPENSSL_KEY_V1'); - $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); - $tag = null; - $httpPass = \json_encode([ - 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), - 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => \bin2hex($iv), - 'tag' => \bin2hex($tag), - 'version' => '1', - ]); - - $task - ->setAttribute('name', $name) - ->setAttribute('status', $status) - ->setAttribute('schedule', $schedule) - ->setAttribute('updated', \time()) - ->setAttribute('next', $next) - ->setAttribute('security', (int) $security) - ->setAttribute('httpMethod', $httpMethod) - ->setAttribute('httpUrl', $httpUrl) - ->setAttribute('httpHeaders', $httpHeaders) - ->setAttribute('httpUser', $httpUser) - ->setAttribute('httpPass', $httpPass) - ; - - if (false === $consoleDB->updateDocument($task->getArrayCopy())) { - throw new Exception('Failed saving tasks to DB', 500); - } - - if ($next) { - ResqueScheduler::enqueueAt($next, 'v1-tasks', 'TasksV1', $task->getArrayCopy()); - } - - $response->json($task->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->delete('/v1/projects/:projectId/tasks/:taskId') + $task = $project->search('$id', $taskId, $project->getAttribute('tasks', [])); + + if (empty($task) || !$task instanceof Document) { + throw new Exception('Task not found', 404); + } + + $cron = CronExpression::factory($schedule); + $next = ($status == 'play') ? $cron->getNextRunDate()->format('U') : null; + + $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); + $key = App::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $tag = null; + $httpPass = \json_encode([ + 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), + 'method' => OpenSSL::CIPHER_AES_128_GCM, + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), + 'version' => '1', + ]); + + $task + ->setAttribute('name', $name) + ->setAttribute('status', $status) + ->setAttribute('schedule', $schedule) + ->setAttribute('updated', \time()) + ->setAttribute('next', $next) + ->setAttribute('security', $security) + ->setAttribute('httpMethod', $httpMethod) + ->setAttribute('httpUrl', $httpUrl) + ->setAttribute('httpHeaders', $httpHeaders) + ->setAttribute('httpUser', $httpUser) + ->setAttribute('httpPass', $httpPass) + ; + + if (false === $consoleDB->updateDocument($task->getArrayCopy())) { + throw new Exception('Failed saving tasks to DB', 500); + } + + if ($next) { + ResqueScheduler::enqueueAt($next, 'v1-tasks', 'TasksV1', $task->getArrayCopy()); + } + + $response->json($task->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::delete('/v1/projects/:projectId/tasks/:taskId') ->desc('Delete Task') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1055,31 +1089,32 @@ $utopia->delete('/v1/projects/:projectId/tasks/:taskId') ->label('sdk.method', 'deleteTask') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('taskId', null, function () { return new UID(); }, 'Task unique ID.') - ->action( - function ($projectId, $taskId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $taskId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $task = $project->search('$id', $taskId, $project->getAttribute('tasks', [])); - - if (empty($task) || !$task instanceof Document) { - throw new Exception('Task not found', 404); - } - - if (!$consoleDB->deleteDocument($task->getId())) { - throw new Exception('Failed to remove tasks from DB', 500); - } - - $response->noContent(); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); + + $task = $project->search('$id', $taskId, $project->getAttribute('tasks', [])); + + if (empty($task) || !$task instanceof Document) { + throw new Exception('Task not found', 404); + } + + if (!$consoleDB->deleteDocument($task->getId())) { + throw new Exception('Failed to remove tasks from DB', 500); + } + + $response->noContent(); + }, ['response', 'consoleDB']); // Platforms -$utopia->post('/v1/projects/:projectId/platforms') +App::post('/v1/projects/:projectId/platforms') ->desc('Create Platform') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1091,70 +1126,72 @@ $utopia->post('/v1/projects/:projectId/platforms') ->param('key', '', function () { return new Text(256); }, 'Package name for android or bundle ID for iOS.', true) ->param('store', '', function () { return new Text(256); }, 'App store or Google Play store ID.', true) ->param('hostname', '', function () { return new Text(256); }, 'Platform client hostname.', true) - ->action( - function ($projectId, $type, $name, $key, $store, $hostname) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $type, $name, $key, $store, $hostname, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $platform = $consoleDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_PLATFORMS, - '$permissions' => [ - 'read' => ['team:'.$project->getAttribute('teamId', null)], - 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], - ], - 'type' => $type, - 'name' => $name, - 'key' => $key, - 'store' => $store, - 'hostname' => $hostname, - 'dateCreated' => \time(), - 'dateUpdated' => \time(), - ]); - - if (false === $platform) { - throw new Exception('Failed saving platform to DB', 500); - } - - $project->setAttribute('platforms', $platform, Document::SET_TYPE_APPEND); - - $project = $consoleDB->updateDocument($project->getArrayCopy()); - - if (false === $project) { - throw new Exception('Failed saving project to DB', 500); - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($platform->getArrayCopy()) - ; + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); + + $platform = $consoleDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_PLATFORMS, + '$permissions' => [ + 'read' => ['team:'.$project->getAttribute('teamId', null)], + 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], + ], + 'type' => $type, + 'name' => $name, + 'key' => $key, + 'store' => $store, + 'hostname' => $hostname, + 'dateCreated' => \time(), + 'dateUpdated' => \time(), + ]); + + if (false === $platform) { + throw new Exception('Failed saving platform to DB', 500); + } + + $project->setAttribute('platforms', $platform, Document::SET_TYPE_APPEND); + + $project = $consoleDB->updateDocument($project->getArrayCopy()); + + if (false === $project) { + throw new Exception('Failed saving project to DB', 500); + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($platform->getArrayCopy()) + ; + }, ['response', 'consoleDB']); -$utopia->get('/v1/projects/:projectId/platforms') +App::get('/v1/projects/:projectId/platforms') ->desc('List Platforms') ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listPlatforms') ->param('projectId', '', function () { return new UID(); }, 'Project unique ID.') - ->action( - function ($projectId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $platforms = $project->getAttribute('platforms', []); - - $response->json($platforms); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->get('/v1/projects/:projectId/platforms/:platformId') + $platforms = $project->getAttribute('platforms', []); + + $response->json($platforms); + }, ['response', 'consoleDB']); + +App::get('/v1/projects/:projectId/platforms/:platformId') ->desc('Get Platform') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1162,25 +1199,26 @@ $utopia->get('/v1/projects/:projectId/platforms/:platformId') ->label('sdk.method', 'getPlatform') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('platformId', null, function () { return new UID(); }, 'Platform unique ID.') - ->action( - function ($projectId, $platformId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $platformId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $platform = $project->search('$id', $platformId, $project->getAttribute('platforms', [])); - - if (empty($platform) || !$platform instanceof Document) { - throw new Exception('Platform not found', 404); - } - - $response->json($platform->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->put('/v1/projects/:projectId/platforms/:platformId') + $platform = $project->search('$id', $platformId, $project->getAttribute('platforms', [])); + + if (empty($platform) || !$platform instanceof Document) { + throw new Exception('Platform not found', 404); + } + + $response->json($platform->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::put('/v1/projects/:projectId/platforms/:platformId') ->desc('Update Platform') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1192,37 +1230,38 @@ $utopia->put('/v1/projects/:projectId/platforms/:platformId') ->param('key', '', function () { return new Text(256); }, 'Package name for android or bundle ID for iOS.', true) ->param('store', '', function () { return new Text(256); }, 'App store or Google Play store ID.', true) ->param('hostname', '', function () { return new Text(256); }, 'Platform client URL.', true) - ->action( - function ($projectId, $platformId, $name, $key, $store, $hostname) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $platformId, $name, $key, $store, $hostname, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $platform = $project->search('$id', $platformId, $project->getAttribute('platforms', [])); - - if (empty($platform) || !$platform instanceof Document) { - throw new Exception('Platform not found', 404); - } - - $platform - ->setAttribute('name', $name) - ->setAttribute('dateUpdated', \time()) - ->setAttribute('key', $key) - ->setAttribute('store', $store) - ->setAttribute('hostname', $hostname) - ; - - if (false === $consoleDB->updateDocument($platform->getArrayCopy())) { - throw new Exception('Failed saving platform to DB', 500); - } - - $response->json($platform->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->delete('/v1/projects/:projectId/platforms/:platformId') + $platform = $project->search('$id', $platformId, $project->getAttribute('platforms', [])); + + if (empty($platform) || !$platform instanceof Document) { + throw new Exception('Platform not found', 404); + } + + $platform + ->setAttribute('name', $name) + ->setAttribute('dateUpdated', \time()) + ->setAttribute('key', $key) + ->setAttribute('store', $store) + ->setAttribute('hostname', $hostname) + ; + + if (false === $consoleDB->updateDocument($platform->getArrayCopy())) { + throw new Exception('Failed saving platform to DB', 500); + } + + $response->json($platform->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::delete('/v1/projects/:projectId/platforms/:platformId') ->desc('Delete Platform') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1230,31 +1269,32 @@ $utopia->delete('/v1/projects/:projectId/platforms/:platformId') ->label('sdk.method', 'deletePlatform') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('platformId', null, function () { return new UID(); }, 'Platform unique ID.') - ->action( - function ($projectId, $platformId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $platformId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $platform = $project->search('$id', $platformId, $project->getAttribute('platforms', [])); - - if (empty($platform) || !$platform instanceof Document) { - throw new Exception('Platform not found', 404); - } - - if (!$consoleDB->deleteDocument($platform->getId())) { - throw new Exception('Failed to remove platform from DB', 500); - } - - $response->noContent(); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); + + $platform = $project->search('$id', $platformId, $project->getAttribute('platforms', [])); + + if (empty($platform) || !$platform instanceof Document) { + throw new Exception('Platform not found', 404); + } + + if (!$consoleDB->deleteDocument($platform->getId())) { + throw new Exception('Failed to remove platform from DB', 500); + } + + $response->noContent(); + }, ['response', 'consoleDB']); // Domains -$utopia->post('/v1/projects/:projectId/domains') +App::post('/v1/projects/:projectId/domains') ->desc('Create Domain') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1262,83 +1302,85 @@ $utopia->post('/v1/projects/:projectId/domains') ->label('sdk.method', 'createDomain') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('domain', null, function () { return new DomainValidator(); }, 'Domain name.') - ->action( - function ($projectId, $domain) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $domain, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $document = $project->search('domain', $domain, $project->getAttribute('domains', [])); - - if (!empty($document)) { - throw new Exception('Domain already exists', 409); - } - - $target = new Domain($request->getServer('_APP_DOMAIN_TARGET', '')); - - if (!$target->isKnown() || $target->isTest()) { - throw new Exception('Unreachable CNAME target ('.$target->get().'), plesse use a domain with a public suffix.', 500); - } - - $domain = new Domain($domain); - - $domain = $consoleDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_DOMAINS, - '$permissions' => [ - 'read' => ['team:'.$project->getAttribute('teamId', null)], - 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], - ], - 'updated' => \time(), - 'domain' => $domain->get(), - 'tld' => $domain->getSuffix(), - 'registerable' => $domain->getRegisterable(), - 'verification' => false, - 'certificateId' => null, - ]); - - if (false === $domain) { - throw new Exception('Failed saving domain to DB', 500); - } - - $project->setAttribute('domains', $domain, Document::SET_TYPE_APPEND); - - $project = $consoleDB->updateDocument($project->getArrayCopy()); - - if (false === $project) { - throw new Exception('Failed saving project to DB', 500); - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($domain->getArrayCopy()) - ; + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->get('/v1/projects/:projectId/domains') + $document = $project->search('domain', $domain, $project->getAttribute('domains', [])); + + if (!empty($document)) { + throw new Exception('Domain already exists', 409); + } + + $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); + + if (!$target->isKnown() || $target->isTest()) { + throw new Exception('Unreachable CNAME target ('.$target->get().'), plesse use a domain with a public suffix.', 500); + } + + $domain = new Domain($domain); + + $domain = $consoleDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_DOMAINS, + '$permissions' => [ + 'read' => ['team:'.$project->getAttribute('teamId', null)], + 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], + ], + 'updated' => \time(), + 'domain' => $domain->get(), + 'tld' => $domain->getSuffix(), + 'registerable' => $domain->getRegisterable(), + 'verification' => false, + 'certificateId' => null, + ]); + + if (false === $domain) { + throw new Exception('Failed saving domain to DB', 500); + } + + $project->setAttribute('domains', $domain, Document::SET_TYPE_APPEND); + + $project = $consoleDB->updateDocument($project->getArrayCopy()); + + if (false === $project) { + throw new Exception('Failed saving project to DB', 500); + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($domain->getArrayCopy()) + ; + }, ['response', 'consoleDB']); + +App::get('/v1/projects/:projectId/domains') ->desc('List Domains') ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listDomains') ->param('projectId', '', function () { return new UID(); }, 'Project unique ID.') - ->action( - function ($projectId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $domains = $project->getAttribute('domains', []); - - $response->json($domains); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->get('/v1/projects/:projectId/domains/:domainId') + $domains = $project->getAttribute('domains', []); + + $response->json($domains); + }, ['response', 'consoleDB']); + +App::get('/v1/projects/:projectId/domains/:domainId') ->desc('Get Domain') ->groups(['api', 'projects']) ->label('scope', 'projects.read') @@ -1346,25 +1388,26 @@ $utopia->get('/v1/projects/:projectId/domains/:domainId') ->label('sdk.method', 'getDomain') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('domainId', null, function () { return new UID(); }, 'Domain unique ID.') - ->action( - function ($projectId, $domainId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $domainId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $domain = $project->search('$id', $domainId, $project->getAttribute('domains', [])); - - if (empty($domain) || !$domain instanceof Document) { - throw new Exception('Domain not found', 404); - } - - $response->json($domain->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->patch('/v1/projects/:projectId/domains/:domainId/verification') + $domain = $project->search('$id', $domainId, $project->getAttribute('domains', [])); + + if (empty($domain) || !$domain instanceof Document) { + throw new Exception('Domain not found', 404); + } + + $response->json($domain->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::patch('/v1/projects/:projectId/domains/:domainId/verification') ->desc('Update Domain Verification Status') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1372,56 +1415,57 @@ $utopia->patch('/v1/projects/:projectId/domains/:domainId/verification') ->label('sdk.method', 'updateDomainVerification') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('domainId', null, function () { return new UID(); }, 'Domain unique ID.') - ->action( - function ($projectId, $domainId) use ($request, $response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $domainId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $domain = $project->search('$id', $domainId, $project->getAttribute('domains', [])); - - if (empty($domain) || !$domain instanceof Document) { - throw new Exception('Domain not found', 404); - } - - $target = new Domain($request->getServer('_APP_DOMAIN_TARGET', '')); - - if (!$target->isKnown() || $target->isTest()) { - throw new Exception('Unreachable CNAME target ('.$target->get().'), plesse use a domain with a public suffix.', 500); - } - - if ($domain->getAttribute('verification') === true) { - return $response->json($domain->getArrayCopy()); - } - - // Verify Domain with DNS records - $validator = new CNAME($target->get()); - - if (!$validator->isValid($domain->getAttribute('domain', ''))) { - throw new Exception('Failed to verify domain', 401); - } - - $domain - ->setAttribute('verification', true) - ; - - if (false === $consoleDB->updateDocument($domain->getArrayCopy())) { - throw new Exception('Failed saving domains to DB', 500); - } - - // Issue a TLS certificate when domain is verified - Resque::enqueue('v1-certificates', 'CertificatesV1', [ - 'document' => $domain->getArrayCopy(), - 'domain' => $domain->getAttribute('domain'), - ]); - - $response->json($domain->getArrayCopy()); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); -$utopia->delete('/v1/projects/:projectId/domains/:domainId') + $domain = $project->search('$id', $domainId, $project->getAttribute('domains', [])); + + if (empty($domain) || !$domain instanceof Document) { + throw new Exception('Domain not found', 404); + } + + $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); + + if (!$target->isKnown() || $target->isTest()) { + throw new Exception('Unreachable CNAME target ('.$target->get().'), plesse use a domain with a public suffix.', 500); + } + + if ($domain->getAttribute('verification') === true) { + return $response->json($domain->getArrayCopy()); + } + + // Verify Domain with DNS records + $validator = new CNAME($target->get()); + + if (!$validator->isValid($domain->getAttribute('domain', ''))) { + throw new Exception('Failed to verify domain', 401); + } + + $domain + ->setAttribute('verification', true) + ; + + if (false === $consoleDB->updateDocument($domain->getArrayCopy())) { + throw new Exception('Failed saving domains to DB', 500); + } + + // Issue a TLS certificate when domain is verified + Resque::enqueue('v1-certificates', 'CertificatesV1', [ + 'document' => $domain->getArrayCopy(), + 'domain' => $domain->getAttribute('domain'), + ]); + + $response->json($domain->getArrayCopy()); + }, ['response', 'consoleDB']); + +App::delete('/v1/projects/:projectId/domains/:domainId') ->desc('Delete Domain') ->groups(['api', 'projects']) ->label('scope', 'projects.write') @@ -1429,24 +1473,25 @@ $utopia->delete('/v1/projects/:projectId/domains/:domainId') ->label('sdk.method', 'deleteDomain') ->param('projectId', null, function () { return new UID(); }, 'Project unique ID.') ->param('domainId', null, function () { return new UID(); }, 'Domain unique ID.') - ->action( - function ($projectId, $domainId) use ($response, $consoleDB) { - $project = $consoleDB->getDocument($projectId); + ->action(function ($projectId, $domainId, $response, $consoleDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ - if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { - throw new Exception('Project not found', 404); - } + $project = $consoleDB->getDocument($projectId); - $domain = $project->search('$id', $domainId, $project->getAttribute('domains', [])); - - if (empty($domain) || !$domain instanceof Document) { - throw new Exception('Domain not found', 404); - } - - if (!$consoleDB->deleteDocument($domain->getId())) { - throw new Exception('Failed to remove domains from DB', 500); - } - - $response->noContent(); + if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS != $project->getCollection()) { + throw new Exception('Project not found', 404); } - ); \ No newline at end of file + + $domain = $project->search('$id', $domainId, $project->getAttribute('domains', [])); + + if (empty($domain) || !$domain instanceof Document) { + throw new Exception('Domain not found', 404); + } + + if (!$consoleDB->deleteDocument($domain->getId())) { + throw new Exception('Failed to remove domains from DB', 500); + } + + $response->noContent(); + }, ['response', 'consoleDB']); \ No newline at end of file diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 9a12b56048..2b9ebfe7eb 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -1,7 +1,6 @@ getId())); - -$fileLogos = [ // Based on this list @see http://stackoverflow.com/a/4212908/2299554 - 'default' => __DIR__.'/../../config/files/none.png', - - // Video Files - 'video/mp4' => __DIR__.'/../../config/files/video.png', - 'video/x-flv' => __DIR__.'/../../config/files/video.png', - 'application/x-mpegURL' => __DIR__.'/../../config/files/video.png', - 'video/MP2T' => __DIR__.'/../../config/files/video.png', - 'video/3gpp' => __DIR__.'/../../config/files/video.png', - 'video/quicktime' => __DIR__.'/../../config/files/video.png', - 'video/x-msvideo' => __DIR__.'/../../config/files/video.png', - 'video/x-ms-wmv' => __DIR__.'/../../config/files/video.png', - - // // Microsoft Word - 'application/msword' => __DIR__.'/../../config/files/word.png', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => __DIR__.'/../../config/files/word.png', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => __DIR__.'/../../config/files/word.png', - 'application/vnd.ms-word.document.macroEnabled.12' => __DIR__.'/../../config/files/word.png', - - // // Microsoft Excel - 'application/vnd.ms-excel' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.ms-excel.sheet.macroEnabled.12' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.ms-excel.template.macroEnabled.12' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.ms-excel.addin.macroEnabled.12' => __DIR__.'/../../config/files/excel.png', - 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => __DIR__.'/../../config/files/excel.png', - - // // Microsoft Power Point - 'application/vnd.ms-powerpoint' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.openxmlformats-officedocument.presentationml.template' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.ms-powerpoint.addin.macroEnabled.12' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.ms-powerpoint.template.macroEnabled.12' => __DIR__.'/../../config/files/ppt.png', - 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' => __DIR__.'/../../config/files/ppt.png', - - // Adobe PDF - 'application/pdf' => __DIR__.'/../../config/files/pdf.png', -]; - -$inputs = [ - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'gif' => 'image/gif', - 'png' => 'image/png', -]; - -$outputs = [ - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'gif' => 'image/gif', - 'png' => 'image/png', - 'webp' => 'image/webp', -]; - -$mimes = [ - 'image/jpeg', - 'image/jpeg', - 'image/gif', - 'image/png', - 'image/webp', - - // Video Files - 'video/mp4', - 'video/x-flv', - 'application/x-mpegURL', - 'video/MP2T', - 'video/3gpp', - 'video/quicktime', - 'video/x-msvideo', - 'video/x-ms-wmv', - - // Microsoft Word - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', - 'application/vnd.ms-word.document.macroEnabled.12', - - // Microsoft Excel - 'application/vnd.ms-excel', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', - 'application/vnd.ms-excel.sheet.macroEnabled.12', - 'application/vnd.ms-excel.template.macroEnabled.12', - 'application/vnd.ms-excel.addin.macroEnabled.12', - 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', - - // Microsoft Power Point - 'application/vnd.ms-powerpoint', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/vnd.openxmlformats-officedocument.presentationml.template', - 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', - 'application/vnd.ms-powerpoint.addin.macroEnabled.12', - 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', - 'application/vnd.ms-powerpoint.template.macroEnabled.12', - 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', - - // Microsoft Access - 'application/vnd.ms-access', - - // Adobe PDF - 'application/pdf', -]; - -$utopia->post('/v1/storage/files') +App::post('/v1/storage/files') ->desc('Create File') ->groups(['api', 'storage']) ->label('scope', 'files.write') @@ -142,133 +33,133 @@ $utopia->post('/v1/storage/files') ->label('sdk.description', '/docs/references/storage/create-file.md') ->label('sdk.consumes', 'multipart/form-data') ->label('sdk.methodType', 'upload') - ->param('file', [], function () { return new File(); }, 'Binary File.', false) + ->param('file', [], function () { return new File(); }, 'Binary file.', false) ->param('read', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - // ->param('folderId', '', function () { return new UID(); }, 'Folder to associate files with.', true) - ->action( - function ($file, $read, $write, $folderId = '') use ($request, $response, $user, $projectDB, $webhook, $audit, $usage) { - $file = $request->getFiles('file'); - $read = (empty($read)) ? ['user:'.$user->getId()] : $read; - $write = (empty($write)) ? ['user:'.$user->getId()] : $write; + ->action(function ($file, $read, $write, $request, $response, $user, $projectDB, $webhooks, $audits, $usage) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Event\Event $usage */ - /* - * Validators - */ - //$fileType = new FileType(array(FileType::FILE_TYPE_PNG, FileType::FILE_TYPE_GIF, FileType::FILE_TYPE_JPEG)); - $fileSize = new FileSize($request->getServer('_APP_STORAGE_LIMIT', 0)); - $upload = new Upload(); + $file = $request->getFiles('file'); + $read = (empty($read)) ? ['user:'.$user->getId()] : $read; + $write = (empty($write)) ? ['user:'.$user->getId()] : $write; - if (empty($file)) { - throw new Exception('No file sent', 400); - } + /* + * Validators + */ + //$fileType = new FileType(array(FileType::FILE_TYPE_PNG, FileType::FILE_TYPE_GIF, FileType::FILE_TYPE_JPEG)); + $fileSize = new FileSize(App::getEnv('_APP_STORAGE_LIMIT', 0)); + $upload = new Upload(); - // Make sure we handle a single file and multiple files the same way - $file['name'] = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name']; - $file['tmp_name'] = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name']; - $file['size'] = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; + if (empty($file)) { + throw new Exception('No file sent', 400); + } - // Check if file type is allowed (feature for project settings?) - //if (!$fileType->isValid($file['tmp_name'])) { - //throw new Exception('File type not allowed', 400); - //} + // Make sure we handle a single file and multiple files the same way + $file['name'] = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name']; + $file['tmp_name'] = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name']; + $file['size'] = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; - // Check if file size is exceeding allowed limit - if (!$fileSize->isValid($file['size'])) { - throw new Exception('File size not allowed', 400); - } + // Check if file type is allowed (feature for project settings?) + //if (!$fileType->isValid($file['tmp_name'])) { + //throw new Exception('File type not allowed', 400); + //} - /* - * Models - */ - $device = Storage::getDevice('local'); + if (!$fileSize->isValid($file['size'])) { // Check if file size is exceeding allowed limit + throw new Exception('File size not allowed', 400); + } - if (!$upload->isValid($file['tmp_name'])) { + $device = Storage::getDevice('files'); + + if (!$upload->isValid($file['tmp_name'])) { + throw new Exception('Invalid file', 403); + } + + // Save to storage + $size = $device->getFileSize($file['tmp_name']); + $path = $device->getPath(\uniqid().'.'.\pathinfo($file['name'], PATHINFO_EXTENSION)); + + if (!$device->upload($file['tmp_name'], $path)) { // TODO deprecate 'upload' and replace with 'move' + throw new Exception('Failed moving file', 500); + } + + $mimeType = $device->getFileMimeType($path); // Get mime-type before compression and encryption + + if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled + $antiVirus = new Network('clamav', 3310); + + if (!$antiVirus->fileScan($path)) { + $device->delete($path); throw new Exception('Invalid file', 403); } - - // Save to storage - $size = $device->getFileSize($file['tmp_name']); - $path = $device->getPath(\uniqid().'.'.\pathinfo($file['name'], PATHINFO_EXTENSION)); - - if (!$device->upload($file['tmp_name'], $path)) { // TODO deprecate 'upload' and replace with 'move' - throw new Exception('Failed moving file', 500); - } - - $mimeType = $device->getFileMimeType($path); // Get mime-type before compression and encryption - - if ($request->getServer('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled - $antiVirus = new Network('clamav', 3310); - - // Check if file size is exceeding allowed limit - if (!$antiVirus->fileScan($path)) { - $device->delete($path); - throw new Exception('Invalid file', 403); - } - } - - // Compression - $compressor = new GZIP(); - $data = $device->read($path); - $data = $compressor->compress($data); - $key = $request->getServer('_APP_OPENSSL_KEY_V1'); - $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); - $data = OpenSSL::encrypt($data, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag); - - if (!$device->write($path, $data)) { - throw new Exception('Failed to save file', 500); - } - - $sizeActual = $device->getFileSize($path); - - $file = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_FILES, - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - 'dateCreated' => \time(), - 'folderId' => $folderId, - 'name' => $file['name'], - 'path' => $path, - 'signature' => $device->getFileHash($path), - 'mimeType' => $mimeType, - 'sizeOriginal' => $size, - 'sizeActual' => $sizeActual, - 'algorithm' => $compressor->getName(), - 'token' => \bin2hex(\random_bytes(64)), - 'comment' => '', - 'fileOpenSSLVersion' => '1', - 'fileOpenSSLCipher' => OpenSSL::CIPHER_AES_128_GCM, - 'fileOpenSSLTag' => \bin2hex($tag), - 'fileOpenSSLIV' => \bin2hex($iv), - ]); - - if (false === $file) { - throw new Exception('Failed saving file to DB', 500); - } - - $webhook - ->setParam('payload', $file->getArrayCopy()) - ; - - $audit - ->setParam('event', 'storage.files.create') - ->setParam('resource', 'storage/files/'.$file->getId()) - ; - - $usage - ->setParam('storage', $sizeActual) - ; - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($file->getArrayCopy()) - ; } - ); -$utopia->get('/v1/storage/files') + // Compression + $compressor = new GZIP(); + $data = $device->read($path); + $data = $compressor->compress($data); + $key = App::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $data = OpenSSL::encrypt($data, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag); + + if (!$device->write($path, $data)) { + throw new Exception('Failed to save file', 500); + } + + $sizeActual = $device->getFileSize($path); + + $file = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_FILES, + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + 'dateCreated' => \time(), + 'folderId' => '', + 'name' => $file['name'], + 'path' => $path, + 'signature' => $device->getFileHash($path), + 'mimeType' => $mimeType, + 'sizeOriginal' => $size, + 'sizeActual' => $sizeActual, + 'algorithm' => $compressor->getName(), + 'token' => \bin2hex(\random_bytes(64)), + 'comment' => '', + 'fileOpenSSLVersion' => '1', + 'fileOpenSSLCipher' => OpenSSL::CIPHER_AES_128_GCM, + 'fileOpenSSLTag' => \bin2hex($tag), + 'fileOpenSSLIV' => \bin2hex($iv), + ]); + + if (false === $file) { + throw new Exception('Failed saving file to DB', 500); + } + + $webhooks + ->setParam('payload', $file->getArrayCopy()) + ; + + $audits + ->setParam('event', 'storage.files.create') + ->setParam('resource', 'storage/files/'.$file->getId()) + ; + + $usage + ->setParam('storage', $sizeActual) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($file->getArrayCopy()) + ; + }, ['request', 'response', 'user', 'projectDB', 'webhooks', 'audits', 'usage']); + +App::get('/v1/storage/files') ->desc('List Files') ->groups(['api', 'storage']) ->label('scope', 'files.read') @@ -280,29 +171,30 @@ $utopia->get('/v1/storage/files') ->param('limit', 25, function () { return new Range(0, 100); }, 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) ->param('offset', 0, function () { return new Range(0, 2000); }, 'Results offset. The default value is 0. Use this param to manage pagination.', true) ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) - ->action( - function ($search, $limit, $offset, $orderType) use ($response, $projectDB) { - $results = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderField' => 'dateCreated', - 'orderType' => $orderType, - 'orderCast' => 'int', - 'search' => $search, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_FILES, - ], - ]); + ->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - $results = \array_map(function ($value) { /* @var $value \Database\Document */ - return $value->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal']); - }, $results); + $results = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'dateCreated', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_FILES, + ], + ]); - $response->json(['sum' => $projectDB->getSum(), 'files' => $results]); - } - ); + $results = \array_map(function ($value) { /* @var $value \Database\Document */ + return $value->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal']); + }, $results); -$utopia->get('/v1/storage/files/:fileId') + $response->json(['sum' => $projectDB->getSum(), 'files' => $results]); + }, ['response', 'projectDB']); + +App::get('/v1/storage/files/:fileId') ->desc('Get File') ->groups(['api', 'storage']) ->label('scope', 'files.read') @@ -311,19 +203,20 @@ $utopia->get('/v1/storage/files/:fileId') ->label('sdk.method', 'getFile') ->label('sdk.description', '/docs/references/storage/get-file.md') ->param('fileId', '', function () { return new UID(); }, 'File unique ID.') - ->action( - function ($fileId) use ($response, $projectDB) { - $file = $projectDB->getDocument($fileId); + ->action(function ($fileId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { - throw new Exception('File not found', 404); - } + $file = $projectDB->getDocument($fileId); - $response->json($file->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal'])); + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); } - ); -$utopia->get('/v1/storage/files/:fileId/preview') + $response->json($file->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal'])); + }, ['response', 'projectDB']); + +App::get('/v1/storage/files/:fileId/preview') ->desc('Get File Preview') ->groups(['api', 'storage']) ->label('scope', 'files.read') @@ -338,115 +231,118 @@ $utopia->get('/v1/storage/files/:fileId/preview') ->param('height', 0, function () { return new Range(0, 4000); }, 'Resize preview image height, Pass an integer between 0 to 4000.', true) ->param('quality', 100, function () { return new Range(0, 100); }, 'Preview image quality. Pass an integer between 0 to 100. Defaults to 100.', true) ->param('background', '', function () { return new HexColor(); }, 'Preview image background color. Only works with transparent images (png). Use a valid HEX color, no # is needed for prefix.', true) - ->param('output', null, function () use ($outputs) { return new WhiteList(\array_merge(\array_keys($outputs), [null])); }, 'Output format type (jpeg, jpg, png, gif and webp).', true) - ->action( - function ($fileId, $width, $height, $quality, $background, $output) use ($request, $response, $projectDB, $project, $inputs, $outputs, $fileLogos) { - $storage = 'local'; + ->param('output', '', function () { return new WhiteList(\array_keys(Config::getParam('storage-outputs'))); }, 'Output format type (jpeg, jpg, png, gif and webp).', true) + ->action(function ($fileId, $width, $height, $quality, $background, $output, $request, $response, $project, $projectDB) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $projectDB */ - if (!\extension_loaded('imagick')) { - throw new Exception('Imagick extension is missing', 500); - } + $storage = 'files'; - if (!Storage::exists($storage)) { - throw new Exception('No such storage device', 400); - } + if (!\extension_loaded('imagick')) { + throw new Exception('Imagick extension is missing', 500); + } - if ((\strpos($request->getServer('HTTP_ACCEPT'), 'image/webp') === false) && ('webp' == $output)) { // Fallback webp to jpeg when no browser support - $output = 'jpg'; - } + if (!Storage::exists($storage)) { + throw new Exception('No such storage device', 400); + } - $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache - $key = \md5($fileId.$width.$height.$quality.$background.$storage.$output); + if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' == $output)) { // Fallback webp to jpeg when no browser support + $output = 'jpg'; + } - $file = $projectDB->getDocument($fileId); + $inputs = Config::getParam('storage-inputs'); + $outputs = Config::getParam('storage-outputs'); + $fileLogos = Config::getParam('storage-logos'); - if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { - throw new Exception('File not found', 404); - } + $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache + $key = \md5($fileId.$width.$height.$quality.$background.$storage.$output); - $path = $file->getAttribute('path'); + $file = $projectDB->getDocument($fileId); + + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); + } + + $path = $file->getAttribute('path'); + $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); + $algorithm = $file->getAttribute('algorithm'); + $cipher = $file->getAttribute('fileOpenSSLCipher'); + $mime = $file->getAttribute('mimeType'); + + if (!\in_array($mime, $inputs)) { + $path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default']; + $algorithm = null; + $cipher = null; + $background = (empty($background)) ? 'eceff1' : $background; $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); - $algorithm = $file->getAttribute('algorithm'); - $cipher = $file->getAttribute('fileOpenSSLCipher'); - $mime = $file->getAttribute('mimeType'); + $key = \md5($path.$width.$height.$quality.$background.$storage.$output); + } - if (!\in_array($mime, $inputs)) { - $path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default']; - $algorithm = null; - $cipher = null; - $background = (empty($background)) ? 'eceff1' : $background; - $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); - $key = \md5($path.$width.$height.$quality.$background.$storage.$output); - } + $compressor = new GZIP(); + $device = Storage::getDevice('files'); - $compressor = new GZIP(); - $device = Storage::getDevice('local'); + if (!\file_exists($path)) { + throw new Exception('File not found', 404); + } - if (!\file_exists($path)) { - throw new Exception('File not found', 404); - } - - $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-'.$project->getId())); // Limit file number or size - $data = $cache->load($key, 60 * 60 * 24 * 30 * 3 /* 3 months */); - - if ($data) { - $output = (empty($output)) ? $type : $output; - - $response - ->setContentType((\in_array($output, $outputs)) ? $outputs[$output] : $outputs['jpg']) - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'hit') - ->send($data) - ; - - return; - } - - $source = $device->read($path); - - if (!empty($cipher)) { // Decrypt - $source = OpenSSL::decrypt( - $source, - $file->getAttribute('fileOpenSSLCipher'), - $request->getServer('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), - 0, - \hex2bin($file->getAttribute('fileOpenSSLIV')), - \hex2bin($file->getAttribute('fileOpenSSLTag')) - ); - } - - if (!empty($algorithm)) { - $source = $compressor->decompress($source); - } - - $resize = new Resize($source); - - $resize->crop((int) $width, (int) $height); - - if (!empty($background)) { - $resize->setBackground('#'.$background); - } + $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-'.$project->getId())); // Limit file number or size + $data = $cache->load($key, 60 * 60 * 24 * 30 * 3 /* 3 months */); + if ($data) { $output = (empty($output)) ? $type : $output; - $response - ->setContentType($outputs[$output]) + return $response + ->setContentType((\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg']) ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send('') + ->addHeader('X-Appwrite-Cache', 'hit') + ->send($data) ; - - $data = $resize->output($output, $quality); - - $cache->save($key, $data); - - echo $data; - - unset($resize); } - ); -$utopia->get('/v1/storage/files/:fileId/download') + $source = $device->read($path); + + if (!empty($cipher)) { // Decrypt + $source = OpenSSL::decrypt( + $source, + $file->getAttribute('fileOpenSSLCipher'), + App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), + 0, + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) + ); + } + + if (!empty($algorithm)) { + $source = $compressor->decompress($source); + } + + $resize = new Resize($source); + + $resize->crop((int) $width, (int) $height); + + if (!empty($background)) { + $resize->setBackground('#'.$background); + } + + $output = (empty($output)) ? $type : $output; + + $data = $resize->output($output, $quality); + + $cache->save($key, $data); + + $response + ->setContentType($outputs[$output]) + ->addHeader('Expires', $date) + ->addHeader('X-Appwrite-Cache', 'miss') + ->send($data) + ; + + unset($resize); + }, ['request', 'response', 'project', 'projectDB']); + +App::get('/v1/storage/files/:fileId/download') ->desc('Get File for Download') ->groups(['api', 'storage']) ->label('scope', 'files.read') @@ -457,50 +353,51 @@ $utopia->get('/v1/storage/files/:fileId/download') ->label('sdk.response.type', '*') ->label('sdk.methodType', 'location') ->param('fileId', '', function () { return new UID(); }, 'File unique ID.') - ->action( - function ($fileId) use ($response, $request, $projectDB) { - $file = $projectDB->getDocument($fileId); + ->action(function ($fileId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { - throw new Exception('File not found', 404); - } + $file = $projectDB->getDocument($fileId); - $path = $file->getAttribute('path', ''); - - if (!\file_exists($path)) { - throw new Exception('File not found in '.$path, 404); - } - - $compressor = new GZIP(); - $device = Storage::getDevice('local'); - - $source = $device->read($path); - - if (!empty($file->getAttribute('fileOpenSSLCipher'))) { // Decrypt - $source = OpenSSL::decrypt( - $source, - $file->getAttribute('fileOpenSSLCipher'), - $request->getServer('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), - 0, - \hex2bin($file->getAttribute('fileOpenSSLIV')), - \hex2bin($file->getAttribute('fileOpenSSLTag')) - ); - } - - $source = $compressor->decompress($source); - - // Response - $response - ->setContentType($file->getAttribute('mimeType')) - ->addHeader('Content-Disposition', 'attachment; filename="'.$file->getAttribute('name', '').'"') - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache - ->addHeader('X-Peak', \memory_get_peak_usage()) - ->send($source) - ; + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); } - ); -$utopia->get('/v1/storage/files/:fileId/view') + $path = $file->getAttribute('path', ''); + + if (!\file_exists($path)) { + throw new Exception('File not found in '.$path, 404); + } + + $compressor = new GZIP(); + $device = Storage::getDevice('files'); + + $source = $device->read($path); + + if (!empty($file->getAttribute('fileOpenSSLCipher'))) { // Decrypt + $source = OpenSSL::decrypt( + $source, + $file->getAttribute('fileOpenSSLCipher'), + App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), + 0, + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) + ); + } + + $source = $compressor->decompress($source); + + // Response + $response + ->setContentType($file->getAttribute('mimeType')) + ->addHeader('Content-Disposition', 'attachment; filename="'.$file->getAttribute('name', '').'"') + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->addHeader('X-Peak', \memory_get_peak_usage()) + ->send($source) + ; + }, ['response', 'projectDB']); + +App::get('/v1/storage/files/:fileId/view') ->desc('Get File for View') ->groups(['api', 'storage']) ->label('scope', 'files.read') @@ -512,66 +409,68 @@ $utopia->get('/v1/storage/files/:fileId/view') ->label('sdk.methodType', 'location') ->param('fileId', '', function () { return new UID(); }, 'File unique ID.') ->param('as', '', function () { return new WhiteList(['pdf', /*'html',*/ 'text']); }, 'Choose a file format to convert your file to. Currently you can only convert word and pdf files to pdf or txt. This option is currently experimental only, use at your own risk.', true) - ->action( - function ($fileId, $as) use ($response, $request, $projectDB, $mimes) { - $file = $projectDB->getDocument($fileId); + ->action(function ($fileId, $as, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { - throw new Exception('File not found', 404); - } + $file = $projectDB->getDocument($fileId); + $mimes = Config::getParam('storage-mimes'); - $path = $file->getAttribute('path', ''); - - if (!\file_exists($path)) { - throw new Exception('File not found in '.$path, 404); - } - - $compressor = new GZIP(); - $device = Storage::getDevice('local'); - - $contentType = 'text/plain'; - - if (\in_array($file->getAttribute('mimeType'), $mimes)) { - $contentType = $file->getAttribute('mimeType'); - } - - $source = $device->read($path); - - if (!empty($file->getAttribute('fileOpenSSLCipher'))) { // Decrypt - $source = OpenSSL::decrypt( - $source, - $file->getAttribute('fileOpenSSLCipher'), - $request->getServer('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), - 0, - \hex2bin($file->getAttribute('fileOpenSSLIV')), - \hex2bin($file->getAttribute('fileOpenSSLTag')) - ); - } - - $output = $compressor->decompress($source); - $fileName = $file->getAttribute('name', ''); - - $contentTypes = [ - 'pdf' => 'application/pdf', - 'text' => 'text/plain', - ]; - - $contentType = (\array_key_exists($as, $contentTypes)) ? $contentTypes[$as] : $contentType; - - // Response - $response - ->setContentType($contentType) - ->addHeader('Content-Security-Policy', 'script-src none;') - ->addHeader('X-Content-Type-Options', 'nosniff') - ->addHeader('Content-Disposition', 'inline; filename="'.$fileName.'"') - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache - ->addHeader('X-Peak', \memory_get_peak_usage()) - ->send($output) - ; + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); } - ); -$utopia->put('/v1/storage/files/:fileId') + $path = $file->getAttribute('path', ''); + + if (!\file_exists($path)) { + throw new Exception('File not found in '.$path, 404); + } + + $compressor = new GZIP(); + $device = Storage::getDevice('files'); + + $contentType = 'text/plain'; + + if (\in_array($file->getAttribute('mimeType'), $mimes)) { + $contentType = $file->getAttribute('mimeType'); + } + + $source = $device->read($path); + + if (!empty($file->getAttribute('fileOpenSSLCipher'))) { // Decrypt + $source = OpenSSL::decrypt( + $source, + $file->getAttribute('fileOpenSSLCipher'), + App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), + 0, + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) + ); + } + + $output = $compressor->decompress($source); + $fileName = $file->getAttribute('name', ''); + + $contentTypes = [ + 'pdf' => 'application/pdf', + 'text' => 'text/plain', + ]; + + $contentType = (\array_key_exists($as, $contentTypes)) ? $contentTypes[$as] : $contentType; + + // Response + $response + ->setContentType($contentType) + ->addHeader('Content-Security-Policy', 'script-src none;') + ->addHeader('X-Content-Type-Options', 'nosniff') + ->addHeader('Content-Disposition', 'inline; filename="'.$fileName.'"') + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->addHeader('X-Peak', \memory_get_peak_usage()) + ->send($output) + ; + }, ['response', 'projectDB']); + +App::put('/v1/storage/files/:fileId') ->desc('Update File') ->groups(['api', 'storage']) ->label('scope', 'files.write') @@ -583,41 +482,43 @@ $utopia->put('/v1/storage/files/:fileId') ->param('fileId', '', function () { return new UID(); }, 'File unique ID.') ->param('read', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - //->param('folderId', '', function () { return new UID(); }, 'Folder to associate files with.', true) - ->action( - function ($fileId, $read, $write, $folderId = '') use ($response, $projectDB, $audit, $webhook) { - $file = $projectDB->getDocument($fileId); + ->action(function ($fileId, $read, $write, $response, $projectDB, $webhooks, $audits) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ - if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { - throw new Exception('File not found', 404); - } + $file = $projectDB->getDocument($fileId); - $file = $projectDB->updateDocument(\array_merge($file->getArrayCopy(), [ - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - 'folderId' => $folderId, - ])); - - if (false === $file) { - throw new Exception('Failed saving file to DB', 500); - } - - $webhook - ->setParam('payload', $file->getArrayCopy()) - ; - - $audit - ->setParam('event', 'storage.files.update') - ->setParam('resource', 'storage/files/'.$file->getId()) - ; - - $response->json($file->getArrayCopy()); + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); } - ); -$utopia->delete('/v1/storage/files/:fileId') + $file = $projectDB->updateDocument(\array_merge($file->getArrayCopy(), [ + '$permissions' => [ + 'read' => $read, + 'write' => $write, + ], + 'folderId' => '', + ])); + + if (false === $file) { + throw new Exception('Failed saving file to DB', 500); + } + + $webhooks + ->setParam('payload', $file->getArrayCopy()) + ; + + $audits + ->setParam('event', 'storage.files.update') + ->setParam('resource', 'storage/files/'.$file->getId()) + ; + + $response->json($file->getArrayCopy()); + }, ['response', 'projectDB', 'webhooks', 'audits']); + +App::delete('/v1/storage/files/:fileId') ->desc('Delete File') ->groups(['api', 'storage']) ->label('scope', 'files.write') @@ -627,40 +528,44 @@ $utopia->delete('/v1/storage/files/:fileId') ->label('sdk.method', 'deleteFile') ->label('sdk.description', '/docs/references/storage/delete-file.md') ->param('fileId', '', function () { return new UID(); }, 'File unique ID.') - ->action( - function ($fileId) use ($response, $projectDB, $webhook, $audit, $usage) { - $file = $projectDB->getDocument($fileId); + ->action(function ($fileId, $response, $projectDB, $webhooks, $audits, $usage) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Event\Event $usage */ + + $file = $projectDB->getDocument($fileId); - if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { - throw new Exception('File not found', 404); - } - - $device = Storage::getDevice('local'); - - if ($device->delete($file->getAttribute('path', ''))) { - if (!$projectDB->deleteDocument($fileId)) { - throw new Exception('Failed to remove file from DB', 500); - } - } - - $webhook - ->setParam('payload', $file->getArrayCopy()) - ; - - $audit - ->setParam('event', 'storage.files.delete') - ->setParam('resource', 'storage/files/'.$file->getId()) - ; - - $usage - ->setParam('storage', $file->getAttribute('size', 0) * -1) - ; - - $response->noContent(); + if (empty($file->getId()) || Database::SYSTEM_COLLECTION_FILES != $file->getCollection()) { + throw new Exception('File not found', 404); } - ); -// $utopia->get('/v1/storage/files/:fileId/scan') + $device = Storage::getDevice('files'); + + if ($device->delete($file->getAttribute('path', ''))) { + if (!$projectDB->deleteDocument($fileId)) { + throw new Exception('Failed to remove file from DB', 500); + } + } + + $webhooks + ->setParam('payload', $file->getArrayCopy()) + ; + + $audits + ->setParam('event', 'storage.files.delete') + ->setParam('resource', 'storage/files/'.$file->getId()) + ; + + $usage + ->setParam('storage', $file->getAttribute('size', 0) * -1) + ; + + $response->noContent(); + }, ['response', 'projectDB', 'webhooks', 'audits', 'usage']); + +// App::get('/v1/storage/files/:fileId/scan') // ->desc('Scan Storage') // ->groups(['api', 'storage']) // ->label('scope', 'god') @@ -669,7 +574,7 @@ $utopia->delete('/v1/storage/files/:fileId') // ->label('sdk.method', 'getFileScan') // ->label('sdk.hide', true) // ->param('fileId', '', function () { return new UID(); }, 'File unique ID.') -// ->param('storage', 'local', function () { return new WhiteList(['local']);}) +// ->param('storage', 'files', function () { return new WhiteList(['files']);}) // ->action( // function ($fileId, $storage) use ($response, $request, $projectDB) { // $file = $projectDB->getDocument($fileId); @@ -693,7 +598,7 @@ $utopia->delete('/v1/storage/files/:fileId') // $source = OpenSSL::decrypt( // $source, // $file->getAttribute('fileOpenSSLCipher'), -// $request->getServer('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), +// App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), // 0, // hex2bin($file->getAttribute('fileOpenSSLIV')), // hex2bin($file->getAttribute('fileOpenSSLTag')) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 8981695aa3..159b0d2689 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,9 +1,7 @@ post('/v1/teams') +App::post('/v1/teams') ->desc('Create Team') ->groups(['api', 'teams']) ->label('scope', 'teams.write') @@ -30,61 +28,64 @@ $utopia->post('/v1/teams') ->label('sdk.description', '/docs/references/teams/create-team.md') ->param('name', null, function () { return new Text(100); }, 'Team name.') ->param('roles', ['owner'], function () { return new ArrayList(new Text(128)); }, 'Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](/docs/permissions).', true) - ->action( - function ($name, $roles) use ($response, $projectDB, $user, $mode) { - Authorization::disable(); + ->action(function ($name, $roles, $response, $user, $projectDB, $mode) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var bool $mode */ - $team = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_TEAMS, + Authorization::disable(); + + $team = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_TEAMS, + '$permissions' => [ + 'read' => ['team:{self}'], + 'write' => ['team:{self}/owner'], + ], + 'name' => $name, + 'sum' => ($mode !== APP_MODE_ADMIN && $user->getId()) ? 1 : 0, + 'dateCreated' => \time(), + ]); + + Authorization::reset(); + + if (false === $team) { + throw new Exception('Failed saving team to DB', 500); + } + + if ($mode !== APP_MODE_ADMIN && $user->getId()) { // Don't add user on server mode + $membership = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_MEMBERSHIPS, '$permissions' => [ - 'read' => ['team:{self}'], - 'write' => ['team:{self}/owner'], + 'read' => ['user:'.$user->getId(), 'team:'.$team->getId()], + 'write' => ['user:'.$user->getId(), 'team:'.$team->getId().'/owner'], ], - 'name' => $name, - 'sum' => ($mode !== APP_MODE_ADMIN && $user->getId()) ? 1 : 0, - 'dateCreated' => \time(), + 'userId' => $user->getId(), + 'teamId' => $team->getId(), + 'roles' => $roles, + 'invited' => \time(), + 'joined' => \time(), + 'confirm' => true, + 'secret' => '', ]); - Authorization::reset(); + // Attach user to team + $user->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND); - if (false === $team) { - throw new Exception('Failed saving team to DB', 500); + $user = $projectDB->updateDocument($user->getArrayCopy()); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); } - - if ($mode !== APP_MODE_ADMIN && $user->getId()) { // Don't add user on app/server mode - $membership = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_MEMBERSHIPS, - '$permissions' => [ - 'read' => ['user:'.$user->getId(), 'team:'.$team->getId()], - 'write' => ['user:'.$user->getId(), 'team:'.$team->getId().'/owner'], - ], - 'userId' => $user->getId(), - 'teamId' => $team->getId(), - 'roles' => $roles, - 'invited' => \time(), - 'joined' => \time(), - 'confirm' => true, - 'secret' => '', - ]); - - // Attach user to team - $user->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND); - - $user = $projectDB->updateDocument($user->getArrayCopy()); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($team->getArrayCopy()) - ; } - ); -$utopia->get('/v1/teams') + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json($team->getArrayCopy()) + ; + }, ['response', 'user', 'projectDB', 'mode']); + +App::get('/v1/teams') ->desc('List Teams') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -96,25 +97,26 @@ $utopia->get('/v1/teams') ->param('limit', 25, function () { return new Range(0, 100); }, 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) ->param('offset', 0, function () { return new Range(0, 2000); }, 'Results offset. The default value is 0. Use this param to manage pagination.', true) ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) - ->action( - function ($search, $limit, $offset, $orderType) use ($response, $projectDB) { - $results = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderField' => 'dateCreated', - 'orderType' => $orderType, - 'orderCast' => 'int', - 'search' => $search, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_TEAMS, - ], - ]); + ->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - $response->json(['sum' => $projectDB->getSum(), 'teams' => $results]); - } - ); + $results = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'dateCreated', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_TEAMS, + ], + ]); -$utopia->get('/v1/teams/:teamId') + $response->json(['sum' => $projectDB->getSum(), 'teams' => $results]); + }, ['response', 'projectDB']); + +App::get('/v1/teams/:teamId') ->desc('Get Team') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -123,19 +125,20 @@ $utopia->get('/v1/teams/:teamId') ->label('sdk.method', 'get') ->label('sdk.description', '/docs/references/teams/get-team.md') ->param('teamId', '', function () { return new UID(); }, 'Team unique ID.') - ->action( - function ($teamId) use ($response, $projectDB) { - $team = $projectDB->getDocument($teamId); + ->action(function ($teamId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $team = $projectDB->getDocument($teamId); - $response->json($team->getArrayCopy([])); + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); } - ); -$utopia->put('/v1/teams/:teamId') + $response->json($team->getArrayCopy([])); + }, ['response', 'projectDB']); + +App::put('/v1/teams/:teamId') ->desc('Update Team') ->groups(['api', 'teams']) ->label('scope', 'teams.write') @@ -145,27 +148,28 @@ $utopia->put('/v1/teams/:teamId') ->label('sdk.description', '/docs/references/teams/update-team.md') ->param('teamId', '', function () { return new UID(); }, 'Team unique ID.') ->param('name', null, function () { return new Text(100); }, 'Team name.') - ->action( - function ($teamId, $name) use ($response, $projectDB) { - $team = $projectDB->getDocument($teamId); + ->action(function ($teamId, $name, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $team = $projectDB->getDocument($teamId); - $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ - 'name' => $name, - ])); - - if (false === $team) { - throw new Exception('Failed saving team to DB', 500); - } - - $response->json($team->getArrayCopy()); + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); } - ); -$utopia->delete('/v1/teams/:teamId') + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ + 'name' => $name, + ])); + + if (false === $team) { + throw new Exception('Failed saving team to DB', 500); + } + + $response->json($team->getArrayCopy()); + }, ['response', 'projectDB']); + +App::delete('/v1/teams/:teamId') ->desc('Delete Team') ->groups(['api', 'teams']) ->label('scope', 'teams.write') @@ -174,38 +178,39 @@ $utopia->delete('/v1/teams/:teamId') ->label('sdk.method', 'delete') ->label('sdk.description', '/docs/references/teams/delete-team.md') ->param('teamId', '', function () { return new UID(); }, 'Team unique ID.') - ->action( - function ($teamId) use ($response, $projectDB) { - $team = $projectDB->getDocument($teamId); + ->action(function ($teamId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $team = $projectDB->getDocument($teamId); - $memberships = $projectDB->getCollection([ - 'limit' => 2000, // TODO add members limit - 'offset' => 0, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, - 'teamId='.$teamId, - ], - ]); - - foreach ($memberships as $member) { - if (!$projectDB->deleteDocument($member->getId())) { - throw new Exception('Failed to remove membership for team from DB', 500); - } - } - - if (!$projectDB->deleteDocument($teamId)) { - throw new Exception('Failed to remove team from DB', 500); - } - - $response->noContent(); + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); } - ); -$utopia->post('/v1/teams/:teamId/memberships') + $memberships = $projectDB->getCollection([ + 'limit' => 2000, // TODO add members limit + 'offset' => 0, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, + 'teamId='.$teamId, + ], + ]); + + foreach ($memberships as $member) { + if (!$projectDB->deleteDocument($member->getId())) { + throw new Exception('Failed to remove membership for team from DB', 500); + } + } + + if (!$projectDB->deleteDocument($teamId)) { + throw new Exception('Failed to remove team from DB', 500); + } + + $response->noContent(); + }, ['response', 'projectDB']); + +App::post('/v1/teams/:teamId/memberships') ->desc('Create Team Membership') ->groups(['api', 'teams']) ->label('scope', 'teams.write') @@ -217,185 +222,192 @@ $utopia->post('/v1/teams/:teamId/memberships') ->param('email', '', function () { return new Email(); }, 'New team member email.') ->param('name', '', function () { return new Text(100); }, 'New team member name.', true) ->param('roles', [], function () { return new ArrayList(new Text(128)); }, 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions).') - ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.') // TODO add our own built-in confirm page - ->action( - function ($teamId, $email, $name, $roles, $url) use ($response, $mail, $project, $user, $audit, $projectDB, &$mode) { - $name = (empty($name)) ? $email : $name; - $team = $projectDB->getDocument($teamId); + ->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add our own built-in confirm page + ->action(function ($teamId, $email, $name, $roles, $url, $response, $project, $user, $projectDB, $locale, $audits, $mails, $mode) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Event\Event $mails */ + /** @var bool $mode */ - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $name = (empty($name)) ? $email : $name; + $team = $projectDB->getDocument($teamId); - $memberships = $projectDB->getCollection([ - 'limit' => 50, - 'offset' => 0, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, - 'teamId='.$team->getId(), - ], - ]); + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); + } - $invitee = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); + $memberships = $projectDB->getCollection([ + 'limit' => 50, + 'offset' => 0, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, + 'teamId='.$team->getId(), + ], + ]); - if (empty($invitee)) { // Create new user if no user with same email found + $invitee = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); - Authorization::disable(); + if (empty($invitee)) { // Create new user if no user with same email found - try { - $invitee = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_USERS, - '$permissions' => [ - 'read' => ['user:{self}', '*'], - 'write' => ['user:{self}'], - ], - 'email' => $email, - 'emailVerification' => false, - 'status' => Auth::USER_STATUS_UNACTIVATED, - 'password' => Auth::passwordHash(Auth::passwordGenerator()), - 'password-update' => \time(), - 'registration' => \time(), - 'reset' => false, - 'name' => $name, - 'tokens' => [], - ], ['email' => $email]); - } catch (Duplicate $th) { - throw new Exception('Account already exists', 409); - } + Authorization::disable(); - Authorization::reset(); - - if (false === $invitee) { - throw new Exception('Failed saving user to DB', 500); - } - } - - $isOwner = false; - - foreach ($memberships as $member) { - if ($member->getAttribute('userId') == $invitee->getId()) { - throw new Exception('User has already been invited or is already a member of this team', 409); - } - - if ($member->getAttribute('userId') == $user->getId() && \in_array('owner', $member->getAttribute('roles', []))) { - $isOwner = true; - } - } - - if (!$isOwner && APP_MODE_ADMIN !== $mode && $user->getId()) { // Not owner, not admin, not app (server) - throw new Exception('User is not allowed to send invitations for this team', 401); - } - - $secret = Auth::tokenGenerator(); - - $membership = new Document([ - '$collection' => Database::SYSTEM_COLLECTION_MEMBERSHIPS, - '$permissions' => [ - 'read' => ['*'], - 'write' => ['user:'.$invitee->getId(), 'team:'.$team->getId().'/owner'], - ], - 'userId' => $invitee->getId(), - 'teamId' => $team->getId(), - 'roles' => $roles, - 'invited' => \time(), - 'joined' => (APP_MODE_ADMIN === $mode || !$user->getId()) ? \time() : 0, - 'confirm' => (APP_MODE_ADMIN === $mode || !$user->getId()), - 'secret' => Auth::hash($secret), - ]); - - if (APP_MODE_ADMIN === $mode || !$user->getId()) { // Allow admin to create membership - Authorization::disable(); - $membership = $projectDB->createDocument($membership->getArrayCopy()); - - $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ - 'sum' => $team->getAttribute('sum', 0) + 1, - ])); - - // Attach user to team - $invitee->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND); - - $invitee = $projectDB->updateDocument($invitee->getArrayCopy()); - - if (false === $invitee) { - throw new Exception('Failed saving user to DB', 500); - } - - Authorization::reset(); - } else { - $membership = $projectDB->createDocument($membership->getArrayCopy()); - } - - if (false === $membership) { - throw new Exception('Failed saving membership to DB', 500); - } - - $url = Template::parseURL($url); - $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['inviteId' => $membership->getId(), 'teamId' => $team->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]); - $url = Template::unParseURL($url); - - $body = new Template(__DIR__.'/../../config/locales/templates/_base.tpl'); - $content = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('account.emails.invitation.body')); - $cta = new Template(__DIR__.'/../../config/locales/templates/_cta.tpl'); - - $body - ->setParam('{{content}}', $content->render()) - ->setParam('{{cta}}', $cta->render()) - ->setParam('{{title}}', Locale::getText('account.emails.invitation.title')) - ->setParam('{{direction}}', Locale::getText('settings.direction')) - ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) - ->setParam('{{team}}', $team->getAttribute('name', '[TEAM-NAME]')) - ->setParam('{{owner}}', $user->getAttribute('name', '')) - ->setParam('{{redirect}}', $url) - ->setParam('{{bg-body}}', '#f6f6f6') - ->setParam('{{bg-content}}', '#ffffff') - ->setParam('{{bg-cta}}', '#3498db') - ->setParam('{{bg-cta-hover}}', '#34495e') - ->setParam('{{text-content}}', '#000000') - ->setParam('{{text-cta}}', '#ffffff') - ; - - if (APP_MODE_ADMIN !== $mode && $user->getId()) { // No need in comfirmation when in admin or app mode - $mail - ->setParam('event', 'teams.membership.create') - ->setParam('recipient', $email) - ->setParam('name', $name) - ->setParam('subject', \sprintf(Locale::getText('account.emails.invitation.title'), $team->getAttribute('name', '[TEAM-NAME]'), $project->getAttribute('name', ['[APP-NAME]']))) - ->setParam('body', $body->render()) - ->trigger(); - ; - } - - $audit - ->setParam('userId', $invitee->getId()) - ->setParam('event', 'teams.membership.create') - ->setParam('resource', 'teams/'.$teamId) - ; - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) // TODO change response of this endpoint - ->json(\array_merge($membership->getArrayCopy([ - '$id', - 'userId', - 'teamId', - 'roles', - 'invited', - 'joined', - 'confirm', - ]), [ + try { + $invitee = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['user:{self}', '*'], + 'write' => ['user:{self}'], + ], 'email' => $email, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => Auth::passwordHash(Auth::passwordGenerator()), + 'password-update' => \time(), + 'registration' => \time(), + 'reset' => false, 'name' => $name, - ])) + 'tokens' => [], + ], ['email' => $email]); + } catch (Duplicate $th) { + throw new Exception('Account already exists', 409); + } + + Authorization::reset(); + + if (false === $invitee) { + throw new Exception('Failed saving user to DB', 500); + } + } + + $isOwner = false; + + foreach ($memberships as $member) { + if ($member->getAttribute('userId') == $invitee->getId()) { + throw new Exception('User has already been invited or is already a member of this team', 409); + } + + if ($member->getAttribute('userId') == $user->getId() && \in_array('owner', $member->getAttribute('roles', []))) { + $isOwner = true; + } + } + + if (!$isOwner && APP_MODE_ADMIN !== $mode && $user->getId()) { // Not owner, not admin, not app (server) + throw new Exception('User is not allowed to send invitations for this team', 401); + } + + $secret = Auth::tokenGenerator(); + + $membership = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_MEMBERSHIPS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['user:'.$invitee->getId(), 'team:'.$team->getId().'/owner'], + ], + 'userId' => $invitee->getId(), + 'teamId' => $team->getId(), + 'roles' => $roles, + 'invited' => \time(), + 'joined' => (APP_MODE_ADMIN === $mode || !$user->getId()) ? \time() : 0, + 'confirm' => (APP_MODE_ADMIN === $mode || !$user->getId()), + 'secret' => Auth::hash($secret), + ]); + + if (APP_MODE_ADMIN === $mode || !$user->getId()) { // Allow admin to create membership + Authorization::disable(); + $membership = $projectDB->createDocument($membership->getArrayCopy()); + + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ + 'sum' => $team->getAttribute('sum', 0) + 1, + ])); + + // Attach user to team + $invitee->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND); + + $invitee = $projectDB->updateDocument($invitee->getArrayCopy()); + + if (false === $invitee) { + throw new Exception('Failed saving user to DB', 500); + } + + Authorization::reset(); + } else { + $membership = $projectDB->createDocument($membership->getArrayCopy()); + } + + if (false === $membership) { + throw new Exception('Failed saving membership to DB', 500); + } + + $url = Template::parseURL($url); + $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['inviteId' => $membership->getId(), 'teamId' => $team->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]); + $url = Template::unParseURL($url); + + $body = new Template(__DIR__.'/../../config/locale/templates/email-base.tpl'); + $content = new Template(__DIR__.'/../../config/locale/translations/templates/'.$locale->getText('account.emails.invitation.body')); + $cta = new Template(__DIR__.'/../../config/locale/templates/email-cta.tpl'); + + $body + ->setParam('{{content}}', $content->render()) + ->setParam('{{cta}}', $cta->render()) + ->setParam('{{title}}', $locale->getText('account.emails.invitation.title')) + ->setParam('{{direction}}', $locale->getText('settings.direction')) + ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) + ->setParam('{{team}}', $team->getAttribute('name', '[TEAM-NAME]')) + ->setParam('{{owner}}', $user->getAttribute('name', '')) + ->setParam('{{redirect}}', $url) + ->setParam('{{bg-body}}', '#f6f6f6') + ->setParam('{{bg-content}}', '#ffffff') + ->setParam('{{bg-cta}}', '#3498db') + ->setParam('{{bg-cta-hover}}', '#34495e') + ->setParam('{{text-content}}', '#000000') + ->setParam('{{text-cta}}', '#ffffff') + ; + + if (APP_MODE_ADMIN !== $mode && $user->getId()) { // No need in comfirmation when in admin or app mode + $mails + ->setParam('event', 'teams.membership.create') + ->setParam('from', ($project->getId() === 'console') ? '' : \sprintf($locale->getText('account.emails.team'), $project->getAttribute('name'))) + ->setParam('recipient', $email) + ->setParam('name', $name) + ->setParam('subject', \sprintf($locale->getText('account.emails.invitation.title'), $team->getAttribute('name', '[TEAM-NAME]'), $project->getAttribute('name', ['[APP-NAME]']))) + ->setParam('body', $body->render()) + ->trigger(); ; } - ); -$utopia->get('/v1/teams/:teamId/memberships') + $audits + ->setParam('userId', $invitee->getId()) + ->setParam('event', 'teams.membership.create') + ->setParam('resource', 'teams/'.$teamId) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) // TODO change response of this endpoint + ->json(\array_merge($membership->getArrayCopy([ + '$id', + 'userId', + 'teamId', + 'roles', + 'invited', + 'joined', + 'confirm', + ]), [ + 'email' => $email, + 'name' => $name, + ])) + ; + }, ['response', 'project', 'user', 'projectDB', 'locale', 'audits', 'mails', 'mode']); + +App::get('/v1/teams/:teamId/memberships') ->desc('Get Team Memberships') ->groups(['api', 'teams']) ->label('scope', 'teams.read') @@ -408,52 +420,53 @@ $utopia->get('/v1/teams/:teamId/memberships') ->param('limit', 25, function () { return new Range(0, 100); }, 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) ->param('offset', 0, function () { return new Range(0, 2000); }, 'Results offset. The default value is 0. Use this param to manage pagination.', true) ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) - ->action( - function ($teamId, $search, $limit, $offset, $orderType) use ($response, $projectDB) { - $team = $projectDB->getDocument($teamId); + ->action(function ($teamId, $search, $limit, $offset, $orderType, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $team = $projectDB->getDocument($teamId); - $memberships = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderField' => 'joined', - 'orderType' => $orderType, - 'orderCast' => 'int', - 'search' => $search, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, - 'teamId='.$teamId, - ], - ]); - - $users = []; - - foreach ($memberships as $membership) { - if (empty($membership->getAttribute('userId', null))) { - continue; - } - - $temp = $projectDB->getDocument($membership->getAttribute('userId', null))->getArrayCopy(['email', 'name']); - - $users[] = \array_merge($temp, $membership->getArrayCopy([ - '$id', - 'userId', - 'teamId', - 'roles', - 'invited', - 'joined', - 'confirm', - ])); - } - - $response->json(['sum' => $projectDB->getSum(), 'memberships' => $users]); + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); } - ); -$utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') + $memberships = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'joined', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, + 'teamId='.$teamId, + ], + ]); + + $users = []; + + foreach ($memberships as $membership) { + if (empty($membership->getAttribute('userId', null))) { + continue; + } + + $temp = $projectDB->getDocument($membership->getAttribute('userId', null))->getArrayCopy(['email', 'name']); + + $users[] = \array_merge($temp, $membership->getArrayCopy([ + '$id', + 'userId', + 'teamId', + 'roles', + 'invited', + 'joined', + 'confirm', + ])); + } + + $response->json(['sum' => $projectDB->getSum(), 'memberships' => $users]); + }, ['response', 'projectDB']); + +App::patch('/v1/teams/:teamId/memberships/:inviteId/status') ->desc('Update Team Membership Status') ->groups(['api', 'teams']) ->label('scope', 'public') @@ -465,127 +478,125 @@ $utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') ->param('inviteId', '', function () { return new UID(); }, 'Invite unique ID.') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') ->param('secret', '', function () { return new Text(256); }, 'Secret key.') - ->action( - function ($teamId, $inviteId, $userId, $secret) use ($response, $request, $user, $audit, $projectDB) { - $protocol = Config::getParam('protocol'); - $membership = $projectDB->getDocument($inviteId); + ->action(function ($teamId, $inviteId, $userId, $secret, $request, $response, $user, $projectDB, $audits) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ - if (empty($membership->getId()) || Database::SYSTEM_COLLECTION_MEMBERSHIPS != $membership->getCollection()) { - throw new Exception('Invite not found', 404); - } + $protocol = $request->getProtocol(); + $membership = $projectDB->getDocument($inviteId); - if ($membership->getAttribute('teamId') !== $teamId) { - throw new Exception('Team IDs don\'t match', 404); - } + if (empty($membership->getId()) || Database::SYSTEM_COLLECTION_MEMBERSHIPS != $membership->getCollection()) { + throw new Exception('Invite not found', 404); + } - Authorization::disable(); + if ($membership->getAttribute('teamId') !== $teamId) { + throw new Exception('Team IDs don\'t match', 404); + } - $team = $projectDB->getDocument($teamId); - - Authorization::reset(); + Authorization::disable(); - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } + $team = $projectDB->getDocument($teamId); + + Authorization::reset(); - if (Auth::hash($secret) !== $membership->getAttribute('secret')) { - throw new Exception('Secret key not valid', 401); - } + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); + } - if ($userId != $membership->getAttribute('userId')) { - throw new Exception('Invite not belong to current user ('.$user->getAttribute('email').')', 401); - } + if (Auth::hash($secret) !== $membership->getAttribute('secret')) { + throw new Exception('Secret key not valid', 401); + } - if (empty($user->getId())) { - $user = $projectDB->getCollectionFirst([ // Get user - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - '$id='.$userId, - ], - ]); - } + if ($userId != $membership->getAttribute('userId')) { + throw new Exception('Invite not belong to current user ('.$user->getAttribute('email').')', 401); + } - if ($membership->getAttribute('userId') !== $user->getId()) { - throw new Exception('Invite not belong to current user ('.$user->getAttribute('email').')', 401); - } + if (empty($user->getId())) { + $user = $projectDB->getCollectionFirst([ // Get user + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + '$id='.$userId, + ], + ]); + } - $membership // Attach user to team - ->setAttribute('joined', \time()) - ->setAttribute('confirm', true) - ; + if ($membership->getAttribute('userId') !== $user->getId()) { + throw new Exception('Invite not belong to current user ('.$user->getAttribute('email').')', 401); + } - $user - ->setAttribute('emailVerification', true) - ->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND) - ; + $membership // Attach user to team + ->setAttribute('joined', \time()) + ->setAttribute('confirm', true) + ; - // Log user in - $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; - $secret = Auth::tokenGenerator(); + $user + ->setAttribute('emailVerification', true) + ->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND) + ; - $user->setAttribute('tokens', new Document([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, - '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]], - 'type' => Auth::TOKEN_TYPE_LOGIN, - 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak - 'expire' => $expiry, - 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), - 'ip' => $request->getIP(), - ]), Document::SET_TYPE_APPEND); + // Log user in + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $secret = Auth::tokenGenerator(); - Authorization::setRole('user:'.$userId); + $user->setAttribute('tokens', new Document([ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]], + 'type' => Auth::TOKEN_TYPE_LOGIN, + 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak + 'expire' => $expiry, + 'userAgent' => $request->getUserAgent('UNKNOWN'), + 'ip' => $request->getIP(), + ]), Document::SET_TYPE_APPEND); - $user = $projectDB->updateDocument($user->getArrayCopy()); + Authorization::setRole('user:'.$userId); - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } + $user = $projectDB->updateDocument($user->getArrayCopy()); - Authorization::disable(); + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } - $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ - 'sum' => $team->getAttribute('sum', 0) + 1, - ])); + Authorization::disable(); - Authorization::reset(); + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ + 'sum' => $team->getAttribute('sum', 0) + 1, + ])); - if (false === $team) { - throw new Exception('Failed saving team to DB', 500); - } + Authorization::reset(); - $audit - ->setParam('userId', $user->getId()) - ->setParam('event', 'teams.membership.update') - ->setParam('resource', 'teams/'.$teamId) - ; + if (false === $team) { + throw new Exception('Failed saving team to DB', 500); + } - if (!Config::getParam('domainVerification')) { - $response - ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) - ; - } + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'teams.membership.update') + ->setParam('resource', 'teams/'.$teamId) + ; + if (!Config::getParam('domainVerification')) { $response - ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) - ->json(\array_merge($membership->getArrayCopy([ - '$id', - 'userId', - 'teamId', - 'roles', - 'invited', - 'joined', - 'confirm', - ]), [ - 'email' => $user->getAttribute('email'), - 'name' => $user->getAttribute('name'), - ])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) ; } - ); -$utopia->delete('/v1/teams/:teamId/memberships/:inviteId') + $response + ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ; + + $response->dynamic(new Document(\array_merge($membership->getArrayCopy(), [ + 'email' => $user->getAttribute('email'), + 'name' => $user->getAttribute('name'), + ])), Response::MODEL_MEMBERSHIP); + + }, ['request', 'response', 'user', 'projectDB', 'audits']); + +App::delete('/v1/teams/:teamId/memberships/:inviteId') ->desc('Delete Team Membership') ->groups(['api', 'teams']) ->label('scope', 'teams.write') @@ -595,44 +606,46 @@ $utopia->delete('/v1/teams/:teamId/memberships/:inviteId') ->label('sdk.description', '/docs/references/teams/delete-team-membership.md') ->param('teamId', '', function () { return new UID(); }, 'Team unique ID.') ->param('inviteId', '', function () { return new UID(); }, 'Invite unique ID.') - ->action( - function ($teamId, $inviteId) use ($response, $projectDB, $audit) { - $membership = $projectDB->getDocument($inviteId); + ->action(function ($teamId, $inviteId, $response, $projectDB, $audits) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $audits */ - if (empty($membership->getId()) || Database::SYSTEM_COLLECTION_MEMBERSHIPS != $membership->getCollection()) { - throw new Exception('Invite not found', 404); - } + $membership = $projectDB->getDocument($inviteId); - if ($membership->getAttribute('teamId') !== $teamId) { - throw new Exception('Team IDs don\'t match', 404); - } - - $team = $projectDB->getDocument($teamId); - - if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { - throw new Exception('Team not found', 404); - } - - if (!$projectDB->deleteDocument($membership->getId())) { - throw new Exception('Failed to remove membership from DB', 500); - } - - if ($membership->getAttribute('confirm')) { // Count only confirmed members - $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ - 'sum' => $team->getAttribute('sum', 0) - 1, - ])); - } - - if (false === $team) { - throw new Exception('Failed saving team to DB', 500); - } - - $audit - ->setParam('userId', $membership->getAttribute('userId')) - ->setParam('event', 'teams.membership.delete') - ->setParam('resource', 'teams/'.$teamId) - ; - - $response->noContent(); + if (empty($membership->getId()) || Database::SYSTEM_COLLECTION_MEMBERSHIPS != $membership->getCollection()) { + throw new Exception('Invite not found', 404); } - ); + + if ($membership->getAttribute('teamId') !== $teamId) { + throw new Exception('Team IDs don\'t match', 404); + } + + $team = $projectDB->getDocument($teamId); + + if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { + throw new Exception('Team not found', 404); + } + + if (!$projectDB->deleteDocument($membership->getId())) { + throw new Exception('Failed to remove membership from DB', 500); + } + + if ($membership->getAttribute('confirm')) { // Count only confirmed members + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ + 'sum' => $team->getAttribute('sum', 0) - 1, + ])); + } + + if (false === $team) { + throw new Exception('Failed saving team to DB', 500); + } + + $audits + ->setParam('userId', $membership->getAttribute('userId')) + ->setParam('event', 'teams.membership.delete') + ->setParam('resource', 'teams/'.$teamId) + ; + + $response->noContent(); + }, ['response', 'projectDB', 'audits']); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index d4467d98ca..551fffb643 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1,9 +1,7 @@ post('/v1/users') +App::post('/v1/users') ->desc('Create User') ->groups(['api', 'users']) ->label('scope', 'users.write') @@ -32,65 +29,66 @@ $utopia->post('/v1/users') ->param('email', '', function () { return new Email(); }, 'User email.') ->param('password', '', function () { return new Password(); }, 'User password. Must be between 6 to 32 chars.') ->param('name', '', function () { return new Text(100); }, 'User name.', true) - ->action( - function ($email, $password, $name) use ($response, $projectDB) { - $profile = $projectDB->getCollectionFirst([ // Get user by email address - 'limit' => 1, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'email='.$email, - ], - ]); + ->action(function ($email, $password, $name, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (!empty($profile)) { - throw new Exception('User already registered', 409); - } + $profile = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); - try { - $user = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_USERS, - '$permissions' => [ - 'read' => ['*'], - 'write' => ['user:{self}'], - ], - 'email' => $email, - 'emailVerification' => false, - 'status' => Auth::USER_STATUS_UNACTIVATED, - 'password' => Auth::passwordHash($password), - 'password-update' => \time(), - 'registration' => \time(), - 'reset' => false, - 'name' => $name, - ], ['email' => $email]); - } catch (Duplicate $th) { - throw new Exception('Account already exists', 409); - } - - $oauth2Keys = []; - - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } - - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json(\array_merge($user->getArrayCopy(\array_merge([ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], $oauth2Keys)), ['roles' => []])); + if (!empty($profile)) { + throw new Exception('User already registered', 409); } - ); - -$utopia->get('/v1/users') + + try { + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['user:{self}'], + ], + 'email' => $email, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => Auth::passwordHash($password), + 'password-update' => \time(), + 'registration' => \time(), + 'reset' => false, + 'name' => $name, + ], ['email' => $email]); + } catch (Duplicate $th) { + throw new Exception('Account already exists', 409); + } + + $oauth2Keys = []; + + foreach (Config::getParam('providers') as $key => $provider) { + if (!$provider['enabled']) { + continue; + } + + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->json(\array_merge($user->getArrayCopy(\array_merge([ + '$id', + 'status', + 'email', + 'registration', + 'emailVerification', + 'name', + ], $oauth2Keys)), ['roles' => []])); + }, ['response', 'projectDB']); + +App::get('/v1/users') ->desc('List Users') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -102,78 +100,35 @@ $utopia->get('/v1/users') ->param('limit', 25, function () { return new Range(0, 100); }, 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) ->param('offset', 0, function () { return new Range(0, 2000); }, 'Results offset. The default value is 0. Use this param to manage pagination.', true) ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) - ->action( - function ($search, $limit, $offset, $orderType) use ($response, $projectDB) { - $results = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderField' => 'registration', - 'orderType' => $orderType, - 'orderCast' => 'int', - 'search' => $search, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_USERS, - ], - ]); + ->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - $oauth2Keys = []; + $results = $projectDB->getCollection([ + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'registration', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + ], + ]); - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } + $oauth2Keys = []; - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; + foreach (Config::getParam('providers') as $key => $provider) { + if (!$provider['enabled']) { + continue; } - $results = \array_map(function ($value) use ($oauth2Keys) { /* @var $value \Database\Document */ - return $value->getArrayCopy(\array_merge( - [ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], - $oauth2Keys - )); - }, $results); - - $response->json(['sum' => $projectDB->getSum(), 'users' => $results]); + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } - ); -$utopia->get('/v1/users/:userId') - ->desc('Get User') - ->groups(['api', 'users']) - ->label('scope', 'users.read') - ->label('sdk.platform', [APP_PLATFORM_SERVER]) - ->label('sdk.namespace', 'users') - ->label('sdk.method', 'get') - ->label('sdk.description', '/docs/references/users/get-user.md') - ->param('userId', '', function () { return new UID(); }, 'User unique ID.') - ->action( - function ($userId) use ($response, $projectDB) { - $user = $projectDB->getDocument($userId); - - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } - - $oauth2Keys = []; - - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } - - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; - } - - $response->json(\array_merge($user->getArrayCopy(\array_merge( + $results = \array_map(function ($value) use ($oauth2Keys) { /* @var $value \Database\Document */ + return $value->getArrayCopy(\array_merge( [ '$id', 'status', @@ -183,54 +138,56 @@ $utopia->get('/v1/users/:userId') 'name', ], $oauth2Keys - )), ['roles' => []])); - } - ); + )); + }, $results); -$utopia->delete('/v1/users/:userId') - ->desc('Delete User') + $response->json(['sum' => $projectDB->getSum(), 'users' => $results]); + }, ['response', 'projectDB']); + +App::get('/v1/users/:userId') + ->desc('Get User') ->groups(['api', 'users']) - ->label('scope', 'users.write') + ->label('scope', 'users.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') - ->label('sdk.method', 'deleteUser') - ->label('sdk.description', '/docs/references/users/delete-user.md') - ->label('abuse-limit', 100) - ->param('userId', '', function () {return new UID();}, 'User unique ID.') - ->action( - function ($userId) use ($response, $deletes, $projectDB) { - $user = $projectDB->getDocument($userId); + ->label('sdk.method', 'get') + ->label('sdk.description', '/docs/references/users/get-user.md') + ->param('userId', '', function () { return new UID(); }, 'User unique ID.') + ->action(function ($userId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } - if (!$projectDB->deleteDocument($userId)) { - throw new Exception('Failed to remove user from DB', 500); - } + $user = $projectDB->getDocument($userId); - if (!$projectDB->deleteUniqueKey(md5('users:email='.$user->getAttribute('email', null)))) { - throw new Exception('Failed to remove unique key from DB', 500); - } - - $reservedId = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_RESERVED, - '$id' => $userId, - '$permissions' => [ - 'read' => ['*'], - ], - ]); - - if (false === $reservedId) { - throw new Exception('Failed saving reserved id to DB', 500); - } - - $deletes->setParam('document', $user); - - $response->noContent(); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); -$utopia->get('/v1/users/:userId/prefs') + $oauth2Keys = []; + + foreach (Config::getParam('providers') as $key => $provider) { + if (!$provider['enabled']) { + continue; + } + + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; + } + + $response->json(\array_merge($user->getArrayCopy(\array_merge( + [ + '$id', + 'status', + 'email', + 'registration', + 'emailVerification', + 'name', + ], + $oauth2Keys + )), ['roles' => []])); + }, ['response', 'projectDB']); + +App::get('/v1/users/:userId/prefs') ->desc('Get User Preferences') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -239,28 +196,29 @@ $utopia->get('/v1/users/:userId/prefs') ->label('sdk.method', 'getPrefs') ->label('sdk.description', '/docs/references/users/get-user-prefs.md') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') - ->action( - function ($userId) use ($response, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $prefs = $user->getAttribute('prefs', ''); - - try { - $prefs = \json_decode($prefs, true); - $prefs = ($prefs) ? $prefs : []; - } catch (\Exception $error) { - throw new Exception('Failed to parse prefs', 500); - } - - $response->json($prefs); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); -$utopia->get('/v1/users/:userId/sessions') + $prefs = $user->getAttribute('prefs', ''); + + try { + $prefs = \json_decode($prefs, true); + $prefs = ($prefs) ? $prefs : []; + } catch (\Exception $error) { + throw new Exception('Failed to parse prefs', 500); + } + + $response->json($prefs); + }, ['response', 'projectDB']); + +App::get('/v1/users/:userId/sessions') ->desc('Get User Sessions') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -269,62 +227,64 @@ $utopia->get('/v1/users/:userId/sessions') ->label('sdk.method', 'getSessions') ->label('sdk.description', '/docs/references/users/get-user-sessions.md') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') - ->action( - function ($userId) use ($response, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $response, $projectDB, $locale, $geodb) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Locale\Locale $locale */ + /** @var GeoIp2\Database\Reader $geodb */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $tokens = $user->getAttribute('tokens', []); - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $sessions = []; - $index = 0; - $countries = Locale::getText('countries'); - - foreach ($tokens as $token) { /* @var $token Document */ - if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) { - continue; - } - - $userAgent = (!empty($token->getAttribute('userAgent'))) ? $token->getAttribute('userAgent') : 'UNKNOWN'; - - $dd = new DeviceDetector($userAgent); - - // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - // $dd->skipBotDetection(); - - $dd->parse(); - - $sessions[$index] = [ - '$id' => $token->getId(), - 'OS' => $dd->getOs(), - 'client' => $dd->getClient(), - 'device' => $dd->getDevice(), - 'brand' => $dd->getBrand(), - 'model' => $dd->getModel(), - 'ip' => $token->getAttribute('ip', ''), - 'geo' => [], - ]; - - try { - $record = $reader->country($token->getAttribute('ip', '')); - $sessions[$index]['geo']['isoCode'] = \strtolower($record->country->isoCode); - $sessions[$index]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); - } catch (\Exception $e) { - $sessions[$index]['geo']['isoCode'] = '--'; - $sessions[$index]['geo']['country'] = Locale::getText('locale.country.unknown'); - } - - ++$index; - } - - $response->json($sessions); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); -$utopia->get('/v1/users/:userId/logs') + $tokens = $user->getAttribute('tokens', []); + $sessions = []; + $index = 0; + $countries = $locale->getText('countries'); + + foreach ($tokens as $token) { /* @var $token Document */ + if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) { + continue; + } + + $userAgent = (!empty($token->getAttribute('userAgent'))) ? $token->getAttribute('userAgent') : 'UNKNOWN'; + + $dd = new DeviceDetector($userAgent); + + // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + // $dd->skipBotDetection(); + + $dd->parse(); + + $sessions[$index] = [ + '$id' => $token->getId(), + 'OS' => $dd->getOs(), + 'client' => $dd->getClient(), + 'device' => $dd->getDevice(), + 'brand' => $dd->getBrand(), + 'model' => $dd->getModel(), + 'ip' => $token->getAttribute('ip', ''), + 'geo' => [], + ]; + + try { + $record = $geodb->country($token->getAttribute('ip', '')); + $sessions[$index]['geo']['isoCode'] = \strtolower($record->country->isoCode); + $sessions[$index]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : $locale->getText('locale.country.unknown'); + } catch (\Exception $e) { + $sessions[$index]['geo']['isoCode'] = '--'; + $sessions[$index]['geo']['country'] = $locale->getText('locale.country.unknown'); + } + + ++$index; + } + + $response->json($sessions); + }, ['response', 'projectDB', 'locale', 'geodb']); + +App::get('/v1/users/:userId/logs') ->desc('Get User Logs') ->groups(['api', 'users']) ->label('scope', 'users.read') @@ -333,79 +293,83 @@ $utopia->get('/v1/users/:userId/logs') ->label('sdk.method', 'getLogs') ->label('sdk.description', '/docs/references/users/get-user-logs.md') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') - ->action( - function ($userId) use ($response, $register, $projectDB, $project) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $response, $register, $project, $projectDB, $locale, $geodb) { + /** @var Utopia\Response $response */ + /** @var Utopia\Registry\Registry $register */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Locale\Locale $locale */ + /** @var GeoIp2\Database\Reader $geodb */ + + $user = $projectDB->getDocument($userId); - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } - - $adapter = new AuditAdapter($register->get('db')); - $adapter->setNamespace('app_'.$project->getId()); - - $audit = new Audit($adapter); - - $countries = Locale::getText('countries'); - - $logs = $audit->getLogsByUserAndActions($user->getId(), [ - 'account.create', - 'account.delete', - 'account.update.name', - 'account.update.email', - 'account.update.password', - 'account.update.prefs', - 'account.sessions.create', - 'account.sessions.delete', - 'account.recovery.create', - 'account.recovery.update', - 'account.verification.create', - 'account.verification.update', - 'teams.membership.create', - 'teams.membership.update', - 'teams.membership.delete', - ]); - - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $output = []; - - foreach ($logs as $i => &$log) { - $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; - - $dd = new DeviceDetector($log['userAgent']); - - $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - - $dd->parse(); - - $output[$i] = [ - 'event' => $log['event'], - 'ip' => $log['ip'], - 'time' => \strtotime($log['time']), - 'OS' => $dd->getOs(), - 'client' => $dd->getClient(), - 'device' => $dd->getDevice(), - 'brand' => $dd->getBrand(), - 'model' => $dd->getModel(), - 'geo' => [], - ]; - - try { - $record = $reader->country($log['ip']); - $output[$i]['geo']['isoCode'] = \strtolower($record->country->isoCode); - $output[$i]['geo']['country'] = $record->country->name; - $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); - } catch (\Exception $e) { - $output[$i]['geo']['isoCode'] = '--'; - $output[$i]['geo']['country'] = Locale::getText('locale.country.unknown'); - } - } - - $response->json($output); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); -$utopia->patch('/v1/users/:userId/status') + $adapter = new AuditAdapter($register->get('db')); + $adapter->setNamespace('app_'.$project->getId()); + + $audit = new Audit($adapter); + + $countries = $locale->getText('countries'); + + $logs = $audit->getLogsByUserAndActions($user->getId(), [ + 'account.create', + 'account.delete', + 'account.update.name', + 'account.update.email', + 'account.update.password', + 'account.update.prefs', + 'account.sessions.create', + 'account.sessions.delete', + 'account.recovery.create', + 'account.recovery.update', + 'account.verification.create', + 'account.verification.update', + 'teams.membership.create', + 'teams.membership.update', + 'teams.membership.delete', + ]); + + $output = []; + + foreach ($logs as $i => &$log) { + $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; + + $dd = new DeviceDetector($log['userAgent']); + + $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $dd->parse(); + + $output[$i] = [ + 'event' => $log['event'], + 'ip' => $log['ip'], + 'time' => \strtotime($log['time']), + 'OS' => $dd->getOs(), + 'client' => $dd->getClient(), + 'device' => $dd->getDevice(), + 'brand' => $dd->getBrand(), + 'model' => $dd->getModel(), + 'geo' => [], + ]; + + try { + $record = $geodb->country($log['ip']); + $output[$i]['geo']['isoCode'] = \strtolower($record->country->isoCode); + $output[$i]['geo']['country'] = $record->country->name; + $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : $locale->getText('locale.country.unknown'); + } catch (\Exception $e) { + $output[$i]['geo']['isoCode'] = '--'; + $output[$i]['geo']['country'] = $locale->getText('locale.country.unknown'); + } + } + + $response->json($output); + }, ['response', 'register', 'project', 'projectDB', 'locale', 'geodb']); + +App::patch('/v1/users/:userId/status') ->desc('Update User Status') ->groups(['api', 'users']) ->label('scope', 'users.write') @@ -415,46 +379,47 @@ $utopia->patch('/v1/users/:userId/status') ->label('sdk.description', '/docs/references/users/update-user-status.md') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') ->param('status', '', function () { return new WhiteList([Auth::USER_STATUS_ACTIVATED, Auth::USER_STATUS_BLOCKED, Auth::USER_STATUS_UNACTIVATED]); }, 'User Status code. To activate the user pass '.Auth::USER_STATUS_ACTIVATED.', to block the user pass '.Auth::USER_STATUS_BLOCKED.' and for disabling the user pass '.Auth::USER_STATUS_UNACTIVATED) - ->action( - function ($userId, $status) use ($response, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $status, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'status' => (int)$status, - ])); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - - $oauth2Keys = []; - - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } - - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; - } - - $response - ->json(\array_merge($user->getArrayCopy(\array_merge([ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], $oauth2Keys)), ['roles' => []])); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); -$utopia->patch('/v1/users/:userId/prefs') + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'status' => (int)$status, + ])); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $oauth2Keys = []; + + foreach (Config::getParam('providers') as $key => $provider) { + if (!$provider['enabled']) { + continue; + } + + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; + } + + $response + ->json(\array_merge($user->getArrayCopy(\array_merge([ + '$id', + 'status', + 'email', + 'registration', + 'emailVerification', + 'name', + ], $oauth2Keys)), ['roles' => []])); + }, ['response', 'projectDB']); + +App::patch('/v1/users/:userId/prefs') ->desc('Update User Preferences') ->groups(['api', 'users']) ->label('scope', 'users.write') @@ -464,40 +429,40 @@ $utopia->patch('/v1/users/:userId/prefs') ->label('sdk.description', '/docs/references/users/update-user-prefs.md') ->param('userId', '', function () { return new UID(); }, 'User unique ID.') ->param('prefs', '', function () { return new Assoc();}, 'Prefs key-value JSON object.') - ->action( - function ($userId, $prefs) use ($response, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $prefs, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $old = \json_decode($user->getAttribute('prefs', '{}'), true); - $old = ($old) ? $old : []; - - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'prefs' => \json_encode(\array_merge($old, $prefs)), - ])); - - if (false === $user) { - throw new Exception('Failed saving user to DB', 500); - } - - $prefs = $user->getAttribute('prefs', ''); - - try { - $prefs = \json_decode($prefs, true); - $prefs = ($prefs) ? $prefs : []; - } catch (\Exception $error) { - throw new Exception('Failed to parse prefs', 500); - } - - $response->json($prefs); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); + $old = \json_decode($user->getAttribute('prefs', '{}'), true); + $old = ($old) ? $old : []; -$utopia->delete('/v1/users/:userId/sessions/:sessionId') + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'prefs' => \json_encode(\array_merge($old, $prefs)), + ])); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $prefs = $user->getAttribute('prefs', ''); + + try { + $prefs = \json_decode($prefs, true); + $prefs = ($prefs) ? $prefs : []; + } catch (\Exception $error) { + throw new Exception('Failed to parse prefs', 500); + } + + $response->json($prefs); + }, ['response', 'projectDB']); + +App::delete('/v1/users/:userId/sessions/:sessionId') ->desc('Delete User Session') ->groups(['api', 'users']) ->label('scope', 'users.write') @@ -508,29 +473,30 @@ $utopia->delete('/v1/users/:userId/sessions/:sessionId') ->label('abuse-limit', 100) ->param('userId', '', function () { return new UID(); }, 'User unique ID.') ->param('sessionId', null, function () { return new UID(); }, 'User unique session ID.') - ->action( - function ($userId, $sessionId) use ($response, $request, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $sessionId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $tokens = $user->getAttribute('tokens', []); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); + } - foreach ($tokens as $token) { /* @var $token Document */ - if ($sessionId == $token->getId()) { - if (!$projectDB->deleteDocument($token->getId())) { - throw new Exception('Failed to remove token from DB', 500); - } + $tokens = $user->getAttribute('tokens', []); + + foreach ($tokens as $token) { /* @var $token Document */ + if ($sessionId == $token->getId()) { + if (!$projectDB->deleteDocument($token->getId())) { + throw new Exception('Failed to remove token from DB', 500); } } - - $response->json(array('result' => 'success')); } - ); -$utopia->delete('/v1/users/:userId/sessions') + $response->json(array('result' => 'success')); + }, ['response', 'projectDB']); + +App::delete('/v1/users/:userId/sessions') ->desc('Delete User Sessions') ->groups(['api', 'users']) ->label('scope', 'users.write') @@ -540,22 +506,68 @@ $utopia->delete('/v1/users/:userId/sessions') ->label('sdk.description', '/docs/references/users/delete-user-sessions.md') ->label('abuse-limit', 100) ->param('userId', '', function () { return new UID(); }, 'User unique ID.') - ->action( - function ($userId) use ($response, $request, $projectDB) { - $user = $projectDB->getDocument($userId); + ->action(function ($userId, $response, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ - if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { - throw new Exception('User not found', 404); - } + $user = $projectDB->getDocument($userId); - $tokens = $user->getAttribute('tokens', []); - - foreach ($tokens as $token) { /* @var $token Document */ - if (!$projectDB->deleteDocument($token->getId())) { - throw new Exception('Failed to remove token from DB', 500); - } - } - - $response->json(array('result' => 'success')); + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); } - ); + + $tokens = $user->getAttribute('tokens', []); + + foreach ($tokens as $token) { /* @var $token Document */ + if (!$projectDB->deleteDocument($token->getId())) { + throw new Exception('Failed to remove token from DB', 500); + } + } + + $response->json(array('result' => 'success')); + }, ['response', 'projectDB']); + +App::delete('/v1/users/:userId') + ->desc('Delete User') + ->groups(['api', 'users']) + ->label('scope', 'users.write') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'deleteUser') + ->label('sdk.description', '/docs/references/users/delete-user.md') + ->label('abuse-limit', 100) + ->param('userId', '', function () {return new UID();}, 'User unique ID.') + ->action(function ($userId, $response, $projectDB, $deletes) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $deletes */ + + $user = $projectDB->getDocument($userId); + + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); + } + if (!$projectDB->deleteDocument($userId)) { + throw new Exception('Failed to remove user from DB', 500); + } + + if (!$projectDB->deleteUniqueKey(md5('users:email='.$user->getAttribute('email', null)))) { + throw new Exception('Failed to remove unique key from DB', 500); + } + + $reservedId = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_RESERVED, + '$id' => $userId, + '$permissions' => [ + 'read' => ['*'], + ], + ]); + + if (false === $reservedId) { + throw new Exception('Failed saving reserved id to DB', 500); + } + + $deletes->setParam('document', $user); + + $response->noContent(); + }, ['response', 'projectDB', 'deletes']); diff --git a/app/app.php b/app/controllers/general.php similarity index 54% rename from app/app.php rename to app/controllers/general.php index e92660b5a0..751e6cdb04 100644 --- a/app/app.php +++ b/app/controllers/general.php @@ -1,11 +1,10 @@ getAttribute('platforms', []), function ($node) { - if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { - return true; - } - - return false; - })); - -$clients = \array_unique(\array_merge($clientsConsole, \array_map(function ($node) { - return $node['hostname']; - }, \array_filter($project->getAttribute('platforms', []), function ($node) { - if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { - return true; - } - - return false; - })))); - -$utopia->init(function () use ($utopia, $request, $response, &$user, $project, $console, $webhook, $mail, $audit, $usage, $clients) { +App::init(function ($utopia, $request, $response, $console, $project, $user, $locale, $webhooks, $audits, $usage, $clients) { + /** @var Appwrite\Swoole\Request $request */ + /** @var Appwrite\Utopia\Response $response */ + /** @var Appwrite\Database\Document $console */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Locale\Locale $locale */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Event\Event $usage */ + /** @var bool $mode */ + /** @var array $clients */ + Authorization::$roles = ['*']; + + $localeParam = (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); + + if (\in_array($localeParam, Config::getParam('locale-codes'))) { + $locale->setDefault($localeParam); + }; + $route = $utopia->match($request); if(!empty($route->getLabel('sdk.platform', [])) && empty($project->getId()) && ($route->getLabel('scope', '') !== 'public')) { throw new Exception('Missing or unknown project ID', 400); } - $referrer = $request->getServer('HTTP_REFERER', ''); - $origin = \parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_HOST); - $protocol = \parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_SCHEME); - $port = \parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_PORT); + $console->setAttribute('platforms', [ // Allways allow current host + '$collection' => Database::SYSTEM_COLLECTION_PLATFORMS, + 'name' => 'Current Host', + 'type' => 'web', + 'hostname' => $request->getHostname(), + ], Document::SET_TYPE_APPEND); - $refDomain = $protocol.'://'.((\in_array($origin, $clients)) + $referrer = $request->getReferer(); + $origin = \parse_url($request->getOrigin($referrer), PHP_URL_HOST); + $protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME); + $port = \parse_url($request->getOrigin($referrer), PHP_URL_PORT); + + $refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()).'://'.((\in_array($origin, $clients)) ? $origin : 'localhost') . (!empty($port) ? ':'.$port : ''); - $selfDomain = new Domain(Config::getParam('hostname')); + $selfDomain = new Domain($request->getHostname()); $endDomain = new Domain($origin); + // var_dump('referer', $referrer); + // var_dump('origin', $origin); + // var_dump('port', $request->getPort()); + // var_dump('hostname', $request->getHostname()); + // var_dump('protocol', $request->getProtocol()); + // var_dump('method', $request->getMethod()); + // var_dump('ip', $request->getIP()); + // var_dump('-----------------'); + // var_dump($request->debug()); + Config::setParam('domainVerification', ($selfDomain->getRegisterable() === $endDomain->getRegisterable()) && $endDomain->getRegisterable() !== ''); + Config::setParam('cookieDomain', ( + $request->getHostname() === 'localhost' || + $request->getHostname() === 'localhost:'.$request->getPort() || + (\filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false) + ) + ? null + : '.'.$request->getHostname() + ); + + Storage::setDevice('files', new Local(APP_STORAGE_UPLOADS.'/app-'.$project->getId())); + Storage::setDevice('functions', new Local(APP_STORAGE_FUNCTIONS.'/app-'.$project->getId())); + /* * Security Headers * * As recommended at: * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers */ - if ($utopia->getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS - if(Config::getParam('protocol') !== 'https') { - return $response->redirect('https://' . Config::getParam('domain').$request->getServer('REQUEST_URI')); + if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS + if($request->getProtocol() !== 'https') { + return $response->redirect('https://'.$request->getHostname().$request->getURI()); } $response->addHeader('Strict-Transport-Security', 'max-age='.(60 * 60 * 24 * 126)); // 126 days @@ -89,7 +108,7 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ $response ->addHeader('Server', 'Appwrite') - ->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url='.\urlencode($request->getServer('REQUEST_URI'))) + ->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url='.\urlencode($request->getURI())) //->addHeader('X-Frame-Options', ($refDomain == 'http://localhost') ? 'SAMEORIGIN' : 'ALLOW-FROM ' . $refDomain) ->addHeader('X-Content-Type-Options', 'nosniff') ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') @@ -104,13 +123,13 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ * Adding Appwrite API domains to allow XDOMAIN communication * Skip this check for non-web platforms which are not requiredto send an origin header */ - $origin = $request->getServer('HTTP_ORIGIN', $request->getServer('HTTP_REFERER', '')); + $origin = $request->getOrigin($request->getReferer('')); $originValidator = new Origin(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', []))); if(!$originValidator->isValid($origin) && \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE]) && $route->getLabel('origin', false) !== '*' - && empty($request->getHeader('X-Appwrite-Key', ''))) { + && empty($request->getHeader('x-appwrite-key', ''))) { throw new Exception($originValidator->getDescription(), 403); } @@ -143,7 +162,7 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ $scopes = $roles[$role]['scopes']; // Allowed scopes for user role // Check if given key match project API keys - $key = $project->search('secret', $request->getHeader('X-Appwrite-Key', ''), $project->getAttribute('keys', [])); + $key = $project->search('secret', $request->getHeader('x-appwrite-key', ''), $project->getAttribute('keys', [])); /* * Try app auth when we have project key and no user @@ -153,7 +172,7 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ $user = new Document([ '$id' => 0, 'status' => Auth::USER_STATUS_ACTIVATED, - 'email' => 'app.'.$project->getId().'@service.'.Config::getParam('domain'), + 'email' => 'app.'.$project->getId().'@service.'.$request->getHostname(), 'password' => '', 'name' => $project->getAttribute('name', 'Untitled'), ]); @@ -198,43 +217,50 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ /* * Background Jobs */ - $webhook + $webhooks ->setParam('projectId', $project->getId()) ->setParam('event', $route->getLabel('webhook', '')) ->setParam('payload', []) ; - $audit + $audits ->setParam('projectId', $project->getId()) ->setParam('userId', $user->getId()) ->setParam('event', '') ->setParam('resource', '') - ->setParam('userAgent', $request->getServer('HTTP_USER_AGENT', '')) + ->setParam('userAgent', $request->getUserAgent('')) ->setParam('ip', $request->getIP()) ->setParam('data', []) ; $usage ->setParam('projectId', $project->getId()) - ->setParam('url', $request->getServer('HTTP_HOST', '').$request->getServer('REQUEST_URI', '')) - ->setParam('method', $request->getServer('REQUEST_METHOD', 'UNKNOWN')) - ->setParam('request', 0) - ->setParam('response', 0) + ->setParam('httpRequest', 1) + ->setParam('httpUrl', $request->getHostname().$request->getURI()) + ->setParam('httpMethod', $request->getMethod()) + ->setParam('networkRequestSize', 0) + ->setParam('networkResponseSize', 0) ->setParam('storage', 0) ; -}); +}, ['utopia', 'request', 'response', 'console', 'project', 'user', 'locale', 'webhooks', 'audits', 'usage', 'clients']); -$utopia->shutdown(function () use ($response, $request, $webhook, $audit, $usage, $deletes, $mode, $project, $utopia) { +App::shutdown(function ($utopia, $request, $response, $project, $webhooks, $audits, $usage, $deletes, $mode) { + /** @var Utopia\App $utopia */ + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Event\Event $webhooks */ + /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Event\Event $usage */ + /** @var Appwrite\Event\Event $deletes */ + /** @var bool $mode */ - /* - * Trigger events for background workers - */ - if (!empty($webhook->getParam('event'))) { - $webhook->trigger(); + if (!empty($webhooks->getParam('event'))) { + $webhooks->trigger(); } - if (!empty($audit->getParam('event'))) { - $audit->trigger(); + if (!empty($audits->getParam('event'))) { + $audits->trigger(); } if (!empty($deletes->getParam('document'))) { @@ -246,29 +272,47 @@ $utopia->shutdown(function () use ($response, $request, $webhook, $audit, $usage if($project->getId() && $mode !== APP_MODE_ADMIN && !empty($route->getLabel('sdk.namespace', null))) { // Don't calculate console usage and admin mode + $usage - ->setParam('request', $request->getSize() + $usage->getParam('storage')) - ->setParam('response', $response->getSize()) + ->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage')) + ->setParam('networkResponseSize', $response->getSize()) ->trigger() ; } -}); +}, ['utopia', 'request', 'response', 'project', 'webhooks', 'audits', 'usage', 'deletes', 'mode']); -$utopia->options(function () use ($request, $response) { - $origin = $request->getServer('HTTP_ORIGIN'); +App::options(function ($request, $response) { + /** @var Appwrite\Swoole\Request $request */ + /** @var Appwrite\Utopia\Response $response */ + + $origin = $request->getOrigin(); $response + ->addHeader('Server', 'Appwrite') ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-SDK-Version, Cache-Control, Expires, Pragma, X-Fallback-Cookies') ->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies') ->addHeader('Access-Control-Allow-Origin', $origin) ->addHeader('Access-Control-Allow-Credentials', 'true') ->send(); -}); +}, ['request', 'response']); -$utopia->error(function ($error /* @var $error Exception */) use ($request, $response, $utopia, $project) { - $env = Config::getParam('env'); - $version = Config::getParam('version'); +App::error(function ($error, $utopia, $request, $response, $layout, $project) { + /** @var Exception $error */ + /** @var Utopia\App $utopia */ + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Utopia\View $layout */ + /** @var Appwrite\Database\Document $project */ + + if(php_sapi_name() === 'cli') { + var_dump(get_class($error)); + var_dump($error->getMessage()); + var_dump($error->getFile()); + var_dump($error->getLine()); + } + + $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); switch ($error->getCode()) { case 400: // Error allowed publicly @@ -287,9 +331,9 @@ $utopia->error(function ($error /* @var $error Exception */) use ($request, $res $message = 'Server Error'; } - $_SERVER = []; // Reset before reporting to error log to avoid keys being compromised + //$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised - $output = ((App::MODE_TYPE_DEVELOPMENT == $env)) ? [ + $output = ((App::isDevelopment())) ? [ 'message' => $error->getMessage(), 'code' => $error->getCode(), 'file' => $error->getFile(), @@ -313,7 +357,6 @@ $utopia->error(function ($error /* @var $error Exception */) use ($request, $res $template = ($route) ? $route->getLabel('error', null) : null; if ($template) { - $layout = new View(__DIR__.'/views/layouts/default.phtml'); $comp = new View($template); $comp @@ -331,103 +374,95 @@ $utopia->error(function ($error /* @var $error Exception */) use ($request, $res ->setParam('litespeed', false) ; - $response->send($layout->render()); + $response->html($layout->render()); } - $response - ->json($output) - ; -}); + $response->dynamic(new Document($output), + $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_LOCALE); + +}, ['error', 'utopia', 'request', 'response', 'layout', 'project']); -$utopia->get('/manifest.json') +App::get('/manifest.json') ->desc('Progressive app manifest file') ->label('scope', 'public') ->label('docs', false) - ->action( - function () use ($response) { - $response->json([ - 'name' => APP_NAME, - 'short_name' => APP_NAME, - 'start_url' => '.', - 'url' => 'https://appwrite.io/', - 'display' => 'standalone', - 'background_color' => '#fff', - 'theme_color' => '#f02e65', - 'description' => 'End to end backend server for frontend and mobile apps. 👩‍💻👨‍💻', - 'icons' => [ - [ - 'src' => 'images/favicon.png', - 'sizes' => '256x256', - 'type' => 'image/png', - ], - ], - ]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/robots.txt') + $response->json([ + 'name' => APP_NAME, + 'short_name' => APP_NAME, + 'start_url' => '.', + 'url' => 'https://appwrite.io/', + 'display' => 'standalone', + 'background_color' => '#fff', + 'theme_color' => '#f02e65', + 'description' => 'End to end backend server for frontend and mobile apps. 👩‍💻👨‍💻', + 'icons' => [ + [ + 'src' => 'images/favicon.png', + 'sizes' => '256x256', + 'type' => 'image/png', + ], + ], + ]); + }, ['response']); + +App::get('/robots.txt') ->desc('Robots.txt File') ->label('scope', 'public') ->label('docs', false) - ->action( - function () use ($response) { - $template = new View(__DIR__.'/views/general/robots.phtml'); - $response->text($template->render(false)); - } - ); + ->action(function ($response) { + $template = new View(__DIR__.'/views/general/robots.phtml'); + $response->text($template->render(false)); + }, ['response']); -$utopia->get('/humans.txt') +App::get('/humans.txt') ->desc('Humans.txt File') ->label('scope', 'public') ->label('docs', false) - ->action( - function () use ($response) { - $template = new View(__DIR__.'/views/general/humans.phtml'); - $response->text($template->render(false)); - } - ); + ->action(function ($response) { + $template = new View(__DIR__.'/views/general/humans.phtml'); + $response->text($template->render(false)); + }, ['response']); -$utopia->get('/.well-known/acme-challenge') +App::get('/.well-known/acme-challenge') ->desc('SSL Verification') ->label('scope', 'public') ->label('docs', false) - ->action( - function () use ($request, $response) { - $base = \realpath(APP_STORAGE_CERTIFICATES); - $path = \str_replace('/.well-known/acme-challenge/', '', $request->getParam('q')); - $absolute = \realpath($base.'/.well-known/acme-challenge/'.$path); + ->action(function ($request, $response) { + $base = \realpath(APP_STORAGE_CERTIFICATES); + $path = \str_replace('/.well-known/acme-challenge/', '', $request->getParam('q')); + $absolute = \realpath($base.'/.well-known/acme-challenge/'.$path); - if(!$base) { - throw new Exception('Storage error', 500); - } - - if(!$absolute) { - throw new Exception('Unknown path', 404); - } - - if(!\substr($absolute, 0, \strlen($base)) === $base) { - throw new Exception('Invalid path', 401); - } - - if(!\file_exists($absolute)) { - throw new Exception('Unknown path', 404); - } - - $content = @\file_get_contents($absolute); - - if(!$content) { - throw new Exception('Failed to get contents', 500); - } - - $response->text($content); + if(!$base) { + throw new Exception('Storage error', 500); } - ); -include_once __DIR__ . '/controllers/shared/api.php'; -include_once __DIR__ . '/controllers/shared/web.php'; + if(!$absolute) { + throw new Exception('Unknown path', 404); + } + + if(!\substr($absolute, 0, \strlen($base)) === $base) { + throw new Exception('Invalid path', 401); + } + + if(!\file_exists($absolute)) { + throw new Exception('Unknown path', 404); + } + + $content = @\file_get_contents($absolute); + + if(!$content) { + throw new Exception('Failed to get contents', 500); + } + + $response->text($content); + }, ['request', 'response']); + +include_once __DIR__ . '/shared/api.php'; +include_once __DIR__ . '/shared/web.php'; foreach(Config::getParam('services', []) as $service) { include_once $service['controller']; -} - -$utopia->run($request, $response); \ No newline at end of file +} \ No newline at end of file diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 097501fbba..ea8c179efe 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -2,339 +2,339 @@ global $utopia, $request, $response; +use Utopia\App; +use Utopia\Response; use Utopia\Validator\Numeric; use Utopia\Validator\Text; use Utopia\Validator\ArrayList; -use Utopia\Response; use Utopia\Validator\Host; use Appwrite\Storage\Validator\File; -$result = []; - -$utopia->get('/v1/mock/tests/foo') +App::get('/v1/mock/tests/foo') ->desc('Mock a get request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'foo') ->label('sdk.method', 'get') ->label('sdk.description', 'Mock a get request for SDK tests') + ->label('sdk.mock', true) ->param('x', '', function () { return new Text(100); }, 'Sample string param') ->param('y', '', function () { return new Numeric(); }, 'Sample numeric param') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') - ->action( - function ($x, $y, $z) { - } - ); + ->action(function ($x, $y, $z) { + }); -$utopia->post('/v1/mock/tests/foo') +App::post('/v1/mock/tests/foo') ->desc('Mock a post request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'foo') ->label('sdk.method', 'post') ->label('sdk.description', 'Mock a post request for SDK tests') + ->label('sdk.mock', true) ->param('x', '', function () { return new Text(100); }, 'Sample string param') ->param('y', '', function () { return new Numeric(); }, 'Sample numeric param') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') - ->action( - function ($x, $y, $z) { - } - ); + ->action(function ($x, $y, $z) { + }); -$utopia->patch('/v1/mock/tests/foo') +App::patch('/v1/mock/tests/foo') ->desc('Mock a patch request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'foo') ->label('sdk.method', 'patch') ->label('sdk.description', 'Mock a get request for SDK tests') + ->label('sdk.mock', true) ->param('x', '', function () { return new Text(100); }, 'Sample string param') ->param('y', '', function () { return new Numeric(); }, 'Sample numeric param') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') - ->action( - function ($x, $y, $z) { - } - ); + ->action(function ($x, $y, $z) { + }); -$utopia->put('/v1/mock/tests/foo') +App::put('/v1/mock/tests/foo') ->desc('Mock a put request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'foo') ->label('sdk.method', 'put') ->label('sdk.description', 'Mock a put request for SDK tests') + ->label('sdk.mock', true) ->param('x', '', function () { return new Text(100); }, 'Sample string param') ->param('y', '', function () { return new Numeric(); }, 'Sample numeric param') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') - ->action( - function ($x, $y, $z) { - } - ); + ->action(function ($x, $y, $z) { + }); -$utopia->delete('/v1/mock/tests/foo') +App::delete('/v1/mock/tests/foo') ->desc('Mock a delete request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'foo') ->label('sdk.method', 'delete') ->label('sdk.description', 'Mock a delete request for SDK tests') + ->label('sdk.mock', true) ->param('x', '', function () { return new Text(100); }, 'Sample string param') ->param('y', '', function () { return new Numeric(); }, 'Sample numeric param') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') - ->action( - function ($x, $y, $z) { - } - ); + ->action(function ($x, $y, $z) { + }); -$utopia->get('/v1/mock/tests/bar') +App::get('/v1/mock/tests/bar') ->desc('Mock a get request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'bar') ->label('sdk.method', 'get') ->label('sdk.description', 'Mock a get request for SDK tests') + ->label('sdk.mock', true) ->param('x', '', function () { return new Text(100); }, 'Sample string param') ->param('y', '', function () { return new Numeric(); }, 'Sample numeric param') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') - ->action( - function ($x, $y, $z) { - } - ); + ->action(function ($x, $y, $z) { + }); -$utopia->post('/v1/mock/tests/bar') +App::post('/v1/mock/tests/bar') ->desc('Mock a post request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'bar') ->label('sdk.method', 'post') ->label('sdk.description', 'Mock a post request for SDK tests') + ->label('sdk.mock', true) ->param('x', '', function () { return new Text(100); }, 'Sample string param') ->param('y', '', function () { return new Numeric(); }, 'Sample numeric param') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') - ->action( - function ($x, $y, $z) { - } - ); + ->action(function ($x, $y, $z) { + }); -$utopia->patch('/v1/mock/tests/bar') +App::patch('/v1/mock/tests/bar') ->desc('Mock a patch request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'bar') ->label('sdk.method', 'patch') ->label('sdk.description', 'Mock a get request for SDK tests') + ->label('sdk.mock', true) ->param('x', '', function () { return new Text(100); }, 'Sample string param') ->param('y', '', function () { return new Numeric(); }, 'Sample numeric param') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') - ->action( - function ($x, $y, $z) { - } - ); + ->action(function ($x, $y, $z) { + }); -$utopia->put('/v1/mock/tests/bar') +App::put('/v1/mock/tests/bar') ->desc('Mock a put request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'bar') ->label('sdk.method', 'put') ->label('sdk.description', 'Mock a put request for SDK tests') + ->label('sdk.mock', true) ->param('x', '', function () { return new Text(100); }, 'Sample string param') ->param('y', '', function () { return new Numeric(); }, 'Sample numeric param') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') - ->action( - function ($x, $y, $z) { - } - ); + ->action(function ($x, $y, $z) { + }); -$utopia->delete('/v1/mock/tests/bar') +App::delete('/v1/mock/tests/bar') ->desc('Mock a delete request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'bar') ->label('sdk.method', 'delete') ->label('sdk.description', 'Mock a delete request for SDK tests') + ->label('sdk.mock', true) ->param('x', '', function () { return new Text(100); }, 'Sample string param') ->param('y', '', function () { return new Numeric(); }, 'Sample numeric param') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') - ->action( - function ($x, $y, $z) { - } - ); + ->action(function ($x, $y, $z) { + }); -$utopia->post('/v1/mock/tests/general/upload') +App::post('/v1/mock/tests/general/upload') ->desc('Mock a post request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'general') ->label('sdk.method', 'upload') ->label('sdk.description', 'Mock a delete request for SDK tests') ->label('sdk.consumes', 'multipart/form-data') + ->label('sdk.mock', true) ->param('x', '', function () { return new Text(100); }, 'Sample string param') ->param('y', '', function () { return new Numeric(); }, 'Sample numeric param') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') ->param('file', [], function () { return new File(); }, 'Sample file param', false) - ->action( - function ($x, $y, $z, $file) use ($request) { - $file = $request->getFiles('file'); - $file['tmp_name'] = (\is_array($file['tmp_name'])) ? $file['tmp_name'] : [$file['tmp_name']]; - $file['name'] = (\is_array($file['name'])) ? $file['name'] : [$file['name']]; - $file['size'] = (\is_array($file['size'])) ? $file['size'] : [$file['size']]; + ->action(function ($x, $y, $z, $file, $request) { + /** @var Utopia\Request $request */ + + $file = $request->getFiles('file'); + $file['tmp_name'] = (\is_array($file['tmp_name'])) ? $file['tmp_name'] : [$file['tmp_name']]; + $file['name'] = (\is_array($file['name'])) ? $file['name'] : [$file['name']]; + $file['size'] = (\is_array($file['size'])) ? $file['size'] : [$file['size']]; - foreach ($file['name'] as $i => $name) { - if ($name !== 'file.png') { - throw new Exception('Wrong file name', 400); - } - } - - foreach ($file['size'] as $i => $size) { - if ($size !== 38756) { - throw new Exception('Wrong file size', 400); - } - } - - foreach ($file['tmp_name'] as $i => $tmpName) { - if (\md5(\file_get_contents($tmpName)) !== 'd80e7e6999a3eb2ae0d631a96fe135a4') { - throw new Exception('Wrong file uploaded', 400); - } + foreach ($file['name'] as $i => $name) { + if ($name !== 'file.png') { + throw new Exception('Wrong file name', 400); } } - ); -$utopia->get('/v1/mock/tests/general/redirect') + foreach ($file['size'] as $i => $size) { + if ($size !== 38756) { + throw new Exception('Wrong file size', 400); + } + } + + foreach ($file['tmp_name'] as $i => $tmpName) { + if (\md5(\file_get_contents($tmpName)) !== 'd80e7e6999a3eb2ae0d631a96fe135a4') { + throw new Exception('Wrong file uploaded', 400); + } + } + }, ['request']); + +App::get('/v1/mock/tests/general/redirect') ->desc('Mock a post request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'general') ->label('sdk.method', 'redirect') ->label('sdk.description', 'Mock a redirect request for SDK tests') - ->action( - function () use ($response) { - $response->redirect('/v1/mock/tests/general/redirected'); - } - ); + ->label('sdk.mock', true) + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/mock/tests/general/redirected') + $response->redirect('/v1/mock/tests/general/redirected'); + }, ['response']); + +App::get('/v1/mock/tests/general/redirected') ->desc('Mock a post request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'general') ->label('sdk.method', 'redirected') ->label('sdk.description', 'Mock a redirected request for SDK tests') - ->action( - function () { - } - ); + ->label('sdk.mock', true) + ->action(function () { + }); -$utopia->get('/v1/mock/tests/general/set-cookie') +App::get('/v1/mock/tests/general/set-cookie') ->desc('Mock a cookie request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'general') ->label('sdk.method', 'setCookie') ->label('sdk.description', 'Mock a set cookie request for SDK tests') - ->action( - function () use ($response) { - $response->addCookie('cookieName', 'cookieValue', \time() + 31536000, '/', 'localhost', true, true); - } - ); + ->label('sdk.mock', true) + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/mock/tests/general/get-cookie') + $response->addCookie('cookieName', 'cookieValue', \time() + 31536000, '/', 'localhost', true, true); + }, ['response']); + +App::get('/v1/mock/tests/general/get-cookie') ->desc('Mock a cookie request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'general') ->label('sdk.method', 'getCookie') ->label('sdk.description', 'Mock a get cookie request for SDK tests') - ->action( - function () use ($request) { - if ($request->getCookie('cookieName', '') !== 'cookieValue') { - throw new Exception('Missing cookie value', 400); - } - } - ); + ->label('sdk.mock', true) + ->action(function ($request) { + /** @var Utopia\Request $request */ -$utopia->get('/v1/mock/tests/general/empty') + if ($request->getCookie('cookieName', '') !== 'cookieValue') { + throw new Exception('Missing cookie value', 400); + } + }, ['request']); + +App::get('/v1/mock/tests/general/empty') ->desc('Mock a post request for SDK tests') ->label('scope', 'public') ->label('sdk.namespace', 'general') ->label('sdk.method', 'empty') ->label('sdk.description', 'Mock a redirected request for SDK tests') - ->action( - function () use ($response) { - $response->noContent(); - exit(); - } - ); + ->label('sdk.mock', true) + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/mock/tests/general/oauth2') + $response->noContent(); + exit(); + }, ['response']); + +App::get('/v1/mock/tests/general/oauth2') ->desc('Mock an OAuth2 login route') ->label('scope', 'public') ->label('docs', false) + ->label('sdk.mock', true) ->param('client_id', '', function () { return new Text(100); }, 'OAuth2 Client ID.') ->param('redirect_uri', '', function () { return new Host(['localhost']); }, 'OAuth2 Redirect URI.') // Important to deny an open redirect attack ->param('scope', '', function () { return new Text(100); }, 'OAuth2 scope list.') ->param('state', '', function () { return new Text(1024); }, 'OAuth2 state.') - ->action( - function ($clientId, $redirectURI, $scope, $state) use ($response) { - $response->redirect($redirectURI.'?'.\http_build_query(['code' => 'abcdef', 'state' => $state])); - } - ); + ->action(function ($clientId, $redirectURI, $scope, $state, $response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/mock/tests/general/oauth2/token') + $response->redirect($redirectURI.'?'.\http_build_query(['code' => 'abcdef', 'state' => $state])); + }, ['response']); + +App::get('/v1/mock/tests/general/oauth2/token') ->desc('Mock an OAuth2 login route') ->label('scope', 'public') ->label('docs', false) + ->label('sdk.mock', true) ->param('client_id', '', function () { return new Text(100); }, 'OAuth2 Client ID.') ->param('redirect_uri', '', function () { return new Host(['localhost']); }, 'OAuth2 Redirect URI.') ->param('client_secret', '', function () { return new Text(100); }, 'OAuth2 scope list.') ->param('code', '', function () { return new Text(100); }, 'OAuth2 state.') - ->action( - function ($clientId, $redirectURI, $clientSecret, $code) use ($response) { - if ($clientId != '1') { - throw new Exception('Invalid client ID'); - } + ->action(function ($clientId, $redirectURI, $clientSecret, $code, $response) { + /** @var Utopia\Response $response */ - if ($clientSecret != '123456') { - throw new Exception('Invalid client secret'); - } - - if ($code != 'abcdef') { - throw new Exception('Invalid token'); - } - - $response->json(['access_token' => '123456']); + if ($clientId != '1') { + throw new Exception('Invalid client ID'); } - ); -$utopia->get('/v1/mock/tests/general/oauth2/user') + if ($clientSecret != '123456') { + throw new Exception('Invalid client secret'); + } + + if ($code != 'abcdef') { + throw new Exception('Invalid token'); + } + + $response->json(['access_token' => '123456']); + }, ['response']); + +App::get('/v1/mock/tests/general/oauth2/user') ->desc('Mock an OAuth2 user route') ->label('scope', 'public') ->label('docs', false) ->param('token', '', function () { return new Text(100); }, 'OAuth2 Access Token.') - ->action( - function ($token) use ($response) { - if ($token != '123456') { - throw new Exception('Invalid token'); - } + ->action(function ($token, $response) { + /** @var Utopia\Response $response */ - $response->json([ - 'id' => 1, - 'name' => 'User Name', - 'email' => 'user@localhost.test', - ]); + if ($token != '123456') { + throw new Exception('Invalid token'); } - ); -$utopia->get('/v1/mock/tests/general/oauth2/success') + $response->json([ + 'id' => 1, + 'name' => 'User Name', + 'email' => 'user@localhost.test', + ]); + }, ['response']); + +App::get('/v1/mock/tests/general/oauth2/success') ->label('scope', 'public') ->label('docs', false) - ->action( - function () use ($response) { - $response->json([ - 'result' => 'success', - ]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/v1/mock/tests/general/oauth2/failure') + $response->json([ + 'result' => 'success', + ]); + }, ['response']); + +App::get('/v1/mock/tests/general/oauth2/failure') ->label('scope', 'public') ->label('docs', false) - ->action( - function () use ($response) { - $response - ->setStatusCode(Response::STATUS_CODE_BAD_REQUEST) - ->json([ - 'result' => 'failure', - ]); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->shutdown(function() use ($response, $request, &$result, $utopia) { + $response + ->setStatusCode(Response::STATUS_CODE_BAD_REQUEST) + ->json([ + 'result' => 'failure', + ]); + }, ['response']); + +App::shutdown(function($utopia, $response, $request) { + /** @var Utopia\App $utopia */ + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + + $result = []; $route = $utopia->match($request); $path = APP_STORAGE_CACHE.'/tests.json'; $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; @@ -352,4 +352,4 @@ $utopia->shutdown(function() use ($response, $request, &$result, $utopia) { } $response->json(['result' => $route->getMethod() . ':' . $route->getURL() . ':passed']); -}, 'mock'); \ No newline at end of file +}, ['utopia', 'response', 'request'], 'mock'); \ No newline at end of file diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index e911c2c556..3fdca7cf17 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -1,12 +1,18 @@ init(function () use ($utopia, $request, $response, $register, $user, $project) { $route = $utopia->match($request); if (empty($project->getId()) && $route->getLabel('abuse-limit', 0) > 0) { // Abuse limit requires an active project scope @@ -22,9 +28,9 @@ $utopia->init(function () use ($utopia, $request, $response, $register, $user, $ $timeLimit->setNamespace('app_'.$project->getId()); $timeLimit ->setParam('{userId}', $user->getId()) - ->setParam('{userAgent}', $request->getServer('HTTP_USER_AGENT', '')) + ->setParam('{userAgent}', $request->getUserAgent('')) ->setParam('{ip}', $request->getIP()) - ->setParam('{url}', $request->getServer('HTTP_HOST', '').$route->getURL()) + ->setParam('{url}', $request->getHostname().$route->getURL()) ; //TODO make sure we get array here @@ -43,7 +49,7 @@ $utopia->init(function () use ($utopia, $request, $response, $register, $user, $ ; } - if ($abuse->check() && $request->getServer('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') { + if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') { throw new Exception('Too many requests', 429); } -}, 'api'); \ No newline at end of file +}, ['utopia', 'request', 'response', 'project', 'user', 'register'], 'api'); \ No newline at end of file diff --git a/app/controllers/shared/web.php b/app/controllers/shared/web.php index a2f85bcb0a..d7b28de099 100644 --- a/app/controllers/shared/web.php +++ b/app/controllers/shared/web.php @@ -1,22 +1,25 @@ init(function () use ($utopia, $response, $request, $layout) { +App::init(function ($utopia, $request, $response, $layout) { + /** @var Utopia\App $utopia */ + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Utopia\View $layout */ /* AJAX check */ if (!empty($request->getQuery('version', ''))) { $layout->setPath(__DIR__.'/../../views/layouts/empty.phtml'); } + $layout ->setParam('title', APP_NAME) - ->setParam('protocol', Config::getParam('protocol')) - ->setParam('domain', Config::getParam('domain')) - ->setParam('home', $request->getServer('_APP_HOME')) - ->setParam('setup', $request->getServer('_APP_SETUP')) + ->setParam('protocol', $request->getProtocol()) + ->setParam('domain', $request->getHostname()) + ->setParam('home', App::getEnv('_APP_HOME')) + ->setParam('setup', App::getEnv('_APP_SETUP')) ->setParam('class', 'unknown') ->setParam('icon', '/images/favicon.png') ->setParam('roles', [ @@ -24,11 +27,11 @@ $utopia->init(function () use ($utopia, $response, $request, $layout) { ['type' => 'developer', 'label' => 'Developer'], ['type' => 'admin', 'label' => 'Admin'], ]) - ->setParam('env', $utopia->getMode()) + ->setParam('environments', Config::getParam('environments')) + ->setParam('mode', App::getMode()) ; $time = (60 * 60 * 24 * 45); // 45 days cache - $isDev = (\Utopia\App::MODE_TYPE_DEVELOPMENT == Config::getParam('env')); $response ->addHeader('Cache-Control', 'public, max-age='.$time) @@ -38,8 +41,8 @@ $utopia->init(function () use ($utopia, $response, $request, $layout) { $route = $utopia->match($request); $scope = $route->getLabel('scope', ''); $layout - ->setParam('version', Config::getParam('version')) - ->setParam('isDev', $isDev) + ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) + ->setParam('isDev', App::isDevelopment()) ->setParam('class', $scope) ; -}, 'web'); +}, ['utopia', 'request', 'response', 'layout'], 'web'); diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index 595d36d33a..2a68c69592 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -1,7 +1,6 @@ init(function () use ($layout) { +App::init(function ($layout) { + /** @var Utopia\View $layout */ + $layout ->setParam('description', 'Appwrite Console allows you to easily manage, monitor, and control your entire backend API and tools.') ->setParam('analytics', 'UA-26264668-5') ; -}, 'console'); +}, ['layout'], 'console'); + +App::shutdown(function ($response, $layout) { + /** @var Utopia\Response $response */ + /** @var Utopia\View $layout */ -$utopia->shutdown(function () use ($response, $request, $layout) { $header = new View(__DIR__.'/../../views/console/comps/header.phtml'); $footer = new View(__DIR__.'/../../views/console/comps/footer.phtml'); $footer - ->setParam('home', $request->getServer('_APP_HOME', '')) - ->setParam('version', Config::getParam('version')) + ->setParam('home', App::getEnv('_APP_HOME', '')) + ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) ; $layout @@ -31,15 +35,17 @@ $utopia->shutdown(function () use ($response, $request, $layout) { ->setParam('footer', [$footer]) ; - $response->send($layout->render()); -}, 'console'); + $response->html($layout->render()); +}, ['response', 'layout'], 'console'); -$utopia->get('/error/:code') +App::get('/error/:code') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'home') ->param('code', null, new \Utopia\Validator\Numeric(), 'Valid status code number', false) - ->action(function ($code) use ($layout) { + ->action(function ($code, $layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/error.phtml'); $page @@ -49,29 +55,33 @@ $utopia->get('/error/:code') $layout ->setParam('title', APP_NAME.' - Error') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console') +App::get('/console') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout, $request) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/index.phtml'); $page - ->setParam('home', $request->getServer('_APP_HOME', '')) + ->setParam('home', App::getEnv('_APP_HOME', '')) ; $layout ->setParam('title', APP_NAME.' - Console') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/account') +App::get('/console/account') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/account/index.phtml'); $cc = new View(__DIR__.'/../../views/console/forms/credit-card.phtml'); @@ -83,38 +93,44 @@ $utopia->get('/console/account') $layout ->setParam('title', 'Account - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/notifications') +App::get('/console/notifications') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/v1/console/notifications/index.phtml'); $layout ->setParam('title', APP_NAME.' - Notifications') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/home') +App::get('/console/home') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/home/index.phtml'); $layout ->setParam('title', APP_NAME.' - Console') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/settings') +App::get('/console/settings') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($request, $layout) { - $target = new Domain($request->getServer('_APP_DOMAIN_TARGET', '')); + ->action(function ($layout) { + /** @var Utopia\View $layout */ + + $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); $page = new View(__DIR__.'/../../views/console/settings/index.phtml'); @@ -126,15 +142,17 @@ $utopia->get('/console/settings') $layout ->setParam('title', APP_NAME.' - Settings') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/webhooks') +App::get('/console/webhooks') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/webhooks/index.phtml'); - + $page ->setParam('events', Config::getParam('events', [])) ; @@ -142,14 +160,16 @@ $utopia->get('/console/webhooks') $layout ->setParam('title', APP_NAME.' - Webhooks') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/keys') +App::get('/console/keys') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { - $scopes = include __DIR__.'/../../../app/config/scopes.php'; + ->action(function ($layout) { + /** @var Utopia\View $layout */ + + $scopes = Config::getParam('scopes'); $page = new View(__DIR__.'/../../views/console/keys/index.phtml'); $page->setParam('scopes', $scopes); @@ -157,38 +177,46 @@ $utopia->get('/console/keys') $layout ->setParam('title', APP_NAME.' - API Keys') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/tasks') +App::get('/console/tasks') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/tasks/index.phtml'); $layout ->setParam('title', APP_NAME.' - Tasks') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/database') +App::get('/console/database') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/database/index.phtml'); $layout ->setParam('title', APP_NAME.' - Database') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/database/collection') +App::get('/console/database/collection') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->param('id', '', function () { return new UID(); }, 'Collection unique ID.') - ->action(function ($id) use ($response, $layout, $projectDB) { + ->action(function ($id, $response, $layout, $projectDB) { + /** @var Utopia\Response $response */ + /** @var Utopia\View $layout */ + /** @var Appwrite\Database\Database $projectDB */ + Authorization::disable(); $collection = $projectDB->getDocument($id, false); Authorization::reset(); @@ -213,14 +241,17 @@ $utopia->get('/console/database/collection') ->addHeader('Expires', 0) ->addHeader('Pragma', 'no-cache') ; - }); + }, ['response', 'layout', 'projectDB']); -$utopia->get('/console/database/document') +App::get('/console/database/document') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->param('collection', '', function () { return new UID(); }, 'Collection unique ID.') - ->action(function ($collection) use ($layout, $projectDB) { + ->action(function ($collection, $layout, $projectDB) { + /** @var Utopia\View $layout */ + /** @var Appwrite\Database\Database $projectDB */ + Authorization::disable(); $collection = $projectDB->getDocument($collection, false); Authorization::reset(); @@ -243,31 +274,34 @@ $utopia->get('/console/database/document') $layout ->setParam('title', APP_NAME.' - Database Document') ->setParam('body', $page); - }); + }, ['layout', 'projectDB']); -$utopia->get('/console/storage') +App::get('/console/storage') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($request, $layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ $page = new View(__DIR__.'/../../views/console/storage/index.phtml'); $page - ->setParam('home', $request->getServer('_APP_HOME', 0)) - ->setParam('fileLimit', $request->getServer('_APP_STORAGE_LIMIT', 0)) - ->setParam('fileLimitHuman', Storage::human($request->getServer('_APP_STORAGE_LIMIT', 0))) + ->setParam('home', App::getEnv('_APP_HOME', 0)) + ->setParam('fileLimit', App::getEnv('_APP_STORAGE_LIMIT', 0)) + ->setParam('fileLimitHuman', Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0))) ; $layout ->setParam('title', APP_NAME.' - Storage') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/users') +App::get('/console/users') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/users/index.phtml'); $page->setParam('providers', Config::getParam('providers')); @@ -275,28 +309,89 @@ $utopia->get('/console/users') $layout ->setParam('title', APP_NAME.' - Users') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/users/user') +App::get('/console/users/user') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/users/user.phtml'); $layout ->setParam('title', APP_NAME.' - User') ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/console/users/teams/team') +App::get('/console/users/teams/team') ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/console/users/team.phtml'); $layout ->setParam('title', APP_NAME.' - Team') ->setParam('body', $page); - }); + }, ['layout']); + +App::get('/console/functions') + ->groups(['web', 'console']) + ->desc('Platform console project functions') + ->label('permission', 'public') + ->label('scope', 'console') + ->action(function ($layout) { + $page = new View(__DIR__.'/../../views/console/functions/index.phtml'); + + $page + ->setParam('environments', Config::getParam('environments')) + ; + + $layout + ->setParam('title', APP_NAME.' - Functions') + ->setParam('body', $page); + }, ['layout']); + +App::get('/console/functions/function') + ->groups(['web', 'console']) + ->desc('Platform console project function') + ->label('permission', 'public') + ->label('scope', 'console') + ->action(function ($layout) { + $page = new View(__DIR__.'/../../views/console/functions/function.phtml'); + + $page + ->setParam('events', Config::getParam('events', [])) + ->setParam('fileLimit', App::getEnv('_APP_STORAGE_LIMIT', 0)) + ->setParam('fileLimitHuman', Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0))) + ->setParam('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)) + ; + + $layout + ->setParam('title', APP_NAME.' - Function') + ->setParam('body', $page); + }, ['layout']); + +App::get('/console/version') + ->groups(['web', 'console']) + ->desc('Check for new version') + ->label('permission', 'public') + ->label('scope', 'console') + ->action(function ($response) { + try { + $version = \json_decode(@\file_get_contents(App::getEnv('_APP_HOME', 'http://localhost').'/v1/health/version'), true); + + if($version && isset($version['version'])) { + return $response->json(['version' => $version['version']]); + } + else { + throw new Exception('Failed to check for a newer version', 500); + } + } catch (\Throwable $th) { + throw new Exception('Failed to check for a newer version', 500); + } + }, ['response']); \ No newline at end of file diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index 5d8a7aefc2..b265fe1c8d 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -1,18 +1,20 @@ init(function () use ($layout) { +App::init(function ($layout) { + /** @var Utopia\View $layout */ + $header = new View(__DIR__.'/../../views/home/comps/header.phtml'); $footer = new View(__DIR__.'/../../views/home/comps/footer.phtml'); $footer - ->setParam('version', Config::getParam('version')) + ->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN')) ; $layout @@ -23,100 +25,115 @@ $utopia->init(function () use ($layout) { ->setParam('header', [$header]) ->setParam('footer', [$footer]) ; -}, 'home'); +}, ['layout'], 'home'); -$utopia->shutdown(function () use ($response, $layout) { - $response->send($layout->render()); -}, 'home'); +App::shutdown(function ($response, $layout) { + /** @var Utopia\Response $response */ + /** @var Utopia\View $layout */ -$utopia->get('/') + $response->html($layout->render()); +}, ['response', 'layout'], 'home'); + +App::get('/') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action( - function () use ($response) { - $response->redirect('/auth/signin'); - } - ); + ->action(function ($response) { + /** @var Utopia\Response $response */ -$utopia->get('/auth/signin') + $response->redirect('/auth/signin'); + }, ['response']); + +App::get('/auth/signin') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/signin.phtml'); $layout ->setParam('title', 'Sign In - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/auth/signup') +App::get('/auth/signup') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ $page = new View(__DIR__.'/../../views/home/auth/signup.phtml'); $layout ->setParam('title', 'Sign Up - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/auth/recovery') +App::get('/auth/recovery') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($request, $layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/recovery.phtml'); $layout ->setParam('title', 'Password Recovery - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/auth/confirm') +App::get('/auth/confirm') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/confirm.phtml'); $layout ->setParam('title', 'Account Confirmation - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/auth/join') +App::get('/auth/join') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/join.phtml'); $layout ->setParam('title', 'Invitation - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/auth/recovery/reset') +App::get('/auth/recovery/reset') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/recovery/reset.phtml'); $layout ->setParam('title', 'Password Reset - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); - -$utopia->get('/auth/oauth2/success') +App::get('/auth/oauth2/success') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/oauth2.phtml'); $layout @@ -125,13 +142,15 @@ $utopia->get('/auth/oauth2/success') ->setParam('header', []) ->setParam('footer', []) ; - }); + }, ['layout']); -$utopia->get('/auth/oauth2/failure') +App::get('/auth/oauth2/failure') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') - ->action(function () use ($layout) { + ->action(function ($layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/home/auth/oauth2.phtml'); $layout @@ -140,14 +159,16 @@ $utopia->get('/auth/oauth2/failure') ->setParam('header', []) ->setParam('footer', []) ; - }); + }, ['layout']); -$utopia->get('/error/:code') +App::get('/error/:code') ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') ->param('code', null, new \Utopia\Validator\Numeric(), 'Valid status code number', false) - ->action(function ($code) use ($layout) { + ->action(function ($code, $layout) { + /** @var Utopia\View $layout */ + $page = new View(__DIR__.'/../../views/error.phtml'); $page @@ -157,410 +178,381 @@ $utopia->get('/error/:code') $layout ->setParam('title', 'Error'.' - '.APP_NAME) ->setParam('body', $page); - }); + }, ['layout']); -$utopia->get('/open-api-2.json') +App::get('/open-api-2.json') ->groups(['web', 'home']) ->label('scope', 'public') ->label('docs', false) ->param('platform', APP_PLATFORM_CLIENT, function () {return new WhiteList([APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER, APP_PLATFORM_CONSOLE]);}, 'Choose target platform.', true) ->param('extensions', 0, function () {return new Range(0, 1);}, 'Show extra data.', true) ->param('tests', 0, function () {return new Range(0, 1);}, 'Include only test services.', true) - ->action( - function ($platform, $extensions, $tests) use ($response, $request, $utopia) { - $services = Config::getParam('services', []); + ->action(function ($platform, $extensions, $tests, $utopia, $request, $response) { + /** @var Utopia\App $utopia */ + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + + $security = [ + APP_PLATFORM_CLIENT => ['Project' => []], + APP_PLATFORM_SERVER => ['Project' => [], 'Key' => []], + APP_PLATFORM_CONSOLE => ['Project' => [], 'Key' => []], + ]; + + $platforms = [ + 'client' => APP_PLATFORM_CLIENT, + 'server' => APP_PLATFORM_SERVER, + 'all' => APP_PLATFORM_CONSOLE, + ]; + + $keys = [ + APP_PLATFORM_CLIENT => [ + 'Project' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Project', + 'description' => 'Your project ID', + 'in' => 'header', + ], + 'Locale' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Locale', + 'description' => '', + 'in' => 'header', + ], + ], + APP_PLATFORM_SERVER => [ + 'Project' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Project', + 'description' => 'Your project ID', + 'in' => 'header', + ], + 'Key' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Key', + 'description' => 'Your secret API key', + 'in' => 'header', + ], + 'Locale' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Locale', + 'description' => '', + 'in' => 'header', + ], + ], + APP_PLATFORM_CONSOLE => [ + 'Project' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Project', + 'description' => 'Your project ID', + 'in' => 'header', + ], + 'Key' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Key', + 'description' => 'Your secret API key', + 'in' => 'header', + ], + 'Locale' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Locale', + 'description' => '', + 'in' => 'header', + ], + 'Mode' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Mode', + 'description' => '', + 'in' => 'header', + ], + ], + ]; + + /* + * Specifications (v3.0.0): + * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md + */ + $output = [ + 'swagger' => '2.0', + 'info' => [ + 'version' => APP_VERSION_STABLE, + 'title' => APP_NAME, + 'description' => 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)', + 'termsOfService' => 'https://appwrite.io/policy/terms', + 'contact' => [ + 'name' => 'Appwrite Team', + 'url' => 'https://appwrite.io/support', + 'email' => App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), + ], + 'license' => [ + 'name' => 'BSD-3-Clause', + 'url' => 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', + ], + ], + 'host' => \parse_url(App::getEnv('_APP_HOME', $request->getHostname()), PHP_URL_HOST), + 'basePath' => '/v1', + 'schemes' => ['https'], + 'consumes' => ['application/json', 'multipart/form-data'], + 'produces' => ['application/json'], + 'securityDefinitions' => $keys[$platform], + 'paths' => [], + 'definitions' => [ + // 'Pet' => [ + // 'required' => ['id', 'name'], + // 'properties' => [ + // 'id' => [ + // 'type' => 'integer', + // 'format' => 'int64', + // ], + // 'name' => [ + // 'type' => 'string', + // ], + // 'tag' => [ + // 'type' => 'string', + // ], + // ], + // ], + // 'Pets' => array( + // 'type' => 'array', + // 'items' => array( + // '$ref' => '#/definitions/Pet', + // ), + // ), + 'Error' => array( + 'required' => array( + 0 => 'code', + 1 => 'message', + ), + 'properties' => array( + 'code' => array( + 'type' => 'integer', + 'format' => 'int32', + ), + 'message' => array( + 'type' => 'string', + ), + ), + ), + ], + 'externalDocs' => [ + 'description' => 'Full API docs, specs and tutorials', + 'url' => $request->getProtocol().'://'.$request->getHostname().'/docs', + ], + ]; + + if ($extensions) { + if (isset($output['securityDefinitions']['Project'])) { + $output['securityDefinitions']['Project']['extensions'] = ['demo' => '5df5acd0d48c2']; + } - function fromCamelCase($input) - { - \preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); - $ret = $matches[0]; - foreach ($ret as &$match) { - $match = $match == \strtoupper($match) ? \strtolower($match) : \lcfirst($match); - } - - return \implode('_', $ret); + if (isset($output['securityDefinitions']['Key'])) { + $output['securityDefinitions']['Key']['extensions'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2']; + } + + if (isset($output['securityDefinitions']['Locale'])) { + $output['securityDefinitions']['Locale']['extensions'] = ['demo' => 'en']; } - function fromCamelCaseToDash($input) - { - return \str_replace([' ', '_'], '-', \strtolower(\preg_replace('/([a-zA-Z])(?=[A-Z])/', '$1-', $input))); + if (isset($output['securityDefinitions']['Mode'])) { + $output['securityDefinitions']['Mode']['extensions'] = ['demo' => '']; } + } - foreach ($services as $service) { /* @noinspection PhpIncludeInspection */ - if ($tests && !isset($service['tests'])) { + foreach ($utopia->getRoutes() as $key => $method) { + foreach ($method as $route) { /* @var $route \Utopia\Route */ + if (!$route->getLabel('docs', true)) { continue; } - if ($tests && !$service['tests']) { + if ($route->getLabel('sdk.mock', false)) { continue; } - - if (!$tests && !$service['sdk']) { + + if (empty($route->getLabel('sdk.namespace', null))) { continue; } - - /** @noinspection PhpIncludeInspection */ - include_once \realpath(__DIR__.'/../../'.$service['controller']); - } - $security = [ - APP_PLATFORM_CLIENT => ['Project' => []], - APP_PLATFORM_SERVER => ['Project' => [], 'Key' => []], - APP_PLATFORM_CONSOLE => ['Project' => [], 'Key' => []], - ]; + if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $route->getLabel('sdk.platform', []))) { + continue; + } - $platforms = [ - 'client' => APP_PLATFORM_CLIENT, - 'server' => APP_PLATFORM_SERVER, - 'all' => APP_PLATFORM_CONSOLE, - ]; + $url = \str_replace('/v1', '', $route->getURL()); + $scope = $route->getLabel('scope', ''); + $hide = $route->getLabel('sdk.hide', false); + $consumes = ['application/json']; - $keys = [ - APP_PLATFORM_CLIENT => [ - 'Project' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Project', - 'description' => 'Your project ID', - 'in' => 'header', - ], - 'Locale' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Locale', - 'description' => '', - 'in' => 'header', - ], - ], - APP_PLATFORM_SERVER => [ - 'Project' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Project', - 'description' => 'Your project ID', - 'in' => 'header', - ], - 'Key' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Key', - 'description' => 'Your secret API key', - 'in' => 'header', - ], - 'Locale' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Locale', - 'description' => '', - 'in' => 'header', - ], - ], - APP_PLATFORM_CONSOLE => [ - 'Project' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Project', - 'description' => 'Your project ID', - 'in' => 'header', - ], - 'Key' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Key', - 'description' => 'Your secret API key', - 'in' => 'header', - ], - 'Locale' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Locale', - 'description' => '', - 'in' => 'header', - ], - 'Mode' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Mode', - 'description' => '', - 'in' => 'header', - ], - ], - ]; + if ($hide) { + continue; + } - /* - * Specifications (v3.0.0): - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md - */ - $output = [ - 'swagger' => '2.0', - 'info' => [ - 'version' => APP_VERSION_STABLE, - 'title' => APP_NAME, - 'description' => 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)', - 'termsOfService' => 'https://appwrite.io/policy/terms', - 'contact' => [ - 'name' => 'Appwrite Team', - 'url' => 'https://appwrite.io/support', - 'email' => $request->getServer('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), - ], - 'license' => [ - 'name' => 'BSD-3-Clause', - 'url' => 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', - ], - ], - 'host' => \parse_url($request->getServer('_APP_HOME', Config::getParam('domain')), PHP_URL_HOST), - 'basePath' => '/v1', - 'schemes' => ['https'], - 'consumes' => ['application/json', 'multipart/form-data'], - 'produces' => ['application/json'], - 'securityDefinitions' => $keys[$platform], - 'paths' => [], - 'definitions' => [ - // 'Pet' => [ - // 'required' => ['id', 'name'], - // 'properties' => [ - // 'id' => [ - // 'type' => 'integer', - // 'format' => 'int64', - // ], - // 'name' => [ - // 'type' => 'string', - // ], - // 'tag' => [ - // 'type' => 'string', + $desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath(__DIR__.'/../../../'.$route->getLabel('sdk.description', '')) : null; + + $temp = [ + 'summary' => $route->getDesc(), + 'operationId' => $route->getLabel('sdk.method', \uniqid()), + 'consumes' => [], + 'tags' => [$route->getLabel('sdk.namespace', 'default')], + 'description' => ($desc) ? \file_get_contents($desc) : '', + + // 'responses' => [ + // 200 => [ + // 'description' => 'An paged array of pets', + // 'schema' => [ + // '$ref' => '#/definitions/Pet', // ], // ], // ], - // 'Pets' => array( - // 'type' => 'array', - // 'items' => array( - // '$ref' => '#/definitions/Pet', - // ), - // ), - 'Error' => array( - 'required' => array( - 0 => 'code', - 1 => 'message', - ), - 'properties' => array( - 'code' => array( - 'type' => 'integer', - 'format' => 'int32', - ), - 'message' => array( - 'type' => 'string', - ), - ), - ), - ], - 'externalDocs' => [ - 'description' => 'Full API docs, specs and tutorials', - 'url' => Config::getParam('protocol').'://'.Config::getParam('domain').'/docs', - ], - ]; + ]; - if ($extensions) { - if (isset($output['securityDefinitions']['Project'])) { - $output['securityDefinitions']['Project']['extensions'] = ['demo' => '5df5acd0d48c2']; - } - - if (isset($output['securityDefinitions']['Key'])) { - $output['securityDefinitions']['Key']['extensions'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2']; - } - - if (isset($output['securityDefinitions']['Locale'])) { - $output['securityDefinitions']['Locale']['extensions'] = ['demo' => 'en']; - } + if ($extensions) { + $platformList = $route->getLabel('sdk.platform', []); - if (isset($output['securityDefinitions']['Mode'])) { - $output['securityDefinitions']['Mode']['extensions'] = ['demo' => '']; - } - } - - foreach ($utopia->getRoutes() as $key => $method) { - foreach ($method as $route) { /* @var $route \Utopia\Route */ - if (!$route->getLabel('docs', true)) { - continue; - } - - if (empty($route->getLabel('sdk.namespace', null))) { - continue; - } - - if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $route->getLabel('sdk.platform', []))) { - continue; - } - - $url = \str_replace('/v1', '', $route->getURL()); - $scope = $route->getLabel('scope', ''); - $hide = $route->getLabel('sdk.hide', false); - $consumes = ['application/json']; - - if ($hide) { - continue; - } - - $desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath('../'.$route->getLabel('sdk.description', '')) : null; - - $temp = [ - 'summary' => $route->getDesc(), - 'operationId' => $route->getLabel('sdk.method', \uniqid()), - 'consumes' => [], - 'tags' => [$route->getLabel('sdk.namespace', 'default')], - 'description' => ($desc) ? \file_get_contents($desc) : '', - - // 'responses' => [ - // 200 => [ - // 'description' => 'An paged array of pets', - // 'schema' => [ - // '$ref' => '#/definitions/Pet', - // ], - // ], - // ], + $temp['extensions'] = [ + 'weight' => $route->getOrder(), + 'cookies' => $route->getLabel('sdk.cookies', false), + 'type' => $route->getLabel('sdk.methodType', ''), + 'demo' => 'docs/examples/'. Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')).'/'.Template::fromCamelCaseToDash($temp['operationId']).'.md', + 'edit' => 'https://github.com/appwrite/appwrite/edit/master' . $route->getLabel('sdk.description', ''), + 'rate-limit' => $route->getLabel('abuse-limit', 0), + 'rate-time' => $route->getLabel('abuse-time', 3600), + 'rate-key' => $route->getLabel('abuse-key', 'url:{url},ip:{ip}'), + 'scope' => $route->getLabel('scope', ''), + 'platforms' => $platformList, ]; + } - if ($extensions) { - $platformList = $route->getLabel('sdk.platform', []); + if ((!empty($scope))) { // && 'public' != $scope + $temp['security'][] = $route->getLabel('sdk.security', $security[$platform]); + } - $temp['extensions'] = [ - 'weight' => $route->getOrder(), - 'cookies' => $route->getLabel('sdk.cookies', false), - 'type' => $route->getLabel('sdk.methodType', ''), - 'demo' => 'docs/examples/'.fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')).'/'.fromCamelCaseToDash($temp['operationId']).'.md', - 'edit' => 'https://github.com/appwrite/appwrite/edit/master' . $route->getLabel('sdk.description', ''), - 'rate-limit' => $route->getLabel('abuse-limit', 0), - 'rate-time' => $route->getLabel('abuse-time', 3600), - 'rate-key' => $route->getLabel('abuse-key', 'url:{url},ip:{ip}'), - 'scope' => $route->getLabel('scope', ''), - 'platforms' => $platformList, - ]; - } - - if ((!empty($scope))) { // && 'public' != $scope - $temp['security'][] = $route->getLabel('sdk.security', $security[$platform]); - } - - $requestBody = [ - 'content' => [ - 'application/x-www-form-urlencoded' => [ - 'schema' => [ - 'type' => 'object', - 'properties' => [], - ], - 'required' => [], + $requestBody = [ + 'content' => [ + 'application/x-www-form-urlencoded' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [], ], + 'required' => [], ], + ], + ]; + + foreach ($route->getParams() as $name => $param) { + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $utopia->getResources($param['resources'])) : $param['validator']; /* @var $validator \Utopia\Validator */ + + $node = [ + 'name' => $name, + 'description' => $param['description'], + 'required' => !$param['optional'], ]; - foreach ($route->getParams() as $name => $param) { - $validator = (\is_callable($param['validator'])) ? $param['validator']() : $param['validator']; /* @var $validator \Utopia\Validator */ - - $node = [ - 'name' => $name, - 'description' => $param['description'], - 'required' => !$param['optional'], - ]; - - switch ((!empty($validator)) ? \get_class($validator) : '') { - case 'Utopia\Validator\Text': - $node['type'] = 'string'; - $node['x-example'] = '['.\strtoupper(fromCamelCase($node['name'])).']'; - break; - case 'Utopia\Validator\Boolean': - $node['type'] = 'boolean'; - $node['x-example'] = false; - break; - case 'Appwrite\Database\Validator\UID': - $node['type'] = 'string'; - $node['x-example'] = '['.\strtoupper(fromCamelCase($node['name'])).']'; - break; - case 'Utopia\Validator\Email': - $node['type'] = 'string'; - $node['format'] = 'email'; - $node['x-example'] = 'email@example.com'; - break; - case 'Utopia\Validator\URL': - $node['type'] = 'string'; - $node['format'] = 'url'; - $node['x-example'] = 'https://example.com'; - break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': - $node['type'] = 'object'; - $node['type'] = 'object'; - $node['x-example'] = '{}'; - //$node['format'] = 'json'; - break; - case 'Appwrite\Storage\Validator\File': - $consumes = ['multipart/form-data']; - $node['type'] = 'file'; - break; - case 'Utopia\Validator\ArrayList': - $node['type'] = 'array'; - $node['collectionFormat'] = 'multi'; - $node['items'] = [ - 'type' => 'string', - ]; - break; - case 'Appwrite\Auth\Validator\Password': - $node['type'] = 'string'; - $node['format'] = 'format'; - $node['x-example'] = 'password'; - break; - case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */ - $node['type'] = 'integer'; - $node['format'] = 'int32'; - $node['x-example'] = $validator->getMin(); - break; - case 'Utopia\Validator\Numeric': - $node['type'] = 'integer'; - $node['format'] = 'int32'; - break; - case 'Utopia\Validator\Length': - $node['type'] = 'string'; - break; - case 'Utopia\Validator\Host': - $node['type'] = 'string'; - $node['format'] = 'url'; - $node['x-example'] = 'https://example.com'; - break; - case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */ - $node['type'] = 'string'; - $node['x-example'] = $validator->getList()[0]; - break; - default: - $node['type'] = 'string'; - break; - } - - if ($param['optional'] && !\is_null($param['default'])) { // Param has default value - $node['default'] = $param['default']; - } - - if (false !== \strpos($url, ':'.$name)) { // Param is in URL path - $node['in'] = 'path'; - $temp['parameters'][] = $node; - } elseif ($key == 'GET') { // Param is in query - $node['in'] = 'query'; - $temp['parameters'][] = $node; - } else { // Param is in payload - $node['in'] = 'formData'; - $temp['parameters'][] = $node; - $requestBody['content']['application/x-www-form-urlencoded']['schema']['properties'][] = $node; - - if (!$param['optional']) { - $requestBody['content']['application/x-www-form-urlencoded']['required'][] = $name; - } - } - - $url = \str_replace(':'.$name, '{'.$name.'}', $url); + switch ((!empty($validator)) ? \get_class($validator) : '') { + case 'Utopia\Validator\Text': + $node['type'] = 'string'; + $node['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']'; + break; + case 'Utopia\Validator\Boolean': + $node['type'] = 'boolean'; + $node['x-example'] = false; + break; + case 'Appwrite\Database\Validator\UID': + $node['type'] = 'string'; + $node['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']'; + break; + case 'Utopia\Validator\Email': + $node['type'] = 'string'; + $node['format'] = 'email'; + $node['x-example'] = 'email@example.com'; + break; + case 'Utopia\Validator\URL': + $node['type'] = 'string'; + $node['format'] = 'url'; + $node['x-example'] = 'https://example.com'; + break; + case 'Utopia\Validator\JSON': + case 'Utopia\Validator\Mock': + case 'Utopia\Validator\Assoc': + $node['type'] = 'object'; + $node['type'] = 'object'; + $node['x-example'] = '{}'; + //$node['format'] = 'json'; + break; + case 'Appwrite\Storage\Validator\File': + $consumes = ['multipart/form-data']; + $node['type'] = 'file'; + break; + case 'Utopia\Validator\ArrayList': + $node['type'] = 'array'; + $node['collectionFormat'] = 'multi'; + $node['items'] = [ + 'type' => 'string', + ]; + break; + case 'Appwrite\Auth\Validator\Password': + $node['type'] = 'string'; + $node['format'] = 'format'; + $node['x-example'] = 'password'; + break; + case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */ + $node['type'] = 'integer'; + $node['format'] = 'int32'; + $node['x-example'] = $validator->getMin(); + break; + case 'Utopia\Validator\Numeric': + $node['type'] = 'integer'; + $node['format'] = 'int32'; + break; + case 'Utopia\Validator\Length': + $node['type'] = 'string'; + break; + case 'Utopia\Validator\Host': + $node['type'] = 'string'; + $node['format'] = 'url'; + $node['x-example'] = 'https://example.com'; + break; + case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */ + $node['type'] = 'string'; + $node['x-example'] = $validator->getList()[0]; + break; + default: + $node['type'] = 'string'; + break; } - $temp['consumes'] = $consumes; + if ($param['optional'] && !\is_null($param['default'])) { // Param has default value + $node['default'] = $param['default']; + } - $output['paths'][$url][\strtolower($route->getMethod())] = $temp; + if (false !== \strpos($url, ':'.$name)) { // Param is in URL path + $node['in'] = 'path'; + $temp['parameters'][] = $node; + } elseif ($key == 'GET') { // Param is in query + $node['in'] = 'query'; + $temp['parameters'][] = $node; + } else { // Param is in payload + $node['in'] = 'formData'; + $temp['parameters'][] = $node; + $requestBody['content']['application/x-www-form-urlencoded']['schema']['properties'][] = $node; + + if (!$param['optional']) { + $requestBody['content']['application/x-www-form-urlencoded']['required'][] = $name; + } + } + + $url = \str_replace(':'.$name, '{'.$name.'}', $url); } + + $temp['consumes'] = $consumes; + + $output['paths'][$url][\strtolower($route->getMethod())] = $temp; } - - /*foreach ($consoleDB->getMocks() as $mock) { - var_dump($mock['name']); - }*/ - - \ksort($output['paths']); - - $response - ->json($output); } - ); \ No newline at end of file + + /*foreach ($consoleDB->getMocks() as $mock) { + var_dump($mock['name']); + }*/ + + \ksort($output['paths']); + + $response + ->json($output); + }, ['utopia', 'request', 'response']); \ No newline at end of file diff --git a/app/http.php b/app/http.php new file mode 100644 index 0000000000..9540375430 --- /dev/null +++ b/app/http.php @@ -0,0 +1,114 @@ +set([ + 'open_http2_protocol' => true, + // 'document_root' => __DIR__.'/../public', + // 'enable_static_handler' => true, + 'timeout' => 7, + 'http_compression' => true, + 'http_compression_level' => 6, + 'package_max_length' => $payloadSize, + ]) +; + +$http->on('WorkerStart', function($serv, $workerId) { + Console::success('Worker '.++$workerId.' started succefully'); +}); + +$http->on('BeforeReload', function($serv, $workerId) { + Console::success('Starting reload...'); +}); + +$http->on('AfterReload', function($serv, $workerId) { + Console::success('Reload completed...'); +}); + +$http->on('start', function (Server $http) use ($payloadSize) { + Console::success('Server started succefully (max payload is '.$payloadSize.' bytes)'); + + Console::info("Master pid {$http->master_pid}, manager pid {$http->manager_pid}"); + + // listen ctrl + c + Process::signal(2, function () use ($http) { + Console::log('Stop by Ctrl+C'); + $http->shutdown(); + }); +}); + +Files::load(__DIR__ . '/../public'); + +include __DIR__ . '/controllers/general.php'; + +$domain = App::getEnv('_APP_DOMAIN', ''); + +Console::info('Issuing a TLS certificate for the master domain ('.$domain.') in 30 seconds. + Make sure your domain points to your server IP or restart your Appwrite server to try again.'); // TODO move this to installation script + +ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ + 'document' => [], + 'domain' => $domain, + 'validateTarget' => false, + 'validateCNAME' => false, +]); + +$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { + $request = new Request($swooleRequest); + $response = new Response($swooleResponse); + + if(Files::isFileLoaded($request->getURI())) { + $time = (60 * 60 * 24 * 365 * 2); // 45 days cache + + $response + ->setContentType(Files::getFileMimeType($request->getURI())) + ->addHeader('Cache-Control', 'public, max-age='.$time) + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache + ->send(Files::getFileContents($request->getURI())) + ; + + return; + } + + $app = new App('Asia/Tel_Aviv'); + + try { + $app->run($request, $response); + } catch (\Throwable $th) { + if(App::isDevelopment()) { + var_dump(get_class($th)); + var_dump($th->getMessage()); + var_dump($th->getFile()); + var_dump($th->getLine()); + $swooleResponse->end('error: '.$th->getMessage()); + } + + $swooleResponse->end('500: Server Error'); + } +}); + +$http->start(); diff --git a/app/init.php b/app/init.php index 327bfbab67..e21a60a722 100644 --- a/app/init.php +++ b/app/init.php @@ -11,19 +11,23 @@ if (\file_exists(__DIR__.'/../vendor/autoload.php')) { require_once __DIR__.'/../vendor/autoload.php'; } +use Appwrite\Auth\Auth; +use Appwrite\Database\Database; +use Appwrite\Database\Adapter\MySQL as MySQLAdapter; +use Appwrite\Database\Adapter\Redis as RedisAdapter; +use Appwrite\Database\Document; +use Appwrite\Database\Validator\Authorization; +use Appwrite\Event\Event; +use Appwrite\Extend\PDO; +use Appwrite\OpenSSL\OpenSSL; use Utopia\App; -use Utopia\Request; -use Utopia\Response; +use Utopia\View; use Utopia\Config\Config; use Utopia\Locale\Locale; use Utopia\Registry\Registry; -use Appwrite\Auth\Auth; -use Appwrite\Database\Database; -use Appwrite\Database\Document; -use Appwrite\Database\Validator\Authorization; -use Appwrite\Database\Adapter\MySQL as MySQLAdapter; -use Appwrite\Database\Adapter\Redis as RedisAdapter; +use GeoIp2\Database\Reader; use PHPMailer\PHPMailer\PHPMailer; +use PDO as PDONative; const APP_NAME = 'Appwrite'; const APP_DOMAIN = 'appwrite.io'; @@ -31,10 +35,11 @@ const APP_EMAIL_TEAM = 'team@localhost.test'; // Default email address const APP_EMAIL_SECURITY = 'security@localhost.test'; // Default security email address const APP_USERAGENT = APP_NAME.'-Server v%s. Please report abuse at %s'; const APP_MODE_ADMIN = 'admin'; -const APP_PAGING_LIMIT = 15; -const APP_CACHE_BUSTER = 125; -const APP_VERSION_STABLE = '0.6.2'; +const APP_PAGING_LIMIT = 12; +const APP_CACHE_BUSTER = 127; +const APP_VERSION_STABLE = '0.7.0'; const APP_STORAGE_UPLOADS = '/storage/uploads'; +const APP_STORAGE_FUNCTIONS = '/storage/functions'; const APP_STORAGE_CACHE = '/storage/cache'; const APP_STORAGE_CERTIFICATES = '/storage/certificates'; const APP_STORAGE_CONFIG = '/storage/config'; @@ -44,72 +49,103 @@ const APP_SOCIAL_FACEBOOK = 'https://www.facebook.com/appwrite.io'; const APP_SOCIAL_LINKEDIN = 'https://www.linkedin.com/company/appwrite'; const APP_SOCIAL_INSTAGRAM = 'https://www.instagram.com/appwrite.io'; const APP_SOCIAL_GITHUB = 'https://github.com/appwrite'; -const APP_SOCIAL_DISCORD = 'https://discord.gg/GSeTUeA'; +const APP_SOCIAL_DISCORD = 'https://appwrite.io/discord'; const APP_SOCIAL_DEV = 'https://dev.to/appwrite'; +const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite'; $register = new Registry(); -$request = new Request(); -$response = new Response(); -$utopia = new App('Asia/Tel_Aviv'); -$utopia->setMode($utopia->getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION)); +App::setMode(App::getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION)); /* * ENV vars */ -Config::load('events', __DIR__.'/../app/config/events.php'); -Config::load('providers', __DIR__.'/../app/config/providers.php'); -Config::load('platforms', __DIR__.'/../app/config/platforms.php'); -Config::load('locales', __DIR__.'/../app/config/locales.php'); -Config::load('collections', __DIR__.'/../app/config/collections.php'); -Config::load('roles', __DIR__.'/../app/config/roles.php'); // User roles and scopes -Config::load('services', __DIR__.'/../app/config/services.php'); // List of services +Config::load('events', __DIR__.'/config/events.php'); +Config::load('providers', __DIR__.'/config/providers.php'); +Config::load('platforms', __DIR__.'/config/platforms.php'); +Config::load('collections', __DIR__.'/config/collections.php'); +Config::load('environments', __DIR__.'/config/environments.php'); +Config::load('roles', __DIR__.'/config/roles.php'); // User roles and scopes +Config::load('scopes', __DIR__.'/config/scopes.php'); // User roles and scopes +Config::load('services', __DIR__.'/config/services.php'); // List of services +Config::load('variables', __DIR__.'/config/variables.php'); // List of env variables +Config::load('avatar-browsers', __DIR__.'/config/avatars/browsers.php'); +Config::load('avatar-credit-cards', __DIR__.'/config/avatars/credit-cards.php'); +Config::load('avatar-flags', __DIR__.'/config/avatars/flags.php'); +Config::load('locale-codes', __DIR__.'/config/locale/codes.php'); +Config::load('locale-currencies', __DIR__.'/config/locale/currencies.php'); +Config::load('locale-eu', __DIR__.'/config/locale/eu.php'); +Config::load('locale-languages', __DIR__.'/config/locale/languages.php'); +Config::load('locale-phones', __DIR__.'/config/locale/phones.php'); +Config::load('storage-logos', __DIR__.'/config/storage/logos.php'); +Config::load('storage-mimes', __DIR__.'/config/storage/mimes.php'); +Config::load('storage-inputs', __DIR__.'/config/storage/inputs.php'); +Config::load('storage-outputs', __DIR__.'/config/storage/outputs.php'); -Config::setParam('env', $utopia->getMode()); -Config::setParam('domain', $request->getServer('HTTP_HOST', '')); -Config::setParam('domainVerification', false); -Config::setParam('version', $request->getServer('_APP_VERSION', 'UNKNOWN')); -Config::setParam('protocol', $request->getServer('HTTP_X_FORWARDED_PROTO', $request->getServer('REQUEST_SCHEME', 'https'))); -Config::setParam('port', (string) \parse_url(Config::getParam('protocol').'://'.$request->getServer('HTTP_HOST', ''), PHP_URL_PORT)); -Config::setParam('hostname', \parse_url(Config::getParam('protocol').'://'.$request->getServer('HTTP_HOST', null), PHP_URL_HOST)); +Resque::setBackend(App::getEnv('_APP_REDIS_HOST', '') + .':'.App::getEnv('_APP_REDIS_PORT', '')); -Resque::setBackend($request->getServer('_APP_REDIS_HOST', '') - .':'.$request->getServer('_APP_REDIS_PORT', '')); +/** + * DB Filters + */ +Database::addFilter('json', + function($value) { + if(!is_array($value)) { + return $value; + } + return json_encode($value); + }, + function($value) { + return json_decode($value, true); + } +); -\define('COOKIE_DOMAIN', - ( - $request->getServer('HTTP_HOST', null) === 'localhost' || - $request->getServer('HTTP_HOST', null) === 'localhost:'.Config::getParam('port') || - (\filter_var(Config::getParam('hostname'), FILTER_VALIDATE_IP) !== false) - ) - ? null - : '.'.Config::getParam('hostname') - ); -\define('COOKIE_SAMESITE', Response::COOKIE_SAMESITE_NONE); +Database::addFilter('encrypt', + function($value) { + $key = App::getEnv('_APP_OPENSSL_KEY_V1'); + $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); + $tag = null; + + return json_encode([ + 'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), + 'method' => OpenSSL::CIPHER_AES_128_GCM, + 'iv' => bin2hex($iv), + 'tag' => bin2hex($tag), + 'version' => '1', + ]); + }, + function($value) { + $value = json_decode($value, true); + $key = App::getEnv('_APP_OPENSSL_KEY_V'.$value['version']); + + return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag'])); + } +); /* * Registry */ -$register->set('db', function () use ($request) { // Register DB connection - $dbHost = $request->getServer('_APP_DB_HOST', ''); - $dbUser = $request->getServer('_APP_DB_USER', ''); - $dbPass = $request->getServer('_APP_DB_PASS', ''); - $dbScheme = $request->getServer('_APP_DB_SCHEMA', ''); +$register->set('db', function () { // Register DB connection + $dbHost = App::getEnv('_APP_DB_HOST', ''); + $dbUser = App::getEnv('_APP_DB_USER', ''); + $dbPass = App::getEnv('_APP_DB_PASS', ''); + $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); $pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array( - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', - PDO::ATTR_TIMEOUT => 5, // Seconds + PDONative::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', + PDONative::ATTR_TIMEOUT => 3, // Seconds + PDONative::ATTR_PERSISTENT => true )); // Connection settings - $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // Return arrays - $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // Handle all errors with exceptions + $pdo->setAttribute(PDONative::ATTR_DEFAULT_FETCH_MODE, PDONative::FETCH_ASSOC); // Return arrays + $pdo->setAttribute(PDONative::ATTR_ERRMODE, PDONative::ERRMODE_EXCEPTION); // Handle all errors with exceptions return $pdo; }); -$register->set('influxdb', function () use ($request) { // Register DB connection - $host = $request->getServer('_APP_INFLUXDB_HOST', ''); - $port = $request->getServer('_APP_INFLUXDB_PORT', ''); +$register->set('influxdb', function () { // Register DB connection + $host = App::getEnv('_APP_INFLUXDB_HOST', ''); + $port = App::getEnv('_APP_INFLUXDB_PORT', ''); if (empty($host) || empty($port)) { return; @@ -119,43 +155,42 @@ $register->set('influxdb', function () use ($request) { // Register DB connectio return $client; }); -$register->set('statsd', function () use ($request) { // Register DB connection - $host = $request->getServer('_APP_STATSD_HOST', 'telegraf'); - $port = $request->getServer('_APP_STATSD_PORT', 8125); +$register->set('statsd', function () { // Register DB connection + $host = App::getEnv('_APP_STATSD_HOST', 'telegraf'); + $port = App::getEnv('_APP_STATSD_PORT', 8125); $connection = new \Domnikl\Statsd\Connection\UdpSocket($host, $port); $statsd = new \Domnikl\Statsd\Client($connection); return $statsd; }); -$register->set('cache', function () use ($request) { // Register cache connection +$register->set('cache', function () { // Register cache connection $redis = new Redis(); - - $redis->connect($request->getServer('_APP_REDIS_HOST', ''), - $request->getServer('_APP_REDIS_PORT', '')); + $redis->pconnect(App::getEnv('_APP_REDIS_HOST', '', 2.5), + App::getEnv('_APP_REDIS_PORT', '')); return $redis; }); -$register->set('smtp', function () use ($request) { +$register->set('smtp', function () { $mail = new PHPMailer(true); $mail->isSMTP(); - $username = $request->getServer('_APP_SMTP_USERNAME', null); - $password = $request->getServer('_APP_SMTP_PASSWORD', null); + $username = App::getEnv('_APP_SMTP_USERNAME', null); + $password = App::getEnv('_APP_SMTP_PASSWORD', null); $mail->XMailer = 'Appwrite Mailer'; - $mail->Host = $request->getServer('_APP_SMTP_HOST', 'smtp'); - $mail->Port = $request->getServer('_APP_SMTP_PORT', 25); + $mail->Host = App::getEnv('_APP_SMTP_HOST', 'smtp'); + $mail->Port = App::getEnv('_APP_SMTP_PORT', 25); $mail->SMTPAuth = (!empty($username) && !empty($password)); $mail->Username = $username; $mail->Password = $password; - $mail->SMTPSecure = $request->getServer('_APP_SMTP_SECURE', false); + $mail->SMTPSecure = App::getEnv('_APP_SMTP_SECURE', false); $mail->SMTPAutoTLS = false; $mail->CharSet = 'UTF-8'; - $from = \urldecode($request->getServer('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')); - $email = $request->getServer('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); + $from = \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')); + $email = App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); $mail->setFrom($email, $from); $mail->addReplyTo($email, $from); @@ -164,154 +199,260 @@ $register->set('smtp', function () use ($request) { return $mail; }); +$register->set('queue-webhooks', function () { + return new Event('v1-webhooks', 'WebhooksV1'); +}); +$register->set('queue-audits', function () { + return new Event('v1-audits', 'AuditsV1'); +}); +$register->set('queue-usage', function () { + return new Event('v1-usage', 'UsageV1'); +}); +$register->set('queue-mails', function () { + return new Event('v1-mails', 'MailsV1'); +}); +$register->set('queue-deletes', function () { + return new Event('v1-deletes', 'DeletesV1'); +}); +$register->set('queue-functions', function () { + return new Event('v1-functions', 'FunctionsV1'); +}); /* * Localization */ -$locale = $request->getParam('locale', $request->getHeader('X-Appwrite-Locale', '')); - Locale::$exceptions = false; - -Locale::setLanguage('af', include __DIR__.'/config/locales/af.php'); -Locale::setLanguage('ar', include __DIR__.'/config/locales/ar.php'); -Locale::setLanguage('bn', include __DIR__.'/config/locales/bn.php'); -Locale::setLanguage('cat', include __DIR__.'/config/locales/cat.php'); -Locale::setLanguage('cz', include __DIR__.'/config/locales/cz.php'); -Locale::setLanguage('de', include __DIR__.'/config/locales/de.php'); -Locale::setLanguage('en', include __DIR__.'/config/locales/en.php'); -Locale::setLanguage('es', include __DIR__.'/config/locales/es.php'); -Locale::setLanguage('fi', include __DIR__.'/config/locales/fi.php'); -Locale::setLanguage('fo', include __DIR__.'/config/locales/fo.php'); -Locale::setLanguage('fr', include __DIR__.'/config/locales/fr.php'); -Locale::setLanguage('gr', include __DIR__.'/config/locales/gr.php'); -Locale::setLanguage('he', include __DIR__.'/config/locales/he.php'); -Locale::setLanguage('hi', include __DIR__.'/config/locales/hi.php'); -Locale::setLanguage('hu', include __DIR__.'/config/locales/hu.php'); -Locale::setLanguage('hy', include __DIR__.'/config/locales/hy.php'); -Locale::setLanguage('id', include __DIR__.'/config/locales/id.php'); -Locale::setLanguage('is', include __DIR__.'/config/locales/is.php'); -Locale::setLanguage('it', include __DIR__.'/config/locales/it.php'); -Locale::setLanguage('ja', include __DIR__.'/config/locales/ja.php'); -Locale::setLanguage('jv', include __DIR__.'/config/locales/jv.php'); -Locale::setLanguage('km', include __DIR__.'/config/locales/km.php'); -Locale::setLanguage('ko', include __DIR__.'/config/locales/ko.php'); -Locale::setLanguage('lt', include __DIR__.'/config/locales/lt.php'); -Locale::setLanguage('ml', include __DIR__.'/config/locales/ml.php'); -Locale::setLanguage('ms', include __DIR__.'/config/locales/ms.php'); -Locale::setLanguage('nl', include __DIR__.'/config/locales/nl.php'); -Locale::setLanguage('no', include __DIR__.'/config/locales/no.php'); -Locale::setLanguage('ph', include __DIR__.'/config/locales/ph.php'); -Locale::setLanguage('pl', include __DIR__.'/config/locales/pl.php'); -Locale::setLanguage('pt-br', include __DIR__.'/config/locales/pt-br.php'); -Locale::setLanguage('pt-pt', include __DIR__.'/config/locales/pt-pt.php'); -Locale::setLanguage('ro', include __DIR__.'/config/locales/ro.php'); -Locale::setLanguage('ru', include __DIR__ . '/config/locales/ru.php'); -Locale::setLanguage('si', include __DIR__ . '/config/locales/si.php'); -Locale::setLanguage('sl', include __DIR__ . '/config/locales/sl.php'); -Locale::setLanguage('sq', include __DIR__ . '/config/locales/sq.php'); -Locale::setLanguage('sv', include __DIR__ . '/config/locales/sv.php'); -Locale::setLanguage('ta', include __DIR__ . '/config/locales/ta.php'); -Locale::setLanguage('th', include __DIR__.'/config/locales/th.php'); -Locale::setLanguage('tr', include __DIR__.'/config/locales/tr.php'); -Locale::setLanguage('ua', include __DIR__.'/config/locales/ua.php'); -Locale::setLanguage('vi', include __DIR__.'/config/locales/vi.php'); -Locale::setLanguage('zh-cn', include __DIR__.'/config/locales/zh-cn.php'); -Locale::setLanguage('zh-tw', include __DIR__.'/config/locales/zh-tw.php'); - -Locale::setDefault('en'); - -if (\in_array($locale, Config::getParam('locales'))) { - Locale::setDefault($locale); -} +Locale::setLanguage('af', include __DIR__.'/config/locale/translations/af.php'); +Locale::setLanguage('ar', include __DIR__.'/config/locale/translations/ar.php'); +Locale::setLanguage('bn', include __DIR__.'/config/locale/translations/bn.php'); +Locale::setLanguage('cat', include __DIR__.'/config/locale/translations/cat.php'); +Locale::setLanguage('cz', include __DIR__.'/config/locale/translations/cz.php'); +Locale::setLanguage('de', include __DIR__.'/config/locale/translations/de.php'); +Locale::setLanguage('en', include __DIR__.'/config/locale/translations/en.php'); +Locale::setLanguage('es', include __DIR__.'/config/locale/translations/es.php'); +Locale::setLanguage('fi', include __DIR__.'/config/locale/translations/fi.php'); +Locale::setLanguage('fo', include __DIR__.'/config/locale/translations/fo.php'); +Locale::setLanguage('fr', include __DIR__.'/config/locale/translations/fr.php'); +Locale::setLanguage('gr', include __DIR__.'/config/locale/translations/gr.php'); +Locale::setLanguage('he', include __DIR__.'/config/locale/translations/he.php'); +Locale::setLanguage('hi', include __DIR__.'/config/locale/translations/hi.php'); +Locale::setLanguage('hu', include __DIR__.'/config/locale/translations/hu.php'); +Locale::setLanguage('hy', include __DIR__.'/config/locale/translations/hy.php'); +Locale::setLanguage('id', include __DIR__.'/config/locale/translations/id.php'); +Locale::setLanguage('is', include __DIR__.'/config/locale/translations/is.php'); +Locale::setLanguage('it', include __DIR__.'/config/locale/translations/it.php'); +Locale::setLanguage('ja', include __DIR__.'/config/locale/translations/ja.php'); +Locale::setLanguage('jv', include __DIR__.'/config/locale/translations/jv.php'); +Locale::setLanguage('km', include __DIR__.'/config/locale/translations/km.php'); +Locale::setLanguage('ko', include __DIR__.'/config/locale/translations/ko.php'); +Locale::setLanguage('lt', include __DIR__.'/config/locale/translations/lt.php'); +Locale::setLanguage('ml', include __DIR__.'/config/locale/translations/ml.php'); +Locale::setLanguage('ms', include __DIR__.'/config/locale/translations/ms.php'); +Locale::setLanguage('nl', include __DIR__.'/config/locale/translations/nl.php'); +Locale::setLanguage('no', include __DIR__.'/config/locale/translations/no.php'); +Locale::setLanguage('ph', include __DIR__.'/config/locale/translations/ph.php'); +Locale::setLanguage('pl', include __DIR__.'/config/locale/translations/pl.php'); +Locale::setLanguage('pt-br', include __DIR__.'/config/locale/translations/pt-br.php'); +Locale::setLanguage('pt-pt', include __DIR__.'/config/locale/translations/pt-pt.php'); +Locale::setLanguage('ro', include __DIR__.'/config/locale/translations/ro.php'); +Locale::setLanguage('ru', include __DIR__ . '/config/locale/translations/ru.php'); +Locale::setLanguage('si', include __DIR__ . '/config/locale/translations/si.php'); +Locale::setLanguage('sl', include __DIR__ . '/config/locale/translations/sl.php'); +Locale::setLanguage('sq', include __DIR__ . '/config/locale/translations/sq.php'); +Locale::setLanguage('sv', include __DIR__ . '/config/locale/translations/sv.php'); +Locale::setLanguage('ta', include __DIR__ . '/config/locale/translations/ta.php'); +Locale::setLanguage('th', include __DIR__.'/config/locale/translations/th.php'); +Locale::setLanguage('tr', include __DIR__.'/config/locale/translations/tr.php'); +Locale::setLanguage('ua', include __DIR__.'/config/locale/translations/ua.php'); +Locale::setLanguage('vi', include __DIR__.'/config/locale/translations/vi.php'); +Locale::setLanguage('zh-cn', include __DIR__.'/config/locale/translations/zh-cn.php'); +Locale::setLanguage('zh-tw', include __DIR__.'/config/locale/translations/zh-tw.php'); \stream_context_set_default([ // Set global user agent and http settings 'http' => [ 'method' => 'GET', 'user_agent' => \sprintf(APP_USERAGENT, - Config::getParam('version'), - $request->getServer('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)), + App::getEnv('_APP_VERSION', 'UNKNOWN'), + App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)), 'timeout' => 2, ], ]); -/* - * Auth & Project Scope - */ -$consoleDB = new Database(); -$consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); -$consoleDB->setNamespace('app_console'); // Should be replaced with param if we want to have parent projects +// Runtime Execution -$consoleDB->setMocks(Config::getParam('collections', [])); -Authorization::disable(); +App::setResource('register', function() use ($register) { + return $register; +}); -$project = $consoleDB->getDocument($request->getParam('project', $request->getHeader('X-Appwrite-Project', ''))); +App::setResource('layout', function($locale) { + $layout = new View(__DIR__.'/views/layouts/default.phtml'); + $layout->setParam('locale', $locale); -Authorization::enable(); + return $layout; +}, ['locale']); -$console = $consoleDB->getDocument('console'); +App::setResource('locale', function() { + return new Locale('en'); +}); -$mode = $request->getParam('mode', $request->getHeader('X-Appwrite-Mode', 'default')); +// Queues +App::setResource('webhooks', function($register) { + return $register->get('queue-webhooks'); +}, ['register']); -Auth::setCookieName('a_session_'.$project->getId()); +App::setResource('audits', function($register) { + return $register->get('queue-audits'); +}, ['register']); -if (APP_MODE_ADMIN === $mode) { - Auth::setCookieName('a_session_'.$console->getId()); -} +App::setResource('usage', function($register) { + return $register->get('queue-usage'); +}, ['register']); -$session = Auth::decodeSession( - $request->getCookie(Auth::$cookieName, // Get sessions - $request->getCookie(Auth::$cookieName.'_legacy', // Get fallback session from old clients (no SameSite support) - $request->getHeader('X-Appwrite-Key', '')))); // Get API Key +App::setResource('mails', function($register) { + return $register->get('queue-mails'); +}, ['register']); -// Get fallback session from clients who block 3rd-party cookies -$response->addHeader('X-Debug-Fallback', 'false'); +App::setResource('deletes', function($register) { + return $register->get('queue-deletes'); +}, ['register']); -if(empty($session['id']) && empty($session['secret'])) { - $response->addHeader('X-Debug-Fallback', 'true'); - $fallback = $request->getHeader('X-Fallback-Cookies', ''); - $fallback = \json_decode($fallback, true); - $session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : '')); -} +App::setResource('functions', function($register) { + return $register->get('queue-functions'); +}, ['register']); -Auth::$unique = $session['id']; -Auth::$secret = $session['secret']; +// Test Mock +App::setResource('clients', function($console, $project) { + /** + * Get All verified client URLs for both console and current projects + * + Filter for duplicated entries + */ + $clientsConsole = \array_map(function ($node) { + return $node['hostname']; + }, \array_filter($console->getAttribute('platforms', []), function ($node) { + if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { + return true; + } -$projectDB = new Database(); -$projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); -$projectDB->setNamespace('app_'.$project->getId()); -$projectDB->setMocks(Config::getParam('collections', [])); + return false; + })); -if (APP_MODE_ADMIN !== $mode) { - $user = $projectDB->getDocument(Auth::$unique); -} -else { - $user = $consoleDB->getDocument(Auth::$unique); + $clients = \array_unique(\array_merge($clientsConsole, \array_map(function ($node) { + return $node['hostname']; + }, \array_filter($project->getAttribute('platforms', []), function ($node) { + if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { + return true; + } - $user - ->setAttribute('$id', 'admin-'.$user->getAttribute('$id')) - ; -} + return false; + })))); -if (empty($user->getId()) // Check a document has been found in the DB - || Database::SYSTEM_COLLECTION_USERS !== $user->getCollection() // Validate returned document is really a user document - || !Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret)) { // Validate user has valid login token - $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); -} + return $clients; +}, ['console', 'project']); -if (APP_MODE_ADMIN === $mode) { - if (!empty($user->search('teamId', $project->getAttribute('teamId'), $user->getAttribute('memberships')))) { - Authorization::disable(); - } else { +App::setResource('user', function($mode, $project, $console, $request, $response, $projectDB, $consoleDB) { + /** @var Utopia\Request $request */ + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $consoleDB */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var bool $mode */ + + Authorization::setDefaultStatus(true); + + Auth::setCookieName('a_session_'.$project->getId()); + + if (APP_MODE_ADMIN === $mode) { + Auth::setCookieName('a_session_'.$console->getId()); + } + + $session = Auth::decodeSession( + $request->getCookie(Auth::$cookieName, // Get sessions + $request->getCookie(Auth::$cookieName.'_legacy', // Get fallback session from old clients (no SameSite support) + $request->getHeader('x-appwrite-key', '')))); // Get API Key + + // Get fallback session from clients who block 3rd-party cookies + $response->addHeader('X-Debug-Fallback', 'false'); + + if(empty($session['id']) && empty($session['secret'])) { + $response->addHeader('X-Debug-Fallback', 'true'); + $fallback = $request->getHeader('x-fallback-cookies', ''); + $fallback = \json_decode($fallback, true); + $session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : '')); + } + + Auth::$unique = $session['id']; + Auth::$secret = $session['secret']; + + if (APP_MODE_ADMIN !== $mode) { + $user = $projectDB->getDocument(Auth::$unique); + } + else { + $user = $consoleDB->getDocument(Auth::$unique); + + $user + ->setAttribute('$id', 'admin-'.$user->getAttribute('$id')) + ; + } + + if (empty($user->getId()) // Check a document has been found in the DB + || Database::SYSTEM_COLLECTION_USERS !== $user->getCollection() // Validate returned document is really a user document + || !Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret)) { // Validate user has valid login token $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); } -} -// Set project mail -$register->get('smtp') - ->setFrom( - $request->getServer('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), - ($project->getId() === 'console') - ? \urldecode($request->getServer('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')) - : \sprintf(Locale::getText('account.emails.team'), $project->getAttribute('name') - ) - ); + if (APP_MODE_ADMIN === $mode) { + if (!empty($user->search('teamId', $project->getAttribute('teamId'), $user->getAttribute('memberships')))) { + Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. + } else { + $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); + } + } + + return $user; +}, ['mode', 'project', 'console', 'request', 'response', 'projectDB', 'consoleDB']); + +App::setResource('project', function($consoleDB, $request) { + /** @var Appwrite\Swoole\Request $request */ + /** @var Appwrite\Database\Database $consoleDB */ + + Authorization::disable(); + + $project = $consoleDB->getDocument($request->getParam('project', + $request->getHeader('x-appwrite-project', ''))); + + Authorization::reset(); + + return $project; +}, ['consoleDB', 'request']); + +App::setResource('console', function($consoleDB) { + return $consoleDB->getDocument('console'); +}, ['consoleDB']); + +App::setResource('consoleDB', function($register) { + $consoleDB = new Database(); + $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $consoleDB->setNamespace('app_console'); // Should be replaced with param if we want to have parent projects + + $consoleDB->setMocks(Config::getParam('collections', [])); + + return $consoleDB; +}, ['register']); + +App::setResource('projectDB', function($register, $project) { + $projectDB = new Database(); + $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $projectDB->setNamespace('app_'.$project->getId()); + $projectDB->setMocks(Config::getParam('collections', [])); + + return $projectDB; +}, ['register', 'project']); + +App::setResource('mode', function($request) { + /** @var Utopia\Request $request */ + return $request->getParam('mode', $request->getHeader('x-appwrite-mode', 'default')); +}, ['request']); + +App::setResource('geodb', function($request) { + return new Reader(__DIR__.'/db/DBIP/dbip-country-lite-2020-01.mmdb'); +}, ['request']); diff --git a/app/preload.php b/app/preload.php new file mode 100644 index 0000000000..63d08c3e13 --- /dev/null +++ b/app/preload.php @@ -0,0 +1,35 @@ +paths(realpath(__DIR__ . '/../app/config')) + ->paths(realpath(__DIR__ . '/../app/controllers')) + ->paths(realpath(__DIR__ . '/../src')) + ->ignore(realpath(__DIR__ . '/../vendor/twig/twig')) + ->ignore(realpath(__DIR__ . '/../vendor/guzzlehttp/guzzle')) + ->ignore(realpath(__DIR__ . '/../vendor/geoip2')) + ->ignore(realpath(__DIR__ . '/../vendor/maxmind')) + ->ignore(realpath(__DIR__ . '/../vendor/maxmind-db')) + ->ignore(realpath(__DIR__ . '/../vendor/psr/log')) + ->ignore(realpath(__DIR__ . '/../vendor/piwik')) + ->ignore(realpath(__DIR__ . '/../vendor/symfony')) + ->load(); \ No newline at end of file diff --git a/app/sdks/client-web/src/sdk.js b/app/sdks/client-web/src/sdk.js index 5c98ea89b3..90c9b8021b 100644 --- a/app/sdks/client-web/src/sdk.js +++ b/app/sdks/client-web/src/sdk.js @@ -787,16 +787,17 @@ * Use this endpoint to send a verification message to your user email address * to confirm they are the valid owners of that address. Both the **userId** * and **secret** arguments will be passed as query parameters to the URL you - * have provider to be attached to the verification email. The provided URL - * should redirect the user back for your app and allow you to complete the + * have provided to be attached to the verification email. The provided URL + * should redirect the user back to your app and allow you to complete the * verification process by verifying both the **userId** and **secret** * parameters. Learn more about how to [complete the verification * process](/docs/client/account#updateAccountVerification). * * Please note that in order to avoid a [Redirect - * Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) + * Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), * the only valid redirect URLs are the ones from domains you have set when * adding your platforms in the console interface. + * * * @param {string} url * @throws {Error} @@ -1217,11 +1218,11 @@ * @param {string} text * @param {number} size * @param {number} margin - * @param {number} download + * @param {boolean} download * @throws {Error} * @return {string} */ - getQR: function(text, size = 400, margin = 1, download = 0) { + getQR: function(text, size = 400, margin = 1, download = false) { if(text === undefined) { throw new Error('Missing required parameter: "text"'); } @@ -1281,18 +1282,16 @@ * * @param {string} collectionId * @param {string[]} filters - * @param {number} offset * @param {number} limit + * @param {number} offset * @param {string} orderField * @param {string} orderType * @param {string} orderCast * @param {string} search - * @param {number} first - * @param {number} last * @throws {Error} * @return {Promise} */ - listDocuments: function(collectionId, filters = [], offset = 0, limit = 50, orderField = '$id', orderType = 'ASC', orderCast = 'string', search = '', first = 0, last = 0) { + listDocuments: function(collectionId, filters = [], limit = 25, offset = 0, orderField = '$id', orderType = 'ASC', orderCast = 'string', search = '') { if(collectionId === undefined) { throw new Error('Missing required parameter: "collectionId"'); } @@ -1305,14 +1304,14 @@ payload['filters'] = filters; } - if(offset) { - payload['offset'] = offset; - } - if(limit) { payload['limit'] = limit; } + if(offset) { + payload['offset'] = offset; + } + if(orderField) { payload['orderField'] = orderField; } @@ -1329,14 +1328,6 @@ payload['search'] = search; } - if(first) { - payload['first'] = first; - } - - if(last) { - payload['last'] = last; - } - return http .get(path, { 'content-type': 'application/json', diff --git a/app/sdks/client-web/src/sdk.min.js b/app/sdks/client-web/src/sdk.min.js index 7b470dc380..2bc06cc935 100644 --- a/app/sdks/client-web/src/sdk.min.js +++ b/app/sdks/client-web/src/sdk.min.js @@ -86,22 +86,20 @@ if(height){payload.height=height} if(color){payload.color=color} if(background){payload.background=background} payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;index; + listDocuments(collectionId: string, filters: string[], limit: number, offset: number, orderField: string, orderType: string, orderCast: string, search: string): Promise; /** * Create Document diff --git a/app/sdks/console-web/docs/examples/bar/delete.md b/app/sdks/console-web/docs/examples/bar/delete.md new file mode 100644 index 0000000000..c60d7a75f0 --- /dev/null +++ b/app/sdks/console-web/docs/examples/bar/delete.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.bar.delete('[]', null, []); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/bar/get.md b/app/sdks/console-web/docs/examples/bar/get.md new file mode 100644 index 0000000000..a690ffc039 --- /dev/null +++ b/app/sdks/console-web/docs/examples/bar/get.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.bar.get('[]', null, []); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/bar/patch.md b/app/sdks/console-web/docs/examples/bar/patch.md new file mode 100644 index 0000000000..46ac2d7438 --- /dev/null +++ b/app/sdks/console-web/docs/examples/bar/patch.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.bar.patch('[]', null, []); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/bar/post.md b/app/sdks/console-web/docs/examples/bar/post.md new file mode 100644 index 0000000000..4b21090c3f --- /dev/null +++ b/app/sdks/console-web/docs/examples/bar/post.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.bar.post('[]', null, []); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/bar/put.md b/app/sdks/console-web/docs/examples/bar/put.md new file mode 100644 index 0000000000..8eac3ebad8 --- /dev/null +++ b/app/sdks/console-web/docs/examples/bar/put.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.bar.put('[]', null, []); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/foo/delete.md b/app/sdks/console-web/docs/examples/foo/delete.md new file mode 100644 index 0000000000..715289a5b8 --- /dev/null +++ b/app/sdks/console-web/docs/examples/foo/delete.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.foo.delete('[]', null, []); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/foo/get.md b/app/sdks/console-web/docs/examples/foo/get.md new file mode 100644 index 0000000000..cb3ee31c9d --- /dev/null +++ b/app/sdks/console-web/docs/examples/foo/get.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.foo.get('[]', null, []); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/foo/patch.md b/app/sdks/console-web/docs/examples/foo/patch.md new file mode 100644 index 0000000000..e39222e71d --- /dev/null +++ b/app/sdks/console-web/docs/examples/foo/patch.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.foo.patch('[]', null, []); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/foo/post.md b/app/sdks/console-web/docs/examples/foo/post.md new file mode 100644 index 0000000000..4c236776f6 --- /dev/null +++ b/app/sdks/console-web/docs/examples/foo/post.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.foo.post('[]', null, []); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/foo/put.md b/app/sdks/console-web/docs/examples/foo/put.md new file mode 100644 index 0000000000..d13c4c44ff --- /dev/null +++ b/app/sdks/console-web/docs/examples/foo/put.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.foo.put('[]', null, []); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/create-execution.md b/app/sdks/console-web/docs/examples/functions/create-execution.md new file mode 100644 index 0000000000..e70d7f931a --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/create-execution.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.createExecution('[FUNCTION_ID]'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/create-tag.md b/app/sdks/console-web/docs/examples/functions/create-tag.md new file mode 100644 index 0000000000..743517ff1a --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/create-tag.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.createTag('[FUNCTION_ID]', '[COMMAND]', document.getElementById('uploader').files[0]); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/create.md b/app/sdks/console-web/docs/examples/functions/create.md new file mode 100644 index 0000000000..6fb506816f --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/create.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.create('[NAME]', 'node-14'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/delete-tag.md b/app/sdks/console-web/docs/examples/functions/delete-tag.md new file mode 100644 index 0000000000..9c1b5cdda3 --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/delete-tag.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.deleteTag('[FUNCTION_ID]', '[TAG_ID]'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/delete.md b/app/sdks/console-web/docs/examples/functions/delete.md new file mode 100644 index 0000000000..f76f809c2b --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/delete.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.delete('[FUNCTION_ID]'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/get-execution.md b/app/sdks/console-web/docs/examples/functions/get-execution.md new file mode 100644 index 0000000000..8287c4709e --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/get-execution.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.getExecution('[FUNCTION_ID]', '[EXECUTION_ID]'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/get-tag.md b/app/sdks/console-web/docs/examples/functions/get-tag.md new file mode 100644 index 0000000000..c6021ccdac --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/get-tag.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.getTag('[FUNCTION_ID]', '[TAG_ID]'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/get.md b/app/sdks/console-web/docs/examples/functions/get.md new file mode 100644 index 0000000000..dc068ffa09 --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/get.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.get('[FUNCTION_ID]'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/list-executions.md b/app/sdks/console-web/docs/examples/functions/list-executions.md new file mode 100644 index 0000000000..9367a9eb52 --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/list-executions.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.listExecutions('[FUNCTION_ID]'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/list-tags.md b/app/sdks/console-web/docs/examples/functions/list-tags.md new file mode 100644 index 0000000000..4542bae44b --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/list-tags.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.listTags('[FUNCTION_ID]'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/list.md b/app/sdks/console-web/docs/examples/functions/list.md new file mode 100644 index 0000000000..e2ce2511e0 --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/list.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.list(); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/update-active.md b/app/sdks/console-web/docs/examples/functions/update-active.md new file mode 100644 index 0000000000..eed2be885c --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/update-active.md @@ -0,0 +1,14 @@ +let sdk = new Appwrite(); + +sdk + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.updateActive('[FUNCTION_ID]', '[ACTIVE]'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/update-tag.md b/app/sdks/console-web/docs/examples/functions/update-tag.md new file mode 100644 index 0000000000..7ea2ba1808 --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/update-tag.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.updateTag('[FUNCTION_ID]', '[TAG]'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/functions/update.md b/app/sdks/console-web/docs/examples/functions/update.md new file mode 100644 index 0000000000..c6a34cff5c --- /dev/null +++ b/app/sdks/console-web/docs/examples/functions/update.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.functions.update('[FUNCTION_ID]', '[NAME]'); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/general/empty.md b/app/sdks/console-web/docs/examples/general/empty.md new file mode 100644 index 0000000000..50fd0e8a5c --- /dev/null +++ b/app/sdks/console-web/docs/examples/general/empty.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.general.empty(); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/general/get-cookie.md b/app/sdks/console-web/docs/examples/general/get-cookie.md new file mode 100644 index 0000000000..5785a0323f --- /dev/null +++ b/app/sdks/console-web/docs/examples/general/get-cookie.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.general.getCookie(); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/general/redirect.md b/app/sdks/console-web/docs/examples/general/redirect.md new file mode 100644 index 0000000000..769533f6f6 --- /dev/null +++ b/app/sdks/console-web/docs/examples/general/redirect.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.general.redirect(); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/general/redirected.md b/app/sdks/console-web/docs/examples/general/redirected.md new file mode 100644 index 0000000000..712e614ac9 --- /dev/null +++ b/app/sdks/console-web/docs/examples/general/redirected.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.general.redirected(); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/general/set-cookie.md b/app/sdks/console-web/docs/examples/general/set-cookie.md new file mode 100644 index 0000000000..ee0f85cc1f --- /dev/null +++ b/app/sdks/console-web/docs/examples/general/set-cookie.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.general.setCookie(); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/general/upload.md b/app/sdks/console-web/docs/examples/general/upload.md new file mode 100644 index 0000000000..bbc2bee92a --- /dev/null +++ b/app/sdks/console-web/docs/examples/general/upload.md @@ -0,0 +1,15 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = sdk.general.upload('[]', null, [], document.getElementById('uploader').files[0]); + +promise.then(function (response) { + console.log(response); // Success +}, function (error) { + console.log(error); // Failure +}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/projects/create-task.md b/app/sdks/console-web/docs/examples/projects/create-task.md index 5698d000e8..9b0f3c1e03 100644 --- a/app/sdks/console-web/docs/examples/projects/create-task.md +++ b/app/sdks/console-web/docs/examples/projects/create-task.md @@ -6,7 +6,7 @@ sdk .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key ; -let promise = sdk.projects.createTask('[PROJECT_ID]', '[NAME]', 'play', '', 0, 'GET', 'https://example.com'); +let promise = sdk.projects.createTask('[PROJECT_ID]', '[NAME]', 'play', '', false, 'GET', 'https://example.com'); promise.then(function (response) { console.log(response); // Success diff --git a/app/sdks/console-web/docs/examples/projects/create-webhook.md b/app/sdks/console-web/docs/examples/projects/create-webhook.md index 728786f9fb..be722f3dbe 100644 --- a/app/sdks/console-web/docs/examples/projects/create-webhook.md +++ b/app/sdks/console-web/docs/examples/projects/create-webhook.md @@ -6,7 +6,7 @@ sdk .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key ; -let promise = sdk.projects.createWebhook('[PROJECT_ID]', '[NAME]', [], '[URL]', 0); +let promise = sdk.projects.createWebhook('[PROJECT_ID]', '[NAME]', [], 'https://example.com', false); promise.then(function (response) { console.log(response); // Success diff --git a/app/sdks/console-web/docs/examples/projects/update-task.md b/app/sdks/console-web/docs/examples/projects/update-task.md index dc95cfda9f..38744a36bd 100644 --- a/app/sdks/console-web/docs/examples/projects/update-task.md +++ b/app/sdks/console-web/docs/examples/projects/update-task.md @@ -6,7 +6,7 @@ sdk .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key ; -let promise = sdk.projects.updateTask('[PROJECT_ID]', '[TASK_ID]', '[NAME]', 'play', '', 0, 'GET', 'https://example.com'); +let promise = sdk.projects.updateTask('[PROJECT_ID]', '[TASK_ID]', '[NAME]', 'play', '', false, 'GET', 'https://example.com'); promise.then(function (response) { console.log(response); // Success diff --git a/app/sdks/console-web/docs/examples/projects/update-webhook.md b/app/sdks/console-web/docs/examples/projects/update-webhook.md index b1ead71e92..cdef2ec479 100644 --- a/app/sdks/console-web/docs/examples/projects/update-webhook.md +++ b/app/sdks/console-web/docs/examples/projects/update-webhook.md @@ -6,7 +6,7 @@ sdk .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key ; -let promise = sdk.projects.updateWebhook('[PROJECT_ID]', '[WEBHOOK_ID]', '[NAME]', [], '[URL]', 0); +let promise = sdk.projects.updateWebhook('[PROJECT_ID]', '[WEBHOOK_ID]', '[NAME]', [], 'https://example.com', false); promise.then(function (response) { console.log(response); // Success diff --git a/app/sdks/console-web/src/sdk.js b/app/sdks/console-web/src/sdk.js index 27eb23b742..b246db4cc5 100644 --- a/app/sdks/console-web/src/sdk.js +++ b/app/sdks/console-web/src/sdk.js @@ -825,16 +825,17 @@ * Use this endpoint to send a verification message to your user email address * to confirm they are the valid owners of that address. Both the **userId** * and **secret** arguments will be passed as query parameters to the URL you - * have provider to be attached to the verification email. The provided URL - * should redirect the user back for your app and allow you to complete the + * have provided to be attached to the verification email. The provided URL + * should redirect the user back to your app and allow you to complete the * verification process by verifying both the **userId** and **secret** * parameters. Learn more about how to [complete the verification * process](/docs/client/account#updateAccountVerification). * * Please note that in order to avoid a [Redirect - * Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) + * Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), * the only valid redirect URLs are the ones from domains you have set when * adding your platforms in the console interface. + * * * @param {string} url * @throws {Error} @@ -1267,11 +1268,11 @@ * @param {string} text * @param {number} size * @param {number} margin - * @param {number} download + * @param {boolean} download * @throws {Error} * @return {string} */ - getQR: function(text, size = 400, margin = 1, download = 0) { + getQR: function(text, size = 400, margin = 1, download = false) { if(text === undefined) { throw new Error('Missing required parameter: "text"'); } @@ -1536,18 +1537,16 @@ * * @param {string} collectionId * @param {string[]} filters - * @param {number} offset * @param {number} limit + * @param {number} offset * @param {string} orderField * @param {string} orderType * @param {string} orderCast * @param {string} search - * @param {number} first - * @param {number} last * @throws {Error} * @return {Promise} */ - listDocuments: function(collectionId, filters = [], offset = 0, limit = 50, orderField = '$id', orderType = 'ASC', orderCast = 'string', search = '', first = 0, last = 0) { + listDocuments: function(collectionId, filters = [], limit = 25, offset = 0, orderField = '$id', orderType = 'ASC', orderCast = 'string', search = '') { if(collectionId === undefined) { throw new Error('Missing required parameter: "collectionId"'); } @@ -1560,14 +1559,14 @@ payload['filters'] = filters; } - if(offset) { - payload['offset'] = offset; - } - if(limit) { payload['limit'] = limit; } + if(offset) { + payload['offset'] = offset; + } + if(orderField) { payload['orderField'] = orderField; } @@ -1584,14 +1583,6 @@ payload['search'] = search; } - if(first) { - payload['first'] = first; - } - - if(last) { - payload['last'] = last; - } - return http .get(path, { 'content-type': 'application/json', @@ -1781,22 +1772,119 @@ .delete(path, { 'content-type': 'application/json', }, payload); - }, + } + }; + + let functions = { /** - * Get Collection Logs + * List Functions * * - * @param {string} collectionId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType * @throws {Error} * @return {Promise} */ - getCollectionLogs: function(collectionId) { - if(collectionId === undefined) { - throw new Error('Missing required parameter: "collectionId"'); + list: function(search = '', limit = 25, offset = 0, orderType = 'ASC') { + let path = '/functions'; + + let payload = {}; + + if(search) { + payload['search'] = search; + } + + if(limit) { + payload['limit'] = limit; + } + + if(offset) { + payload['offset'] = offset; + } + + if(orderType) { + payload['orderType'] = orderType; + } + + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Create Function + * + * + * @param {string} name + * @param {string} env + * @param {object} vars + * @param {string[]} events + * @param {string} schedule + * @param {number} timeout + * @throws {Error} + * @return {Promise} + */ + create: function(name, env, vars = [], events = [], schedule = '', timeout = 15) { + if(name === undefined) { + throw new Error('Missing required parameter: "name"'); } - let path = '/database/collections/{collectionId}/logs'.replace(new RegExp('{collectionId}', 'g'), collectionId); + if(env === undefined) { + throw new Error('Missing required parameter: "env"'); + } + + let path = '/functions'; + + let payload = {}; + + if(name) { + payload['name'] = name; + } + + if(env) { + payload['env'] = env; + } + + if(vars) { + payload['vars'] = vars; + } + + if(events) { + payload['events'] = events; + } + + if(schedule) { + payload['schedule'] = schedule; + } + + if(timeout) { + payload['timeout'] = timeout; + } + + return http + .post(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Get Function + * + * + * @param {string} functionId + * @throws {Error} + * @return {Promise} + */ + get: function(functionId) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); let payload = {}; @@ -1804,6 +1892,352 @@ .get(path, { 'content-type': 'application/json', }, payload); + }, + + /** + * Update Function + * + * + * @param {string} functionId + * @param {string} name + * @param {object} vars + * @param {string[]} events + * @param {string} schedule + * @param {number} timeout + * @throws {Error} + * @return {Promise} + */ + update: function(functionId, name, vars = [], events = [], schedule = '', timeout = 15) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(name === undefined) { + throw new Error('Missing required parameter: "name"'); + } + + let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(name) { + payload['name'] = name; + } + + if(vars) { + payload['vars'] = vars; + } + + if(events) { + payload['events'] = events; + } + + if(schedule) { + payload['schedule'] = schedule; + } + + if(timeout) { + payload['timeout'] = timeout; + } + + return http + .put(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Delete Function + * + * + * @param {string} functionId + * @throws {Error} + * @return {Promise} + */ + delete: function(functionId) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + return http + .delete(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * List Executions + * + * + * @param {string} functionId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {Error} + * @return {Promise} + */ + listExecutions: function(functionId, search = '', limit = 25, offset = 0, orderType = 'ASC') { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + let path = '/functions/{functionId}/executions'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(search) { + payload['search'] = search; + } + + if(limit) { + payload['limit'] = limit; + } + + if(offset) { + payload['offset'] = offset; + } + + if(orderType) { + payload['orderType'] = orderType; + } + + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Create Execution + * + * + * @param {string} functionId + * @param {number} async + * @throws {Error} + * @return {Promise} + */ + createExecution: function(functionId, async = 1) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + let path = '/functions/{functionId}/executions'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(async) { + payload['async'] = async; + } + + return http + .post(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Get Execution + * + * + * @param {string} functionId + * @param {string} executionId + * @throws {Error} + * @return {Promise} + */ + getExecution: function(functionId, executionId) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(executionId === undefined) { + throw new Error('Missing required parameter: "executionId"'); + } + + let path = '/functions/{functionId}/executions/{executionId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{executionId}', 'g'), executionId); + + let payload = {}; + + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Update Function Tag + * + * + * @param {string} functionId + * @param {string} tag + * @throws {Error} + * @return {Promise} + */ + updateTag: function(functionId, tag) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(tag === undefined) { + throw new Error('Missing required parameter: "tag"'); + } + + let path = '/functions/{functionId}/tag'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(tag) { + payload['tag'] = tag; + } + + return http + .patch(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * List Tags + * + * + * @param {string} functionId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {Error} + * @return {Promise} + */ + listTags: function(functionId, search = '', limit = 25, offset = 0, orderType = 'ASC') { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + let path = '/functions/{functionId}/tags'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(search) { + payload['search'] = search; + } + + if(limit) { + payload['limit'] = limit; + } + + if(offset) { + payload['offset'] = offset; + } + + if(orderType) { + payload['orderType'] = orderType; + } + + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Create Tag + * + * + * @param {string} functionId + * @param {string} command + * @param {File} code + * @throws {Error} + * @return {Promise} + */ + createTag: function(functionId, command, code) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(command === undefined) { + throw new Error('Missing required parameter: "command"'); + } + + if(code === undefined) { + throw new Error('Missing required parameter: "code"'); + } + + let path = '/functions/{functionId}/tags'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(command) { + payload['command'] = command; + } + + if(code) { + payload['code'] = code; + } + + return http + .post(path, { + 'content-type': 'multipart/form-data', + }, payload); + }, + + /** + * Get Tag + * + * + * @param {string} functionId + * @param {string} tagId + * @throws {Error} + * @return {Promise} + */ + getTag: function(functionId, tagId) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(tagId === undefined) { + throw new Error('Missing required parameter: "tagId"'); + } + + let path = '/functions/{functionId}/tags/{tagId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{tagId}', 'g'), tagId); + + let payload = {}; + + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Delete Tag + * + * + * @param {string} functionId + * @param {string} tagId + * @throws {Error} + * @return {Promise} + */ + deleteTag: function(functionId, tagId) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(tagId === undefined) { + throw new Error('Missing required parameter: "tagId"'); + } + + let path = '/functions/{functionId}/tags/{tagId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{tagId}', 'g'), tagId); + + let payload = {}; + + return http + .delete(path, { + 'content-type': 'application/json', + }, payload); } }; @@ -2204,14 +2638,34 @@ * List Projects * * + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType * @throws {Error} * @return {Promise} */ - list: function() { + list: function(search = '', limit = 25, offset = 0, orderType = 'ASC') { let path = '/projects'; let payload = {}; + if(search) { + payload['search'] = search; + } + + if(limit) { + payload['limit'] = limit; + } + + if(offset) { + payload['offset'] = offset; + } + + if(orderType) { + payload['orderType'] = orderType; + } + return http .get(path, { 'content-type': 'application/json', @@ -2996,7 +3450,7 @@ * @param {string} name * @param {string} status * @param {string} schedule - * @param {number} security + * @param {boolean} security * @param {string} httpMethod * @param {string} httpUrl * @param {string[]} httpHeaders @@ -3117,7 +3571,7 @@ * @param {string} name * @param {string} status * @param {string} schedule - * @param {number} security + * @param {boolean} security * @param {string} httpMethod * @param {string} httpUrl * @param {string[]} httpHeaders @@ -3292,7 +3746,7 @@ * @param {string} name * @param {string[]} events * @param {string} url - * @param {number} security + * @param {boolean} security * @param {string} httpUser * @param {string} httpPass * @throws {Error} @@ -3390,7 +3844,7 @@ * @param {string} name * @param {string[]} events * @param {string} url - * @param {number} security + * @param {boolean} security * @param {string} httpUser * @param {string} httpPass * @throws {Error} @@ -4498,6 +4952,7 @@ account: account, avatars: avatars, database: database, + functions: functions, health: health, locale: locale, projects: projects, diff --git a/app/sdks/console-web/src/sdk.min.js b/app/sdks/console-web/src/sdk.min.js index f6a2781f21..6501248b84 100644 --- a/app/sdks/console-web/src/sdk.min.js +++ b/app/sdks/console-web/src/sdk.min.js @@ -86,7 +86,7 @@ if(height){payload.height=height} if(color){payload.color=color} if(background){payload.background=background} payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;index; + listDocuments(collectionId: string, filters: string[], limit: number, offset: number, orderField: string, orderType: string, orderCast: string, search: string): Promise; /** * Create Document @@ -605,15 +605,167 @@ declare namespace Appwrite { */ deleteDocument(collectionId: string, documentId: string): Promise; + } + + export interface Functions { + /** - * Get Collection Logs + * List Functions * * - * @param {string} collectionId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType * @throws {Error} * @return {Promise} */ - getCollectionLogs(collectionId: string): Promise; + list(search: string, limit: number, offset: number, orderType: string): Promise; + + /** + * Create Function + * + * + * @param {string} name + * @param {string} env + * @param {object} vars + * @param {string[]} events + * @param {string} schedule + * @param {number} timeout + * @throws {Error} + * @return {Promise} + */ + create(name: string, env: string, vars: object, events: string[], schedule: string, timeout: number): Promise; + + /** + * Get Function + * + * + * @param {string} functionId + * @throws {Error} + * @return {Promise} + */ + get(functionId: string): Promise; + + /** + * Update Function + * + * + * @param {string} functionId + * @param {string} name + * @param {object} vars + * @param {string[]} events + * @param {string} schedule + * @param {number} timeout + * @throws {Error} + * @return {Promise} + */ + update(functionId: string, name: string, vars: object, events: string[], schedule: string, timeout: number): Promise; + + /** + * Delete Function + * + * + * @param {string} functionId + * @throws {Error} + * @return {Promise} + */ + delete(functionId: string): Promise; + + /** + * List Executions + * + * + * @param {string} functionId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {Error} + * @return {Promise} + */ + listExecutions(functionId: string, search: string, limit: number, offset: number, orderType: string): Promise; + + /** + * Create Execution + * + * + * @param {string} functionId + * @param {number} async + * @throws {Error} + * @return {Promise} + */ + createExecution(functionId: string, async: number): Promise; + + /** + * Get Execution + * + * + * @param {string} functionId + * @param {string} executionId + * @throws {Error} + * @return {Promise} + */ + getExecution(functionId: string, executionId: string): Promise; + + /** + * Update Function Tag + * + * + * @param {string} functionId + * @param {string} tag + * @throws {Error} + * @return {Promise} + */ + updateTag(functionId: string, tag: string): Promise; + + /** + * List Tags + * + * + * @param {string} functionId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {Error} + * @return {Promise} + */ + listTags(functionId: string, search: string, limit: number, offset: number, orderType: string): Promise; + + /** + * Create Tag + * + * + * @param {string} functionId + * @param {string} command + * @param {File} code + * @throws {Error} + * @return {Promise} + */ + createTag(functionId: string, command: string, code: File): Promise; + + /** + * Get Tag + * + * + * @param {string} functionId + * @param {string} tagId + * @throws {Error} + * @return {Promise} + */ + getTag(functionId: string, tagId: string): Promise; + + /** + * Delete Tag + * + * + * @param {string} functionId + * @param {string} tagId + * @throws {Error} + * @return {Promise} + */ + deleteTag(functionId: string, tagId: string): Promise; } @@ -845,10 +997,14 @@ declare namespace Appwrite { * List Projects * * + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType * @throws {Error} * @return {Promise} */ - list(): Promise; + list(search: string, limit: number, offset: number, orderType: string): Promise; /** * Create Project @@ -1115,7 +1271,7 @@ declare namespace Appwrite { * @param {string} name * @param {string} status * @param {string} schedule - * @param {number} security + * @param {boolean} security * @param {string} httpMethod * @param {string} httpUrl * @param {string[]} httpHeaders @@ -1124,7 +1280,7 @@ declare namespace Appwrite { * @throws {Error} * @return {Promise} */ - createTask(projectId: string, name: string, status: string, schedule: string, security: number, httpMethod: string, httpUrl: string, httpHeaders: string[], httpUser: string, httpPass: string): Promise; + createTask(projectId: string, name: string, status: string, schedule: string, security: boolean, httpMethod: string, httpUrl: string, httpHeaders: string[], httpUser: string, httpPass: string): Promise; /** * Get Task @@ -1146,7 +1302,7 @@ declare namespace Appwrite { * @param {string} name * @param {string} status * @param {string} schedule - * @param {number} security + * @param {boolean} security * @param {string} httpMethod * @param {string} httpUrl * @param {string[]} httpHeaders @@ -1155,7 +1311,7 @@ declare namespace Appwrite { * @throws {Error} * @return {Promise} */ - updateTask(projectId: string, taskId: string, name: string, status: string, schedule: string, security: number, httpMethod: string, httpUrl: string, httpHeaders: string[], httpUser: string, httpPass: string): Promise; + updateTask(projectId: string, taskId: string, name: string, status: string, schedule: string, security: boolean, httpMethod: string, httpUrl: string, httpHeaders: string[], httpUser: string, httpPass: string): Promise; /** * Delete Task @@ -1197,13 +1353,13 @@ declare namespace Appwrite { * @param {string} name * @param {string[]} events * @param {string} url - * @param {number} security + * @param {boolean} security * @param {string} httpUser * @param {string} httpPass * @throws {Error} * @return {Promise} */ - createWebhook(projectId: string, name: string, events: string[], url: string, security: number, httpUser: string, httpPass: string): Promise; + createWebhook(projectId: string, name: string, events: string[], url: string, security: boolean, httpUser: string, httpPass: string): Promise; /** * Get Webhook @@ -1225,13 +1381,13 @@ declare namespace Appwrite { * @param {string} name * @param {string[]} events * @param {string} url - * @param {number} security + * @param {boolean} security * @param {string} httpUser * @param {string} httpPass * @throws {Error} * @return {Promise} */ - updateWebhook(projectId: string, webhookId: string, name: string, events: string[], url: string, security: number, httpUser: string, httpPass: string): Promise; + updateWebhook(projectId: string, webhookId: string, name: string, events: string[], url: string, security: boolean, httpUser: string, httpPass: string): Promise; /** * Delete Webhook diff --git a/app/sdks/server-go/docs/examples/functions/create-execution.md b/app/sdks/server-go/docs/examples/functions/create-execution.md new file mode 100644 index 0000000000..ea060d6f7f --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/create-execution.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.CreateExecution("[FUNCTION_ID]", 0) + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/create-tag.md b/app/sdks/server-go/docs/examples/functions/create-tag.md new file mode 100644 index 0000000000..84d5e80787 --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/create-tag.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.CreateTag("[FUNCTION_ID]", "node-14", "[COMMAND]", "[CODE]") + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/create.md b/app/sdks/server-go/docs/examples/functions/create.md new file mode 100644 index 0000000000..a87a23b944 --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/create.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.Create("[NAME]", , [], "", 0) + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/delete-tag.md b/app/sdks/server-go/docs/examples/functions/delete-tag.md new file mode 100644 index 0000000000..caf36146da --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/delete-tag.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.DeleteTag("[FUNCTION_ID]", "[TAG_ID]") + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/delete.md b/app/sdks/server-go/docs/examples/functions/delete.md new file mode 100644 index 0000000000..e49addb2b1 --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/delete.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.Delete("[FUNCTION_ID]") + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/get-execution.md b/app/sdks/server-go/docs/examples/functions/get-execution.md new file mode 100644 index 0000000000..13a3ad4685 --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/get-execution.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.GetExecution("[FUNCTION_ID]", "[EXECUTION_ID]") + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/get-tag.md b/app/sdks/server-go/docs/examples/functions/get-tag.md new file mode 100644 index 0000000000..640543d94b --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/get-tag.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.GetTag("[FUNCTION_ID]", "[TAG_ID]") + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/get.md b/app/sdks/server-go/docs/examples/functions/get.md new file mode 100644 index 0000000000..2bba5028c0 --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/get.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.Get("[FUNCTION_ID]") + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/list-executions.md b/app/sdks/server-go/docs/examples/functions/list-executions.md new file mode 100644 index 0000000000..5115928c29 --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/list-executions.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.ListExecutions("[FUNCTION_ID]", "[SEARCH]", 0, 0, "ASC") + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/list-tags.md b/app/sdks/server-go/docs/examples/functions/list-tags.md new file mode 100644 index 0000000000..b6e495988e --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/list-tags.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.ListTags("[FUNCTION_ID]", "[SEARCH]", 0, 0, "ASC") + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/list.md b/app/sdks/server-go/docs/examples/functions/list.md new file mode 100644 index 0000000000..0a208e367d --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/list.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.List("[SEARCH]", 0, 0, "ASC") + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/update-active.md b/app/sdks/server-go/docs/examples/functions/update-active.md new file mode 100644 index 0000000000..da3b0f1227 --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/update-active.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.UpdateActive("[FUNCTION_ID]", "[ACTIVE]") + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/update-tag.md b/app/sdks/server-go/docs/examples/functions/update-tag.md new file mode 100644 index 0000000000..299b095736 --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/update-tag.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.UpdateTag("[FUNCTION_ID]", "[TAG]") + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/docs/examples/functions/update.md b/app/sdks/server-go/docs/examples/functions/update.md new file mode 100644 index 0000000000..0f8f9e0190 --- /dev/null +++ b/app/sdks/server-go/docs/examples/functions/update.md @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go" +) + +func main() { + var client := appwrite.Client{} + + client.SetProject("5df5acd0d48c2") // Your project ID + client.SetKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key + + var service := appwrite.Functions{ + client: &client + } + + var response, error := service.Update("[FUNCTION_ID]", "[NAME]", , [], "", 0) + + if error != nil { + panic(error) + } + + fmt.Println(response) +} \ No newline at end of file diff --git a/app/sdks/server-go/functions.go b/app/sdks/server-go/functions.go new file mode 100644 index 0000000000..3f82eb1fb2 --- /dev/null +++ b/app/sdks/server-go/functions.go @@ -0,0 +1,186 @@ +package appwrite + +import ( + "strings" +) + +// Functions service +type Functions struct { + client Client +} + +func NewFunctions(clt Client) Functions { + service := Functions{ + client: clt, + } + + return service +} + +// List +func (srv *Functions) List(Search string, Limit int, Offset int, OrderType string) (map[string]interface{}, error) { + path := "/functions" + + params := map[string]interface{}{ + "search": Search, + "limit": Limit, + "offset": Offset, + "orderType": OrderType, + } + + return srv.client.Call("GET", path, nil, params) +} + +// Create +func (srv *Functions) Create(Name string, Vars object, Events []interface{}, Schedule string, Timeout int) (map[string]interface{}, error) { + path := "/functions" + + params := map[string]interface{}{ + "name": Name, + "vars": Vars, + "events": Events, + "schedule": Schedule, + "timeout": Timeout, + } + + return srv.client.Call("POST", path, nil, params) +} + +// Get +func (srv *Functions) Get(FunctionId string) (map[string]interface{}, error) { + r := strings.NewReplacer("{functionId}", FunctionId) + path := r.Replace("/functions/{functionId}") + + params := map[string]interface{}{ + } + + return srv.client.Call("GET", path, nil, params) +} + +// Update +func (srv *Functions) Update(FunctionId string, Name string, Vars object, Events []interface{}, Schedule string, Timeout int) (map[string]interface{}, error) { + r := strings.NewReplacer("{functionId}", FunctionId) + path := r.Replace("/functions/{functionId}") + + params := map[string]interface{}{ + "name": Name, + "vars": Vars, + "events": Events, + "schedule": Schedule, + "timeout": Timeout, + } + + return srv.client.Call("PUT", path, nil, params) +} + +// Delete +func (srv *Functions) Delete(FunctionId string) (map[string]interface{}, error) { + r := strings.NewReplacer("{functionId}", FunctionId) + path := r.Replace("/functions/{functionId}") + + params := map[string]interface{}{ + } + + return srv.client.Call("DELETE", path, nil, params) +} + +// ListExecutions +func (srv *Functions) ListExecutions(FunctionId string, Search string, Limit int, Offset int, OrderType string) (map[string]interface{}, error) { + r := strings.NewReplacer("{functionId}", FunctionId) + path := r.Replace("/functions/{functionId}/executions") + + params := map[string]interface{}{ + "search": Search, + "limit": Limit, + "offset": Offset, + "orderType": OrderType, + } + + return srv.client.Call("GET", path, nil, params) +} + +// CreateExecution +func (srv *Functions) CreateExecution(FunctionId string, Async int) (map[string]interface{}, error) { + r := strings.NewReplacer("{functionId}", FunctionId) + path := r.Replace("/functions/{functionId}/executions") + + params := map[string]interface{}{ + "async": Async, + } + + return srv.client.Call("POST", path, nil, params) +} + +// GetExecution +func (srv *Functions) GetExecution(FunctionId string, ExecutionId string) (map[string]interface{}, error) { + r := strings.NewReplacer("{functionId}", FunctionId, "{executionId}", ExecutionId) + path := r.Replace("/functions/{functionId}/executions/{executionId}") + + params := map[string]interface{}{ + } + + return srv.client.Call("GET", path, nil, params) +} + +// UpdateTag +func (srv *Functions) UpdateTag(FunctionId string, Tag string) (map[string]interface{}, error) { + r := strings.NewReplacer("{functionId}", FunctionId) + path := r.Replace("/functions/{functionId}/tag") + + params := map[string]interface{}{ + "tag": Tag, + } + + return srv.client.Call("PATCH", path, nil, params) +} + +// ListTags +func (srv *Functions) ListTags(FunctionId string, Search string, Limit int, Offset int, OrderType string) (map[string]interface{}, error) { + r := strings.NewReplacer("{functionId}", FunctionId) + path := r.Replace("/functions/{functionId}/tags") + + params := map[string]interface{}{ + "search": Search, + "limit": Limit, + "offset": Offset, + "orderType": OrderType, + } + + return srv.client.Call("GET", path, nil, params) +} + +// CreateTag +func (srv *Functions) CreateTag(FunctionId string, Env string, Command string, Code string) (map[string]interface{}, error) { + r := strings.NewReplacer("{functionId}", FunctionId) + path := r.Replace("/functions/{functionId}/tags") + + params := map[string]interface{}{ + "env": Env, + "command": Command, + "code": Code, + } + + return srv.client.Call("POST", path, nil, params) +} + +// GetTag +func (srv *Functions) GetTag(FunctionId string, TagId string) (map[string]interface{}, error) { + r := strings.NewReplacer("{functionId}", FunctionId, "{tagId}", TagId) + path := r.Replace("/functions/{functionId}/tags/{tagId}") + + params := map[string]interface{}{ + } + + return srv.client.Call("GET", path, nil, params) +} + +// DeleteTag +func (srv *Functions) DeleteTag(FunctionId string, TagId string) (map[string]interface{}, error) { + r := strings.NewReplacer("{functionId}", FunctionId, "{tagId}", TagId) + path := r.Replace("/functions/{functionId}/tags/{tagId}") + + params := map[string]interface{}{ + } + + return srv.client.Call("DELETE", path, nil, params) +} diff --git a/app/sdks/server-java/src/main/java/services/Functions.java b/app/sdks/server-java/src/main/java/services/Functions.java new file mode 100644 index 0000000000..a04ad1b5a9 --- /dev/null +++ b/app/sdks/server-java/src/main/java/services/Functions.java @@ -0,0 +1,255 @@ +package .services; + + + +import okhttp3.Call; +import .Client; +import .enums.OrderType; + +import java.io.File; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Map.entry; + +public class Functions extends Service { + public Functions(Client client){ + super(client); + } + + /// List Functions + public Call list(String search, int limit, int offset, OrderType orderType) { + final String path = "/functions"; + + final Map params = Map.ofEntries( + entry("search", search), + entry("limit", limit), + entry("offset", offset), + entry("orderType", orderType.name()) + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("GET", path, headers, params); + } + + /// Create Function + public Call create(String name, Object vars, List events, String schedule, int timeout) { + final String path = "/functions"; + + final Map params = Map.ofEntries( + entry("name", name), + entry("vars", vars), + entry("events", events), + entry("schedule", schedule), + entry("timeout", timeout) + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("POST", path, headers, params); + } + + /// Get Function + public Call get(String functionId) { + final String path = "/functions/{functionId}".replace("{functionId}", functionId); + + final Map params = Map.ofEntries( + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("GET", path, headers, params); + } + + /// Update Function + public Call update(String functionId, String name, Object vars, List events, String schedule, int timeout) { + final String path = "/functions/{functionId}".replace("{functionId}", functionId); + + final Map params = Map.ofEntries( + entry("name", name), + entry("vars", vars), + entry("events", events), + entry("schedule", schedule), + entry("timeout", timeout) + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("PUT", path, headers, params); + } + + /// Delete Function + public Call delete(String functionId) { + final String path = "/functions/{functionId}".replace("{functionId}", functionId); + + final Map params = Map.ofEntries( + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("DELETE", path, headers, params); + } + + /// List Executions + public Call listExecutions(String functionId, String search, int limit, int offset, OrderType orderType) { + final String path = "/functions/{functionId}/executions".replace("{functionId}", functionId); + + final Map params = Map.ofEntries( + entry("search", search), + entry("limit", limit), + entry("offset", offset), + entry("orderType", orderType.name()) + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("GET", path, headers, params); + } + + /// Create Execution + public Call createExecution(String functionId, int async) { + final String path = "/functions/{functionId}/executions".replace("{functionId}", functionId); + + final Map params = Map.ofEntries( + entry("async", async) + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("POST", path, headers, params); + } + + /// Get Execution + public Call getExecution(String functionId, String executionId) { + final String path = "/functions/{functionId}/executions/{executionId}".replace("{functionId}", functionId).replace("{executionId}", executionId); + + final Map params = Map.ofEntries( + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("GET", path, headers, params); + } + + /// Update Function Tag + public Call updateTag(String functionId, String tag) { + final String path = "/functions/{functionId}/tag".replace("{functionId}", functionId); + + final Map params = Map.ofEntries( + entry("tag", tag) + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("PATCH", path, headers, params); + } + + /// List Tags + public Call listTags(String functionId, String search, int limit, int offset, OrderType orderType) { + final String path = "/functions/{functionId}/tags".replace("{functionId}", functionId); + + final Map params = Map.ofEntries( + entry("search", search), + entry("limit", limit), + entry("offset", offset), + entry("orderType", orderType.name()) + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("GET", path, headers, params); + } + + /// Create Tag + public Call createTag(String functionId, String env, String command, String code) { + final String path = "/functions/{functionId}/tags".replace("{functionId}", functionId); + + final Map params = Map.ofEntries( + entry("env", env), + entry("command", command), + entry("code", code) + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("POST", path, headers, params); + } + + /// Get Tag + public Call getTag(String functionId, String tagId) { + final String path = "/functions/{functionId}/tags/{tagId}".replace("{functionId}", functionId).replace("{tagId}", tagId); + + final Map params = Map.ofEntries( + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("GET", path, headers, params); + } + + /// Delete Tag + public Call deleteTag(String functionId, String tagId) { + final String path = "/functions/{functionId}/tags/{tagId}".replace("{functionId}", functionId).replace("{tagId}", tagId); + + final Map params = Map.ofEntries( + ); + + + + final Map headers = Map.ofEntries( + entry("content-type", "application/json") + ); + + return client.call("DELETE", path, headers, params); + } +} \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/create-execution.md b/app/sdks/server-nodejs/docs/examples/functions/create-execution.md new file mode 100644 index 0000000000..9ed33ae50b --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/create-execution.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.createExecution('[FUNCTION_ID]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/create-tag.md b/app/sdks/server-nodejs/docs/examples/functions/create-tag.md new file mode 100644 index 0000000000..5048701d05 --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/create-tag.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.createTag('[FUNCTION_ID]', 'node-14', '[COMMAND]', '[CODE]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/create.md b/app/sdks/server-nodejs/docs/examples/functions/create.md new file mode 100644 index 0000000000..3ac02eb506 --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/create.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.create('[NAME]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/delete-tag.md b/app/sdks/server-nodejs/docs/examples/functions/delete-tag.md new file mode 100644 index 0000000000..fc2fe8a53b --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/delete-tag.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.deleteTag('[FUNCTION_ID]', '[TAG_ID]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/delete.md b/app/sdks/server-nodejs/docs/examples/functions/delete.md new file mode 100644 index 0000000000..048c96d99c --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/delete.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.delete('[FUNCTION_ID]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/get-execution.md b/app/sdks/server-nodejs/docs/examples/functions/get-execution.md new file mode 100644 index 0000000000..482f7ee713 --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/get-execution.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.getExecution('[FUNCTION_ID]', '[EXECUTION_ID]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/get-tag.md b/app/sdks/server-nodejs/docs/examples/functions/get-tag.md new file mode 100644 index 0000000000..5caa2134b6 --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/get-tag.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.getTag('[FUNCTION_ID]', '[TAG_ID]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/get.md b/app/sdks/server-nodejs/docs/examples/functions/get.md new file mode 100644 index 0000000000..cfc2d0fae2 --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/get.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.get('[FUNCTION_ID]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/list-executions.md b/app/sdks/server-nodejs/docs/examples/functions/list-executions.md new file mode 100644 index 0000000000..ceecf677a6 --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/list-executions.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.listExecutions('[FUNCTION_ID]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/list-tags.md b/app/sdks/server-nodejs/docs/examples/functions/list-tags.md new file mode 100644 index 0000000000..147d65afe2 --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/list-tags.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.listTags('[FUNCTION_ID]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/list.md b/app/sdks/server-nodejs/docs/examples/functions/list.md new file mode 100644 index 0000000000..3cb999c51b --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/list.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.list(); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/update-active.md b/app/sdks/server-nodejs/docs/examples/functions/update-active.md new file mode 100644 index 0000000000..1c2f0da2e2 --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/update-active.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.updateActive('[FUNCTION_ID]', '[ACTIVE]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/update-tag.md b/app/sdks/server-nodejs/docs/examples/functions/update-tag.md new file mode 100644 index 0000000000..8b9baefaf0 --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/update-tag.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.updateTag('[FUNCTION_ID]', '[TAG]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/docs/examples/functions/update.md b/app/sdks/server-nodejs/docs/examples/functions/update.md new file mode 100644 index 0000000000..73672ae162 --- /dev/null +++ b/app/sdks/server-nodejs/docs/examples/functions/update.md @@ -0,0 +1,19 @@ +const sdk = require('node-appwrite'); + +// Init SDK +let client = new sdk.Client(); + +let functions = new sdk.Functions(client); + +client + .setProject('5df5acd0d48c2') // Your project ID + .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +let promise = functions.update('[FUNCTION_ID]', '[NAME]'); + +promise.then(function (response) { + console.log(response); +}, function (error) { + console.log(error); +}); \ No newline at end of file diff --git a/app/sdks/server-nodejs/index.js b/app/sdks/server-nodejs/index.js index f5259f59dc..deb571190d 100644 --- a/app/sdks/server-nodejs/index.js +++ b/app/sdks/server-nodejs/index.js @@ -1,6 +1,7 @@ const Client = require('./lib/client.js'); const Avatars = require('./lib/services/avatars.js'); const Database = require('./lib/services/database.js'); +const Functions = require('./lib/services/functions.js'); const Health = require('./lib/services/health.js'); const Locale = require('./lib/services/locale.js'); const Storage = require('./lib/services/storage.js'); @@ -11,6 +12,7 @@ module.exports = { Client, Avatars, Database, + Functions, Health, Locale, Storage, diff --git a/app/sdks/server-nodejs/lib/services/functions.js b/app/sdks/server-nodejs/lib/services/functions.js new file mode 100644 index 0000000000..0dfd8d945d --- /dev/null +++ b/app/sdks/server-nodejs/lib/services/functions.js @@ -0,0 +1,282 @@ +const Service = require('../service.js'); + +class Functions extends Service { + + /** + * List Functions + * + * @param string search + * @param number limit + * @param number offset + * @param string orderType + * @throws Exception + * @return {} + */ + async list(search = '', limit = 25, offset = 0, orderType = 'ASC') { + let path = '/functions'; + + return await this.client.call('get', path, { + 'content-type': 'application/json', + }, + { + 'search': search, + 'limit': limit, + 'offset': offset, + 'orderType': orderType + }); + } + + /** + * Create Function + * + * @param string name + * @param object vars + * @param string[] events + * @param string schedule + * @param number timeout + * @throws Exception + * @return {} + */ + async create(name, vars = [], events = [], schedule = '', timeout = 15) { + let path = '/functions'; + + return await this.client.call('post', path, { + 'content-type': 'application/json', + }, + { + 'name': name, + 'vars': vars, + 'events': events, + 'schedule': schedule, + 'timeout': timeout + }); + } + + /** + * Get Function + * + * @param string functionId + * @throws Exception + * @return {} + */ + async get(functionId) { + let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); + + return await this.client.call('get', path, { + 'content-type': 'application/json', + }, + { + }); + } + + /** + * Update Function + * + * @param string functionId + * @param string name + * @param object vars + * @param string[] events + * @param string schedule + * @param number timeout + * @throws Exception + * @return {} + */ + async update(functionId, name, vars = [], events = [], schedule = '', timeout = 15) { + let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); + + return await this.client.call('put', path, { + 'content-type': 'application/json', + }, + { + 'name': name, + 'vars': vars, + 'events': events, + 'schedule': schedule, + 'timeout': timeout + }); + } + + /** + * Delete Function + * + * @param string functionId + * @throws Exception + * @return {} + */ + async delete(functionId) { + let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); + + return await this.client.call('delete', path, { + 'content-type': 'application/json', + }, + { + }); + } + + /** + * List Executions + * + * @param string functionId + * @param string search + * @param number limit + * @param number offset + * @param string orderType + * @throws Exception + * @return {} + */ + async listExecutions(functionId, search = '', limit = 25, offset = 0, orderType = 'ASC') { + let path = '/functions/{functionId}/executions'.replace(new RegExp('{functionId}', 'g'), functionId); + + return await this.client.call('get', path, { + 'content-type': 'application/json', + }, + { + 'search': search, + 'limit': limit, + 'offset': offset, + 'orderType': orderType + }); + } + + /** + * Create Execution + * + * @param string functionId + * @param number async + * @throws Exception + * @return {} + */ + async createExecution(functionId, async = 1) { + let path = '/functions/{functionId}/executions'.replace(new RegExp('{functionId}', 'g'), functionId); + + return await this.client.call('post', path, { + 'content-type': 'application/json', + }, + { + 'async': async + }); + } + + /** + * Get Execution + * + * @param string functionId + * @param string executionId + * @throws Exception + * @return {} + */ + async getExecution(functionId, executionId) { + let path = '/functions/{functionId}/executions/{executionId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{executionId}', 'g'), executionId); + + return await this.client.call('get', path, { + 'content-type': 'application/json', + }, + { + }); + } + + /** + * Update Function Tag + * + * @param string functionId + * @param string tag + * @throws Exception + * @return {} + */ + async updateTag(functionId, tag) { + let path = '/functions/{functionId}/tag'.replace(new RegExp('{functionId}', 'g'), functionId); + + return await this.client.call('patch', path, { + 'content-type': 'application/json', + }, + { + 'tag': tag + }); + } + + /** + * List Tags + * + * @param string functionId + * @param string search + * @param number limit + * @param number offset + * @param string orderType + * @throws Exception + * @return {} + */ + async listTags(functionId, search = '', limit = 25, offset = 0, orderType = 'ASC') { + let path = '/functions/{functionId}/tags'.replace(new RegExp('{functionId}', 'g'), functionId); + + return await this.client.call('get', path, { + 'content-type': 'application/json', + }, + { + 'search': search, + 'limit': limit, + 'offset': offset, + 'orderType': orderType + }); + } + + /** + * Create Tag + * + * @param string functionId + * @param string env + * @param string command + * @param string code + * @throws Exception + * @return {} + */ + async createTag(functionId, env, command, code) { + let path = '/functions/{functionId}/tags'.replace(new RegExp('{functionId}', 'g'), functionId); + + return await this.client.call('post', path, { + 'content-type': 'application/json', + }, + { + 'env': env, + 'command': command, + 'code': code + }); + } + + /** + * Get Tag + * + * @param string functionId + * @param string tagId + * @throws Exception + * @return {} + */ + async getTag(functionId, tagId) { + let path = '/functions/{functionId}/tags/{tagId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{tagId}', 'g'), tagId); + + return await this.client.call('get', path, { + 'content-type': 'application/json', + }, + { + }); + } + + /** + * Delete Tag + * + * @param string functionId + * @param string tagId + * @throws Exception + * @return {} + */ + async deleteTag(functionId, tagId) { + let path = '/functions/{functionId}/tags/{tagId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{tagId}', 'g'), tagId); + + return await this.client.call('delete', path, { + 'content-type': 'application/json', + }, + { + }); + } +} + +module.exports = Functions; \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/create-execution.md b/app/sdks/server-php/docs/examples/functions/create-execution.md new file mode 100644 index 0000000000..ca232c8771 --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/create-execution.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->createExecution('[FUNCTION_ID]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/create-tag.md b/app/sdks/server-php/docs/examples/functions/create-tag.md new file mode 100644 index 0000000000..cc4ff73e90 --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/create-tag.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->createTag('[FUNCTION_ID]', 'node-14', '[COMMAND]', '[CODE]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/create.md b/app/sdks/server-php/docs/examples/functions/create.md new file mode 100644 index 0000000000..7ec5dc2831 --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/create.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->create('[NAME]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/delete-tag.md b/app/sdks/server-php/docs/examples/functions/delete-tag.md new file mode 100644 index 0000000000..681c9c3aa7 --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/delete-tag.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->deleteTag('[FUNCTION_ID]', '[TAG_ID]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/delete.md b/app/sdks/server-php/docs/examples/functions/delete.md new file mode 100644 index 0000000000..12476f1d6a --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/delete.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->delete('[FUNCTION_ID]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/get-execution.md b/app/sdks/server-php/docs/examples/functions/get-execution.md new file mode 100644 index 0000000000..774c7ecc3f --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/get-execution.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->getExecution('[FUNCTION_ID]', '[EXECUTION_ID]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/get-tag.md b/app/sdks/server-php/docs/examples/functions/get-tag.md new file mode 100644 index 0000000000..b1c2f23dcf --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/get-tag.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->getTag('[FUNCTION_ID]', '[TAG_ID]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/get.md b/app/sdks/server-php/docs/examples/functions/get.md new file mode 100644 index 0000000000..0b6b438c65 --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/get.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->get('[FUNCTION_ID]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/list-executions.md b/app/sdks/server-php/docs/examples/functions/list-executions.md new file mode 100644 index 0000000000..a0403c653d --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/list-executions.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->listExecutions('[FUNCTION_ID]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/list-tags.md b/app/sdks/server-php/docs/examples/functions/list-tags.md new file mode 100644 index 0000000000..3c2e6b6e87 --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/list-tags.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->listTags('[FUNCTION_ID]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/list.md b/app/sdks/server-php/docs/examples/functions/list.md new file mode 100644 index 0000000000..bfbfde406f --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/list.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->list(); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/update-active.md b/app/sdks/server-php/docs/examples/functions/update-active.md new file mode 100644 index 0000000000..18b7316f3b --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/update-active.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->updateActive('[FUNCTION_ID]', '[ACTIVE]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/update-tag.md b/app/sdks/server-php/docs/examples/functions/update-tag.md new file mode 100644 index 0000000000..68099d8fbb --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/update-tag.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->updateTag('[FUNCTION_ID]', '[TAG]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/examples/functions/update.md b/app/sdks/server-php/docs/examples/functions/update.md new file mode 100644 index 0000000000..34315a129a --- /dev/null +++ b/app/sdks/server-php/docs/examples/functions/update.md @@ -0,0 +1,15 @@ +setProject('5df5acd0d48c2') // Your project ID + ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key +; + +$functions = new Functions($client); + +$result = $functions->update('[FUNCTION_ID]', '[NAME]'); \ No newline at end of file diff --git a/app/sdks/server-php/docs/functions.md b/app/sdks/server-php/docs/functions.md new file mode 100644 index 0000000000..39e28d55b8 --- /dev/null +++ b/app/sdks/server-php/docs/functions.md @@ -0,0 +1,186 @@ +# Functions Service + +## List Functions + +```http request +GET https://appwrite.io/v1/functions +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| search | string | Search term to filter your list results. | | +| limit | integer | Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request. | 25 | +| offset | integer | Results offset. The default value is 0. Use this param to manage pagination. | 0 | +| orderType | string | Order result by ASC or DESC order. | ASC | + +## Create Function + +```http request +POST https://appwrite.io/v1/functions +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| name | string | Function name. | | +| vars | object | Key-value JSON object. | [] | +| events | array | Events list. | [] | +| schedule | string | Schedule CRON syntax. | | +| timeout | integer | Function maximum execution time in seconds. | 15 | + +## Get Function + +```http request +GET https://appwrite.io/v1/functions/{functionId} +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| functionId | string | **Required** Function unique ID. | | + +## Update Function + +```http request +PUT https://appwrite.io/v1/functions/{functionId} +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| functionId | string | **Required** Function unique ID. | | +| name | string | Function name. | | +| vars | object | Key-value JSON object. | [] | +| events | array | Events list. | [] | +| schedule | string | Schedule CRON syntax. | | +| timeout | integer | Function maximum execution time in seconds. | 15 | + +## Delete Function + +```http request +DELETE https://appwrite.io/v1/functions/{functionId} +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| functionId | string | **Required** Function unique ID. | | + +## List Executions + +```http request +GET https://appwrite.io/v1/functions/{functionId}/executions +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| functionId | string | **Required** Function unique ID. | | +| search | string | Search term to filter your list results. | | +| limit | integer | Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request. | 25 | +| offset | integer | Results offset. The default value is 0. Use this param to manage pagination. | 0 | +| orderType | string | Order result by ASC or DESC order. | ASC | + +## Create Execution + +```http request +POST https://appwrite.io/v1/functions/{functionId}/executions +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| functionId | string | **Required** Function unique ID. | | +| async | integer | Execute code asynchronously. Pass 1 for true, 0 for false. Default value is 1. | 1 | + +## Get Execution + +```http request +GET https://appwrite.io/v1/functions/{functionId}/executions/{executionId} +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| functionId | string | **Required** Function unique ID. | | +| executionId | string | **Required** Execution unique ID. | | + +## Update Function Tag + +```http request +PATCH https://appwrite.io/v1/functions/{functionId}/tag +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| functionId | string | **Required** Function unique ID. | | +| tag | string | Tag unique ID. | | + +## List Tags + +```http request +GET https://appwrite.io/v1/functions/{functionId}/tags +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| functionId | string | **Required** Function unique ID. | | +| search | string | Search term to filter your list results. | | +| limit | integer | Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request. | 25 | +| offset | integer | Results offset. The default value is 0. Use this param to manage pagination. | 0 | +| orderType | string | Order result by ASC or DESC order. | ASC | + +## Create Tag + +```http request +POST https://appwrite.io/v1/functions/{functionId}/tags +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| functionId | string | **Required** Function unique ID. | | +| env | string | Execution enviornment. | | +| command | string | Code execution command. | | +| code | string | Code package. Use the Appwrite code packager to create a deployable package file. | | + +## Get Tag + +```http request +GET https://appwrite.io/v1/functions/{functionId}/tags/{tagId} +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| functionId | string | **Required** Function unique ID. | | +| tagId | string | **Required** Tag unique ID. | | + +## Delete Tag + +```http request +DELETE https://appwrite.io/v1/functions/{functionId}/tags/{tagId} +``` + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| functionId | string | **Required** Function unique ID. | | +| tagId | string | **Required** Tag unique ID. | | + diff --git a/app/sdks/server-php/src/Appwrite/Services/Functions.php b/app/sdks/server-php/src/Appwrite/Services/Functions.php new file mode 100644 index 0000000000..1771652072 --- /dev/null +++ b/app/sdks/server-php/src/Appwrite/Services/Functions.php @@ -0,0 +1,300 @@ +client->call(Client::METHOD_GET, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * Create Function + * + * @param string $name + * @param array $vars + * @param array $events + * @param string $schedule + * @param int $timeout + * @throws Exception + * @return array + */ + public function create(string $name, array $vars = , array $events = [], string $schedule = '', int $timeout = 15):array + { + $path = str_replace([], [], '/functions'); + $params = []; + + $params['name'] = $name; + $params['vars'] = $vars; + $params['events'] = $events; + $params['schedule'] = $schedule; + $params['timeout'] = $timeout; + + return $this->client->call(Client::METHOD_POST, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * Get Function + * + * @param string $functionId + * @throws Exception + * @return array + */ + public function get(string $functionId):array + { + $path = str_replace(['{functionId}'], [$functionId], '/functions/{functionId}'); + $params = []; + + + return $this->client->call(Client::METHOD_GET, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * Update Function + * + * @param string $functionId + * @param string $name + * @param array $vars + * @param array $events + * @param string $schedule + * @param int $timeout + * @throws Exception + * @return array + */ + public function update(string $functionId, string $name, array $vars = , array $events = [], string $schedule = '', int $timeout = 15):array + { + $path = str_replace(['{functionId}'], [$functionId], '/functions/{functionId}'); + $params = []; + + $params['name'] = $name; + $params['vars'] = $vars; + $params['events'] = $events; + $params['schedule'] = $schedule; + $params['timeout'] = $timeout; + + return $this->client->call(Client::METHOD_PUT, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * Delete Function + * + * @param string $functionId + * @throws Exception + * @return array + */ + public function delete(string $functionId):array + { + $path = str_replace(['{functionId}'], [$functionId], '/functions/{functionId}'); + $params = []; + + + return $this->client->call(Client::METHOD_DELETE, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * List Executions + * + * @param string $functionId + * @param string $search + * @param int $limit + * @param int $offset + * @param string $orderType + * @throws Exception + * @return array + */ + public function listExecutions(string $functionId, string $search = '', int $limit = 25, int $offset = 0, string $orderType = 'ASC'):array + { + $path = str_replace(['{functionId}'], [$functionId], '/functions/{functionId}/executions'); + $params = []; + + $params['search'] = $search; + $params['limit'] = $limit; + $params['offset'] = $offset; + $params['orderType'] = $orderType; + + return $this->client->call(Client::METHOD_GET, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * Create Execution + * + * @param string $functionId + * @param int $async + * @throws Exception + * @return array + */ + public function createExecution(string $functionId, int $async = 1):array + { + $path = str_replace(['{functionId}'], [$functionId], '/functions/{functionId}/executions'); + $params = []; + + $params['async'] = $async; + + return $this->client->call(Client::METHOD_POST, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * Get Execution + * + * @param string $functionId + * @param string $executionId + * @throws Exception + * @return array + */ + public function getExecution(string $functionId, string $executionId):array + { + $path = str_replace(['{functionId}', '{executionId}'], [$functionId, $executionId], '/functions/{functionId}/executions/{executionId}'); + $params = []; + + + return $this->client->call(Client::METHOD_GET, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * Update Function Tag + * + * @param string $functionId + * @param string $tag + * @throws Exception + * @return array + */ + public function updateTag(string $functionId, string $tag):array + { + $path = str_replace(['{functionId}'], [$functionId], '/functions/{functionId}/tag'); + $params = []; + + $params['tag'] = $tag; + + return $this->client->call(Client::METHOD_PATCH, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * List Tags + * + * @param string $functionId + * @param string $search + * @param int $limit + * @param int $offset + * @param string $orderType + * @throws Exception + * @return array + */ + public function listTags(string $functionId, string $search = '', int $limit = 25, int $offset = 0, string $orderType = 'ASC'):array + { + $path = str_replace(['{functionId}'], [$functionId], '/functions/{functionId}/tags'); + $params = []; + + $params['search'] = $search; + $params['limit'] = $limit; + $params['offset'] = $offset; + $params['orderType'] = $orderType; + + return $this->client->call(Client::METHOD_GET, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * Create Tag + * + * @param string $functionId + * @param string $env + * @param string $command + * @param string $code + * @throws Exception + * @return array + */ + public function createTag(string $functionId, string $env, string $command, string $code):array + { + $path = str_replace(['{functionId}'], [$functionId], '/functions/{functionId}/tags'); + $params = []; + + $params['env'] = $env; + $params['command'] = $command; + $params['code'] = $code; + + return $this->client->call(Client::METHOD_POST, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * Get Tag + * + * @param string $functionId + * @param string $tagId + * @throws Exception + * @return array + */ + public function getTag(string $functionId, string $tagId):array + { + $path = str_replace(['{functionId}', '{tagId}'], [$functionId, $tagId], '/functions/{functionId}/tags/{tagId}'); + $params = []; + + + return $this->client->call(Client::METHOD_GET, $path, [ + 'content-type' => 'application/json', + ], $params); + } + + /** + * Delete Tag + * + * @param string $functionId + * @param string $tagId + * @throws Exception + * @return array + */ + public function deleteTag(string $functionId, string $tagId):array + { + $path = str_replace(['{functionId}', '{tagId}'], [$functionId, $tagId], '/functions/{functionId}/tags/{tagId}'); + $params = []; + + + return $this->client->call(Client::METHOD_DELETE, $path, [ + 'content-type' => 'application/json', + ], $params); + } + +} \ No newline at end of file diff --git a/app/sdks/server-python/appwrite/services/functions.py b/app/sdks/server-python/appwrite/services/functions.py new file mode 100644 index 0000000000..1688f21f47 --- /dev/null +++ b/app/sdks/server-python/appwrite/services/functions.py @@ -0,0 +1,178 @@ +from ..service import Service + + +class Functions(Service): + + def __init__(self, client): + super(Functions, self).__init__(client) + + def list(self, search='', limit=25, offset=0, order_type='ASC'): + """List Functions""" + + params = {} + path = '/functions' + params['search'] = search + params['limit'] = limit + params['offset'] = offset + params['orderType'] = order_type + + return self.client.call('get', path, { + 'content-type': 'application/json', + }, params) + + def create(self, name, vars=[], events=[], schedule='', timeout=15): + """Create Function""" + + params = {} + path = '/functions' + params['name'] = name + params['vars'] = vars + params['events'] = events + params['schedule'] = schedule + params['timeout'] = timeout + + return self.client.call('post', path, { + 'content-type': 'application/json', + }, params) + + def get(self, function_id): + """Get Function""" + + params = {} + path = '/functions/{functionId}' + path = path.replace('{functionId}', function_id) + + return self.client.call('get', path, { + 'content-type': 'application/json', + }, params) + + def update(self, function_id, name, vars=[], events=[], schedule='', timeout=15): + """Update Function""" + + params = {} + path = '/functions/{functionId}' + path = path.replace('{functionId}', function_id) + params['name'] = name + params['vars'] = vars + params['events'] = events + params['schedule'] = schedule + params['timeout'] = timeout + + return self.client.call('put', path, { + 'content-type': 'application/json', + }, params) + + def delete(self, function_id): + """Delete Function""" + + params = {} + path = '/functions/{functionId}' + path = path.replace('{functionId}', function_id) + + return self.client.call('delete', path, { + 'content-type': 'application/json', + }, params) + + def list_executions(self, function_id, search='', limit=25, offset=0, order_type='ASC'): + """List Executions""" + + params = {} + path = '/functions/{functionId}/executions' + path = path.replace('{functionId}', function_id) + params['search'] = search + params['limit'] = limit + params['offset'] = offset + params['orderType'] = order_type + + return self.client.call('get', path, { + 'content-type': 'application/json', + }, params) + + def create_execution(self, function_id, async=1): + """Create Execution""" + + params = {} + path = '/functions/{functionId}/executions' + path = path.replace('{functionId}', function_id) + params['async'] = async + + return self.client.call('post', path, { + 'content-type': 'application/json', + }, params) + + def get_execution(self, function_id, execution_id): + """Get Execution""" + + params = {} + path = '/functions/{functionId}/executions/{executionId}' + path = path.replace('{functionId}', function_id) + path = path.replace('{executionId}', execution_id) + + return self.client.call('get', path, { + 'content-type': 'application/json', + }, params) + + def update_tag(self, function_id, tag): + """Update Function Tag""" + + params = {} + path = '/functions/{functionId}/tag' + path = path.replace('{functionId}', function_id) + params['tag'] = tag + + return self.client.call('patch', path, { + 'content-type': 'application/json', + }, params) + + def list_tags(self, function_id, search='', limit=25, offset=0, order_type='ASC'): + """List Tags""" + + params = {} + path = '/functions/{functionId}/tags' + path = path.replace('{functionId}', function_id) + params['search'] = search + params['limit'] = limit + params['offset'] = offset + params['orderType'] = order_type + + return self.client.call('get', path, { + 'content-type': 'application/json', + }, params) + + def create_tag(self, function_id, env, command, code): + """Create Tag""" + + params = {} + path = '/functions/{functionId}/tags' + path = path.replace('{functionId}', function_id) + params['env'] = env + params['command'] = command + params['code'] = code + + return self.client.call('post', path, { + 'content-type': 'application/json', + }, params) + + def get_tag(self, function_id, tag_id): + """Get Tag""" + + params = {} + path = '/functions/{functionId}/tags/{tagId}' + path = path.replace('{functionId}', function_id) + path = path.replace('{tagId}', tag_id) + + return self.client.call('get', path, { + 'content-type': 'application/json', + }, params) + + def delete_tag(self, function_id, tag_id): + """Delete Tag""" + + params = {} + path = '/functions/{functionId}/tags/{tagId}' + path = path.replace('{functionId}', function_id) + path = path.replace('{tagId}', tag_id) + + return self.client.call('delete', path, { + 'content-type': 'application/json', + }, params) diff --git a/app/sdks/server-python/docs/examples/functions/create-execution.md b/app/sdks/server-python/docs/examples/functions/create-execution.md new file mode 100644 index 0000000000..aa8e955751 --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/create-execution.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.create_execution('[FUNCTION_ID]') diff --git a/app/sdks/server-python/docs/examples/functions/create-tag.md b/app/sdks/server-python/docs/examples/functions/create-tag.md new file mode 100644 index 0000000000..533f3095c3 --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/create-tag.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.create_tag('[FUNCTION_ID]', 'node-14', '[COMMAND]', '[CODE]') diff --git a/app/sdks/server-python/docs/examples/functions/create.md b/app/sdks/server-python/docs/examples/functions/create.md new file mode 100644 index 0000000000..2b63ea7191 --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/create.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.create('[NAME]') diff --git a/app/sdks/server-python/docs/examples/functions/delete-tag.md b/app/sdks/server-python/docs/examples/functions/delete-tag.md new file mode 100644 index 0000000000..554817cf5e --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/delete-tag.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.delete_tag('[FUNCTION_ID]', '[TAG_ID]') diff --git a/app/sdks/server-python/docs/examples/functions/delete.md b/app/sdks/server-python/docs/examples/functions/delete.md new file mode 100644 index 0000000000..9c1575c1e8 --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/delete.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.delete('[FUNCTION_ID]') diff --git a/app/sdks/server-python/docs/examples/functions/get-execution.md b/app/sdks/server-python/docs/examples/functions/get-execution.md new file mode 100644 index 0000000000..b979241983 --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/get-execution.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.get_execution('[FUNCTION_ID]', '[EXECUTION_ID]') diff --git a/app/sdks/server-python/docs/examples/functions/get-tag.md b/app/sdks/server-python/docs/examples/functions/get-tag.md new file mode 100644 index 0000000000..b5c3f23059 --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/get-tag.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.get_tag('[FUNCTION_ID]', '[TAG_ID]') diff --git a/app/sdks/server-python/docs/examples/functions/get.md b/app/sdks/server-python/docs/examples/functions/get.md new file mode 100644 index 0000000000..0e4036db4d --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/get.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.get('[FUNCTION_ID]') diff --git a/app/sdks/server-python/docs/examples/functions/list-executions.md b/app/sdks/server-python/docs/examples/functions/list-executions.md new file mode 100644 index 0000000000..75f5260b46 --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/list-executions.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.list_executions('[FUNCTION_ID]') diff --git a/app/sdks/server-python/docs/examples/functions/list-tags.md b/app/sdks/server-python/docs/examples/functions/list-tags.md new file mode 100644 index 0000000000..a992416640 --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/list-tags.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.list_tags('[FUNCTION_ID]') diff --git a/app/sdks/server-python/docs/examples/functions/list.md b/app/sdks/server-python/docs/examples/functions/list.md new file mode 100644 index 0000000000..a2b1e7d3db --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/list.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.list() diff --git a/app/sdks/server-python/docs/examples/functions/update-active.md b/app/sdks/server-python/docs/examples/functions/update-active.md new file mode 100644 index 0000000000..72f8314055 --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/update-active.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.update_active('[FUNCTION_ID]', '[ACTIVE]') diff --git a/app/sdks/server-python/docs/examples/functions/update-tag.md b/app/sdks/server-python/docs/examples/functions/update-tag.md new file mode 100644 index 0000000000..4ab93d1b6a --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/update-tag.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.update_tag('[FUNCTION_ID]', '[TAG]') diff --git a/app/sdks/server-python/docs/examples/functions/update.md b/app/sdks/server-python/docs/examples/functions/update.md new file mode 100644 index 0000000000..751a5d2538 --- /dev/null +++ b/app/sdks/server-python/docs/examples/functions/update.md @@ -0,0 +1,13 @@ +from appwrite.client import Client +from appwrite.services.functions import Functions + +client = Client() + +(client + .set_project('5df5acd0d48c2') # Your project ID + .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key +) + +functions = Functions(client) + +result = functions.update('[FUNCTION_ID]', '[NAME]') diff --git a/app/sdks/server-ruby/lib/appwrite.rb b/app/sdks/server-ruby/lib/appwrite.rb index 8e88842789..1b4f38bea2 100644 --- a/app/sdks/server-ruby/lib/appwrite.rb +++ b/app/sdks/server-ruby/lib/appwrite.rb @@ -5,6 +5,7 @@ require_relative 'appwrite/client' require_relative 'appwrite/service' require_relative 'appwrite/services/avatars' require_relative 'appwrite/services/database' +require_relative 'appwrite/services/functions' require_relative 'appwrite/services/health' require_relative 'appwrite/services/locale' require_relative 'appwrite/services/storage' diff --git a/app/sdks/server-ruby/lib/appwrite/services/functions.rb b/app/sdks/server-ruby/lib/appwrite/services/functions.rb new file mode 100644 index 0000000000..e6fb0c5f12 --- /dev/null +++ b/app/sdks/server-ruby/lib/appwrite/services/functions.rb @@ -0,0 +1,193 @@ +module Appwrite + class Functions < Service + + def list(search: '', limit: 25, offset: 0, order_type: 'ASC') + path = '/functions' + + params = { + 'search': search, + 'limit': limit, + 'offset': offset, + 'orderType': order_type + } + + return @client.call('get', path, { + 'content-type' => 'application/json', + }, params); + end + + def create(name:, vars: [], events: [], schedule: '', timeout: 15) + path = '/functions' + + params = { + 'name': name, + 'vars': vars, + 'events': events, + 'schedule': schedule, + 'timeout': timeout + } + + return @client.call('post', path, { + 'content-type' => 'application/json', + }, params); + end + + def get(function_id:) + path = '/functions/{functionId}' + .gsub('{function_id}', function_id) + + params = { + } + + return @client.call('get', path, { + 'content-type' => 'application/json', + }, params); + end + + def update(function_id:, name:, vars: [], events: [], schedule: '', timeout: 15) + path = '/functions/{functionId}' + .gsub('{function_id}', function_id) + + params = { + 'name': name, + 'vars': vars, + 'events': events, + 'schedule': schedule, + 'timeout': timeout + } + + return @client.call('put', path, { + 'content-type' => 'application/json', + }, params); + end + + def delete(function_id:) + path = '/functions/{functionId}' + .gsub('{function_id}', function_id) + + params = { + } + + return @client.call('delete', path, { + 'content-type' => 'application/json', + }, params); + end + + def list_executions(function_id:, search: '', limit: 25, offset: 0, order_type: 'ASC') + path = '/functions/{functionId}/executions' + .gsub('{function_id}', function_id) + + params = { + 'search': search, + 'limit': limit, + 'offset': offset, + 'orderType': order_type + } + + return @client.call('get', path, { + 'content-type' => 'application/json', + }, params); + end + + def create_execution(function_id:, async: 1) + path = '/functions/{functionId}/executions' + .gsub('{function_id}', function_id) + + params = { + 'async': async + } + + return @client.call('post', path, { + 'content-type' => 'application/json', + }, params); + end + + def get_execution(function_id:, execution_id:) + path = '/functions/{functionId}/executions/{executionId}' + .gsub('{function_id}', function_id) + .gsub('{execution_id}', execution_id) + + params = { + } + + return @client.call('get', path, { + 'content-type' => 'application/json', + }, params); + end + + def update_tag(function_id:, tag:) + path = '/functions/{functionId}/tag' + .gsub('{function_id}', function_id) + + params = { + 'tag': tag + } + + return @client.call('patch', path, { + 'content-type' => 'application/json', + }, params); + end + + def list_tags(function_id:, search: '', limit: 25, offset: 0, order_type: 'ASC') + path = '/functions/{functionId}/tags' + .gsub('{function_id}', function_id) + + params = { + 'search': search, + 'limit': limit, + 'offset': offset, + 'orderType': order_type + } + + return @client.call('get', path, { + 'content-type' => 'application/json', + }, params); + end + + def create_tag(function_id:, env:, command:, code:) + path = '/functions/{functionId}/tags' + .gsub('{function_id}', function_id) + + params = { + 'env': env, + 'command': command, + 'code': code + } + + return @client.call('post', path, { + 'content-type' => 'application/json', + }, params); + end + + def get_tag(function_id:, tag_id:) + path = '/functions/{functionId}/tags/{tagId}' + .gsub('{function_id}', function_id) + .gsub('{tag_id}', tag_id) + + params = { + } + + return @client.call('get', path, { + 'content-type' => 'application/json', + }, params); + end + + def delete_tag(function_id:, tag_id:) + path = '/functions/{functionId}/tags/{tagId}' + .gsub('{function_id}', function_id) + .gsub('{tag_id}', tag_id) + + params = { + } + + return @client.call('delete', path, { + 'content-type' => 'application/json', + }, params); + end + + + protected + + private + end +end \ No newline at end of file diff --git a/app/tasks/init.php b/app/tasks/doctor.php similarity index 76% rename from app/tasks/init.php rename to app/tasks/doctor.php index a6e6af101d..0836628bde 100644 --- a/app/tasks/init.php +++ b/app/tasks/doctor.php @@ -1,49 +1,28 @@ -#!/bin/env php task('ssl') - ->desc('Validate server certificates') - ->action(function () use ($request) { - $domain = $request->getServer('_APP_DOMAIN', ''); - - Console::log('Issue a TLS certificate for master domain ('.$domain.')'); - - ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ - 'document' => [], - 'domain' => $domain, - 'validateTarget' => false, - 'validateCNAME' => false, - ]); - }); - $cli ->task('doctor') ->desc('Validate server health') - ->action(function () use ($request, $register) { + ->action(function () use ($register) { Console::log(" __ ____ ____ _ _ ____ __ ____ ____ __ __ / _\ ( _ \( _ \/ )( \( _ \( )(_ _)( __) ( )/ \ / \ ) __/ ) __/\ /\ / ) / )( )( ) _) _ )(( O ) \_/\_/(__) (__) (_/\_)(__\_)(__) (__) (____)(_)(__)\__/ "); - Console::log("\n".'👩‍⚕️ Running '.APP_NAME.' Doctor for version '.$request->getServer('_APP_VERSION', 'UNKNOWN').' ...'."\n"); + Console::log("\n".'👩‍⚕️ Running '.APP_NAME.' Doctor for version '.App::getEnv('_APP_VERSION', 'UNKNOWN').' ...'."\n"); Console::log('Checking for production best practices...'); - $domain = new Domain($request->getServer('_APP_DOMAIN')); + $domain = new Domain(App::getEnv('_APP_DOMAIN')); if(!$domain->isKnown() || $domain->isTest()) { Console::log('🔴 Hostname has a public suffix'); @@ -52,7 +31,7 @@ $cli Console::log('🟢 Hostname has a public suffix'); } - $domain = new Domain($request->getServer('_APP_DOMAIN_TARGET')); + $domain = new Domain(App::getEnv('_APP_DOMAIN_TARGET')); if(!$domain->isKnown() || $domain->isTest()) { Console::log('🔴 CNAME target has a public suffix'); @@ -61,30 +40,30 @@ $cli Console::log('🟢 CNAME target has a public suffix'); } - if($request->getServer('_APP_OPENSSL_KEY_V1', 'your-secret-key') === 'your-secret-key') { + if(App::getEnv('_APP_OPENSSL_KEY_V1', 'your-secret-key') === 'your-secret-key') { Console::log('🔴 Using a unique secret key for encryption'); } else { Console::log('🟢 Using a unique secret key for encryption'); } - if($request->getServer('_APP_ENV', 'development') === 'development') { + if(App::getEnv('_APP_ENV', 'development') === 'development') { Console::log('🔴 App enviornment is set for production'); } else { Console::log('🟢 App enviornment is set for production'); } - if($request->getServer('_APP_OPTIONS_ABUSE', 'disabled') === 'disabled') { + if(App::getEnv('_APP_OPTIONS_ABUSE', 'disabled') === 'disabled') { Console::log('🔴 Abuse protection is enabled'); } else { Console::log('🟢 Abuse protection is enabled'); } - $authWhitelistEmails = $request->getServer('_APP_CONSOLE_WHITELIST_EMAILS', null); - $authWhitelistIPs = $request->getServer('_APP_CONSOLE_WHITELIST_IPS', null); - $authWhitelistDomains = $request->getServer('_APP_CONSOLE_WHITELIST_DOMAINS', null); + $authWhitelistEmails = App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null); + $authWhitelistIPs = App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null); + $authWhitelistDomains = App::getEnv('_APP_CONSOLE_WHITELIST_DOMAINS', null); if(empty($authWhitelistEmails) && empty($authWhitelistDomains) @@ -96,7 +75,7 @@ $cli Console::log('🟢 Console access limits are enabled'); } - if(empty($request->getServer('_APP_OPTIONS_FORCE_HTTPS', null))) { + if(empty(App::getEnv('_APP_OPTIONS_FORCE_HTTPS', null))) { Console::log('🔴 HTTP force option is disabled'); } else { @@ -132,7 +111,7 @@ $cli Console::error('Cache............disconnected 👎'); } - if($request->getServer('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled + if(App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled try { $antiVirus = new Network('clamav', 3310); @@ -161,8 +140,8 @@ $cli Console::error('SMTP.............disconnected 👎'); } - $host = $request->getServer('_APP_STATSD_HOST', 'telegraf'); - $port = $request->getServer('_APP_STATSD_PORT', 8125); + $host = App::getEnv('_APP_STATSD_HOST', 'telegraf'); + $port = App::getEnv('_APP_STATSD_PORT', 8125); if($fp = @\fsockopen('udp://'.$host, $port, $errCode, $errStr, 2)){ Console::success('StatsD..............connected 👍'); @@ -171,8 +150,8 @@ $cli Console::error('StatsD...........disconnected 👎'); } - $host = $request->getServer('_APP_INFLUXDB_HOST', ''); - $port = $request->getServer('_APP_INFLUXDB_PORT', ''); + $host = App::getEnv('_APP_INFLUXDB_HOST', ''); + $port = App::getEnv('_APP_INFLUXDB_PORT', ''); if($fp = @\fsockopen($host, $port, $errCode, $errStr, 2)){ Console::success('InfluxDB............connected 👍'); @@ -238,10 +217,10 @@ $cli try { Console::log(''); - $version = \json_decode(@\file_get_contents($request->getServer('_APP_HOME', 'http://localhost').'/v1/health/version'), true); + $version = \json_decode(@\file_get_contents(App::getEnv('_APP_HOME', 'http://localhost').'/v1/health/version'), true); if($version && isset($version['version'])) { - if(\version_compare($version['version'], $request->getServer('_APP_VERSION', 'UNKNOWN')) === 0) { + if(\version_compare($version['version'], App::getEnv('_APP_VERSION', 'UNKNOWN')) === 0) { Console::info('You are running the latest version of '.APP_NAME.'! 🥳'); } else { @@ -254,6 +233,4 @@ $cli } catch (\Throwable $th) { Console::error('Failed to check for a newer version'."\n"); } - }); - -$cli->run(); + }); \ No newline at end of file diff --git a/app/tasks/install.php b/app/tasks/install.php new file mode 100644 index 0000000000..3b3aa4ea67 --- /dev/null +++ b/app/tasks/install.php @@ -0,0 +1,152 @@ +task('install') + ->desc('Install Appwrite') + ->action(function () { + /** + * 1. Start - DONE + * 2. Check for older setup and get older version - DONE + * 2.1 If older version is equal or bigger(?) than current version, **stop setup** + * 2.2. Get ENV vars - DONE + * 2.2.1 Fetch from older docker-compose.yml file + * 2.2.2 Fetch from older .env file (manually parse) + * 2.3 Use old ENV vars as default values + * 2.4 Ask for all required vars not given as CLI args and if in interactive mode + * Otherwise, just use default vars. - DONE + * 3. Ask user to backup important volumes, env vars, and SQL tables + * In th future we can try and automate this for smaller/medium size setups + * 4. Drop new docker-compose.yml setup (located inside the container, no network dependencies with appwrite.io) - DONE + * 5. Run docker-compose up -d - DONE + * 6. Run data migration + */ + $vars = Config::getParam('variables'); + $path = '/usr/src/code/appwrite'; + $version = null; + $defaultHTTPPort = '80'; + $defaultHTTPSPort = '443'; + + Console::success('Starting Appwrite installation...'); + + // Create directory with write permissions + if (null !== $path && !\file_exists(\dirname($path))) { + if (!@\mkdir(\dirname($path), 0755, true)) { + Console::error('Can\'t create directory '.\dirname($path)); + exit(1); + } + } + + $data = @file_get_contents($path.'/docker-compose.yml'); + + if($data !== false) { + $compose = new Compose($data); + $appwrite = $compose->getService('appwrite'); + $version = ($appwrite) ? $appwrite->getImageVersion() : $version; + $ports = $compose->getService('traefik')->getPorts(); + + if($version) { + foreach($compose->getServices() as $service) { // Fetch all env vars from previous compose file + if(!$service) { + continue; + } + + $env = $service->getEnvironment()->list(); + + foreach ($env as $key => $value) { + foreach($vars as &$var) { + if($var['name'] === $key) { + $var['default'] = $value; + } + } + } + } + + $data = @file_get_contents($path.'/.env'); + + if($data !== false) { // Fetch all env vars from previous .env file + $env = new Env($data); + + foreach ($env->list() as $key => $value) { + foreach($vars as &$var) { + if($var['name'] === $key) { + $var['default'] = $value; + } + } + } + } + + foreach ($ports as $key => $value) { + if($value === $defaultHTTPPort) { + $defaultHTTPPort = $key; + } + + if($value === $defaultHTTPSPort) { + $defaultHTTPSPort = $key; + } + } + } + } + + $httpPort = Console::confirm('Choose your server HTTP port: (default: '.$defaultHTTPPort.')'); + $httpPort = ($httpPort) ? $httpPort : $defaultHTTPPort; + + $httpsPort = Console::confirm('Choose your server HTTPS port: (default: '.$defaultHTTPSPort.')'); + $httpsPort = ($httpsPort) ? $httpsPort : $defaultHTTPPort; + + $input = []; + + foreach($vars as $key => $var) { + if(!$var['required']) { + $input[$var['name']] = $var['default']; + continue; + } + + $input[$var['name']] = Console::confirm($var['question'].' (default: \''.$var['default'].'\')'); + + if(empty($input[$key])) { + $input[$var['name']] = $var['default']; + } + } + + $templateForCompose = new View(__DIR__.'/../views/install/compose.phtml'); + $templateForEnv = new View(__DIR__.'/../views/install/env.phtml'); + + $templateForCompose + ->setParam('httpPort', $httpPort) + ->setParam('httpsPort', $httpsPort) + ->setParam('version', APP_VERSION_STABLE) + ; + + $templateForEnv + ->setParam('vars', $input) + ; + + if(!file_put_contents($path.'/docker-compose.yml', $templateForCompose->render(false))) { + Console::error('Failed to save Docker Compose file'); + exit(1); + } + + if(!file_put_contents($path.'/.env', $templateForEnv->render(false))) { + Console::error('Failed to save environment variables file'); + exit(1); + } + + $stdout = ''; + $stderr = ''; + + //Console::execute("docker-compose -f {$path}.'/docker-compose.yml up -d --remove-orphans", null, $stdout, $stderr); + + if ($stderr !== '') { + Console::error("Failed to install Appwrite dockers"); + } else { + Console::success("Appwrite installed successfully"); + } + }); \ No newline at end of file diff --git a/app/tasks/migrate.php b/app/tasks/migrate.php index fe833e27f6..efa06e1fed 100644 --- a/app/tasks/migrate.php +++ b/app/tasks/migrate.php @@ -1,18 +1,13 @@ -#!/bin/env php get('db'); $callbacks = [ @@ -152,9 +147,7 @@ function fixDocument(Document $document) { ->removeAttribute('$uid') ; - //Console::log('Switched from $uid to $id: '.$document->getCollection().'/'.$document->getId()); - - foreach($document as &$attr) { + foreach($document as &$attr) { // Handle child documents if($attr instanceof Document) { $attr = fixDocument($attr); } @@ -172,7 +165,7 @@ function fixDocument(Document $document) { } $cli - ->task('run') + ->task('migrate') ->action(function () use ($console, $projectDB, $consoleDB, $callbacks) { Console::success('Starting Data Migration'); @@ -214,6 +207,4 @@ $cli } Console::success('Data Migration Completed'); - }); - -$cli->run(); + }); \ No newline at end of file diff --git a/app/tasks/sdks.php b/app/tasks/sdks.php index bd87ccff93..b70dbc7be3 100644 --- a/app/tasks/sdks.php +++ b/app/tasks/sdks.php @@ -1,11 +1,8 @@ -#!/bin/env php run(); + }); \ No newline at end of file diff --git a/app/tasks/ssl.php b/app/tasks/ssl.php new file mode 100644 index 0000000000..2c32324fa3 --- /dev/null +++ b/app/tasks/ssl.php @@ -0,0 +1,23 @@ +task('ssl') + ->desc('Validate server certificates') + ->action(function () { + $domain = App::getEnv('_APP_DOMAIN', ''); + + Console::log('Issue a TLS certificate for master domain ('.$domain.') in 30 seconds. + Make sure your domain points to your server or restart to try again.'); + + ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ + 'document' => [], + 'domain' => $domain, + 'validateTarget' => false, + 'validateCNAME' => false, + ]); + }); \ No newline at end of file diff --git a/app/views/console/account/index.phtml b/app/views/console/account/index.phtml index da40715f19..c8c86f729f 100644 --- a/app/views/console/account/index.phtml +++ b/app/views/console/account/index.phtml @@ -10,6 +10,8 @@
  • +

    Overview

    +
    -

    Overview

    -
    User Avatar @@ -184,7 +184,7 @@
  • -

    Devices

    +

    Devices

    - + - + diff --git a/app/views/console/comps/header.phtml b/app/views/console/comps/header.phtml index 9300582af4..921a9c933e 100644 --- a/app/views/console/comps/header.phtml +++ b/app/views/console/comps/header.phtml @@ -1,7 +1,7 @@ -
    +
    @@ -14,7 +14,7 @@ data-switch data-ls-bind="{{router.params.project}}" data-unsync="1" - data-ls-loop="projects" data-ls-as="option" aria-label="Switch Project"> + data-ls-loop="projects.projects" data-ls-as="option" aria-label="Switch Project"> @@ -72,8 +72,8 @@ data-analytics-label="Logo Link"> Appwrite Logo - Appwrite Light Logo - Appwrite Dark Logo + Appwrite Light Logo + Appwrite Dark Logo
    @@ -120,6 +120,16 @@ Users
  • +
  • + + + Functions + +
MANAGE @@ -193,8 +203,8 @@
- Appwrite Light Logo - Appwrite Dark Logo + Appwrite Light Logo + Appwrite Dark Logo
diff --git a/app/views/console/keys/index.phtml b/app/views/console/keys/index.phtml index 0cb4c59cea..71b1054350 100644 --- a/app/views/console/keys/index.phtml +++ b/app/views/console/keys/index.phtml @@ -55,8 +55,8 @@ $scopes = $this->getParam('scopes', []);
$scope) : ?> -
- +
+  
@@ -150,8 +150,8 @@ $scopes = $this->getParam('scopes', []);
$scope) : ?> -
- +
+  
diff --git a/app/views/console/storage/index.phtml b/app/views/console/storage/index.phtml index 234082d656..d924912324 100644 --- a/app/views/console/storage/index.phtml +++ b/app/views/console/storage/index.phtml @@ -103,8 +103,8 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0); Filename - Type - Size + Type + Size Created @@ -194,13 +194,13 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
- + - + - + diff --git a/app/views/console/tasks/index.phtml b/app/views/console/tasks/index.phtml index 5380dcde60..4c906fba98 100644 --- a/app/views/console/tasks/index.phtml +++ b/app/views/console/tasks/index.phtml @@ -37,7 +37,7 @@
- +   SSL/TLS Disabled @@ -45,7 +45,7 @@
- + @@ -56,16 +56,16 @@ - +
- + - + None. @@ -100,7 +100,7 @@ @@ -172,8 +172,8 @@

Warning: Untrusted or self-signed certificates may not be secure. @@ -258,8 +258,8 @@

-   Play -   Pause +   Play   +   Pause  
diff --git a/app/views/console/users/index.phtml b/app/views/console/users/index.phtml index 321220d0af..bccddbb93c 100644 --- a/app/views/console/users/index.phtml +++ b/app/views/console/users/index.phtml @@ -128,7 +128,7 @@ $providers = $this->getParam('providers', []); Blocked
- + @@ -256,7 +256,7 @@ $providers = $this->getParam('providers', []); - + diff --git a/app/views/console/users/team.phtml b/app/views/console/users/team.phtml index 262fd23a92..9e1ff6b483 100644 --- a/app/views/console/users/team.phtml +++ b/app/views/console/users/team.phtml @@ -31,7 +31,7 @@
  • -

    General

    +

    Overview

    @@ -110,8 +110,8 @@
       - - + +
      Pending Approval
  • @@ -200,7 +200,7 @@
    • -
    • Created:
    • +
    • Created:
    • -

      General

      +

      Overview

    • diff --git a/app/workers/audits.php b/app/workers/audits.php index 9ffb85e57e..2c7e67a6e8 100644 --- a/app/workers/audits.php +++ b/app/workers/audits.php @@ -1,13 +1,14 @@ args['userAgent']; $ip = $this->args['ip']; $data = $this->args['data']; - $pdo = $register->get('db', true); + $db = $register->get('db', true); - $adapter = new AuditAdapter($pdo); + $adapter = new AuditAdapter($db); $adapter->setNamespace('app_'.$projectId); $audit = new Audit($adapter); diff --git a/app/workers/certificates.php b/app/workers/certificates.php index 22c3a14e63..4b81a9ea9a 100644 --- a/app/workers/certificates.php +++ b/app/workers/certificates.php @@ -1,9 +1,12 @@ setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $consoleDB->setNamespace('app_console'); // Main DB + $consoleDB->setMocks(Config::getParam('collections', [])); /** * 1. Get new domain document - DONE @@ -62,7 +70,7 @@ class CertificatesV1 } if($validateTarget) { - $target = new Domain($request->getServer('_APP_DOMAIN_TARGET', '')); + $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); if(!$target->isKnown() || $target->isTest()) { throw new Exception('Unreachable CNAME target ('.$target->get().'), plesse use a domain with a public suffix.'); @@ -104,10 +112,10 @@ class CertificatesV1 throw new Exception('Renew isn\'t required'); } - $staging = (Config::getParam('env') === App::MODE_TYPE_PRODUCTION) ? '' : ' --dry-run'; + $staging = (App::isProduction()) ? '' : ' --dry-run'; $response = \shell_exec("certbot certonly --webroot --noninteractive --agree-tos{$staging} \ - --email ".$request->getServer('_APP_SYSTEM_EMAIL_ADDRESS', 'security@localhost.test')." \ + --email ".App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', 'security@localhost.test')." \ -w ".APP_STORAGE_CERTIFICATES." \ -d {$domain->get()}"); diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 8d12bd64d0..c8f423c641 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -4,11 +4,14 @@ require_once __DIR__.'/../init.php'; \cli_set_process_title('Deletes V1 Worker'); -echo APP_NAME.' deletes worker v1 has started'; +echo APP_NAME.' deletes worker v1 has started'."\n"; use Appwrite\Database\Database; +use Appwrite\Database\Adapter\MySQL as MySQLAdapter; +use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Document; use Appwrite\Storage\Device\Local; +use Utopia\Config\Config; class DeletesV1 { @@ -20,8 +23,6 @@ class DeletesV1 public function perform() { - global $consoleDB, $request; - $document = $this->args['document']; $document = new Document($document); @@ -45,7 +46,12 @@ class DeletesV1 protected function deleteProject(Document $document) { - global $consoleDB; + global $register; + + $consoleDB = new Database(); + $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $consoleDB->setNamespace('app_console'); // Main DB + $consoleDB->setMocks(Config::getParam('collections', [])); // Delete all DBs $consoleDB->deleteNamespace($document->getId()); diff --git a/app/workers/functions.php b/app/workers/functions.php new file mode 100644 index 0000000000..902fa479f7 --- /dev/null +++ b/app/workers/functions.php @@ -0,0 +1,360 @@ + Network (docker stats --no-stream --format="{{.NetIO}}" appwrite) + * -> CPU Time + * -> Invoctions (+1) + * Report to usage worker + */ + +// Double-check Cleanup + +class FunctionsV1 +{ + public $args = []; + + public function setUp() + { + } + + public function perform() + { + global $environments, $register; + + $projectId = $this->args['projectId']; + $functionId = $this->args['functionId']; + $functionTag = $this->args['functionTag']; + $executionId = $this->args['executionId']; + $functionTrigger = $this->args['functionTrigger']; + + $projectDB = new Database(); + $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $projectDB->setNamespace('app_'.$projectId); + $projectDB->setMocks(Config::getParam('collections', [])); + + Authorization::disable(); + $function = $projectDB->getDocument($functionId); + Authorization::reset(); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + Authorization::disable(); + $tag = $projectDB->getDocument($functionTag); + Authorization::reset(); + + if($tag->getAttribute('functionId') !== $function->getId()) { + throw new Exception('Tag not found', 404); + } + + Authorization::disable(); + + $execution = $projectDB->getDocument($executionId); + + if (empty($execution->getId()) || Database::SYSTEM_COLLECTION_EXECUTIONS != $execution->getCollection()) { + $execution = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_EXECUTIONS, + '$permissions' => [ + 'read' => [], + 'write' => [], + ], + 'dateCreated' => \time(), + 'functionId' => $function->getId(), + 'status' => 'processing', // waiting / processing / completed / failed + 'exitCode' => 0, + 'stdout' => '', + 'stderr' => '', + 'time' => 0, + ]); + + if (false === $execution) { + throw new Exception('Failed saving execution to DB', 500); + } + } + + Authorization::reset(); + + $environment = (isset($environments[$function->getAttribute('env', '')])) + ? $environments[$function->getAttribute('env', '')] + : null; + + if(\is_null($environment)) { + throw new Exception('Environment "'.$function->getAttribute('env', '').' is not supported'); + } + + $vars = \array_merge($function->getAttribute('vars', []), [ + 'APPWRITE_FUNCTION_ID' => $functionId, + 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), + 'APPWRITE_FUNCTION_TAG' => $functionTag, + 'APPWRITE_FUNCTION_TRIGGER' => $functionTrigger, + 'APPWRITE_FUNCTION_ENV_NAME' => $environment['name'], + 'APPWRITE_FUNCTION_ENV_VERSION' => $environment['version'], + ]); + + \array_walk($vars, function (&$value, $key) { + $value = (empty($value)) ? 'null' : $value; + $value = "\t\t\t--env {$key}={$value} \\"; + }); + + $tagPath = $tag->getAttribute('codePath', ''); + $tagPathTarget = '/tmp/project-'.$projectId.'/'.$tag->getId().'/code.tar.gz'; + $tagPathTargetDir = \pathinfo($tagPathTarget, PATHINFO_DIRNAME); + $container = 'appwrite-function-'.$tag->getId(); + $command = \escapeshellcmd($tag->getAttribute('command', '')); + + if(!\is_readable($tagPath)) { + throw new Exception('Code is not readable: '.$tag->getAttribute('codePath', '')); + } + + if (!\file_exists($tagPathTargetDir)) { + if (!\mkdir($tagPathTargetDir, 0755, true)) { + throw new Exception('Can\'t create directory '.$tagPathTargetDir); + } + } + + if (!\file_exists($tagPathTarget)) { + if(!\copy($tagPath, $tagPathTarget)) { + throw new Exception('Can\'t create temporary code file '.$tagPathTarget); + } + } + + $stdout = ''; + $stderr = ''; + + $executionStart = \microtime(true); + + $exitCode = Console::execute('docker ps --all --format "name={{.Names}}&status={{.Status}}&labels={{.Labels}}" --filter label=appwrite-type=function' + , null, $stdout, $stderr, 30); + + $executionEnd = \microtime(true); + + $list = []; + $stdout = \explode("\n", $stdout); + + \array_map(function($value) use (&$list) { + $container = []; + + \parse_str($value, $container); + + if(isset($container['name'])) { + $container = [ + 'name' => $container['name'], + 'online' => (\substr($container['status'], 0, 2) === 'Up'), + 'status' => $container['status'], + 'labels' => $container['labels'], + ]; + + \array_map(function($value) use (&$container) { + $value = \explode('=', $value); + + if(isset($value[0]) && isset($value[1])) { + $container[$value[0]] = $value[1]; + } + }, \explode(',', $container['labels'])); + + $list[$container['name']] = $container; + } + }, $stdout); + + Console::info("Functions listed in " . ($executionEnd - $executionStart) . " seconds with exit code {$exitCode}"); + + if(isset($list[$container]) && !$list[$container]['online']) { + $stdout = ''; + $stderr = ''; + + if(Console::execute("docker rm {$container}", null, $stdout, $stderr, 30) !== 0) { + throw new Exception('Failed to remove offline container: '.$stderr); + } + + unset($list[$container]); + } + + if(!isset($list[$container])) { // Create contianer if not ready + $stdout = ''; + $stderr = ''; + + $executionStart = \microtime(true); + + $exitCode = Console::execute("docker run \ + -d \ + --entrypoint=\"\" \ + --cpus=4 \ + --memory=128m \ + --memory-swap=128m \ + --rm \ + --name={$container} \ + --label appwrite-type=function \ + --label appwrite-created=".\time()." \ + --volume {$tagPathTargetDir}:/tmp:rw \ + --workdir /usr/local/src \ + ".\implode("\n", $vars)." + {$environment['image']} \ + sh -c 'mv /tmp/code.tar.gz /usr/local/src/code.tar.gz && tar -zxf /usr/local/src/code.tar.gz --strip 1 && rm /usr/local/src/code.tar.gz && tail -f /dev/null'" + , null, $stdout, $stderr, 30); + + $executionEnd = \microtime(true); + + if($exitCode !== 0) { + throw new Exception('Failed to create function environment: '.$stderr); + } + + Console::info("Function created in " . ($executionEnd - $executionStart) . " seconds with exit code {$exitCode}"); + } + else { + Console::info('Container is ready to run'); + } + + $stdout = ''; + $stderr = ''; + + $executionStart = \microtime(true); + + $exitCode = Console::execute("docker exec {$container} {$command}" + , null, $stdout, $stderr, $function->getAttribute('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900))); + + $executionEnd = \microtime(true); + + Console::info("Function executed in " . ($executionEnd - $executionStart) . " seconds with exit code {$exitCode}"); + + Authorization::disable(); + + $execution = $projectDB->updateDocument(array_merge($execution->getArrayCopy(), [ + 'tagId' => $tag->getId(), + 'status' => ($exitCode === 0) ? 'completed' : 'failed', + 'exitCode' => $exitCode, + 'stdout' => mb_substr($stdout, -4000), // log last 4000 chars output + 'stderr' => mb_substr($stderr, -4000), // log last 4000 chars output + 'time' => ($executionEnd - $executionStart), + ])); + + Authorization::reset(); + + if (false === $function) { + throw new Exception('Failed saving execution to DB', 500); + } + + Console::success(count($list).' running containers counted'); + + $max = (int) App::getEnv('_APP_FUNCTIONS_CONTAINERS'); + + if(\count($list) > $max) { + Console::info('Starting containers cleanup'); + + $sorted = []; + + foreach($list as $env) { + $sorted[] = [ + 'name' => $env['name'], + 'created' => (int)$env['appwrite-created'] + ]; + } + + \usort($sorted, function ($item1, $item2) { + return $item1['created'] <=> $item2['created']; + }); + + while(\count($sorted) > $max) { + $first = \array_shift($sorted); + $stdout = ''; + $stderr = ''; + + if(Console::execute("docker stop {$first['name']}", null, $stdout, $stderr, 30) !== 0) { + Console::error('Failed to remove container: '.$stderr); + } + + Console::info('Removed container: '.$first['name']); + } + } + } + + public function tearDown() + { + } +} diff --git a/app/workers/mails.php b/app/workers/mails.php index 6835f3071f..1ea82eec30 100644 --- a/app/workers/mails.php +++ b/app/workers/mails.php @@ -1,10 +1,12 @@ args['event']; + $from = $this->args['from']; $recipient = $this->args['recipient']; $name = $this->args['name']; $subject = $this->args['subject']; $body = $this->args['body']; - $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ + /** @var \PHPMailer\PHPMailer\PHPMailer $mail */ + $mail = $register->get('smtp'); + // Set project mail + /*$register->get('smtp') + ->setFrom( + App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), + ($project->getId() === 'console') + ? \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')) + : \sprintf(Locale::getText('account.emails.team'), $project->getAttribute('name') + ) + );*/ + + $mail->clearAddresses(); + $mail->clearAllRecipients(); + $mail->clearReplyTos(); + $mail->clearAttachments(); + $mail->clearBCCs(); + $mail->clearCCs(); + + $mail->setFrom(App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), (empty($from) ? \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')) : $from)); $mail->addAddress($recipient, $name); $mail->Subject = $subject; $mail->Body = $body; diff --git a/app/workers/tasks.php b/app/workers/tasks.php index db664d567e..cfb908cb86 100644 --- a/app/workers/tasks.php +++ b/app/workers/tasks.php @@ -1,15 +1,19 @@ setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $consoleDB->setNamespace('app_console'); // Main DB + $consoleDB->setMocks(Config::getParam('collections', [])); /* * 1. Get Original Task @@ -57,7 +66,7 @@ class TasksV1 $task = $consoleDB->getDocument($taskId); - Authorization::enable(); + Authorization::reset(); if (\is_null($task->getId()) || Database::SYSTEM_COLLECTION_TASKS !== $task->getCollection()) { throw new Exception('Task Not Found'); @@ -95,8 +104,8 @@ class TasksV1 \curl_setopt($ch, CURLOPT_HEADER, 0); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(APP_USERAGENT, - Config::getParam('version'), - $request->getServer('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) + App::getEnv('_APP_VERSION', 'UNKNOWN'), + App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) )); \curl_setopt( $ch, @@ -187,7 +196,7 @@ class TasksV1 throw new Exception('Failed saving tasks to DB'); } - Authorization::enable(); + Authorization::reset(); // ResqueScheduler::enqueueAt($next, 'v1-tasks', 'TasksV1', $task->getArrayCopy()); // Sync task rescheduale diff --git a/app/workers/usage.php b/app/workers/usage.php index 0d0811c33f..56da254f4f 100644 --- a/app/workers/usage.php +++ b/app/workers/usage.php @@ -1,12 +1,13 @@ args['projectId']; - $method = $this->args['method']; - $request = $this->args['request']; - $response = $this->args['response']; - $storage = $this->args['storage']; - $statsd = $register->get('statsd', true); - $tags = ",project={$projectId},version=".Config::getParam('version').''; + $projectId = $this->args['projectId']; + $httpMethod = $this->args['httpMethod']; + $httpRequest = $this->args['httpRequest']; + + $networkRequestSize = $this->args['networkRequestSize']; + $networkResponseSize = $this->args['networkResponseSize']; + + $storage = $this->args['storage']; + + $functionExecution = $this->args['functionExecution']; + $functionExecutionTime = $this->args['functionExecutionTime']; + $functionId = $this->args['functionId']; + + $tags = ",project={$projectId},version=".App::getEnv('_APP_VERSION', 'UNKNOWN').''; // the global namespace is prepended to every key (optional) $statsd->setNamespace('appwrite.usage'); - $statsd->increment('requests.all'.$tags.',method='.\strtolower($method)); + if($httpRequest >= 1) { + $statsd->increment('requests.all'.$tags.',method='.\strtolower($httpMethod)); + } + + if($functionExecution >= 1) { + $statsd->increment('executions.all'.$tags.',functionId='.$functionId); + $statsd->count('executions.time'.$tags.',functionId='.$functionId, $functionExecutionTime); + } - $statsd->count('network.all'.$tags, $request + $response); - $statsd->count('network.inbound'.$tags, $request); - $statsd->count('network.outbound'.$tags, $response); + $statsd->count('network.all'.$tags, $networkRequestSize + $networkResponseSize); + $statsd->count('network.inbound'.$tags, $networkRequestSize); + $statsd->count('network.outbound'.$tags, $networkResponseSize); $statsd->count('storage.all'.$tags, $storage); } diff --git a/app/workers/webhooks.php b/app/workers/webhooks.php index ecd9360d26..323180831f 100644 --- a/app/workers/webhooks.php +++ b/app/workers/webhooks.php @@ -1,14 +1,18 @@ setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $consoleDB->setNamespace('app_console'); // Main DB + $consoleDB->setMocks(Config::getParam('collections', [])); + $errors = []; // Event @@ -35,7 +44,7 @@ class WebhooksV1 $project = $consoleDB->getDocument($projectId); - Authorization::enable(); + Authorization::reset(); if (\is_null($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS !== $project->getCollection()) { throw new Exception('Project Not Found'); @@ -60,8 +69,8 @@ class WebhooksV1 \curl_setopt($ch, CURLOPT_HEADER, 0); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(APP_USERAGENT, - Config::getParam('version'), - $request->getServer('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) + App::getEnv('_APP_VERSION', 'UNKNOWN'), + App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) )); \curl_setopt( $ch, diff --git a/bin/doctor b/bin/doctor index 4934a11e1e..b2a4547156 100755 --- a/bin/doctor +++ b/bin/doctor @@ -1,3 +1,3 @@ -#!/bin/bash +#!/bin/sh -php /usr/share/nginx/html/app/tasks/init.php doctor \ No newline at end of file +php /usr/src/code/app/cli.php doctor $@ \ No newline at end of file diff --git a/bin/install b/bin/install new file mode 100755 index 0000000000..e669e91e6b --- /dev/null +++ b/bin/install @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/cli.php install $@ \ No newline at end of file diff --git a/bin/migrate b/bin/migrate index 6a4d95f9ce..28ebbd19e7 100755 --- a/bin/migrate +++ b/bin/migrate @@ -1,3 +1,3 @@ -#!/bin/bash +#!/bin/sh -php /usr/share/nginx/html/app/tasks/migrate.php run \ No newline at end of file +php /usr/src/code/app/cli.php migrate $@ \ No newline at end of file diff --git a/bin/schedule b/bin/schedule new file mode 100644 index 0000000000..74da3fe1e0 --- /dev/null +++ b/bin/schedule @@ -0,0 +1,3 @@ +#!/bin/sh + +REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" RESQUE_PHP='/usr/src/code/vendor/autoload.php' php /usr/src/code/vendor/bin/resque-scheduler \ No newline at end of file diff --git a/bin/ssl b/bin/ssl new file mode 100755 index 0000000000..83dcf6a026 --- /dev/null +++ b/bin/ssl @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/cli.php ssl $@ \ No newline at end of file diff --git a/bin/start b/bin/start index 4b0616978e..7dd4ecddce 100755 --- a/bin/start +++ b/bin/start @@ -1,8 +1,8 @@ -#!/bin/bash +#!/bin/sh export PHP_VERSION=$PHP_VERSION -chown -Rf www-data.www-data /usr/share/nginx/html/ +chown -Rf www-data.www-data /usr/src/code/ sed 's/%_APP_STORAGE_LIMIT%/'$_APP_STORAGE_LIMIT'/g' /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf @@ -31,7 +31,7 @@ for _curVar in $(env | grep _APP_ | awk -F = '{print $1}');do done # Init server settings -php /usr/share/nginx/html/app/tasks/init.php ssl +php /usr/src/code/app/tasks/init.php ssl # Start supervisord and services /usr/bin/supervisord -n -c /etc/supervisord.conf diff --git a/bin/test b/bin/test old mode 100644 new mode 100755 index 39ad0f846c..a2153fc536 --- a/bin/test +++ b/bin/test @@ -1,3 +1,3 @@ -#!/bin/bash +#!/bin/sh -/usr/share/nginx/html/vendor/bin/phpunit --configuration /usr/share/nginx/html/phpunit.xml \ No newline at end of file +/usr/src/code/vendor/bin/phpunit --configuration /usr/src/code/phpunit.xml $@ \ No newline at end of file diff --git a/bin/worker-audits b/bin/worker-audits new file mode 100644 index 0000000000..99b3eac2e7 --- /dev/null +++ b/bin/worker-audits @@ -0,0 +1,3 @@ +#!/bin/sh + +QUEUE='v1-audits' APP_INCLUDE='/usr/src/code/app/workers/audits.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php \ No newline at end of file diff --git a/bin/worker-certificates b/bin/worker-certificates new file mode 100644 index 0000000000..6d360aff35 --- /dev/null +++ b/bin/worker-certificates @@ -0,0 +1,3 @@ +#!/bin/sh + +QUEUE='v1-certificates' APP_INCLUDE='/usr/src/code/app/workers/certificates.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php \ No newline at end of file diff --git a/bin/worker-deletes b/bin/worker-deletes new file mode 100644 index 0000000000..d053381db7 --- /dev/null +++ b/bin/worker-deletes @@ -0,0 +1,3 @@ +#!/bin/sh + +QUEUE='v1-deletes' APP_INCLUDE='/usr/src/code/app/workers/deletes.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php \ No newline at end of file diff --git a/bin/worker-functions b/bin/worker-functions new file mode 100644 index 0000000000..787addab0a --- /dev/null +++ b/bin/worker-functions @@ -0,0 +1,3 @@ +#!/bin/sh + +QUEUE='v1-functions' APP_INCLUDE='/usr/src/code/app/workers/functions.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php \ No newline at end of file diff --git a/bin/worker-mails b/bin/worker-mails new file mode 100644 index 0000000000..8a1a9de977 --- /dev/null +++ b/bin/worker-mails @@ -0,0 +1,3 @@ +#!/bin/sh + +QUEUE='v1-mails' APP_INCLUDE='/usr/src/code/app/workers/mails.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php \ No newline at end of file diff --git a/bin/worker-tasks b/bin/worker-tasks new file mode 100644 index 0000000000..236b34cb75 --- /dev/null +++ b/bin/worker-tasks @@ -0,0 +1,3 @@ +#!/bin/sh + +QUEUE='v1-tasks' APP_INCLUDE='/usr/src/code/app/workers/tasks.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php \ No newline at end of file diff --git a/bin/worker-usage b/bin/worker-usage new file mode 100644 index 0000000000..2b967f8f08 --- /dev/null +++ b/bin/worker-usage @@ -0,0 +1,3 @@ +#!/bin/sh + +QUEUE='v1-usage' APP_INCLUDE='/usr/src/code/app/workers/usage.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php \ No newline at end of file diff --git a/bin/worker-webhooks b/bin/worker-webhooks new file mode 100644 index 0000000000..d09a4a39ac --- /dev/null +++ b/bin/worker-webhooks @@ -0,0 +1,3 @@ +#!/bin/sh + +QUEUE='v1-webhooks' APP_INCLUDE='/usr/src/code/app/workers/webhooks.php' REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php \ No newline at end of file diff --git a/composer.json b/composer.json index bde9cb7a31..b7671ffec4 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "ext-imagick": "*", "ext-mbstring": "*", "ext-json": "*", + "ext-yaml": "*", "ext-dom": "*", "ext-redis": "*", "ext-pdo": "*", @@ -32,13 +33,13 @@ "appwrite/php-clamav": "1.0.*", - "utopia-php/framework": "0.4.1", + "utopia-php/framework": "0.9.0", "utopia-php/abuse": "0.2.*", "utopia-php/audit": "0.3.*", "utopia-php/cache": "0.2.*", "utopia-php/cli": "0.6.2", "utopia-php/config": "0.2.*", - "utopia-php/locale": "0.2.*", + "utopia-php/locale": "0.3.*", "utopia-php/registry": "0.2.*", "utopia-php/domains": "0.2.*", @@ -52,6 +53,7 @@ "phpmailer/phpmailer": "6.1.6" }, "require-dev": { + "swoole/ide-helper": "4.5.2", "appwrite/sdk-generator": "master", "phpunit/phpunit": "^7.0" }, diff --git a/composer.lock b/composer.lock index 43ed0c57b1..fe22b1171e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7f6cbe77fe2e0f8bdff33c37a4d9ca11", + "content-hash": "c0550b37e526b97fc0fe0b8ff3025350", "packages": [ { "name": "appwrite/php-clamav", @@ -146,12 +146,12 @@ "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd" + "reference": "8a7ecad675253e4654ea05505233285377405215" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/95c63ab2117a72f48f5a55da9740a3273d45b7fd", - "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8a7ecad675253e4654ea05505233285377405215", + "reference": "8a7ecad675253e4654ea05505233285377405215", "shasum": "" }, "require": { @@ -194,25 +194,39 @@ "ssl", "tls" ], - "time": "2020-04-08T08:27:21+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-08-23T12:54:47+00:00" }, { "name": "dasprid/enum", - "version": "1.0.0", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/DASPRiD/Enum.git", - "reference": "631ef6e638e9494b0310837fa531bedd908fc22b" + "reference": "6ccc0d7141a7f149e3c56cb0ce5f05d9152cfd07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/631ef6e638e9494b0310837fa531bedd908fc22b", - "reference": "631ef6e638e9494b0310837fa531bedd908fc22b", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6ccc0d7141a7f149e3c56cb0ce5f05d9152cfd07", + "reference": "6ccc0d7141a7f149e3c56cb0ce5f05d9152cfd07", "shasum": "" }, "require-dev": { - "phpunit/phpunit": "^6.4", - "squizlabs/php_codesniffer": "^3.1" + "phpunit/phpunit": "^7 | ^8 | ^9", + "squizlabs/php_codesniffer": "^3.4" }, "type": "library", "autoload": { @@ -228,7 +242,8 @@ { "name": "Ben Scholzen 'DASPRiD'", "email": "mail@dasprids.de", - "homepage": "https://dasprids.de/" + "homepage": "https://dasprids.de/", + "role": "Developer" } ], "description": "PHP 7.1 enum implementation", @@ -236,7 +251,7 @@ "enum", "map" ], - "time": "2017-10-25T22:45:27+00:00" + "time": "2020-07-30T16:37:13+00:00" }, { "name": "domnikl/statsd", @@ -392,12 +407,12 @@ "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e" + "reference": "e8ed4dbf49b260ff129ff0e0400718c3269971bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", - "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/e8ed4dbf49b260ff129ff0e0400718c3269971bf", + "reference": "e8ed4dbf49b260ff129ff0e0400718c3269971bf", "shasum": "" }, "require": { @@ -451,7 +466,29 @@ "rest", "web service" ], - "time": "2020-06-16T21:01:06+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://github.com/alexeyshockov", + "type": "github" + }, + { + "url": "https://github.com/gmponos", + "type": "github" + }, + { + "url": "https://github.com/sagikazarmark", + "type": "github" + } + ], + "time": "2020-07-02T06:52:04+00:00" }, { "name": "guzzlehttp/promises", @@ -638,29 +675,29 @@ }, { "name": "maxmind-db/reader", - "version": "v1.6.0", + "version": "v1.7.0", "source": { "type": "git", "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", - "reference": "febd4920bf17c1da84cef58e56a8227dfb37fbe4" + "reference": "942553da239f12051275f9c666538b5dd09e2908" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/febd4920bf17c1da84cef58e56a8227dfb37fbe4", - "reference": "febd4920bf17c1da84cef58e56a8227dfb37fbe4", + "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/942553da239f12051275f9c666538b5dd09e2908", + "reference": "942553da239f12051275f9c666538b5dd09e2908", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=7.2" }, "conflict": { - "ext-maxminddb": "<1.6.0,>=2.0.0" + "ext-maxminddb": "<1.7.0,>=2.0.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "2.*", "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpcov": "^3.0", - "phpunit/phpunit": "5.*", + "phpunit/phpcov": ">=6.0.0", + "phpunit/phpunit": ">=8.0.0,<10.0.0", "squizlabs/php_codesniffer": "3.*" }, "suggest": { @@ -694,7 +731,7 @@ "geolocation", "maxmind" ], - "time": "2019-12-19T22:59:03+00:00" + "time": "2020-08-07T22:10:05+00:00" }, { "name": "maxmind/web-service-common", @@ -894,6 +931,12 @@ } ], "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "funding": [ + { + "url": "https://github.com/synchro", + "type": "github" + } + ], "time": "2020-05-27T12:24:03+00:00" }, { @@ -1169,12 +1212,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe" + "reference": "045643b91eaa34c4c37150ac477765c13552af33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", - "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/045643b91eaa34c4c37150ac477765c13552af33", + "reference": "045643b91eaa34c4c37150ac477765c13552af33", "shasum": "" }, "require": { @@ -1246,7 +1289,7 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-08-04T21:02:56+00:00" }, { "name": "symfony/polyfill-intl-normalizer", @@ -1766,16 +1809,16 @@ }, { "name": "utopia-php/framework", - "version": "0.4.0", + "version": "0.9.0", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "30aeb2aeecf8ea2ab83242efad0f5f9fab8d4be5" + "reference": "2d40a89b6d5d6a9c08423bff1dcebe1809b0efc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/30aeb2aeecf8ea2ab83242efad0f5f9fab8d4be5", - "reference": "30aeb2aeecf8ea2ab83242efad0f5f9fab8d4be5", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/2d40a89b6d5d6a9c08423bff1dcebe1809b0efc7", + "reference": "2d40a89b6d5d6a9c08423bff1dcebe1809b0efc7", "shasum": "" }, "require": { @@ -1806,20 +1849,20 @@ "php", "upf" ], - "time": "2020-06-25T18:21:48+00:00" + "time": "2020-08-26T17:18:23+00:00" }, { "name": "utopia-php/locale", - "version": "0.2.1", + "version": "0.3.2", "source": { "type": "git", "url": "https://github.com/utopia-php/locale.git", - "reference": "f2ed7f0b50fe961d65600871e8f8d9dea3167500" + "reference": "89c488fbff65fc87c048786c3d76b6003fbaa833" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/locale/zipball/f2ed7f0b50fe961d65600871e8f8d9dea3167500", - "reference": "f2ed7f0b50fe961d65600871e8f8d9dea3167500", + "url": "https://api.github.com/repos/utopia-php/locale/zipball/89c488fbff65fc87c048786c3d76b6003fbaa833", + "reference": "89c488fbff65fc87c048786c3d76b6003fbaa833", "shasum": "" }, "require": { @@ -1852,20 +1895,20 @@ "upf", "utopia" ], - "time": "2020-06-20T11:41:46+00:00" + "time": "2020-06-29T20:53:16+00:00" }, { "name": "utopia-php/registry", - "version": "0.2.1", + "version": "0.2.3", "source": { "type": "git", "url": "https://github.com/utopia-php/registry.git", - "reference": "2cd2d318067a6b310c35c9f3ad73d2ce3c1ca7d5" + "reference": "948aa82942a1bde8b97ec74fdc1a83fb24122788" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/registry/zipball/2cd2d318067a6b310c35c9f3ad73d2ce3c1ca7d5", - "reference": "2cd2d318067a6b310c35c9f3ad73d2ce3c1ca7d5", + "url": "https://api.github.com/repos/utopia-php/registry/zipball/948aa82942a1bde8b97ec74fdc1a83fb24122788", + "reference": "948aa82942a1bde8b97ec74fdc1a83fb24122788", "shasum": "" }, "require": { @@ -1899,7 +1942,7 @@ "upf", "utopia" ], - "time": "2020-06-20T11:46:06+00:00" + "time": "2020-07-04T22:14:07+00:00" } ], "packages-dev": [ @@ -1909,7 +1952,7 @@ "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator", - "reference": "404cf6bb4c75f8ae3b2419e04f6afd215ed706a5" + "reference": "95e29472f8416eb8ce6a83f6b5af9c139ec73ea2" }, "require": { "ext-curl": "*", @@ -1939,7 +1982,7 @@ } ], "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", - "time": "2020-07-26T07:11:06+00:00" + "time": "2020-08-17T18:12:07+00:00" }, { "name": "doctrine/instantiator", @@ -1995,6 +2038,20 @@ "constructor", "instantiate" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], "time": "2020-06-15T18:51:04+00:00" }, { @@ -2152,6 +2209,12 @@ "object", "object graph" ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], "time": "2020-06-29T13:22:24+00:00" }, { @@ -2311,23 +2374,23 @@ "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "1ac416df3f66c542f2d3688925105b539f064b64" + "reference": "13d9a6bb0b5bbb89d085c7d557c4b55499128eb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/1ac416df3f66c542f2d3688925105b539f064b64", - "reference": "1ac416df3f66c542f2d3688925105b539f064b64", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/13d9a6bb0b5bbb89d085c7d557c4b55499128eb9", + "reference": "13d9a6bb0b5bbb89d085c7d557c4b55499128eb9", "shasum": "" }, "require": { "ext-filter": "*", "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0", - "phpdocumentor/type-resolver": "^1.0", - "webmozart/assert": "^1" + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "1.3.*" + "mockery/mockery": "~1.3.2" }, "type": "library", "extra": { @@ -2355,34 +2418,33 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-07-21T08:16:41+00:00" + "time": "2020-08-22T07:19:02+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "dev-master", + "version": "1.x-dev", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "94f3ddc5d77e49daadadd33b798b933e52dde82c" + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/94f3ddc5d77e49daadadd33b798b933e52dde82c", - "reference": "94f3ddc5d77e49daadadd33b798b933e52dde82c", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", "shasum": "" }, "require": { - "php": "^7.2", + "php": "^7.2 || ^8.0", "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "ext-tokenizer": "^7.2", - "mockery/mockery": "~1" + "ext-tokenizer": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-1.x": "1.x-dev" } }, "autoload": { @@ -2401,7 +2463,7 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-06-19T19:40:27+00:00" + "time": "2020-06-27T10:12:23+00:00" }, { "name": "phpspec/prophecy", @@ -2671,7 +2733,7 @@ }, { "name": "phpunit/php-token-stream", - "version": "3.1.1", + "version": "3.1.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", @@ -2716,6 +2778,7 @@ "keywords": [ "tokenizer" ], + "abandoned": true, "time": "2019-09-17T06:23:10+00:00" }, { @@ -3368,6 +3431,40 @@ "homepage": "https://github.com/sebastianbergmann/version", "time": "2016-10-03T07:35:21+00:00" }, + { + "name": "swoole/ide-helper", + "version": "4.5.2", + "source": { + "type": "git", + "url": "https://github.com/swoole/ide-helper.git", + "reference": "e45892fb913204fc776bdec019ca8b723e7249b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swoole/ide-helper/zipball/e45892fb913204fc776bdec019ca8b723e7249b1", + "reference": "e45892fb913204fc776bdec019ca8b723e7249b1", + "shasum": "" + }, + "require-dev": { + "guzzlehttp/guzzle": "~6.5.0", + "laminas/laminas-code": "~3.4.0", + "squizlabs/php_codesniffer": "~3.5.0", + "symfony/filesystem": "~4.0" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Team Swoole", + "email": "team@swoole.com" + } + ], + "description": "IDE help files for Swoole.", + "time": "2020-05-28T19:17:49+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "dev-master", @@ -3573,12 +3670,12 @@ "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "f32950c872a995a93807909bab69387f47afaa25" + "reference": "b48bd18dc6f967ad09af9eab521cdf0e68fb6a95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/f32950c872a995a93807909bab69387f47afaa25", - "reference": "f32950c872a995a93807909bab69387f47afaa25", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/b48bd18dc6f967ad09af9eab521cdf0e68fb6a95", + "reference": "b48bd18dc6f967ad09af9eab521cdf0e68fb6a95", "shasum": "" }, "require": { @@ -3593,7 +3690,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.13-dev" + "dev-master": "2.14-dev" } }, "autoload": { @@ -3630,20 +3727,30 @@ "keywords": [ "templating" ], - "time": "2020-06-22T15:25:21+00:00" + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2020-08-23T15:56:05+00:00" }, { "name": "webmozart/assert", - "version": "1.9.0", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "9dc4f203e36f2b486149058bade43c851dd97451" + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/9dc4f203e36f2b486149058bade43c851dd97451", - "reference": "9dc4f203e36f2b486149058bade43c851dd97451", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", "shasum": "" }, "require": { @@ -3679,7 +3786,7 @@ "check", "validate" ], - "time": "2020-06-16T10:16:42+00:00" + "time": "2020-07-08T17:02:28+00:00" } ], "aliases": [], @@ -3693,6 +3800,7 @@ "ext-imagick": "*", "ext-mbstring": "*", "ext-json": "*", + "ext-yaml": "*", "ext-dom": "*", "ext-redis": "*", "ext-pdo": "*", diff --git a/docker-compose.nginx.yml b/docker-compose.nginx.yml new file mode 100644 index 0000000000..ba79f4e4d7 --- /dev/null +++ b/docker-compose.nginx.yml @@ -0,0 +1,215 @@ +version: '3' + +services: + traefik: + image: traefik:v2.2 + container_name: appwrite-traefik + command: + - --log.level=DEBUG + - --api.insecure=true + - --providers.file.directory=/storage/config + - --providers.file.watch=true + - --providers.docker=true + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --accesslog=true + restart: unless-stopped + ports: + - 80:80 + - 443:443 + - 8080:8080 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - appwrite-config:/storage/config:ro + - appwrite-certificates:/storage/certificates:ro + depends_on: + - appwrite + networks: + - gateway + - appwrite + + appwrite: + container_name: appwrite + build: + context: . + args: + - TESTING=true + - VERSION=dev + restart: unless-stopped + networks: + - appwrite + labels: + - traefik.http.routers.appwrite.rule=PathPrefix(`/`) + - traefik.http.routers.appwrite-secure.rule=PathPrefix(`/`) + - traefik.http.routers.appwrite-secure.tls=true + volumes: + - appwrite-uploads:/storage/uploads:rw + - appwrite-cache:/storage/cache:rw + - appwrite-config:/storage/config:rw + - appwrite-certificates:/storage/certificates:rw + - appwrite-functions:/storage/functions:rw + - ./phpunit.xml:/usr/src/code/phpunit.xml + - ./tests:/usr/src/code/tests + - ./app:/usr/src/code/app + # - ./vendor:/usr/src/code/vendor + - ./docs:/usr/src/code/docs + - ./public:/usr/src/code/public + - ./src:/usr/src/code/src + ports: + - 9501:80 + depends_on: + - mariadb + - redis + # - smtp + - clamav + - influxdb + - telegraf + - maildev + environment: + #- _APP_ENV=production + - _APP_ENV=development + - _APP_OPTIONS_ABUSE=disabled + - _APP_OPTIONS_FORCE_HTTPS=disabled + - _APP_OPENSSL_KEY_V1=your-secret-key + - _APP_DOMAIN=demo.appwrite.io + - _APP_DOMAIN_TARGET=demo.appwrite.io + - _APP_REDIS_HOST=redis + - _APP_REDIS_PORT=6379 + - _APP_DB_HOST=mariadb + - _APP_DB_PORT=3306 + - _APP_DB_SCHEMA=appwrite + - _APP_DB_USER=user + - _APP_DB_PASS=password + - _APP_INFLUXDB_HOST=influxdb + - _APP_INFLUXDB_PORT=8086 + - _APP_STATSD_HOST=telegraf + - _APP_STATSD_PORT=8125 + - _APP_SMTP_HOST=maildev + - _APP_SMTP_PORT=25 + + mariadb: + image: appwrite/mariadb:1.0.3 # fix issues when upgrading using: mysql_upgrade -u root -p + container_name: appwrite-mariadb + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-mariadb:/var/lib/mysql:rw + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD=rootsecretpassword + - MYSQL_DATABASE=appwrite + - MYSQL_USER=user + - MYSQL_PASSWORD=password + command: 'mysqld --innodb-flush-method=fsync' + + maildev: + image: djfarrelly/maildev + container_name: appwrite-maildev + restart: unless-stopped + ports: + - '1080:80' + networks: + - appwrite + + # smtp: + # image: appwrite/smtp:1.0.1 + # container_name: appwrite-smtp + # restart: unless-stopped + # networks: + # - appwrite + # environment: + # - MAILNAME=appwrite + # - RELAY_NETWORKS=:192.168.0.0/24:10.0.0.0/16 + + redis: + image: redis:5.0 + container_name: appwrite-redis + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-redis:/data:rw + + clamav: + image: appwrite/clamav:1.0.12 + container_name: appwrite-clamav + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-uploads:/storage/uploads + + influxdb: + image: influxdb:1.6 + container_name: appwrite-influxdb + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-influxdb:/var/lib/influxdb:rw + + telegraf: + image: appwrite/telegraf:1.0.0 + container_name: appwrite-telegraf + restart: unless-stopped + networks: + - appwrite + + # redis-commander: + # image: rediscommander/redis-commander:latest + # restart: unless-stopped + # networks: + # - appwrite + # environment: + # - REDIS_HOSTS=redis + # ports: + # - "8081:8081" + + # resque: + # image: registry.gitlab.com/appwrite/appwrite/resque-web:v1.0.2 + # restart: unless-stopped + # networks: + # - appwrite + # ports: + # - "5678:5678" + # environment: + # - RESQUE_WEB_HOST=redis + # - RESQUE_WEB_PORT=6379 + # - RESQUE_WEB_HTTP_BASIC_AUTH_USER=user + # - RESQUE_WEB_HTTP_BASIC_AUTH_PASSWORD=password + + # chronograf: + # image: chronograf:1.5 + # container_name: appwrite-chronograf + # restart: unless-stopped + # networks: + # - appwrite + # volumes: + # - appwrite-chronograf:/var/lib/chronograf + # ports: + # - "8888:8888" + # environment: + # - INFLUXDB_URL=http://influxdb:8086 + # - KAPACITOR_URL=http://kapacitor:9092 + # - AUTH_DURATION=48h + # - TOKEN_SECRET=duperduper5674829!jwt + # - GH_CLIENT_ID=d86f7145a41eacfc52cc + # - GH_CLIENT_SECRET=9e0081062367a2134e7f2ea95ba1a32d08b6c8ab + # - GH_ORGS=appwrite + +networks: + gateway: + appwrite: + +volumes: + appwrite-mariadb: + appwrite-redis: + appwrite-cache: + appwrite-uploads: + appwrite-certificates: + appwrite-functions: + appwrite-influxdb: + appwrite-chronograf: + appwrite-config: diff --git a/docker-compose.swoole.yml b/docker-compose.swoole.yml new file mode 100644 index 0000000000..7acda9ee5f --- /dev/null +++ b/docker-compose.swoole.yml @@ -0,0 +1,398 @@ +version: '3' + +services: + traefik: + image: traefik:2.2 + container_name: appwrite-traefik + command: + - --log.level=DEBUG + - --api.insecure=true + - --providers.file.directory=/storage/config + - --providers.file.watch=true + - --providers.docker=true + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --accesslog=true + restart: unless-stopped + ports: + - 80:80 + - 443:443 + - 8080:8080 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - appwrite-config:/storage/config:ro + - appwrite-certificates:/storage/certificates:ro + depends_on: + - appwrite + networks: + - gateway + - appwrite + + appwrite: + container_name: appwrite + build: + context: . + args: + - TESTING=true + - VERSION=dev + restart: unless-stopped + ports: + - 9501:80 + networks: + - appwrite + labels: + - traefik.http.routers.appwrite.rule=PathPrefix(`/`) + - traefik.http.routers.appwrite-secure.rule=PathPrefix(`/`) + - traefik.http.routers.appwrite-secure.tls=true + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - appwrite-uploads:/storage/uploads:rw + - appwrite-cache:/storage/cache:rw + - appwrite-config:/storage/config:rw + - appwrite-certificates:/storage/certificates:rw + - appwrite-functions:/storage/functions:rw + - ./phpunit.xml:/usr/src/code/phpunit.xml + - ./tests:/usr/src/code/tests + - ./app:/usr/src/code/app + # - ./vendor:/usr/src/code/vendor + - ./docs:/usr/src/code/docs + - ./public:/usr/src/code/public + - ./src:/usr/src/code/src + - ./debug:/tmp + depends_on: + - mariadb + - redis + - clamav + - influxdb + environment: + - _APP_ENV + - _APP_OPTIONS_ABUSE + - _APP_OPTIONS_FORCE_HTTPS + - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN + - _APP_DOMAIN_TARGET + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_INFLUXDB_HOST + - _APP_INFLUXDB_PORT + + appwrite-worker-usage: + entrypoint: worker-usage + container_name: appwrite-worker-usage + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - telegraf + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_STATSD_HOST + - _APP_STATSD_PORT + + appwrite-worker-audits: + entrypoint: worker-audits + container_name: appwrite-worker-audits + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-webhooks: + entrypoint: worker-webhooks + container_name: appwrite-worker-webhooks + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-tasks: + entrypoint: worker-tasks + container_name: appwrite-worker-tasks + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-deletes: + entrypoint: worker-deletes + container_name: appwrite-worker-deletes + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + volumes: + - appwrite-uploads:/storage/uploads:rw + - appwrite-cache:/storage/cache:rw + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-certificates: + entrypoint: worker-certificates + container_name: appwrite-worker-certificates + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + volumes: + - appwrite-config:/storage/config:rw + - appwrite-certificates:/storage/certificates:rw + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-functions: + entrypoint: worker-functions + container_name: appwrite-worker-functions + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - appwrite-functions:/storage/functions:rw + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + + appwrite-worker-mails: + entrypoint: worker-mails + container_name: appwrite-worker-mails + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - maildev + # - smtp + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_SMTP_HOST + - _APP_SMTP_PORT + + appwrite-schedule: + entrypoint: schedule + container_name: appwrite-schedule + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + + mariadb: + image: appwrite/mariadb:1.0.3 # fix issues when upgrading using: mysql_upgrade -u root -p + container_name: appwrite-mariadb + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-mariadb:/var/lib/mysql:rw + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD=rootsecretpassword + - MYSQL_DATABASE=appwrite + - MYSQL_USER=user + - MYSQL_PASSWORD=password + command: 'mysqld --innodb-flush-method=fsync' + + maildev: + image: djfarrelly/maildev + container_name: appwrite-maildev + restart: unless-stopped + ports: + - '1080:80' + networks: + - appwrite + + # smtp: + # image: appwrite/smtp:1.0.1 + # container_name: appwrite-smtp + # restart: unless-stopped + # networks: + # - appwrite + # environment: + # - MAILNAME=appwrite + # - RELAY_NETWORKS=:192.168.0.0/24:10.0.0.0/16 + + redis: + image: redis:5.0 + container_name: appwrite-redis + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-redis:/data:rw + + clamav: + image: appwrite/clamav:1.0.12 + container_name: appwrite-clamav + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-uploads:/storage/uploads + + influxdb: + image: influxdb:1.6 + container_name: appwrite-influxdb + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-influxdb:/var/lib/influxdb:rw + + telegraf: + image: appwrite/telegraf:1.0.0 + container_name: appwrite-telegraf + restart: unless-stopped + networks: + - appwrite + + # redis-commander: + # image: rediscommander/redis-commander:latest + # restart: unless-stopped + # networks: + # - appwrite + # environment: + # - REDIS_HOSTS=redis + # ports: + # - "8081:8081" + + # resque: + # image: registry.gitlab.com/appwrite/appwrite/resque-web:v1.0.2 + # restart: unless-stopped + # networks: + # - appwrite + # ports: + # - "5678:5678" + # environment: + # - RESQUE_WEB_HOST=redis + # - RESQUE_WEB_PORT=6379 + # - RESQUE_WEB_HTTP_BASIC_AUTH_USER=user + # - RESQUE_WEB_HTTP_BASIC_AUTH_PASSWORD=password + + # chronograf: + # image: chronograf:1.5 + # container_name: appwrite-chronograf + # restart: unless-stopped + # networks: + # - appwrite + # volumes: + # - appwrite-chronograf:/var/lib/chronograf + # ports: + # - "8888:8888" + # environment: + # - INFLUXDB_URL=http://influxdb:8086 + # - KAPACITOR_URL=http://kapacitor:9092 + # - AUTH_DURATION=48h + # - TOKEN_SECRET=duperduper5674829!jwt + # - GH_CLIENT_ID=d86f7145a41eacfc52cc + # - GH_CLIENT_SECRET=9e0081062367a2134e7f2ea95ba1a32d08b6c8ab + # - GH_ORGS=appwrite + + # webgrind: + # image: 'jokkedk/webgrind:latest' + # volumes: + # - './debug:/tmp' + # ports: + # - '3001:80' + +networks: + gateway: + appwrite: + +volumes: + appwrite-mariadb: + appwrite-redis: + appwrite-cache: + appwrite-uploads: + appwrite-certificates: + appwrite-functions: + appwrite-influxdb: + appwrite-chronograf: + appwrite-config: diff --git a/docker-compose.yml b/docker-compose.yml index 74a8ae3844..484f73b42f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '3' services: traefik: image: traefik:2.2 - container_name: appwrite_traefik + container_name: appwrite-traefik command: - --log.level=DEBUG - --api.insecure=true @@ -36,6 +36,8 @@ services: - TESTING=true - VERSION=dev restart: unless-stopped + ports: + - 9501:80 networks: - appwrite labels: @@ -43,50 +45,238 @@ services: - traefik.http.routers.appwrite-secure.rule=PathPrefix(`/`) - traefik.http.routers.appwrite-secure.tls=true volumes: + - /var/run/docker.sock:/var/run/docker.sock - appwrite-uploads:/storage/uploads:rw - appwrite-cache:/storage/cache:rw - appwrite-config:/storage/config:rw - appwrite-certificates:/storage/certificates:rw - - ./phpunit.xml:/usr/share/nginx/html/phpunit.xml - - ./tests:/usr/share/nginx/html/tests - - ./app:/usr/share/nginx/html/app - # - ./vendor:/usr/share/nginx/html/vendor - - ./docs:/usr/share/nginx/html/docs - - ./public:/usr/share/nginx/html/public - - ./src:/usr/share/nginx/html/src + - appwrite-functions:/storage/functions:rw + - ./phpunit.xml:/usr/src/code/phpunit.xml + - ./tests:/usr/src/code/tests + - ./app:/usr/src/code/app + # - ./vendor:/usr/src/code/vendor + - ./docs:/usr/src/code/docs + - ./public:/usr/src/code/public + - ./src:/usr/src/code/src + - ./debug:/tmp depends_on: - mariadb - redis - # - smtp - clamav - influxdb - - telegraf - - maildev environment: - #- _APP_ENV=production - - _APP_ENV=development - - _APP_OPTIONS_ABUSE=disabled - - _APP_OPTIONS_FORCE_HTTPS=disabled - - _APP_OPENSSL_KEY_V1=your-secret-key - - _APP_DOMAIN=demo.appwrite.io - - _APP_DOMAIN_TARGET=demo.appwrite.io - - _APP_REDIS_HOST=redis - - _APP_REDIS_PORT=6379 - - _APP_DB_HOST=mariadb - - _APP_DB_PORT=3306 - - _APP_DB_SCHEMA=appwrite - - _APP_DB_USER=user - - _APP_DB_PASS=password - - _APP_INFLUXDB_HOST=influxdb - - _APP_INFLUXDB_PORT=8086 - - _APP_STATSD_HOST=telegraf - - _APP_STATSD_PORT=8125 - - _APP_SMTP_HOST=maildev - - _APP_SMTP_PORT=25 + - _APP_ENV + - _APP_OPTIONS_ABUSE + - _APP_OPTIONS_FORCE_HTTPS + - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN + - _APP_DOMAIN_TARGET + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_INFLUXDB_HOST + - _APP_INFLUXDB_PORT + - _APP_STORAGE_LIMIT + - _APP_FUNCTIONS_TIMEOUT + - _APP_FUNCTIONS_CONTAINERS + + appwrite-worker-usage: + entrypoint: worker-usage + container_name: appwrite-worker-usage + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - telegraf + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_STATSD_HOST + - _APP_STATSD_PORT + + appwrite-worker-audits: + entrypoint: worker-audits + container_name: appwrite-worker-audits + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-webhooks: + entrypoint: worker-webhooks + container_name: appwrite-worker-webhooks + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-tasks: + entrypoint: worker-tasks + container_name: appwrite-worker-tasks + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-deletes: + entrypoint: worker-deletes + container_name: appwrite-worker-deletes + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + volumes: + - appwrite-uploads:/storage/uploads:rw + - appwrite-cache:/storage/cache:rw + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-certificates: + entrypoint: worker-certificates + container_name: appwrite-worker-certificates + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + volumes: + - appwrite-config:/storage/config:rw + - appwrite-certificates:/storage/certificates:rw + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-functions: + entrypoint: worker-functions + container_name: appwrite-worker-functions + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /tmp:/tmp:rw + - appwrite-functions:/storage/functions:rw + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_FUNCTIONS_TIMEOUT + - _APP_FUNCTIONS_CONTAINERS + + appwrite-worker-mails: + entrypoint: worker-mails + container_name: appwrite-worker-mails + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - maildev + # - smtp + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_SMTP_HOST + - _APP_SMTP_PORT + + appwrite-schedule: + entrypoint: schedule + container_name: appwrite-schedule + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT mariadb: image: appwrite/mariadb:1.0.3 # fix issues when upgrading using: mysql_upgrade -u root -p - container_name: appwrite_mariadb + container_name: appwrite-mariadb restart: unless-stopped networks: - appwrite @@ -103,7 +293,7 @@ services: maildev: image: djfarrelly/maildev - container_name: appwrite_maildev + container_name: appwrite-maildev restart: unless-stopped ports: - '1080:80' @@ -112,7 +302,7 @@ services: # smtp: # image: appwrite/smtp:1.0.1 - # container_name: appwrite_smtp + # container_name: appwrite-smtp # restart: unless-stopped # networks: # - appwrite @@ -122,7 +312,7 @@ services: redis: image: redis:5.0 - container_name: appwrite_redis + container_name: appwrite-redis restart: unless-stopped networks: - appwrite @@ -131,7 +321,7 @@ services: clamav: image: appwrite/clamav:1.0.12 - container_name: appwrite_clamav + container_name: appwrite-clamav restart: unless-stopped networks: - appwrite @@ -140,7 +330,7 @@ services: influxdb: image: influxdb:1.6 - container_name: appwrite_influxdb + container_name: appwrite-influxdb restart: unless-stopped networks: - appwrite @@ -149,7 +339,7 @@ services: telegraf: image: appwrite/telegraf:1.0.0 - container_name: appwrite_telegraf + container_name: appwrite-telegraf restart: unless-stopped networks: - appwrite @@ -177,24 +367,31 @@ services: # - RESQUE_WEB_HTTP_BASIC_AUTH_USER=user # - RESQUE_WEB_HTTP_BASIC_AUTH_PASSWORD=password - chronograf: - image: chronograf:1.5 - container_name: appwrite_chronograf - restart: unless-stopped - networks: - - appwrite - volumes: - - appwrite-chronograf:/var/lib/chronograf - ports: - - "8888:8888" - environment: - - INFLUXDB_URL=http://influxdb:8086 - - KAPACITOR_URL=http://kapacitor:9092 - - AUTH_DURATION=48h - - TOKEN_SECRET=duperduper5674829!jwt - - GH_CLIENT_ID=d86f7145a41eacfc52cc - - GH_CLIENT_SECRET=9e0081062367a2134e7f2ea95ba1a32d08b6c8ab - - GH_ORGS=appwrite + # chronograf: + # image: chronograf:1.5 + # container_name: appwrite-chronograf + # restart: unless-stopped + # networks: + # - appwrite + # volumes: + # - appwrite-chronograf:/var/lib/chronograf + # ports: + # - "8888:8888" + # environment: + # - INFLUXDB_URL=http://influxdb:8086 + # - KAPACITOR_URL=http://kapacitor:9092 + # - AUTH_DURATION=48h + # - TOKEN_SECRET=duperduper5674829!jwt + # - GH_CLIENT_ID=d86f7145a41eacfc52cc + # - GH_CLIENT_SECRET=9e0081062367a2134e7f2ea95ba1a32d08b6c8ab + # - GH_ORGS=appwrite + + # webgrind: + # image: 'jokkedk/webgrind:latest' + # volumes: + # - './debug:/tmp' + # ports: + # - '3001:80' networks: gateway: @@ -206,6 +403,7 @@ volumes: appwrite-cache: appwrite-uploads: appwrite-certificates: + appwrite-functions: appwrite-influxdb: appwrite-chronograf: appwrite-config: diff --git a/docker/environments/README.md b/docker/environments/README.md new file mode 100644 index 0000000000..6ffaf1d4fb --- /dev/null +++ b/docker/environments/README.md @@ -0,0 +1,5 @@ +# Appwrite Functions Environments + +Docker based enviornments for Appwrite Functions. You can use this Docker images to locally tests your functions by executing them on your local desktop or server. + +All the supported enviornments are based on Docker Alpine images. \ No newline at end of file diff --git a/docker/environments/deno-1.2/Dockerfile b/docker/environments/deno-1.2/Dockerfile new file mode 100644 index 0000000000..b6877da806 --- /dev/null +++ b/docker/environments/deno-1.2/Dockerfile @@ -0,0 +1,11 @@ +FROM hayd/deno:alpine-1.2.0 + +LABEL maintainer="team@appwrite.io" + +RUN apk add tar + +RUN mkdir /usr/local/src + +WORKDIR /usr/local/src/ + +ENV DENO_DIR=/usr/local/src/.appwrite \ No newline at end of file diff --git a/docker/environments/deno-1.2/build.sh b/docker/environments/deno-1.2/build.sh new file mode 100644 index 0000000000..5eaaffb491 --- /dev/null +++ b/docker/environments/deno-1.2/build.sh @@ -0,0 +1,7 @@ +echo 'Starting Deno 1.2 build...' + +docker build --tag appwrite/env-deno:1.2 . + +echo 'Pushing Deno 1.2 build to registry...' + +docker push appwrite/env-deno:1.2 \ No newline at end of file diff --git a/docker/environments/node-14.5/Dockerfile b/docker/environments/node-14.5/Dockerfile new file mode 100644 index 0000000000..c2f3458a3f --- /dev/null +++ b/docker/environments/node-14.5/Dockerfile @@ -0,0 +1,9 @@ +FROM node:14.5-alpine + +LABEL maintainer="team@appwrite.io" + +RUN apk add tar + +RUN mkdir /usr/local/src + +WORKDIR /usr/local/src/ \ No newline at end of file diff --git a/docker/environments/node-14.5/build.sh b/docker/environments/node-14.5/build.sh new file mode 100644 index 0000000000..d81449f267 --- /dev/null +++ b/docker/environments/node-14.5/build.sh @@ -0,0 +1,7 @@ +echo 'Starting Node 14.5 build...' + +docker build --tag appwrite/env-node:14.5 . + +echo 'Pushing Node 14.5 build to registry...' + +docker push appwrite/env-node:14.5 \ No newline at end of file diff --git a/docker/environments/php-7.4/Dockerfile b/docker/environments/php-7.4/Dockerfile new file mode 100644 index 0000000000..08af0999be --- /dev/null +++ b/docker/environments/php-7.4/Dockerfile @@ -0,0 +1,9 @@ +FROM php:7.4-cli-alpine + +LABEL maintainer="team@appwrite.io" + +RUN apk add tar + +RUN mkdir /usr/local/src + +WORKDIR /usr/local/src/ \ No newline at end of file diff --git a/docker/environments/php-7.4/build.sh b/docker/environments/php-7.4/build.sh new file mode 100644 index 0000000000..84b20e4d42 --- /dev/null +++ b/docker/environments/php-7.4/build.sh @@ -0,0 +1,7 @@ +echo 'Starting PHP 7.4 build...' + +docker build --tag appwrite/env-php:7.4 . + +echo 'Pushing PHP 7.4 build to registry...' + +docker push appwrite/env-php:7.4 \ No newline at end of file diff --git a/docker/environments/python-3.8/Dockerfile b/docker/environments/python-3.8/Dockerfile new file mode 100644 index 0000000000..91e2fbc27f --- /dev/null +++ b/docker/environments/python-3.8/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.8-alpine + +LABEL maintainer="team@appwrite.io" + +RUN apk add tar + +RUN mkdir /usr/local/src + +WORKDIR /usr/local/src/ + +ENV PIP_TARGET=/usr/local/src/.appwrite \ No newline at end of file diff --git a/docker/environments/python-3.8/build.sh b/docker/environments/python-3.8/build.sh new file mode 100644 index 0000000000..2dc97b783b --- /dev/null +++ b/docker/environments/python-3.8/build.sh @@ -0,0 +1,7 @@ +echo 'Starting Python 3.8 build...' + +docker build --tag appwrite/env-python:3.8 . + +echo 'Pushing Python 3.8 build to registry...' + +docker push appwrite/env-python:3.8 \ No newline at end of file diff --git a/docker/environments/ruby-2.7/Dockerfile b/docker/environments/ruby-2.7/Dockerfile new file mode 100644 index 0000000000..5b93233762 --- /dev/null +++ b/docker/environments/ruby-2.7/Dockerfile @@ -0,0 +1,12 @@ +FROM ruby:2.7-alpine + +LABEL maintainer="team@appwrite.io" + +RUN apk add tar + +RUN mkdir /usr/local/src + +WORKDIR /usr/local/src/ + +ENV GEM_PATH=/usr/local/src/.appwrite/2.1.0 +ENV GEM_SPEC_CACHE=/usr/local/src/.appwrite/specs \ No newline at end of file diff --git a/docker/environments/ruby-2.7/build.sh b/docker/environments/ruby-2.7/build.sh new file mode 100644 index 0000000000..377d62a231 --- /dev/null +++ b/docker/environments/ruby-2.7/build.sh @@ -0,0 +1,7 @@ +echo 'Starting Ruby 2.7 build...' + +docker build --tag appwrite/env-ruby:2.7 . + +echo 'Pushing Ruby 2.7 build to registry...' + +docker push appwrite/env-ruby:2.7 \ No newline at end of file diff --git a/docker/nginx.conf.template b/docker/nginx.conf.template index f0f748d943..d53dd7f770 100644 --- a/docker/nginx.conf.template +++ b/docker/nginx.conf.template @@ -66,7 +66,7 @@ http { ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; - root /usr/share/nginx/html/public; + root /usr/src/code/public; index index.php index.html index.htm; server_tokens off; @@ -122,7 +122,7 @@ http { # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { - root /usr/share/nginx/html; + root /usr/src/code; } # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 diff --git a/docker/supervisord.conf b/docker/supervisord.conf index 43036d9628..4d3361cc94 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -49,11 +49,11 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes = 0 [program:v1-webhooks] -command=php /usr/share/nginx/html/vendor/bin/resque +command=php /usr/src/code/vendor/bin/resque autostart=true autorestart=true priority=10 -environment=QUEUE='v1-webhooks',APP_INCLUDE='/usr/share/nginx/html/app/workers/webhooks.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' +environment=QUEUE='v1-webhooks',APP_INCLUDE='/usr/src/code/app/workers/webhooks.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' stdout_events_enabled=true stderr_events_enabled=true stopsignal=QUIT @@ -66,11 +66,11 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes = 0 [program:v1-mails] -command=php /usr/share/nginx/html/vendor/bin/resque +command=php /usr/src/code/vendor/bin/resque autostart=true autorestart=true priority=10 -environment=QUEUE='v1-mails',APP_INCLUDE='/usr/share/nginx/html/app/workers/mails.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' +environment=QUEUE='v1-mails',APP_INCLUDE='/usr/src/code/app/workers/mails.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' stdout_events_enabled=true stderr_events_enabled=true stopsignal=QUIT @@ -83,11 +83,11 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes = 0 [program:v1-audits] -command=php /usr/share/nginx/html/vendor/bin/resque +command=php /usr/src/code/vendor/bin/resque autostart=true autorestart=true priority=10 -environment=QUEUE='v1-audits',APP_INCLUDE='/usr/share/nginx/html/app/workers/audits.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' +environment=QUEUE='v1-audits',APP_INCLUDE='/usr/src/code/app/workers/audits.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' stdout_events_enabled=true stderr_events_enabled=true stopsignal=QUIT @@ -100,11 +100,11 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes = 0 [program:v1-usage] -command=php /usr/share/nginx/html/vendor/bin/resque +command=php /usr/src/code/vendor/bin/resque autostart=true autorestart=true priority=10 -environment=QUEUE='v1-usage',APP_INCLUDE='/usr/share/nginx/html/app/workers/usage.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' +environment=QUEUE='v1-usage',APP_INCLUDE='/usr/src/code/app/workers/usage.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' stdout_events_enabled=true stderr_events_enabled=true stopsignal=QUIT @@ -117,11 +117,11 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes = 0 [program:v1-tasks] -command=php /usr/share/nginx/html/vendor/bin/resque +command=php /usr/src/code/vendor/bin/resque autostart=true autorestart=true priority=10 -environment=QUEUE='v1-tasks',APP_INCLUDE='/usr/share/nginx/html/app/workers/tasks.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' +environment=QUEUE='v1-tasks',APP_INCLUDE='/usr/src/code/app/workers/tasks.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' stdout_events_enabled=true stderr_events_enabled=true stopsignal=QUIT @@ -134,11 +134,11 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes = 0 [program:v1-deletes] -command=php /usr/share/nginx/html/vendor/bin/resque +command=php /usr/src/code/vendor/bin/resque autostart=true autorestart=true priority=10 -environment=QUEUE='v1-deletes',APP_INCLUDE='/usr/share/nginx/html/app/workers/deletes.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' +environment=QUEUE='v1-deletes',APP_INCLUDE='/usr/src/code/app/workers/deletes.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' stdout_events_enabled=true stderr_events_enabled=true stopsignal=QUIT @@ -151,11 +151,28 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes = 0 [program:v1-certificates] -command=php /usr/share/nginx/html/vendor/bin/resque +command=php /usr/src/code/vendor/bin/resque autostart=true autorestart=true priority=10 -environment=QUEUE='v1-certificates',APP_INCLUDE='/usr/share/nginx/html/app/workers/certificates.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' +environment=QUEUE='v1-certificates',APP_INCLUDE='/usr/src/code/app/workers/certificates.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' +stdout_events_enabled=true +stderr_events_enabled=true +stopsignal=QUIT +startretries=10 +;stdout_logfile=/dev/stdout +;stdout_logfile_maxbytes=0 +stdout_logfile=/var/log/appwrite.log +stdout_logfile_maxbytes=5000000 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes = 0 + +[program:v1-functions] +command=php /usr/src/code/vendor/bin/resque +autostart=true +autorestart=true +priority=10 +environment=QUEUE='v1-functions',APP_INCLUDE='/usr/src/code/app/workers/functions.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' stdout_events_enabled=true stderr_events_enabled=true stopsignal=QUIT @@ -168,11 +185,11 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes = 0 [program:v1-schedule] -command=php /usr/share/nginx/html/vendor/bin/resque-scheduler +command=php /usr/src/code/vendor/bin/resque-scheduler autostart=true autorestart=true priority=10 -environment=REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s',RESQUE_PHP='/usr/share/nginx/html/vendor/autoload.php' +environment=REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s',RESQUE_PHP='/usr/src/code/vendor/autoload.php' stdout_events_enabled=true stderr_events_enabled=true stopsignal=QUIT diff --git a/docs/services/functions.md b/docs/services/functions.md new file mode 100644 index 0000000000..69c21dbc4f --- /dev/null +++ b/docs/services/functions.md @@ -0,0 +1,3 @@ +The Functions service allows you to create custom behaviour that can be triggered by any supported Appwrite system events or by a predefined schedule. + +Appwrite Cloud Functions lets you automatically run backend code in response to events triggered by Appwrite or by setting it to be executed in a predefined schedule. Your code is stored in a secure and encrypted way on your Appwrite instance and is executed in an isolated enviornment. \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index b45d16bf7e..6c5e02dee4 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -38,12 +38,15 @@ const configApp = { 'public/scripts/views/forms/clone.js', 'public/scripts/views/forms/add.js', + 'public/scripts/views/forms/chart.js', 'public/scripts/views/forms/code.js', 'public/scripts/views/forms/color.js', 'public/scripts/views/forms/copy.js', 'public/scripts/views/forms/document.js', 'public/scripts/views/forms/document-preview.js', 'public/scripts/views/forms/filter.js', + 'public/scripts/views/forms/headers.js', + 'public/scripts/views/forms/key-value.js', 'public/scripts/views/forms/move-down.js', 'public/scripts/views/forms/move-up.js', 'public/scripts/views/forms/nav.js', @@ -66,6 +69,7 @@ const configApp = { 'public/scripts/views/general/setup.js', 'public/scripts/views/general/switch.js', 'public/scripts/views/general/theme.js', + 'public/scripts/views/general/version.js', 'public/scripts/views/paging/back.js', 'public/scripts/views/paging/next.js', diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js index cc18f5d327..211a3dfa73 100644 --- a/public/dist/scripts/app-all.js +++ b/public/dist/scripts/app-all.js @@ -99,7 +99,7 @@ if(color){payload['color']=color;} if(background){payload['background']=background;} payload['project']=config.project;payload['key']=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;index","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i=31536000){value=Math.floor(seconds/31536000);unit="year";} +else if(seconds>=86400){value=Math.floor(seconds/86400);unit="day";} +else if(seconds>=3600){value=Math.floor(seconds/3600);unit="hour";} +else if(seconds>=60){value=Math.floor(seconds/60);unit="minute";} +if(value!=1){unit=unit+"s";} +return value+" "+unit+" "+direction;}).add("ms2hum",function($value){let temp=$value;const years=Math.floor(temp/31536000),days=Math.floor((temp%=31536000)/86400),hours=Math.floor((temp%=86400)/3600),minutes=Math.floor((temp%=3600)/60),seconds=temp%60;if(days||hours||seconds||minutes){return((years?years+"y ":"")+ (days?days+"d ":"")+ (hours?hours+"h ":"")+ (minutes?minutes+"m ":"")+ Number.parseFloat(seconds).toFixed(0)+"s");} -return"< 1s";}).add("markdown",function($value,markdown){return markdown.render($value);}).add("pageCurrent",function($value,env){return Math.ceil(parseInt($value||0)/env.PAGING_LIMIT)+1;}).add("pageTotal",function($value,env){let total=Math.ceil(parseInt($value||0)/env.PAGING_LIMIT);return total?total:1;}).add("humanFileSize",function($value){if(!$value){return 0;} +return"< 1s";}).add("seconds2hum",function($value){var seconds=($value).toFixed(3);var minutes=($value/(60)).toFixed(1);var hours=($value/(60*60)).toFixed(1);var days=($value/(60*60*24)).toFixed(1);if(seconds<60){return seconds+"s";}else if(minutes<60){return minutes+"m";}else if(hours<24){return hours+"h";}else{return days+"d"}}).add("markdown",function($value,markdown){return markdown.render($value);}).add("pageCurrent",function($value,env){return Math.ceil(parseInt($value||0)/env.PAGING_LIMIT)+1;}).add("pageTotal",function($value,env){let total=Math.ceil(parseInt($value||0)/env.PAGING_LIMIT);return total?total:1;}).add("humanFileSize",function($value){if(!$value){return 0;} let thresh=1000;if(Math.abs($value)=thresh&&u'+ units[u]+"");}).add("statsTotal",function($value){if(!$value){return 0;} @@ -2580,7 +2634,10 @@ return result.length;}).add("documentAction",function(container){let collection= return'database.updateDocument';}).add("documentSuccess",function(container){let document=container.get('project-document');if(document&&!document.$id){return',redirect';} return'';}).add("firstElement",function($value){if($value&&$value[0]){return $value[0];} return $value;}).add("platformsLimit",function($value){return $value;}).add("limit",function($value){let postfix=($value.length>=50)?'...':'';return $value.substring(0,50)+postfix;;}).add("arraySentence",function($value){if(!Array.isArray($value)){return'';} -return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} +return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');}).add("envName",function($value,env){if(env&&env.ENVIRONMENTS&&env.ENVIRONMENTS[$value]){return env.ENVIRONMENTS[$value].name;} +return'';}).add("envLogo",function($value,env){if(env&&env.ENVIRONMENTS&&env.ENVIRONMENTS[$value]){return env.ENVIRONMENTS[$value].logo;} +return'';}).add("envVersion",function($value,env){if(env&&env.ENVIRONMENTS&&env.ENVIRONMENTS[$value]){return env.ENVIRONMENTS[$value].version;} +return'';});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} let abbr;if(number>=1e12){abbr="T";}else if(number>=1e9){abbr="B";}else if(number>=1e6){abbr="M";}else if(number>=1e3){abbr="K";}else{abbr="";} return annotate(number,maxPlaces,forcePlaces,abbr);} function annotate(number,maxPlaces,forcePlaces,abbr){let rounded=0;switch(abbr){case"T":rounded=number/1e12;break;case"B":rounded=number/1e9;break;case"M":rounded=number/1e6;break;case"K":rounded=number/1e3;break;case"":rounded=number;break;} @@ -2588,10 +2645,8 @@ if(maxPlaces!==false){let test=new RegExp("\\.\\d{"+(maxPlaces+1)+",}$");if(test if(forcePlaces!==false){rounded=Number(rounded).toFixed(forcePlaces);} return rounded+abbr;} window.ls.container.get("view").add({selector:"data-acl",controller:function(element,document,router,alerts){document.body.classList.remove("console");document.body.classList.remove("home");document.body.classList.add(router.getCurrent().view.scope);if(!router.getCurrent().view.project){document.body.classList.add("hide-nav");document.body.classList.remove("show-nav");}else{document.body.classList.add("show-nav");document.body.classList.remove("hide-nav");} -if("/console"===router.getCurrent().path){document.body.classList.add("index");}else{document.body.classList.remove("index");}}}).add({selector:"data-forms-headers",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";value.type="text";value.className="margin-bottom-no";value.placeholder="Value";wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.value=key.value.toLowerCase()+":"+value.value.toLowerCase();};let syncB=function(){let split=element.value.toLowerCase().split(":");key.value=split[0]||"";value.value=split[1]||"";key.value=key.value.trim();value.value=value.value.trim();};syncB();}}).add({selector:"data-prism",controller:function(window,document,element,alerts){Prism.highlightElement(element);let copy=document.createElement("i");copy.className="icon-docs copy";copy.title="Copy to Clipboard";copy.textContent="Click Here to Copy";copy.addEventListener("click",function(){window.getSelection().removeAllRanges();let range=document.createRange();range.selectNode(element);window.getSelection().addRange(range);try{document.execCommand("copy");alerts.add({text:"Copied to clipboard",class:""},3000);}catch(err){alerts.add({text:"Failed to copy text ",class:"error"},3000);} -window.getSelection().removeAllRanges();});element.parentNode.parentNode.appendChild(copy);}}).add({selector:"data-ls-ui-chart",controller:function(element,container,date,document){let child=document.createElement("canvas");child.width=500;child.height=175;let stats=container.get("usage");if(!stats||!stats["requests"]||!stats["requests"]["data"]){return;} -let config={type:"line",data:{labels:[],datasets:[{label:"Requests",backgroundColor:"rgba(230, 248, 253, 0.3)",borderColor:"#29b5d9",borderWidth:2,data:[0,0,0,0,0,0,0],fill:true}]},options:{responsive:true,title:{display:false,text:"Stats"},legend:{display:false},tooltips:{mode:"index",intersect:false,caretPadding:0},hover:{mode:"nearest",intersect:true},scales:{xAxes:[{display:false}],yAxes:[{display:false}]}}};for(let i=0;ielement.trim());return function(serviceForm,router,window){let url=window.location.href;keys.map(node=>{node=node.split("=");let key=node[0]||"";let name=node[1]||key;let value=getValue(key,"param",serviceForm);url=updateQueryString(name,value?value:null,url);});if(url!==window.location.href){window.history.pushState({},"",url);router.reset();}};},trigger:function(events){return function(document){events=events.trim().split(",");for(let i=0;i=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');} +break;default:break;}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-headers",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";value.type="text";value.className="margin-bottom-no";value.placeholder="Value";wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.value=key.value.toLowerCase()+":"+value.value.toLowerCase();};let syncB=function(){let split=element.value.toLowerCase().split(":");key.value=split[0]||"";value.value=split[1]||"";key.value=key.value.trim();value.value=value.value.trim();};syncB();}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-key-value",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";value.type="text";value.className="margin-bottom-no";value.placeholder="Value";wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.name=key.value;element.value=value.value;};let syncB=function(){key.value=element.name||"";value.value=element.value||"";};syncB();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-down",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-down]")).map(function(obj){obj.addEventListener("click",function(){if(element.nextElementSibling){console.log('down',element.offsetHeight);element.parentNode.insertBefore(element.nextElementSibling,element);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-up",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-up]")).map(function(obj){obj.addEventListener("click",function(){if(element.previousElementSibling){console.log('up',element);element.parentNode.insertBefore(element,element.previousElementSibling);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-nav",repeat:false,controller:function(element,view,container,document){let titles=document.querySelectorAll('[data-forms-nav-anchor]');let links=element.querySelectorAll('[data-forms-nav-link]');let minLink=null;let check=function(){let minDistance=null;let minElement=null;for(let i=0;i=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');} console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-apple",controller:function(element){let container=document.createElement("div");let row=document.createElement("div");let col1=document.createElement("div");let col2=document.createElement("div");let keyID=document.createElement("input");let keyLabel=document.createElement("label");let teamID=document.createElement("input");let teamLabel=document.createElement("label");let p8=document.createElement("textarea");let p8Label=document.createElement("label");keyLabel.textContent='Key ID';teamLabel.textContent='Team ID';p8Label.textContent='P8 File';row.classList.add('row');row.classList.add('thin');container.appendChild(row);container.appendChild(p8Label);container.appendChild(p8);row.appendChild(col1);row.appendChild(col2);col1.classList.add('col');col1.classList.add('span-6');col1.appendChild(keyLabel);col1.appendChild(keyID);col2.classList.add('col');col2.classList.add('span-6');col2.appendChild(teamLabel);col2.appendChild(teamID);keyID.type='text';keyID.placeholder='SHAB13ROFN';teamID.type='text';teamID.placeholder='ELA2CD3AED';p8.accept='.p8';p8.classList.add('margin-bottom-no');element.parentNode.insertBefore(container,element.nextSibling);element.addEventListener('change',sync);keyID.addEventListener('change',update);teamID.addEventListener('change',update);p8.addEventListener('change',update);function update(){let json={};json.keyID=keyID.value;json.teamID=teamID.value;json.p8=p8.value;element.value=JSON.stringify(json);} function sync(){if(!element.value){return;} let json={};try{json=JSON.parse(element.value);}catch(error){console.error('Failed to parse secret key');} @@ -2672,7 +2729,7 @@ var file=document.createElement("li");var image=document.createElement("img");im result+"/preview?width="+ previewWidth+"&height="+ previewHeight+"&project="+project+"&mode=admin";image.alt=previewAlt;file.className="file avatar";file.tabIndex=0;file.appendChild(image);preview.appendChild(file);var remove=(function(result){return function(event){render(result.$id);element.value='';};})(result);file.addEventListener("click",remove);file.addEventListener("keypress",remove);element.value=result;};input.addEventListener("change",function(){var message=alerts.add({text:labelLoading,class:""},0);var files=input.files;var read=JSON.parse(expression.parse(element.dataset["read"]||"[]"));var write=JSON.parse(expression.parse(element.dataset["write"]||"[]"));sdk.storage.createFile(files[0],read,write,1).then(function(response){onComplete(message);render(response.$id);},function(error){alerts.add({text:"An error occurred!",class:""},3000);onComplete(message);});input.disabled=true;});element.addEventListener("change",function(){if(!element.value){return;} -render(element.value);wrapper.scrollIntoView();});upload.addEventListener("keypress",function(){input.click();});element.parentNode.insertBefore(wrapper,element);wrapper.appendChild(preview);wrapper.appendChild(progress);wrapper.appendChild(upload);upload.appendChild(input);render(output);if(searchButton){let searchOpen=document.createElement("button");searchOpen.type='button';searchOpen.innerHTML=' Search';searchOpen.classList.add('reverse');let path=container.scope(searchButton);searchOpen.addEventListener('click',function(){search.selected=element.value;search.path=path;document.dispatchEvent(new CustomEvent("open-file-serach",{bubbles:false,cancelable:true}));});wrapper.appendChild(searchOpen);}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-cookies",controller:function(element,alerts,cookie,env){if(!cookie.get("cookie-alert")){let text=element.dataset["cookies"]||"";alerts.add({text:text,class:"cookie-alert",link:env.HOME+"/policy/cookies",callback:function(){cookie.set("cookie-alert","true",365*10);}},0);}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-page-title",repeat:true,controller:function(element,document,expression){document.title=expression.parse(element.getAttribute("data-page-title"))||document.title;}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-to',repeat:false,controller:function(element,window){let button=window.document.createElement('button');button.className='scroll-to icon-up-dir';button.alt='Back To Top';button.title='Back To Top';button.addEventListener('click',function(){element.scrollIntoView(true,{behavior:'smooth'});button.blur();},false);element.appendChild(button);}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-direction',repeat:false,controller:function(element,window){let position=0;let check=function(){let direction=window.document.documentElement.scrollTop;if(direction>position){element.classList.remove('scroll-to-top') +render(element.value);wrapper.scrollIntoView();});upload.addEventListener("keypress",function(){input.click();});element.parentNode.insertBefore(wrapper,element);wrapper.appendChild(preview);wrapper.appendChild(progress);wrapper.appendChild(upload);upload.appendChild(input);render(output);if(searchButton){let searchOpen=document.createElement("button");searchOpen.type='button';searchOpen.innerHTML=' Search';searchOpen.classList.add('reverse');let path=container.scope(searchButton);searchOpen.addEventListener('click',function(){search.selected=element.value;search.path=path;document.dispatchEvent(new CustomEvent("open-file-serach",{bubbles:false,cancelable:true}));});wrapper.appendChild(searchOpen);}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-cookies",controller:function(element,alerts,cookie,env){if(!cookie.get("cookie-alert")){let text=element.dataset["cookies"]||"";alerts.add({text:text,class:"cookie-alert",link:env.HOME+"/policy/cookies",label:'Learn More',callback:function(){cookie.set("cookie-alert","true",365*10);}},0);}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-page-title",repeat:true,controller:function(element,document,expression){document.title=expression.parse(element.getAttribute("data-page-title"))||document.title;}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-to',repeat:false,controller:function(element,window){let button=window.document.createElement('button');button.className='scroll-to icon-up-dir';button.alt='Back To Top';button.title='Back To Top';button.addEventListener('click',function(){element.scrollIntoView(true,{behavior:'smooth'});button.blur();},false);element.appendChild(button);}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-direction',repeat:false,controller:function(element,window){let position=0;let check=function(){let direction=window.document.documentElement.scrollTop;if(direction>position){element.classList.remove('scroll-to-top') element.classList.add('scroll-to-bottom')} else{element.classList.remove('scroll-to-bottom') element.classList.add('scroll-to-top')} @@ -2680,7 +2737,13 @@ position=direction;let current=Math.ceil(direction/window.innerHeight);element.s else{element.classList.remove('scroll-end')}};window.addEventListener('scroll',check,false);window.addEventListener('resize',check,false);check();}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-setup",controller:function(element,console,form,alerts,router){element.addEventListener("submit",function(event){event.preventDefault();let loaderId=alerts.add({text:'Creating new project...',class:""},0);let formData=form.toJson(element);formData["name"]=formData["name"]||(element.dataset["defaultName"]||"");console.teams.create(formData["name"]||"").then(function(data){let team=data["$id"];formData=JSON.parse(JSON.stringify(formData).replace(new RegExp("{{teamId}}","g"),team));console.projects.create(formData["name"],team).then(function(project){alerts.remove(loaderId);window.location.href="/console/home?project="+project["$id"];},function(){throw new Error("Failed to setup project");});},function(){throw new Error("Setup failed creating project team");});});}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-switch",controller:function(element,router,document){let check=function(c){if(!element.value){return;} if(element.value===router.params.project){return;} return router.change("/console/home?project="+element.value);};element.addEventListener("change",function(){check();});}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-general-theme",controller:function(element,router,document){let toggle=function(c){if(document.body.classList.contains('theme-light')){document.body.classList.remove('theme-light');document.body.classList.add('theme-dark');window.localStorage.setItem('user-theme','theme-dark')} -else{document.body.classList.remove('theme-dark');document.body.classList.add('theme-light');window.localStorage.setItem('user-theme','theme-light')}};element.addEventListener("click",function(){toggle();});}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-paging-back",controller:function(element,container,expression,env){let paths=[];let limit=env.PAGING_LIMIT;let check=function(){let offset=parseInt(expression.parse(element.dataset["offset"])||"0");paths=paths.concat(expression.getPaths());if(offset-limit<0){element.disabled=true;}else{element.disabled=false;element.value=offset-limit;}};check();for(let i=0;i=sum){element.disabled=true;}else{element.disabled=false;element.value=offset+limit;}};check();for(let i=0;ib)return true +if(a=sum){element.disabled=true;}else{element.disabled=false;element.value=offset+limit;}};check();for(let i=0;i","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i=31536000){value=Math.floor(seconds/31536000);unit="year";} +else if(seconds>=86400){value=Math.floor(seconds/86400);unit="day";} +else if(seconds>=3600){value=Math.floor(seconds/3600);unit="hour";} +else if(seconds>=60){value=Math.floor(seconds/60);unit="minute";} +if(value!=1){unit=unit+"s";} +return value+" "+unit+" "+direction;}).add("ms2hum",function($value){let temp=$value;const years=Math.floor(temp/31536000),days=Math.floor((temp%=31536000)/86400),hours=Math.floor((temp%=86400)/3600),minutes=Math.floor((temp%=3600)/60),seconds=temp%60;if(days||hours||seconds||minutes){return((years?years+"y ":"")+ (days?days+"d ":"")+ (hours?hours+"h ":"")+ (minutes?minutes+"m ":"")+ Number.parseFloat(seconds).toFixed(0)+"s");} -return"< 1s";}).add("markdown",function($value,markdown){return markdown.render($value);}).add("pageCurrent",function($value,env){return Math.ceil(parseInt($value||0)/env.PAGING_LIMIT)+1;}).add("pageTotal",function($value,env){let total=Math.ceil(parseInt($value||0)/env.PAGING_LIMIT);return total?total:1;}).add("humanFileSize",function($value){if(!$value){return 0;} +return"< 1s";}).add("seconds2hum",function($value){var seconds=($value).toFixed(3);var minutes=($value/(60)).toFixed(1);var hours=($value/(60*60)).toFixed(1);var days=($value/(60*60*24)).toFixed(1);if(seconds<60){return seconds+"s";}else if(minutes<60){return minutes+"m";}else if(hours<24){return hours+"h";}else{return days+"d"}}).add("markdown",function($value,markdown){return markdown.render($value);}).add("pageCurrent",function($value,env){return Math.ceil(parseInt($value||0)/env.PAGING_LIMIT)+1;}).add("pageTotal",function($value,env){let total=Math.ceil(parseInt($value||0)/env.PAGING_LIMIT);return total?total:1;}).add("humanFileSize",function($value){if(!$value){return 0;} let thresh=1000;if(Math.abs($value)=thresh&&u'+ units[u]+"");}).add("statsTotal",function($value){if(!$value){return 0;} @@ -266,7 +272,10 @@ return result.length;}).add("documentAction",function(container){let collection= return'database.updateDocument';}).add("documentSuccess",function(container){let document=container.get('project-document');if(document&&!document.$id){return',redirect';} return'';}).add("firstElement",function($value){if($value&&$value[0]){return $value[0];} return $value;}).add("platformsLimit",function($value){return $value;}).add("limit",function($value){let postfix=($value.length>=50)?'...':'';return $value.substring(0,50)+postfix;;}).add("arraySentence",function($value){if(!Array.isArray($value)){return'';} -return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} +return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');}).add("envName",function($value,env){if(env&&env.ENVIRONMENTS&&env.ENVIRONMENTS[$value]){return env.ENVIRONMENTS[$value].name;} +return'';}).add("envLogo",function($value,env){if(env&&env.ENVIRONMENTS&&env.ENVIRONMENTS[$value]){return env.ENVIRONMENTS[$value].logo;} +return'';}).add("envVersion",function($value,env){if(env&&env.ENVIRONMENTS&&env.ENVIRONMENTS[$value]){return env.ENVIRONMENTS[$value].version;} +return'';});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} let abbr;if(number>=1e12){abbr="T";}else if(number>=1e9){abbr="B";}else if(number>=1e6){abbr="M";}else if(number>=1e3){abbr="K";}else{abbr="";} return annotate(number,maxPlaces,forcePlaces,abbr);} function annotate(number,maxPlaces,forcePlaces,abbr){let rounded=0;switch(abbr){case"T":rounded=number/1e12;break;case"B":rounded=number/1e9;break;case"M":rounded=number/1e6;break;case"K":rounded=number/1e3;break;case"":rounded=number;break;} @@ -274,10 +283,8 @@ if(maxPlaces!==false){let test=new RegExp("\\.\\d{"+(maxPlaces+1)+",}$");if(test if(forcePlaces!==false){rounded=Number(rounded).toFixed(forcePlaces);} return rounded+abbr;} window.ls.container.get("view").add({selector:"data-acl",controller:function(element,document,router,alerts){document.body.classList.remove("console");document.body.classList.remove("home");document.body.classList.add(router.getCurrent().view.scope);if(!router.getCurrent().view.project){document.body.classList.add("hide-nav");document.body.classList.remove("show-nav");}else{document.body.classList.add("show-nav");document.body.classList.remove("hide-nav");} -if("/console"===router.getCurrent().path){document.body.classList.add("index");}else{document.body.classList.remove("index");}}}).add({selector:"data-forms-headers",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";value.type="text";value.className="margin-bottom-no";value.placeholder="Value";wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.value=key.value.toLowerCase()+":"+value.value.toLowerCase();};let syncB=function(){let split=element.value.toLowerCase().split(":");key.value=split[0]||"";value.value=split[1]||"";key.value=key.value.trim();value.value=value.value.trim();};syncB();}}).add({selector:"data-prism",controller:function(window,document,element,alerts){Prism.highlightElement(element);let copy=document.createElement("i");copy.className="icon-docs copy";copy.title="Copy to Clipboard";copy.textContent="Click Here to Copy";copy.addEventListener("click",function(){window.getSelection().removeAllRanges();let range=document.createRange();range.selectNode(element);window.getSelection().addRange(range);try{document.execCommand("copy");alerts.add({text:"Copied to clipboard",class:""},3000);}catch(err){alerts.add({text:"Failed to copy text ",class:"error"},3000);} -window.getSelection().removeAllRanges();});element.parentNode.parentNode.appendChild(copy);}}).add({selector:"data-ls-ui-chart",controller:function(element,container,date,document){let child=document.createElement("canvas");child.width=500;child.height=175;let stats=container.get("usage");if(!stats||!stats["requests"]||!stats["requests"]["data"]){return;} -let config={type:"line",data:{labels:[],datasets:[{label:"Requests",backgroundColor:"rgba(230, 248, 253, 0.3)",borderColor:"#29b5d9",borderWidth:2,data:[0,0,0,0,0,0,0],fill:true}]},options:{responsive:true,title:{display:false,text:"Stats"},legend:{display:false},tooltips:{mode:"index",intersect:false,caretPadding:0},hover:{mode:"nearest",intersect:true},scales:{xAxes:[{display:false}],yAxes:[{display:false}]}}};for(let i=0;ielement.trim());return function(serviceForm,router,window){let url=window.location.href;keys.map(node=>{node=node.split("=");let key=node[0]||"";let name=node[1]||key;let value=getValue(key,"param",serviceForm);url=updateQueryString(name,value?value:null,url);});if(url!==window.location.href){window.history.pushState({},"",url);router.reset();}};},trigger:function(events){return function(document){events=events.trim().split(",");for(let i=0;i=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');} +break;default:break;}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-headers",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";value.type="text";value.className="margin-bottom-no";value.placeholder="Value";wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.value=key.value.toLowerCase()+":"+value.value.toLowerCase();};let syncB=function(){let split=element.value.toLowerCase().split(":");key.value=split[0]||"";value.value=split[1]||"";key.value=key.value.trim();value.value=value.value.trim();};syncB();}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-key-value",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";value.type="text";value.className="margin-bottom-no";value.placeholder="Value";wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.name=key.value;element.value=value.value;};let syncB=function(){key.value=element.name||"";value.value=element.value||"";};syncB();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-down",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-down]")).map(function(obj){obj.addEventListener("click",function(){if(element.nextElementSibling){console.log('down',element.offsetHeight);element.parentNode.insertBefore(element.nextElementSibling,element);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-up",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-up]")).map(function(obj){obj.addEventListener("click",function(){if(element.previousElementSibling){console.log('up',element);element.parentNode.insertBefore(element,element.previousElementSibling);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-nav",repeat:false,controller:function(element,view,container,document){let titles=document.querySelectorAll('[data-forms-nav-anchor]');let links=element.querySelectorAll('[data-forms-nav-link]');let minLink=null;let check=function(){let minDistance=null;let minElement=null;for(let i=0;i=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');} console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-apple",controller:function(element){let container=document.createElement("div");let row=document.createElement("div");let col1=document.createElement("div");let col2=document.createElement("div");let keyID=document.createElement("input");let keyLabel=document.createElement("label");let teamID=document.createElement("input");let teamLabel=document.createElement("label");let p8=document.createElement("textarea");let p8Label=document.createElement("label");keyLabel.textContent='Key ID';teamLabel.textContent='Team ID';p8Label.textContent='P8 File';row.classList.add('row');row.classList.add('thin');container.appendChild(row);container.appendChild(p8Label);container.appendChild(p8);row.appendChild(col1);row.appendChild(col2);col1.classList.add('col');col1.classList.add('span-6');col1.appendChild(keyLabel);col1.appendChild(keyID);col2.classList.add('col');col2.classList.add('span-6');col2.appendChild(teamLabel);col2.appendChild(teamID);keyID.type='text';keyID.placeholder='SHAB13ROFN';teamID.type='text';teamID.placeholder='ELA2CD3AED';p8.accept='.p8';p8.classList.add('margin-bottom-no');element.parentNode.insertBefore(container,element.nextSibling);element.addEventListener('change',sync);keyID.addEventListener('change',update);teamID.addEventListener('change',update);p8.addEventListener('change',update);function update(){let json={};json.keyID=keyID.value;json.teamID=teamID.value;json.p8=p8.value;element.value=JSON.stringify(json);} function sync(){if(!element.value){return;} let json={};try{json=JSON.parse(element.value);}catch(error){console.error('Failed to parse secret key');} @@ -358,7 +367,7 @@ var file=document.createElement("li");var image=document.createElement("img");im result+"/preview?width="+ previewWidth+"&height="+ previewHeight+"&project="+project+"&mode=admin";image.alt=previewAlt;file.className="file avatar";file.tabIndex=0;file.appendChild(image);preview.appendChild(file);var remove=(function(result){return function(event){render(result.$id);element.value='';};})(result);file.addEventListener("click",remove);file.addEventListener("keypress",remove);element.value=result;};input.addEventListener("change",function(){var message=alerts.add({text:labelLoading,class:""},0);var files=input.files;var read=JSON.parse(expression.parse(element.dataset["read"]||"[]"));var write=JSON.parse(expression.parse(element.dataset["write"]||"[]"));sdk.storage.createFile(files[0],read,write,1).then(function(response){onComplete(message);render(response.$id);},function(error){alerts.add({text:"An error occurred!",class:""},3000);onComplete(message);});input.disabled=true;});element.addEventListener("change",function(){if(!element.value){return;} -render(element.value);wrapper.scrollIntoView();});upload.addEventListener("keypress",function(){input.click();});element.parentNode.insertBefore(wrapper,element);wrapper.appendChild(preview);wrapper.appendChild(progress);wrapper.appendChild(upload);upload.appendChild(input);render(output);if(searchButton){let searchOpen=document.createElement("button");searchOpen.type='button';searchOpen.innerHTML=' Search';searchOpen.classList.add('reverse');let path=container.scope(searchButton);searchOpen.addEventListener('click',function(){search.selected=element.value;search.path=path;document.dispatchEvent(new CustomEvent("open-file-serach",{bubbles:false,cancelable:true}));});wrapper.appendChild(searchOpen);}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-cookies",controller:function(element,alerts,cookie,env){if(!cookie.get("cookie-alert")){let text=element.dataset["cookies"]||"";alerts.add({text:text,class:"cookie-alert",link:env.HOME+"/policy/cookies",callback:function(){cookie.set("cookie-alert","true",365*10);}},0);}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-page-title",repeat:true,controller:function(element,document,expression){document.title=expression.parse(element.getAttribute("data-page-title"))||document.title;}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-to',repeat:false,controller:function(element,window){let button=window.document.createElement('button');button.className='scroll-to icon-up-dir';button.alt='Back To Top';button.title='Back To Top';button.addEventListener('click',function(){element.scrollIntoView(true,{behavior:'smooth'});button.blur();},false);element.appendChild(button);}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-direction',repeat:false,controller:function(element,window){let position=0;let check=function(){let direction=window.document.documentElement.scrollTop;if(direction>position){element.classList.remove('scroll-to-top') +render(element.value);wrapper.scrollIntoView();});upload.addEventListener("keypress",function(){input.click();});element.parentNode.insertBefore(wrapper,element);wrapper.appendChild(preview);wrapper.appendChild(progress);wrapper.appendChild(upload);upload.appendChild(input);render(output);if(searchButton){let searchOpen=document.createElement("button");searchOpen.type='button';searchOpen.innerHTML=' Search';searchOpen.classList.add('reverse');let path=container.scope(searchButton);searchOpen.addEventListener('click',function(){search.selected=element.value;search.path=path;document.dispatchEvent(new CustomEvent("open-file-serach",{bubbles:false,cancelable:true}));});wrapper.appendChild(searchOpen);}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-cookies",controller:function(element,alerts,cookie,env){if(!cookie.get("cookie-alert")){let text=element.dataset["cookies"]||"";alerts.add({text:text,class:"cookie-alert",link:env.HOME+"/policy/cookies",label:'Learn More',callback:function(){cookie.set("cookie-alert","true",365*10);}},0);}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-page-title",repeat:true,controller:function(element,document,expression){document.title=expression.parse(element.getAttribute("data-page-title"))||document.title;}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-to',repeat:false,controller:function(element,window){let button=window.document.createElement('button');button.className='scroll-to icon-up-dir';button.alt='Back To Top';button.title='Back To Top';button.addEventListener('click',function(){element.scrollIntoView(true,{behavior:'smooth'});button.blur();},false);element.appendChild(button);}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-direction',repeat:false,controller:function(element,window){let position=0;let check=function(){let direction=window.document.documentElement.scrollTop;if(direction>position){element.classList.remove('scroll-to-top') element.classList.add('scroll-to-bottom')} else{element.classList.remove('scroll-to-bottom') element.classList.add('scroll-to-top')} @@ -366,7 +375,13 @@ position=direction;let current=Math.ceil(direction/window.innerHeight);element.s else{element.classList.remove('scroll-end')}};window.addEventListener('scroll',check,false);window.addEventListener('resize',check,false);check();}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-setup",controller:function(element,console,form,alerts,router){element.addEventListener("submit",function(event){event.preventDefault();let loaderId=alerts.add({text:'Creating new project...',class:""},0);let formData=form.toJson(element);formData["name"]=formData["name"]||(element.dataset["defaultName"]||"");console.teams.create(formData["name"]||"").then(function(data){let team=data["$id"];formData=JSON.parse(JSON.stringify(formData).replace(new RegExp("{{teamId}}","g"),team));console.projects.create(formData["name"],team).then(function(project){alerts.remove(loaderId);window.location.href="/console/home?project="+project["$id"];},function(){throw new Error("Failed to setup project");});},function(){throw new Error("Setup failed creating project team");});});}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-switch",controller:function(element,router,document){let check=function(c){if(!element.value){return;} if(element.value===router.params.project){return;} return router.change("/console/home?project="+element.value);};element.addEventListener("change",function(){check();});}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-general-theme",controller:function(element,router,document){let toggle=function(c){if(document.body.classList.contains('theme-light')){document.body.classList.remove('theme-light');document.body.classList.add('theme-dark');window.localStorage.setItem('user-theme','theme-dark')} -else{document.body.classList.remove('theme-dark');document.body.classList.add('theme-light');window.localStorage.setItem('user-theme','theme-light')}};element.addEventListener("click",function(){toggle();});}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-paging-back",controller:function(element,container,expression,env){let paths=[];let limit=env.PAGING_LIMIT;let check=function(){let offset=parseInt(expression.parse(element.dataset["offset"])||"0");paths=paths.concat(expression.getPaths());if(offset-limit<0){element.disabled=true;}else{element.disabled=false;element.value=offset-limit;}};check();for(let i=0;i=sum){element.disabled=true;}else{element.disabled=false;element.value=offset+limit;}};check();for(let i=0;ib)return true +if(a=sum){element.disabled=true;}else{element.disabled=false;element.value=offset+limit;}};check();for(let i=0;i")}:root .theme-dark{--config-color-background:#061F2F;--config-color-background-dark:#262d50;--config-color-background-fade:#1c223a;--config-color-background-fade-super:#1a1f35;--config-color-background-focus:#1a1f35;--config-color-background-input:#dce8f5;--config-color-tooltip-text:#061F2F;--config-color-tooltip-background:#dce8f5;--config-color-link:#4caedb;--config-color-placeholder:#9ea1af;--config-color-focus:#c7d8eb;--config-color-focus-fade:#1e233e;--config-color-focus-hover:#d3deea;--config-color-focus-glow:#d3deea;--config-color-focus-dark:#657586;--config-color-normal:#c7d8eb;--config-color-dark:#c7d8eb;--config-color-fade:#bec3e0;--config-color-fade-light:#181818;--config-color-fade-super:#262D50;--config-color-danger:#d84a4a;--config-color-success:#34b86d;--config-color-warning:#e0d56d;--config-color-info:#386fd2;--config-border-color:#262D50;--config-prism-background:#1F253F;--config-prism-numbers:#1F253F;--config-note-background:#171e33;--config-note-border:#262D50;--config-warning-background:#1F253F;--config-warning-border:#262D50;--config-social-twitter:var(--config-color-normal);--config-social-github:var(--config-color-normal);--config-social-discord:var(--config-color-normal);--config-social-facebook:var(--config-color-normal);--config-language-bash:var(--config-color-normal);--config-language-bash-contrast:var(--config-color-background);--config-language-javascript:var(--config-color-normal);--config-language-javascript-contrast:var(--config-color-background);--config-language-web:var(--config-color-normal);--config-language-web-contrast:var(--config-color-background);--config-language-yaml:var(--config-color-normal);--config-language-yaml-contrast:var(--config-color-background);--config-language-html:var(--config-color-normal);--config-language-html-contrast:var(--config-color-background);--config-language-php:var(--config-color-normal);--config-language-php-contrast:var(--config-color-background);--config-language-nodejs:var(--config-color-normal);--config-language-nodejs-contrast:var(--config-color-background);--config-language-ruby:var(--config-color-normal);--config-language-ruby-contrast:var(--config-color-background);--config-language-python:var(--config-color-normal);--config-language-python-contrast:var(--config-color-background);--config-language-go:var(--config-color-normal);--config-language-go-contrast:var(--config-color-background);--config-language-dart:var(--config-color-normal);--config-language-dart-contrast:var(--config-color-background);--config-language-flutter:var(--config-color-normal);--config-language-flutter-contrast:var(--config-color-background);--config-modal-note-background:#15192b;--config-modal-note-border:#161b31;--config-modal-note-color:var(--config-color-normal);--config-switch-background:var(--config-color-normal);--config-console-background:#20263f;--config-console-nav-start:#1c2139;--config-console-nav-end:#151929;--config-console-nav-border:#171b30;--config-console-nav-switch-background:var(--config-color-focus);--config-console-nav-switch-color:var(--config-color-background);--config-console-nav-switch-arrow:url("data:image/svg+xml;utf8,")}.theme-light .force-light{display:block!important}.theme-dark .force-dark{display:block!important}.force-dark{display:none!important}.force-light{display:none!important}@font-face{font-family:Poppins;font-style:normal;font-weight:100;src:url(/fonts/poppins-v9-latin-100.eot);src:local('Poppins Thin'),local('Poppins-Thin'),url(/fonts/poppins-v9-latin-100.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-100.woff2) format('woff2'),url(/fonts/poppins-v9-latin-100.woff) format('woff'),url(/fonts/poppins-v9-latin-100.ttf) format('truetype'),url(/fonts/poppins-v9-latin-100.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:300;src:url(/fonts/poppins-v9-latin-300.eot);src:local('Poppins Light'),local('Poppins-Light'),url(/fonts/poppins-v9-latin-300.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-300.woff2) format('woff2'),url(/fonts/poppins-v9-latin-300.woff) format('woff'),url(/fonts/poppins-v9-latin-300.ttf) format('truetype'),url(/fonts/poppins-v9-latin-300.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:400;src:url(/fonts/poppins-v9-latin-regular.eot);src:local('Poppins Regular'),local('Poppins-Regular'),url(/fonts/poppins-v9-latin-regular.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-regular.woff2) format('woff2'),url(/fonts/poppins-v9-latin-regular.woff) format('woff'),url(/fonts/poppins-v9-latin-regular.ttf) format('truetype'),url(/fonts/poppins-v9-latin-regular.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:500;src:url(/fonts/poppins-v9-latin-500.eot);src:local('Poppins Medium'),local('Poppins-Medium'),url(/fonts/poppins-v9-latin-500.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-500.woff2) format('woff2'),url(/fonts/poppins-v9-latin-500.woff) format('woff'),url(/fonts/poppins-v9-latin-500.ttf) format('truetype'),url(/fonts/poppins-v9-latin-500.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:600;src:url(/fonts/poppins-v9-latin-600.eot);src:local('Poppins SemiBold'),local('Poppins-SemiBold'),url(/fonts/poppins-v9-latin-600.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-600.woff2) format('woff2'),url(/fonts/poppins-v9-latin-600.woff) format('woff'),url(/fonts/poppins-v9-latin-600.ttf) format('truetype'),url(/fonts/poppins-v9-latin-600.svg#Poppins) format('svg')}@font-face{font-family:'Source Code Pro';font-style:normal;font-weight:400;src:url(/fonts/source-code-pro-v11-latin-regular.eot);src:local('Source Code Pro Regular'),local('SourceCodePro-Regular'),url(/fonts/source-code-pro-v11-latin-regular.eot?#iefix) format('embedded-opentype'),url(/fonts/source-code-pro-v11-latin-regular.woff2) format('woff2'),url(/fonts/source-code-pro-v11-latin-regular.woff) format('woff'),url(/fonts/source-code-pro-v11-latin-regular.ttf) format('truetype'),url(/fonts/source-code-pro-v11-latin-regular.svg#SourceCodePro) format('svg')}.padding{padding:30px}.padding-top{padding-top:30px!important}.padding-top-large{padding-top:50px!important}.padding-top-xl{padding-top:80px!important}.padding-bottom{padding-bottom:30px!important}.padding-bottom-large{padding-bottom:50px!important}.padding-bottom-xl{padding-bottom:80px!important}.margin-end{margin-right:20px!important}.margin-start{margin-left:20px!important}.margin-end-small{margin-right:10px!important}.margin-start-small{margin-left:10px!important}.margin-end-large{margin-right:50px!important}.margin-start-large{margin-left:50px!important}.margin-end-no{margin-right:0!important}.margin-start-no{margin-left:0!important}.margin-end-negative{margin-right:-30px!important}.margin-start-negative{margin-left:-30px!important}.margin-end-negative-small{margin-right:-15px!important}.margin-start-negative-small{margin-left:-15px!important}.margin-end-negative-tiny{margin-right:-5px!important}.margin-start-negative-tiny{margin-left:-5px!important}.margin-top{margin-top:30px!important}.margin-bottom{margin-bottom:30px!important}.margin-top-no{margin-top:0!important}.margin-bottom-no{margin-bottom:0!important}.margin-top-xxl{margin-top:140px!important}.margin-top-xl{margin-top:80px!important}.margin-top-large{margin-top:50px!important}.margin-top-small{margin-top:15px!important}.margin-top-tiny{margin-top:5px!important}.margin-top-negative{margin-top:-30px!important}.margin-top-negative-tiny{margin-top:-5px!important}.margin-top-negative-small{margin-top:-15px!important}.margin-top-negative-large{margin-top:-50px!important}.margin-top-negative-xl{margin-top:-80px!important}.margin-top-negative-xxl{margin-top:-100px!important}.margin-bottom-xxl{margin-bottom:140px!important}.margin-bottom-xl{margin-bottom:80px!important}.margin-bottom-large{margin-bottom:50px!important}.margin-bottom-small{margin-bottom:15px!important}.margin-bottom-tiny{margin-bottom:5px!important}.margin-bottom-negative{margin-bottom:-30px!important}.margin-bottom-negative-tiny{margin-bottom:-5px!important}.margin-bottom-negative-small{margin-bottom:-15px!important}.margin-bottom-negative-large{margin-bottom:-50px!important}.margin-bottom-negative-xl{margin-bottom:-80px!important}.margin-bottom-negative-xl{margin-bottom:-100px!important}.force-left,.ide{direction:ltr;text-align:left}.force-right{direction:rtl;text-align:right}.pull-left{float:left}.pull-right{float:right}.ratio-wide{height:0;overflow:hidden;padding-top:56%;position:relative;width:100%}.ratio-wide>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-square{height:0;overflow:hidden;padding-top:56%;position:relative;width:100%}.ratio-square>*{position:absolute;top:0;left:0;width:100%;height:100%}.clear:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.phones-only{display:none}@media only screen and (max-width:550px){.phones-only{display:inherit!important}}.tablets-only{display:none}@media only screen and (min-width:551px) and (max-width:1198px){.tablets-only{display:inherit!important}}.desktops-only{display:none}@media only screen and (min-width:1199px){.desktops-only{display:inherit!important}}.phones-only-inline{display:none}@media only screen and (max-width:550px){.phones-only-inline{display:inline-block!important}}.tablets-only-inline{display:none}@media only screen and (min-width:551px) and (max-width:1198px){.tablets-only-inline{display:inline-block!important}}.desktops-only-inline{display:none}@media only screen and (min-width:1199px){.desktops-only-inline{display:inline-block!important}}*{font-family:Poppins,sans-serif;-webkit-font-smoothing:antialiased;font-weight:300}h1,h2,h3,h4,h5,h6{margin:0}h4,h5,h6{font-weight:400}.link,a{color:var(--config-color-link);text-decoration:none;border-left:2px solid transparent;border-right:2px solid transparent;transition:.2s;cursor:pointer}.link.disabled,a.disabled{opacity:.5}.link.tag:hover,a.tag:hover{opacity:.9}.link.danger,a.danger{color:var(--config-color-danger)}.link.link-animation-enabled,a.link-animation-enabled{display:inline-block}.link.link-animation-enabled:hover,a.link-animation-enabled:hover{transform:translateY(-2px)}.link-return-animation--start>i{display:inline-block;transition:.2s}.link-return-animation--start:hover>i{transform:translateX(-2px)}.link-return-animation--end>i{display:inline-block;transition:.2s}.link-return-animation--end:hover>i{transform:translateX(2px)}b,strong{font-weight:500}p{margin:0 0 20px 0;line-height:26px}small{font-size:16px;color:var(--config-color-fade)}.text-size-small{font-size:13px}.text-size-xs{font-size:10px}.text-size-normal{font-size:16px}.text-height-large{height:30px;line-height:30px}.text-height-small{line-height:13px}.text-one-liner{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.text-bold{font-weight:400!important}.text-bold-large{font-weight:500!important}.text-bold-xl{font-weight:600!important}.text-danger{color:var(--config-color-danger)!important}.text-success{color:var(--config-color-success)!important}.text-upper{text-transform:uppercase}.text-warning{color:var(--config-color-warning)}.text-focus{color:var(--config-color-focus)}.text-fade{color:var(--config-color-fade)}.text-green{color:var(--config-color-success)}.text-red{color:var(--config-color-danger)}.text-info{color:var(--config-color-info)}.text-yellow{color:#ffe28b}.text-disclaimer{font-size:11px;color:var(--config-color-fade)}.text-fade-extra{color:var(--config-color-fade);opacity:.5}.text-line-high-large{line-height:30px}.text-line-high-xl{line-height:40px}.text-sign{margin:5px 0;font-size:25px;width:25px;height:25px;line-height:25px;display:inline-block}.text-align-center{text-align:center}.text-align-start{text-align:left}.text-align-end{text-align:right}.text-align-left{text-align:left}.text-align-right{text-align:right}.text-dir-ltr{direction:ltr;display:inline-block}.text-dir-rtl{direction:rtl;display:inline-block}.icon-dot-3:before{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-o-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}i[class*=' icon-']:before,i[class^=icon-]:before{display:inline;line-height:unset}table{width:calc(100% + 60px);border-collapse:collapse;margin:-30px;border-radius:10px;overflow:hidden;position:relative;table-layout:fixed}table.y-scroll{overflow-y:auto}table thead{box-shadow:0 0 2px rgba(0,0,0,.25);border-bottom:solid 1px var(--config-color-fade-super);font-size:14px}table.small{font-size:14px}table.open-end tbody tr:last-child{border-bottom:none;font-weight:700;background:#f7fbf7}table.full tbody td,table.full tbody th{vertical-align:top;white-space:normal;overflow:auto;line-height:24px;padding-top:20px;padding-bottom:20px;height:auto}table .avatar{width:30px;height:30px}table tr{border-bottom:solid 1px var(--config-color-fade-super)}table tr:last-child{border-bottom:none}table tr:nth-child(even){background:var(--config-color-background-fade-super)}table tr.selected{background:var(--config-note-background)}table tr.selected td,table tr.selected td span{font-weight:500}table th{text-align:left;font-weight:400}table th i{color:var(--config-color-fade);font-size:10px;display:inline-block;vertical-align:top;line-height:16px;padding:0 3px}table td,table th{height:65px;padding:0 15px;line-height:50px}table td:first-child,table th:first-child{padding-left:30px}table td:last-child,table th:last-child{padding-right:30px}table td,table th{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){table.vertical{border-top:solid 1px var(--config-color-fade-super);display:block;overflow:hidden;padding-top:12px}table.vertical .hide{display:none}table.vertical tbody,table.vertical td,table.vertical th,table.vertical thead,table.vertical tr{width:100%;display:block}table.vertical th,table.vertical tr{padding-top:12px;padding-bottom:12px}table.vertical th:first-child,table.vertical tr:first-child{padding-top:0}table.vertical td,table.vertical th{padding:5px 20px!important;text-overflow:ellipsis;white-space:normal;height:40px;line-height:40px;width:calc(100% - 40px)}table.vertical td:first-child,table.vertical td:last-child,table.vertical th:first-child,table.vertical th:last-child{padding:0 10px}table.vertical td:last-child,table.vertical th:last-child{padding-bottom:0}table.vertical td p,table.vertical th p{display:inline-block;width:calc(100% - 40px)}table.vertical td:not([data-title=""]):before{content:attr(data-title);margin-right:4px;font-weight:400}table.vertical thead{display:none}}.zone{max-width:var(--config-width-xl);margin:0 auto 40px auto}.zone.xxxl{max-width:calc(1400px - 100px)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.zone.xxxl{max-width:100%}}.zone.xxl{max-width:var(--config-width-xxl)}.zone.xl{max-width:var(--config-width-xl)}.zone.large{max-width:var(--config-width-large)}.zone.medium{max-width:var(--config-width-medium)}.zone.small{max-width:var(--config-width-small)}.row{position:relative;margin:0 -50px;padding-left:50px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.row{margin:0 -30px;padding-left:30px}}.row.force-ltr>.col{float:left}.row.force-rtl>.col{float:right}.row.force-reverse>.col{float:right}.row.wide{margin:0 -100px;padding-left:100px}.row.wide>.span-1{width:calc(8.33333333% * 1 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-2{width:calc(8.33333333% * 2 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-3{width:calc(8.33333333% * 3 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-4{width:calc(8.33333333% * 4 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-5{width:calc(8.33333333% * 5 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-6{width:calc(8.33333333% * 6 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-7{width:calc(8.33333333% * 7 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-8{width:calc(8.33333333% * 8 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-9{width:calc(8.33333333% * 9 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-10{width:calc(8.33333333% * 10 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-11{width:calc(8.33333333% * 11 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-12{width:calc(8.33333333% * 12 - 100px);box-sizing:content-box;padding-right:100px}.row.thin{margin:0 -20px;padding-left:20px}.row.thin>.span-1{width:calc(8.33333333% * 1 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-2{width:calc(8.33333333% * 2 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-3{width:calc(8.33333333% * 3 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-4{width:calc(8.33333333% * 4 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-5{width:calc(8.33333333% * 5 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-6{width:calc(8.33333333% * 6 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-7{width:calc(8.33333333% * 7 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-8{width:calc(8.33333333% * 8 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-9{width:calc(8.33333333% * 9 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-10{width:calc(8.33333333% * 10 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-11{width:calc(8.33333333% * 11 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-12{width:calc(8.33333333% * 12 - 20px);box-sizing:content-box;padding-right:20px}.row.modalize{margin:0 -30px;padding-left:30px}.row.modalize>.span-1{width:calc(8.33333333% * 1 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-2{width:calc(8.33333333% * 2 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-3{width:calc(8.33333333% * 3 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-4{width:calc(8.33333333% * 4 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-5{width:calc(8.33333333% * 5 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-6{width:calc(8.33333333% * 6 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-7{width:calc(8.33333333% * 7 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-8{width:calc(8.33333333% * 8 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-9{width:calc(8.33333333% * 9 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-10{width:calc(8.33333333% * 10 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-11{width:calc(8.33333333% * 11 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-12{width:calc(8.33333333% * 12 - 30px);box-sizing:content-box;padding-right:30px}.row:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.row .col{float:left;box-sizing:border-box}.row .col.sticky-top{position:sticky;top:90px}.row .col.sticky-bottom{position:sticky;bottom:0}.row .span-1{width:calc(8.33333333% * 1 - 40px);box-sizing:content-box;padding-right:40px}.row .span-2{width:calc(8.33333333% * 2 - 40px);box-sizing:content-box;padding-right:40px}.row .span-3{width:calc(8.33333333% * 3 - 40px);box-sizing:content-box;padding-right:40px}.row .span-4{width:calc(8.33333333% * 4 - 40px);box-sizing:content-box;padding-right:40px}.row .span-5{width:calc(8.33333333% * 5 - 40px);box-sizing:content-box;padding-right:40px}.row .span-6{width:calc(8.33333333% * 6 - 40px);box-sizing:content-box;padding-right:40px}.row .span-7{width:calc(8.33333333% * 7 - 40px);box-sizing:content-box;padding-right:40px}.row .span-8{width:calc(8.33333333% * 8 - 40px);box-sizing:content-box;padding-right:40px}.row .span-9{width:calc(8.33333333% * 9 - 40px);box-sizing:content-box;padding-right:40px}.row .span-10{width:calc(8.33333333% * 10 - 40px);box-sizing:content-box;padding-right:40px}.row .span-11{width:calc(8.33333333% * 11 - 40px);box-sizing:content-box;padding-right:40px}.row .span-12{width:calc(8.33333333% * 12 - 40px);box-sizing:content-box;padding-right:40px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.row.responsive{width:100%;padding:0;margin:0}.row.responsive>.span-1,.row.responsive>.span-10,.row.responsive>.span-11,.row.responsive>.span-12,.row.responsive>.span-2,.row.responsive>.span-3,.row.responsive>.span-4,.row.responsive>.span-5,.row.responsive>.span-6,.row.responsive>.span-7,.row.responsive>.span-8,.row.responsive>.span-9{width:calc(8.33333333% * 12 - 0px)!important;box-sizing:content-box!important;padding-right:0!important;width:100%!important}}.tiles{position:relative}.tiles:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.tiles>*{margin-right:50px!important;float:left;width:calc(33.3333% - 33.3333px)}.tiles>* .photo-title{width:calc(100% + 30px);height:15px;margin:-15px -15px 10px -15px;border-radius:10px 10px 0 0;background:var(--config-color-fade-super);border-bottom:solid 1px var(--config-color-fade-super)}.tiles>:nth-child(3n){margin-right:0!important}@media only screen and (min-width:551px) and (max-width:1198px){.tiles>li{width:calc(50% - 25px)}.tiles>li:nth-child(3n){margin-right:50px!important}.tiles>li:nth-child(2n){margin-right:0!important}}@media only screen and (max-width:550px){.tiles>li{width:100%;margin-right:0!important}}@font-face{font-family:fontello;src:url(data:application/octet-stream;base64,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) format('woff'),url(data:application/octet-stream;base64,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6DAAAAAxAADoMQAA6DEAAAAyAADoMgAA6DIAAAAzAADoMwAA6DMAAAA0AADoNAAA6DQAAAA1AADoNQAA6DUAAAA2AADoNgAA6DYAAAA3AADoNwAA6DcAAAA4AADoOAAA6DgAAAA5AADoOQAA6DkAAAA6AADoOgAA6DoAAAA7AADoOwAA6DsAAAA8AADoPAAA6DwAAAA9AADoPQAA6D0AAAA+AADoPgAA6D4AAAA/AADoPwAA6D8AAABAAADoQAAA6EAAAABBAADoQQAA6EEAAABCAADoQgAA6EIAAABDAADoQwAA6EMAAABEAADoRAAA6EQAAABFAADoRQAA6EUAAABGAADoRgAA6EYAAABHAADoRwAA6EcAAABIAADoSAAA6EgAAABJAADoSQAA6EkAAABKAADoSgAA6EoAAABLAADoSwAA6EsAAABMAADoTAAA6EwAAABNAADwjgAA8I4AAABOAADwmwAA8JsAAABPAADwsAAA8LAAAABQAADwxQAA8MUAAABRAADwygAA8MoAAABSAADwywAA8MsAAABTAADwzQAA8M0AAABUAADw3AAA8NwAAABVAADw4QAA8OEAAABWAADxGAAA8RgAAABXAADxHAAA8RwAAABYAADxIQAA8SEAAABZAADxMgAA8TIAAABaAADxNwAA8TcAAABbAADxOAAA8TgAAABcAADxcQAA8XEAAABdAADxegAA8XoAAABeAADxkgAA8ZIAAABfAADxkwAA8ZMAAABgAADxnAAA8ZwAAABhAADxoAAA8aAAAABiAADxrQAA8a0AAABjAADxwAAA8cAAAABkAADxzQAA8c0AAABlAADx3AAA8dwAAABmAADx5QAA8eUAAABnAADx/gAA8f4AAABoAADyMQAA8jEAAABpAADyOgAA8joAAABqAADylgAA8pYAAABrAADyxgAA8sYAAABsAAAAAgAA/7ECygMMABUAHgAlQCIABQEFbwMBAQQBbwAEAgRvAAIAAm8AAABmExcRERcyBgUaKyUUBiMhIiY1ND4DFxYyNzIeAwMUBiIuATYeAQLKRjH+JDFGChgqPi1JykoqQiYcCI98tHoEgqyERTxYWDwwVFY8KAFISCY+VFYBwFh+frCAAnwAAAL//v/OA+oC7gAOAB4AZEuwDVBYQCMAAwQEA2MFAQACAQIAAW0AAQFuAAQCAgRU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OgAvAC8AZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AAAAAAgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtAQIBAwEEAQUBBgEHAQgBCQEKAQsBDAENAQ4BDwEQAREBEgETARQBFQEWARcBGAEZARoBGwEcAR0BHgEfASABIQEiASMBJAElASYBJwEoASkBKgErASwBLQEuAS8BMAExATIBMwE0ATUBNgE3ATgBOQE6ATsBPAE9AT4BPwFAAUEBQgFDAUQBRQFGAUcBSAFJAUoBSwFMAU0BTgFPAVABUQFSAVMBVAFVAVYBVwFYAVkBWgFbAVwBXQFeAV8BYAFhAWIBYwFkAWUBZgFnAWgBaQFqAWsBbAFtAW4ABHVzZXIGZm9sZGVyBGxpc3QFbG9naW4DY29nB3R3aXR0ZXILYXJ0aWNsZS1hbHQGY2FuY2VsBGhvbWUIZG93bi1kaXIIZmFjZWJvb2sIYXN0ZXJpc2sGdXBsb2FkCXN0b3B3YXRjaAZleHBvcnQFaGVhcnQEcGx1cwZ1cC1kaXIEbWVudQlsZWZ0LW9wZW4KcmlnaHQtb3BlbgVpbmJveAZ3cmVuY2gHY29tbWVudA1zdGFja292ZXJmbG93CHF1ZXN0aW9uCm9rLWNpcmNsZWQHd2FybmluZwRtYWlsBGxpbmsHa2V5LWludgV0cmFzaAhkb3dubG9hZAdnbGFzc2VzBnFyY29kZQdzaHVmZmxlA2V5ZQRsb2NrBnNlYXJjaARiZWxsBXVzZXJzCGxvY2F0aW9uCWJyaWVmY2FzZQlpbnN0YWdyYW0FY2xvY2sFcGhvbmUIY2FsZW5kYXIFcHJpbnQEZWRpdARib2xkBml0YWxpYwZyb2NrZXQId2hhdHNhcHAFZG90LTMMaW5mby1jaXJjbGVkCHZpZGVvY2FtC3F1b3RlLXJpZ2h0B3BpY3R1cmUHcGFsZXR0ZQRsYW1wCWJvb2stb3BlbgJvawhjaGF0LWFsdAdhcmNoaXZlBHBsYXkFcGF1c2UJZG93bi1vcGVuB3VwLW9wZW4FbWludXMIZXhjaGFuZ2UHbmV0d29yawdkaXNjb3JkCG1vb24taW52B3N1bi1pbnYOY2FuY2VsLWNpcmNsZWQJbGlnaHRuaW5nA2RldghsaW5rLWV4dA5naXRodWItY2lyY2xlZAZmaWx0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IyAgPLAEI0IjOLErARQrsARDLrArKy2wRCywABUgR7AAI0KyAAEBFRQTLrAxKi2wRSywABUgR7AAI0KyAAEBFRQTLrAxKi2wRiyxAAEUE7AyKi2wRyywNCotsEgssAAWRSMgLiBGiiNhOLErARQrLbBJLLAII0KwSCstsEossgAAQSstsEsssgABQSstsEwssgEAQSstsE0ssgEBQSstsE4ssgAAQistsE8ssgABQistsFAssgEAQistsFEssgEBQistsFIssgAAPistsFMssgABPistsFQssgEAPistsFUssgEBPistsFYssgAAQCstsFcssgABQCstsFgssgEAQCstsFkssgEBQCstsFossgAAQystsFsssgABQystsFwssgEAQystsF0ssgEBQystsF4ssgAAPystsF8ssgABPystsGAssgEAPystsGEssgEBPystsGIssDcrLrErARQrLbBjLLA3K7A7Ky2wZCywNyuwPCstsGUssAAWsDcrsD0rLbBmLLA4Ky6xKwEUKy2wZyywOCuwOystsGgssDgrsDwrLbBpLLA4K7A9Ky2waiywOSsusSsBFCstsGsssDkrsDsrLbBsLLA5K7A8Ky2wbSywOSuwPSstsG4ssDorLrErARQrLbBvLLA6K7A7Ky2wcCywOiuwPCstsHEssDorsD0rLbByLLMJBAIDRVghGyMhWUIrsAhlsAMkUHiwARUwLQBLuADIUlixAQGOWbABuQgACABjcLEABUKyAAEAKrEABUKzCgIBCCqxAAVCsw4AAQgqsQAGQroCwAABAAkqsQAHQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMMAgEMKrgB/4WwBI2xAgBEAAA=) format('truetype')}[class*=" icon-"]:before,[class^=icon-]:before{font-family:fontello;font-style:normal;font-weight:400;speak:none;display:inline-block;text-decoration:inherit;width:1em;margin-right:.2em;text-align:center;font-variant:normal;text-transform:none;line-height:1em;margin-left:.2em}.icon-user:before{content:'\e800'}.icon-folder:before{content:'\e801'}.icon-list:before{content:'\e802'}.icon-login:before{content:'\e803'}.icon-cog:before{content:'\e804'}.icon-twitter:before{content:'\e805'}.icon-article-alt:before{content:'\e806'}.icon-cancel:before{content:'\e807'}.icon-home:before{content:'\e808'}.icon-down-dir:before{content:'\e809'}.icon-facebook:before{content:'\e80a'}.icon-asterisk:before{content:'\e80b'}.icon-upload:before{content:'\e80c'}.icon-stopwatch:before{content:'\e80d'}.icon-export:before{content:'\e80e'}.icon-heart:before{content:'\e80f'}.icon-plus:before{content:'\e810'}.icon-up-dir:before{content:'\e811'}.icon-menu:before{content:'\e812'}.icon-left-open:before{content:'\e813'}.icon-right-open:before{content:'\e814'}.icon-inbox:before{content:'\e815'}.icon-wrench:before{content:'\e816'}.icon-comment:before{content:'\e817'}.icon-stackoverflow:before{content:'\e818'}.icon-question:before{content:'\e819'}.icon-ok-circled:before{content:'\e81a'}.icon-warning:before{content:'\e81b'}.icon-mail:before{content:'\e81c'}.icon-link:before{content:'\e81d'}.icon-key-inv:before{content:'\e81e'}.icon-trash:before{content:'\e81f'}.icon-download:before{content:'\e820'}.icon-glasses:before{content:'\e821'}.icon-qrcode:before{content:'\e822'}.icon-shuffle:before{content:'\e823'}.icon-eye:before{content:'\e824'}.icon-lock:before{content:'\e825'}.icon-search:before{content:'\e826'}.icon-bell:before{content:'\e827'}.icon-users:before{content:'\e828'}.icon-location:before{content:'\e829'}.icon-briefcase:before{content:'\e82a'}.icon-instagram:before{content:'\e82b'}.icon-clock:before{content:'\e82c'}.icon-phone:before{content:'\e82d'}.icon-calendar:before{content:'\e82e'}.icon-print:before{content:'\e82f'}.icon-edit:before{content:'\e830'}.icon-bold:before{content:'\e831'}.icon-italic:before{content:'\e832'}.icon-rocket:before{content:'\e833'}.icon-whatsapp:before{content:'\e834'}.icon-dot-3:before{content:'\e835'}.icon-info-circled:before{content:'\e836'}.icon-videocam:before{content:'\e837'}.icon-quote-right:before{content:'\e838'}.icon-picture:before{content:'\e839'}.icon-palette:before{content:'\e83a'}.icon-lamp:before{content:'\e83b'}.icon-book-open:before{content:'\e83c'}.icon-ok:before{content:'\e83d'}.icon-chat-alt:before{content:'\e83e'}.icon-archive:before{content:'\e83f'}.icon-play:before{content:'\e840'}.icon-pause:before{content:'\e841'}.icon-down-open:before{content:'\e842'}.icon-up-open:before{content:'\e843'}.icon-minus:before{content:'\e844'}.icon-exchange:before{content:'\e845'}.icon-network:before{content:'\e846'}.icon-discord:before{content:'\e847'}.icon-moon-inv:before{content:'\e848'}.icon-sun-inv:before{content:'\e849'}.icon-cancel-circled:before{content:'\e84a'}.icon-lightning:before{content:'\e84b'}.icon-dev:before{content:'\e84c'}.icon-link-ext:before{content:'\f08e'}.icon-github-circled:before{content:'\f09b'}.icon-filter:before{content:'\f0b0'}.icon-docs:before{content:'\f0c5'}.icon-list-bullet:before{content:'\f0ca'}.icon-list-numbered:before{content:'\f0cb'}.icon-underline:before{content:'\f0cd'}.icon-sort:before{content:'\f0dc'}.icon-linkedin:before{content:'\f0e1'}.icon-smile:before{content:'\f118'}.icon-keyboard:before{content:'\f11c'}.icon-code:before{content:'\f121'}.icon-shield:before{content:'\f132'}.icon-angle-circled-left:before{content:'\f137'}.icon-angle-circled-right:before{content:'\f138'}.icon-bitbucket:before{content:'\f171'}.icon-windows:before{content:'\f17a'}.icon-dot-circled:before{content:'\f192'}.icon-wheelchair:before{content:'\f193'}.icon-bank:before{content:'\f19c'}.icon-google:before{content:'\f1a0'}.icon-building-filled:before{content:'\f1ad'}.icon-database:before{content:'\f1c0'}.icon-lifebuoy:before{content:'\f1cd'}.icon-header:before{content:'\f1dc'}.icon-binoculars:before{content:'\f1e5'}.icon-chart-area:before{content:'\f1fe'}.icon-pinterest:before{content:'\f231'}.icon-medium:before{content:'\f23a'}.icon-gitlab:before{content:'\f296'}.icon-telegram:before{content:'\f2c6'}.datalist-polyfill{list-style:none;display:none;background:#fff;box-shadow:0 2px 2px #999;position:absolute;left:0;top:0;margin:0;padding:0;max-height:300px;overflow-y:auto}.datalist-polyfill:empty{display:none!important}.datalist-polyfill>li{padding:3px;font:13px "Lucida Grande",Sans-Serif}.datalist-polyfill__active{background:#3875d7;color:#fff}date-input-polyfill{z-index:1000!important;max-width:320px!important;width:320px!important}date-input-polyfill .monthSelect-wrapper,date-input-polyfill .yearSelect-wrapper{height:50px;line-height:50px;padding:0;width:40%!important;margin-bottom:10px!important}date-input-polyfill .monthSelect-wrapper select,date-input-polyfill .yearSelect-wrapper select{padding:0 12px;height:50px;line-height:50px;box-sizing:border-box}date-input-polyfill .yearSelect-wrapper{width:35%!important}date-input-polyfill table{width:100%!important;max-width:100%!important;padding:0 12px 12px 12px!important;box-sizing:border-box;margin:0}date-input-polyfill table td:first-child,date-input-polyfill table td:last-child,date-input-polyfill table th:first-child,date-input-polyfill table th:last-child{width:32px!important;padding:4px!important}date-input-polyfill select{margin-bottom:10px}date-input-polyfill button{width:25%!important;height:50px!important;line-height:50px!important;margin-bottom:10px!important;background:inherit;position:relative;color:inherit;padding:inherit;box-sizing:inherit;border-radius:inherit;font-size:inherit;box-shadow:none;border:none;border-bottom:none!important}::placeholder{color:var(--config-color-placeholder);text-align:left}::-webkit-input-placeholder{text-align:left}input:-moz-placeholder{text-align:left}input,textarea{background:var(--config-color-background-input)}input[type=file],input[type=file]::-webkit-file-upload-button{cursor:pointer}.button,button{display:inline-block;background:var(--config-color-focus);border-radius:26px;border:none;color:var(--config-color-background-fade);height:52px;line-height:52px;padding:0 25px;cursor:pointer;font-size:16px;box-sizing:border-box;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.button:focus,.button:hover,button:focus,button:hover{background:var(--config-color-focus-hover)}.button.fly,button.fly{position:fixed;z-index:2;bottom:30px;right:30px}@media only screen and (max-width:550px){.button.fly,button.fly{right:15px}}.button.fill,button.fill{display:block;width:100%;text-align:center;padding:0 10px!important}.button.fill-aligned,button.fill-aligned{display:block;width:100%;text-align:left;padding:0 20px!important}.button.icon,button.icon{padding-right:30px!important}.button.icon-reduce,button.icon-reduce{padding-left:15px!important}.button.reverse,button.reverse{background:0 0;height:50px;line-height:48px;padding:0 23px;color:var(--config-color-focus);border:solid 2px var(--config-color-focus)}.button.reverse:focus,.button.reverse:hover,button.reverse:focus,button.reverse:hover{color:var(--config-color-focus-hover);border-color:var(--config-color-focus-hover)}.button.small,button.small{padding:0 15px;height:40px;line-height:36px;font-size:13px}.button.round,button.round{width:52px;padding:0}.button.round.small,button.round.small{font-size:12px;width:30px;height:30px;line-height:30px}.button.white,button.white{background:#fff;color:var(--config-color-focus)}.button.white.reverse,button.white.reverse{color:#fff;background:0 0;border:solid 2px #fff}.button.trans,button.trans{background:0 0!important}.button.trans.reverse,button.trans.reverse{background:0 0!important}.button.success,button.success{background:var(--config-color-success)}.button.success.reverse,button.success.reverse{color:var(--config-color-success);background:#fff;border:solid 2px var(--config-color-success)}.button.danger,button.danger{background:var(--config-color-danger);color:#fff}.button.danger.reverse,button.danger.reverse{color:var(--config-color-danger);background:var(--config-color-background-fade);border:solid 2px var(--config-color-danger)}.button.dark,button.dark{background:var(--config-color-dark);color:var(--config-color-background-fade)}.button.dark.reverse,button.dark.reverse{color:var(--config-color-dark);background:var(--config-color-background-fade);border:solid 2px var(--config-color-dark)}.button .disabled,.button.disabled,.button:disabled,button .disabled,button.disabled,button:disabled{color:var(--config-color-normal);background:var(--config-color-background-dark);opacity:.6;cursor:default}.button.link,button.link{background:0 0;border-radius:0;color:var(--config-color-link);height:auto;line-height:normal;padding:0;padding-right:0!important}.button.link:focus,button.link:focus{box-shadow:inherit}.button.strip,button.strip{background:0 0;height:auto;line-height:16px;color:inherit;padding:0 5px}.button.facebook,button.facebook{color:#fff!important;background:#4070b4!important}.button.twitter,button.twitter{color:#fff!important;background:#56c2ea!important}.button.linkedin,button.linkedin{color:#fff!important;background:#0076b5!important}.button.github,button.github{color:#fff!important;background:#7e7c7c!important}.button:focus,button:focus{outline:0}label{margin-bottom:15px;display:block;line-height:normal}.input,input[type=date],input[type=datetime-local],input[type=email],input[type=file],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=url],select,textarea{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px}.input[type=file],input[type=date][type=file],input[type=datetime-local][type=file],input[type=email][type=file],input[type=file][type=file],input[type=number][type=file],input[type=password][type=file],input[type=search][type=file],input[type=tel][type=file],input[type=text][type=file],input[type=url][type=file],select[type=file],textarea[type=file]{line-height:0;padding:15px;height:auto}.input:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=email]:focus,input[type=file]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus,select:focus,textarea:focus{outline:0;border-color:#b3d7fd}.input:disabled,input[type=date]:disabled,input[type=datetime-local]:disabled,input[type=email]:disabled,input[type=file]:disabled,input[type=number]:disabled,input[type=password]:disabled,input[type=search]:disabled,input[type=tel]:disabled,input[type=text]:disabled,input[type=url]:disabled,select:disabled,textarea:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.input.strip,input[type=date].strip,input[type=datetime-local].strip,input[type=email].strip,input[type=file].strip,input[type=number].strip,input[type=password].strip,input[type=search].strip,input[type=tel].strip,input[type=text].strip,input[type=url].strip,select.strip,textarea.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:right 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.input.strip:focus,input[type=date].strip:focus,input[type=datetime-local].strip:focus,input[type=email].strip:focus,input[type=file].strip:focus,input[type=number].strip:focus,input[type=password].strip:focus,input[type=search].strip:focus,input[type=tel].strip:focus,input[type=text].strip:focus,input[type=url].strip:focus,select.strip:focus,textarea.strip:focus{border-color:#b3d7fd}.input:-webkit-autofill::first-line,input[type=date]:-webkit-autofill::first-line,input[type=datetime-local]:-webkit-autofill::first-line,input[type=email]:-webkit-autofill::first-line,input[type=file]:-webkit-autofill::first-line,input[type=number]:-webkit-autofill::first-line,input[type=password]:-webkit-autofill::first-line,input[type=search]:-webkit-autofill::first-line,input[type=tel]:-webkit-autofill::first-line,input[type=text]:-webkit-autofill::first-line,input[type=url]:-webkit-autofill::first-line,select:-webkit-autofill::first-line,textarea:-webkit-autofill::first-line{font-weight:300;font-size:16px}input[type=email],input[type=url]{direction:ltr}input[type=email]::placeholder,input[type=url]::placeholder{text-align:left;direction:ltr}select{background:0 0;-webkit-appearance:none;background-image:var(--config-console-nav-switch-arrow);background-position:right 15px top 50%;background-repeat:no-repeat;background-color:var(--config-color-background-input);width:calc(100% - 62px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:45px}select:-webkit-autofill{background-image:url("data:image/svg+xml;utf8,")!important;background-position:100% 50%!important;background-repeat:no-repeat!important}input[type=search],input[type=search].strip{background:0 0;-webkit-appearance:none;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAdZJREFUWIXt1s2LjWEYBvDfnDMzFpNIamZIFrMiJYMyFmKhZKfOwoiFr2LFn2BByG6WVrKwMcjWxgoLIlKIUk6RrzAjZWZ8LO731FlwvB+PUbjq6X0X7/VeV/d9P9fz8IdRL8Hpw3x8w0xaOz9GNxq4gJeZcGs1cRab0fU7xLfgMSYzoT3YgNXYhIO4iM+4iTWphGs4jikcFSXvhEGczr4/UFW8C2N4jXUFudvwCYeqGNgnSr6yJH8rpkWLCqMfE9hdUryFE3iC3qLEk7ij+kT34Q32FiHV8Qr7K4q3cArXihCGxd5elMjARnzBvE4f1dreV+AtnicycC/7/7K8BhaIvqXCO3zFwrwGZtCT0EAtW9N5DTSxWGR/CizNns/yEgbFEK5NZGCnaEPHE7e9Ai9wA6OJDIzistgJubFdxHB/RfFVYgCHixJruI5x5dNwDm6J47sUhkTvjpUw0Y1zeOrXR3hHjOA9zmBuTs4Arog4/yhuUZWwHPdFMh7280BZgiP4ILJ/UuymqRQmejPxphiquzgvKnMJDzOxB9glZqiRiecykbfHdawX98EhcdxO4BGu4nYm2EJDzEKPSMIdYrBnFYUq8d/EP2di1gey3cS4ErflvxffASbhcakIINaMAAAAAElFTkSuQmCC);background-color:var(--config-color-background-input);background-position:left 15px top 50%;background-repeat:no-repeat;background-size:20px 20px;width:calc(100% - 60px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-left:45px}select[multiple]{min-height:75px;padding:5px 10px!important;padding-right:50px!important}select[multiple] option{padding:10px 4px;border-bottom:solid 1px #f1f1f1}select[multiple] option:last-child{border-bottom:none}textarea{min-height:75px;resize:vertical;line-height:32px;padding:5px 15px}textarea.tall{min-height:180px}fieldset{border:none;margin:0;padding:0}.counter{font-size:13px;text-align:right;color:var(--config-color-fade);margin-top:-20px;margin-bottom:20px}.file-preview{background:var(--config-color-background-input) url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAIElEQVQoU2NkYGAwZsAEZ9GFGIeIQix+wfQgyDODXSEAcUwGCrDSHgkAAAAASUVORK5CYII=)!important;border:solid 1px #e2e2e2;box-shadow:inset 0 0 3px #a0a0a0;border-radius:8px;width:calc(100% - 2px);max-height:180px;visibility:visible!important}.video-preview{padding-top:56%;position:relative;border-radius:10px;background:#e7e7e7;overflow:hidden;margin:0}.video-preview iframe{position:absolute;top:0;width:100%;height:100%;border:none}.map-preview{padding-top:50%;position:relative;margin-bottom:10px;border-radius:10px;background:#e7e7e7;overflow:hidden;box-shadow:0 0 30px rgba(218,218,218,.5)}.map-preview iframe{position:absolute;top:0;width:100%;height:100%;border:none}.tooltip{position:relative}.tooltip.large:hover:after{white-space:normal;width:280px}.tooltip.small:hover:after{white-space:normal;width:180px}.tooltip:hover:after{white-space:nowrap;background:var(--config-color-tooltip-background);border-radius:5px;bottom:26px;color:var(--config-color-tooltip-text);content:attr(data-tooltip);padding:5px 15px;position:absolute;font-size:13px;line-height:20px;z-index:98;left:20%;margin-left:-30px;word-break:break-word}.tooltip:hover:before{border:solid;border-color:var(--config-color-tooltip-background) transparent;border-width:6px 6px 0 6px;bottom:20px;content:"";position:absolute;z-index:99;left:5px}.tooltip.down:hover:after{top:26px;bottom:inherit}.tooltip.down:hover:before{top:20px;border-width:0 6px 6px 6px;bottom:inherit}.tag{display:inline-block;background:var(--config-color-fade-light);color:var(--config-color-fade);border-radius:12px;line-height:24px;padding:0 8px;font-size:12px;box-shadow:none!important;border:none;height:auto;width:auto;white-space:nowrap;text-overflow:ellipsis}.tag:hover{border:none}.tag.green{background:var(--config-color-success);color:#fff}.tag.red{background:var(--config-color-danger);color:#fff}.tag.yellow{background:#ffe28b;color:#494949}.tag.focus{background:var(--config-color-focus);color:#fff}.tag.dark{background:var(--config-color-dark);color:#e7e7e7}.tag.blue{background:var(--config-color-info);color:#fff}.tag.link{background:var(--config-color-link);color:#fff}input[type=checkbox],input[type=radio]{width:26px;height:16px;position:relative;-webkit-appearance:none;border-radius:0;border:none;background:0 0;vertical-align:middle;margin:0}input[type=checkbox]:after,input[type=radio]:after{content:"";display:block;width:20px;height:20px;background:var(--config-color-background-fade);top:-5px;border-radius:50%;position:absolute;border:solid 3px var(--config-color-focus);vertical-align:middle}input[type=checkbox]:checked:after,input[type=radio]:checked:after{text-align:center;font-family:fontello;content:'\e83d';font-size:16px;line-height:20px;color:var(--config-color-background-fade);background:var(--config-color-focus)}input[type=checkbox][type=radio]:checked:after,input[type=radio][type=radio]:checked:after{content:'';display:block;width:10px;height:10px;border-radius:50%;background:var(--config-color-background-fade);border:solid 8px var(--config-color-focus)}input[type=checkbox]:focus,input[type=radio]:focus{outline:0}input[type=checkbox]:focus:after,input[type=checkbox]:hover:after,input[type=radio]:focus:after,input[type=radio]:hover:after{outline:0;border-color:#000}input[type=checkbox]:checked:focus:after,input[type=checkbox]:checked:hover:after,input[type=radio]:checked:focus:after,input[type=radio]:checked:hover:after{border-color:var(--config-color-focus)}.input-copy{position:relative}.input-copy input,.input-copy textarea{padding-right:65px;width:calc(100% - 82px);resize:none}.input-copy .copy{position:absolute;top:0;right:0;border-left:solid 1px var(--config-color-fade-light);height:calc(100% - 2px);width:50px;line-height:50px;text-align:center;background:var(--config-color-background-focus);margin:1px;border-radius:0 10px 10px 0}.paging{color:var(--config-color-fade);padding:5px 15px;font-size:12px}.paging form{display:inline-block}.paging button:disabled{color:var(--config-color-background-fade);opacity:.6}.blue-snap iframe{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px;float:none!important;height:40px!important;width:calc(100% - 32px)!important;border:solid 1px #e2e2e2!important;background:0 0!important;position:static!important}.blue-snap iframe[type=file]{line-height:0;padding:15px;height:auto}.blue-snap iframe:focus{outline:0;border-color:#b3d7fd}.blue-snap iframe:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.blue-snap iframe.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:right 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.blue-snap iframe.strip:focus{border-color:#b3d7fd}.blue-snap iframe:-webkit-autofill::first-line{font-weight:300;font-size:16px}.blue-snap .error{font-size:12px;margin-top:-25px;color:var(--config-color-danger);height:40px;padding-left:2px}.pell{height:auto;padding-bottom:0;margin-bottom:0;padding-top:0;background:var(--config-color-background-input);line-height:normal!important;position:relative}.pell.hide{padding:0!important;height:1px;min-height:1px;max-height:1px;border:none;box-shadow:none;margin-bottom:20px;opacity:0}.pell [contenteditable=true]:empty:before{content:attr(placeholder);display:block;color:var(--config-color-placeholder)}.pell .pell-actionbar{border-bottom:solid 1px var(--config-color-fade-light);margin:0 -15px 15px -15px;padding:10px 15px;position:sticky;top:70px;background:var(--config-color-background-input);border-radius:10px 10px 0 0}.pell .pell-content{min-height:100px;display:block;padding:10px;margin:-10px;cursor:text}.pell .pell-content:focus{outline:0}.pell button{background:inherit;color:inherit;margin:0;padding:0;padding-right:15px;height:40px;line-height:40px;box-shadow:none;cursor:pointer;font-size:13px;border-radius:0}.pell button.pell-button-selected,.pell button:focus,.pell button:hover{color:var(--config-color-link)}.pell h1,.pell h2,.pell h3,.pell h4,.pell h5,.pell h6{text-align:inherit;margin-bottom:30px}.pell b,.pell strong{font-weight:700}.pell ol,.pell ul{margin:0 0 20px 0}.pell ol li,.pell ul li{display:list-item!important;list-style:inherit;list-style-position:inside!important;margin:0 20px 2px 20px}.pell ol li p,.pell ul li p{margin:0;display:inline}.pell ol li{list-style:decimal}.pell ol li::before{content:'';display:none}label.switch{line-height:42px}.switch,input[type=checkbox].button.switch,input[type=checkbox].switch{width:52px;height:32px;line-height:32px;border-radius:21px;background:var(--config-color-fade);display:inline-block;margin:0;padding:5px;padding-left:5px;padding-right:30px}.switch.on,.switch:checked,input[type=checkbox].button.switch.on,input[type=checkbox].button.switch:checked,input[type=checkbox].switch.on,input[type=checkbox].switch:checked{background-color:var(--config-color-success);padding-left:25px;padding-right:5px}.switch.on:focus,.switch.on:hover,.switch:checked:focus,.switch:checked:hover,input[type=checkbox].button.switch.on:focus,input[type=checkbox].button.switch.on:hover,input[type=checkbox].button.switch:checked:focus,input[type=checkbox].button.switch:checked:hover,input[type=checkbox].switch.on:focus,input[type=checkbox].switch.on:hover,input[type=checkbox].switch:checked:focus,input[type=checkbox].switch:checked:hover{background:var(--config-color-success)}.switch:focus,.switch:hover,input[type=checkbox].button.switch:focus,input[type=checkbox].button.switch:hover,input[type=checkbox].switch:focus,input[type=checkbox].switch:hover{background:var(--config-color-fade)}.switch:focus:after,.switch:hover:after,input[type=checkbox].button.switch:focus:after,input[type=checkbox].button.switch:hover:after,input[type=checkbox].switch:focus:after,input[type=checkbox].switch:hover:after{background:#fff}.switch:after,input[type=checkbox].button.switch:after,input[type=checkbox].switch:after{content:"";display:block;width:22px;height:22px;background:#fff;border-radius:50%;border:none;position:static;top:0}.password-meter{margin:-41px 10px 30px 10px;height:2px;background:0 0;max-width:100%;z-index:2;position:relative}.password-meter.weak{background:var(--config-color-danger)}.password-meter.medium{background:var(--config-color-success)}.password-meter.strong{background:var(--config-color-success)}.color-input:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.color-input .color-preview{width:53px;height:53px;float:left;margin-right:10px;background:#000;border-radius:10px;box-shadow:inset 0 0 3px #a0a0a0;position:relative}.color-input .color-preview input{opacity:0;position:absolute;top:0;bottom:0;left:0;right:0;width:100%;height:100%;cursor:pointer}.color-input input{text-transform:uppercase;float:left;width:calc(100% - 95px)}.grecaptcha-badge{box-shadow:none!important;border-radius:10px!important;overflow:hidden!important;background:#4d92df!important;bottom:25px}.grecaptcha-badge:hover{width:256px!important}.back{font-size:15px;line-height:24px;height:24px;margin-left:-15px;margin-top:-25px;margin-bottom:20px}.back span{font-weight:inherit!important}@media only screen and (max-width:550px){.back{margin-left:-5px}}hr{height:1px;background:var(--config-border-color)!important;border:none}hr.fade{opacity:.7}.upload{position:relative}.upload:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.upload input{position:absolute;top:0;left:0;opacity:0;cursor:pointer}.upload.single .preview{height:0;position:relative;padding-top:100%;width:100%;margin-bottom:15px!important}.upload.single .preview li{position:absolute;top:0;width:calc(100% - 20px);height:calc(100% - 20px);margin-right:0!important;margin-bottom:0!important}.upload .button{float:left;margin-right:10px!important}.upload .button.disabled,.upload .button.disabled:hover{background:0 0;color:inherit;border-color:inherit}.upload .count{float:left;line-height:52px}.upload .progress{background:var(--config-color-success);height:6px;border-radius:3px;margin-bottom:15px!important}.upload .preview:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.upload .preview li{float:left;margin-right:20px!important;margin-bottom:15px!important;background:var(--config-color-background-fade-super);width:150px;height:150px;line-height:148px;text-align:center;border-radius:20px;overflow:hidden;position:relative;cursor:pointer;border:solid 1px var(--config-color-background-dark)}.upload .preview li:hover:before{background:var(--config-color-focus)}.upload .preview li:before{content:'\e807';font-family:fontello;font-size:12px;position:absolute;width:20px;height:20px;display:block;top:8px;right:8px;text-align:center;line-height:20px;vertical-align:middle;border-radius:50%;background:#484848;color:#fff;z-index:1}.upload .preview li img{vertical-align:middle;max-height:150px;max-width:150px;-webkit-filter:drop-shadow(0 0 6px rgba(0, 0, 0, .3));filter:drop-shadow(0 0 1px rgba(0, 0, 0, .3))}.upload.wide .preview li{height:0;width:100%;position:relative;padding-top:30.547%;background:#e7e7e7;border-radius:10px;overflow:hidden;border:solid 1px #f9f9f9;margin:0}.upload.wide .preview li img{border-radius:10px;position:absolute;top:0;width:100%;display:block;opacity:1;max-width:inherit;max-height:inherit}ol{list-style:none;counter-reset:x-counter;padding:0}ol li{counter-increment:x-counter;line-height:30px;margin-bottom:30px;margin-left:45px}ol li::before{display:inline-block;content:counter(x-counter);color:var(--config-color-background-fade);background:var(--config-color-focus);border:solid 2px var(--config-color-focus);margin-right:15px;margin-left:-45px;width:26px;height:26px;border-radius:50%;text-align:center;line-height:26px}.required{color:var(--config-color-danger);font-size:8px;position:relative;top:-8px}.drop-list{position:relative;outline:0}.drop-list.open ul{display:block}.drop-list ul{position:relative;background:var(--config-color-background-fade);border-radius:10px;box-shadow:0 0 3px rgba(0,0,0,.05);padding:30px;display:block;border-bottom:none;box-shadow:0 0 6px rgba(0,0,0,.1);display:none;position:absolute;bottom:calc(100% + 10px);z-index:2;padding:0;left:-10px;max-width:280px;min-width:240px}.drop-list ul.padding-small{padding:15px}.drop-list ul.y-scroll{overflow-y:auto}.drop-list ul.danger{background:var(--config-color-danger);color:#fff}.drop-list ul.danger .box{color:var(--config-color-normal);background:var(--config-color-background-fade)}.drop-list ul.danger>.button,.drop-list ul.danger>button{background:#fff;color:var(--config-color-danger)}.drop-list ul.note{background:var(--config-note-background)}.drop-list ul.focus{background:var(--config-color-focus);color:var(--config-color-background-fade)}.drop-list ul.focus .button,.drop-list ul.focus button{background:var(--config-color-background-fade);color:var(--config-color-focus)}.drop-list ul.line{background:0 0;border:solid 1px var(--config-color-background-dark);box-shadow:none}.drop-list ul.warning{background:var(--config-color-success);color:#2d2d2d}.drop-list ul.warning .button,.drop-list ul.warning button{background:rgba(45,45,45,.8);color:var(--config-color-success)}.drop-list ul .tabs{border-bottom:solid 1px var(--config-border-color);margin:0 -30px;padding:0 30px!important}.drop-list ul>footer{margin:0 -30px -30px -30px;padding:15px 30px;background:var(--config-color-background-fade);border:solid 1px var(--config-border-color);border-radius:0 0 10px 10px}.drop-list ul hr{height:1px;background:var(--config-console-background);border:none;margin:30px -30px}.drop-list ul .label{position:absolute;top:10px;z-index:2;right:10px}.drop-list ul.fade-bottom{position:relative;overflow:hidden}.drop-list ul.fade-bottom:after{content:"";position:absolute;display:block;bottom:15px;width:100%;background:#000;background:linear-gradient(180deg,rgba(0,0,0,0) 0,var(--config-color-background-fade) 80%);height:100px;margin:0 -15px}.drop-list ul .header{position:static;height:40px;padding:20px 30px 20px 30px;margin-bottom:30px;margin:-30px -30px 20px -30px;background:var(--config-color-background-fade);border-bottom:solid 1px #efefef}.drop-list ul ul.numbers>li{position:relative;margin-left:30px;margin-right:50px}.drop-list ul ul.numbers>li hr{margin-left:-60px;margin-right:-80px}.drop-list ul ul.numbers>li .settings{position:absolute;top:3px;right:-50px}.drop-list ul ul.numbers>li::after{display:block;width:25px;height:25px;line-height:25px;font-size:13px;font-weight:500;border-radius:50%;background:var(--config-color-focus);color:var(--config-color-background);counter-increment:section;content:counter(section);text-align:center;position:absolute;top:3px;left:-45px}.drop-list ul .scroll{margin:0 -30px;overflow-y:scroll}.drop-list ul .scroll table{width:100%;margin:0}.drop-list ul ul.sortable{counter-reset:section}.drop-list ul ul.sortable>li [data-move-down].round,.drop-list ul ul.sortable>li [data-move-up].round,.drop-list ul ul.sortable>li [data-remove].round{background:var(--config-color-focus);color:var(--config-color-background-fade);width:25px;height:25px;line-height:25px;display:inline-block;text-align:center;padding:0;margin-right:5px}.drop-list ul ul.sortable>li [data-move-down].round:disabled,.drop-list ul ul.sortable>li [data-move-up].round:disabled,.drop-list ul ul.sortable>li [data-remove].round:disabled{display:none}.drop-list ul ul.sortable>li:first-child [data-move-up]{display:none}.drop-list ul ul.sortable>li:first-child [data-move-up]:disabled{display:inline-block;background:var(--config-color-background)}.drop-list ul ul.sortable>li:last-child [data-move-down]{display:none}.drop-list ul ul.sortable>li:last-child [data-move-down]:disabled{display:inline-block;background:var(--config-color-background)}.drop-list ul .toggle{position:relative;border-top:1px solid var(--config-console-background);border-bottom:1px solid var(--config-console-background);margin:0 -30px;padding:30px 30px 0 30px;height:65px;overflow:hidden}.drop-list ul .toggle.list{border-bottom:none}.drop-list ul .toggle.sorts button.ls-ui-open{width:calc(100% - 100px)}.drop-list ul .toggle button.ls-ui-open{right:0;position:absolute;top:0;width:100%;height:95px;background:0 0;opacity:.5;border-radius:0}.drop-list ul .toggle .icon-minus,.drop-list ul .toggle .icon-up-open{display:none}.drop-list ul .toggle .content{display:none}.drop-list ul .toggle.open{height:auto}.drop-list ul .toggle.open .icon-minus,.drop-list ul .toggle.open .icon-up-open{display:block}.drop-list ul .toggle.open .icon-down-open,.drop-list ul .toggle.open .icon-plus{display:none}.drop-list ul .toggle.open .content{display:block}.drop-list ul .list li{border-bottom:solid 2px var(--config-border-color);margin:0 -30px 30px -30px;padding:0 30px 30px 30px}.drop-list ul .list li:last-child{padding-bottom:0;margin-bottom:0;border-bottom:none}@media only screen and (max-width:550px){.drop-list ul .list li .actions{float:none}}.drop-list ul.new{text-align:center}.drop-list ul.new i{font-size:80px;line-height:80px;font-family:Poppins,sans-serif;font-style:normal;font-weight:300}.drop-list ul.new b{margin-top:20px;display:block}.drop-list ul .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.drop-list ul .info hr{background:var(--config-modal-note-border)!important}.drop-list ul .table-wrap{margin:0 -30px;overflow-y:scroll}.drop-list ul .table-wrap table{margin:0}.drop-list ul:before{border:solid;border-color:var(--config-color-background-fade) transparent;border-width:8px 8px 0 8px;bottom:-8px;content:"";position:absolute;z-index:99;left:30px}.drop-list ul.arrow-end:before{right:30px;left:unset}.drop-list ul li{border-bottom:solid 1px var(--config-color-fade-super);margin:0;padding:0}.drop-list ul li:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.drop-list ul li:first-child{border-radius:10px 10px 0 0}.drop-list ul li:last-child{border-radius:0 0 10px 10px}.drop-list ul li:hover{background:var(--config-color-fade-super)}.drop-list ul li:first-child:hover,.drop-list ul li:last-child:hover{border-color:transparent}.drop-list ul li .link,.drop-list ul li a,.drop-list ul li button.link{display:block;vertical-align:middle;height:auto;line-height:30px;display:inline-block;padding:10px 15px!important;color:inherit;font-size:14px;border:none;cursor:pointer;width:calc(100% - 30px);text-align:left;box-sizing:content-box}.drop-list ul li.disabled .link:hover,.drop-list ul li.disabled a:hover{background:0 0}.drop-list ul li .avatar{width:30px;height:30px;margin-right:10px;float:left}.drop-list ul li:last-child{border-bottom:none}.drop-list.bottom ul{bottom:auto;margin-top:-2px}.drop-list.bottom ul:before{bottom:auto;top:-8px;border-width:0 8px 8px 8px}.drop-list.end ul{right:-10px;left:auto}.disabled{opacity:.2;cursor:default}.disabled .button,.disabled .link,.disabled a,.disabled button{cursor:default!important}.disabled .button:hover,.disabled .link:hover,.disabled a:hover,.disabled button:hover{background:0 0}.tags{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px;background:var(--config-color-background-input);min-height:42px;height:auto;cursor:text}.tags[type=file]{line-height:0;padding:15px;height:auto}.tags:focus{outline:0;border-color:#b3d7fd}.tags:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.tags.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:right 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.tags.strip:focus{border-color:#b3d7fd}.tags:-webkit-autofill::first-line{font-weight:300;font-size:16px}.tags .add{display:inline-block!important;border:none;padding:0;width:auto;margin:0;max-width:100%;min-width:200px}.tags ul.tags-list{display:inline;white-space:pre-line}.tags ul.tags-list li{display:inline-block!important;margin-right:10px;font-size:16px;padding:5px 10px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.tags ul.tags-list li::before{float:right;content:'\e807';font-family:fontello;font-style:normal;display:inline-block;text-align:center;line-height:16px;width:16px;height:16px;font-size:12px;background:#000;color:#fff;border-radius:50%;margin-top:4px;margin-bottom:4px;margin-left:6px;margin-right:0}.switch-theme{background:var(--config-switch-background);border-radius:19px;height:26px;width:44px;margin:9px 0}.switch-theme button{padding:3px;display:block;background:0 0;height:26px;width:100%}.switch-theme i{background:var(--config-color-background-fade);border-radius:50%;height:18px;width:18px;line-height:18px;font-size:12px;padding:0;margin:0;color:var(--config-color-fade)}.switch-theme i.force-light{float:right}.switch-theme i.force-dark{float:left}.console{width:100%;padding:0;overscroll-behavior:none}.console body{position:relative;width:calc(100% - 320px);padding-top:70px;padding-bottom:0;padding-right:50px;padding-left:270px;margin:0;color:var(--config-color-normal);background:var(--config-console-background)}.console body .project-only{display:none!important}.console body.show-nav .project-only{display:inline-block!important}.console body.hide-nav{padding-left:50px;width:calc(100% - 100px)}.console body.hide-nav header{width:calc(100% - 50px)}.console body.hide-nav header .logo{display:inline-block}.console body.hide-nav .console-back{display:block}.console body.hide-nav .console-index{display:none}.console body.hide-nav .account{display:none}.console body.index .console-back{display:none}.console body.index .console-index{display:block}.console body.index .account{display:block}.console body .console-index{display:block}.console body .console-back{display:none}.console main{min-height:480px}.console header{position:fixed;top:0;width:calc(100% - 280px);height:40px;line-height:40px;padding:15px 30px;background:var(--config-color-background-fade);box-shadow:0 0 2px rgba(0,0,0,.1);margin:0 -50px;z-index:2;font-size:14px}.console header .logo{display:none;border:none}.console header .logo:hover{border:none;opacity:.8}.console header .logo img{height:26px;margin:7px 0}.console header .setup-new{width:40px;height:40px;line-height:40px}.console header .list{width:240px}.console header .list select{height:40px;line-height:40px;padding-top:0;padding-bottom:0;border:none;border-radius:26px;background-color:var(--config-console-nav-switch-background);color:var(--config-console-nav-switch-color)}.console header .account{margin-left:25px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.console header .switch-theme{margin:2px 0}.console header .avatar{height:40px;width:40px}.console header .account-button{background:0 0;position:absolute;width:100%;height:40px;border-radius:0;z-index:1}.console header .notifications{position:relative;font-size:20px}.console header .notifications a{color:#1b3445}.console header .notifications:after{position:absolute;content:"";display:block;background:var(--config-color-danger);width:8px;height:8px;border-radius:50%;top:3px;right:3px}.console header nav{background:#1b3445;background:linear-gradient(var(--config-console-nav-start),var(--config-console-nav-end));color:#788c99;position:fixed;height:100%;width:220px;top:0;left:0}.console header nav .logo{height:39px;padding:15px 20px;display:block}.console header nav .logo img{display:inline-block;margin-top:7px;margin-bottom:14px}.console header nav .logo svg g{fill:var(--config-color-focus)}.console header nav .icon{display:block;border:none;margin:18px 10px 50px 10px}.console header nav .icon img{display:block}.console header nav .icon:hover{border-bottom:none}.console header nav .icon:hover svg g{fill:var(--config-color-focus)}.console header nav .container{overflow:auto;height:calc(100% - 133px);width:100%}.console header nav .project-box{padding:20px;text-align:center;display:block;border:none;line-height:100px;height:100px}.console header nav .project-box img{max-height:80px;max-width:80%;display:inline-block;vertical-align:middle}.console header nav .project{display:block;padding:85px 25px 20px 25px;color:#788c99;position:relative;border:none;height:20px}.console header nav .project:hover{border-bottom:none}.console header nav .project .name{height:20px;line-height:20px;margin:0;padding:0;display:inline-block;max-width:100%}.console header nav .project .arrow{display:block;position:absolute;right:5px;top:10px;width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #788c99;transform:rotate(225deg)}.console header nav .project img{position:absolute;bottom:40px;display:block;margin-bottom:10px;max-height:35px;max-width:40%}.console header nav .subtitle{padding:0 30px;display:block;font-size:12px;font-weight:300}.console header nav .links{margin-bottom:15px!important}.console header nav .links.top{border:none;padding-bottom:0;margin-bottom:5px!important}.console header nav .links.bottom{position:absolute;bottom:0;left:0;right:0;padding-bottom:0;border:none;margin-bottom:0!important;box-shadow:0 0 10px rgba(0,0,0,.1)}.console header nav .links.bottom a{border-top:solid 1px var(--config-console-nav-border);border-bottom:none}.console header nav .links .sub{display:inline-block;border:none;width:25px;height:25px;line-height:25px;border-radius:50%;padding:0;background:var(--config-color-focus);color:#fff;text-align:center;font-size:12px;margin:18px}.console header nav .links .sub i{width:auto;margin:0}.console header nav .links .sub:hover{border:none}.console header nav .links a{padding:8px 20px;border:none;display:block;color:#87a5b9;font-weight:400;border-left:solid 5px transparent;font-size:13px}.console header nav .links a i{margin-right:8px;width:22px;display:inline-block}.console header nav .links a.selected,.console header nav .links a:hover{color:#e4e4e4}.console header nav:after{content:'';display:block;position:absolute;background:#302839;height:100px;width:100%;bottom:-100px}.console>footer{width:calc(100% + 100px);margin:0 -50px;box-sizing:border-box;background:0 0;padding-right:30px;padding-left:30px}.console>footer ul{float:none;text-align:center}.console>footer ul li{float:none;display:inline-block}.console .projects{position:relative}.console .projects:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.console .projects li{float:left;margin-right:50px;margin-bottom:50px;width:270px}.console .projects li:nth-child(3n){margin-right:0}.console .dashboard{padding:20px;min-height:95px;overflow:hidden;position:relative;z-index:1}.console .dashboard hr{margin:20px -20px;height:2px;background:var(--config-console-background)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console .dashboard hr{height:3px}}.console .dashboard footer{margin:-20px;padding:20px;background:#fcfeff;border:none;color:var(--config-color-link)}.console .dashboard .col{position:relative}.console .dashboard .col:last-child:after{display:none}.console .dashboard .col:after{content:"";display:block;width:2px;background:var(--config-console-background);height:calc(100% + 110px);position:absolute;top:-20px;bottom:-20px;right:24px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console .dashboard .col:after{width:calc(100% + 40px);height:3px;position:static;margin:20px -20px}}.console .dashboard .value{color:var(--config-color-focus);vertical-align:bottom;line-height:45px}.console .dashboard .value .sum{font-size:45px;line-height:45px;font-weight:700;vertical-align:bottom}.console .dashboard .unit{font-weight:500;line-height:20px;vertical-align:bottom;font-size:16px;display:inline-block;margin-bottom:5px;margin-left:5px;color:var(--config-color-focus)}.console .dashboard .metric{color:var(--config-color-focus);font-weight:400;font-size:13px;line-height:16px}.console .dashboard .range{color:var(--config-color-fade);font-weight:400;font-size:14px;line-height:16px}.console .dashboard a{display:block;font-weight:400;font-size:14px;line-height:16px;padding:0;border:none}.console .dashboard .chart-metric{width:19%}@media only screen and (min-width:551px) and (max-width:1198px),only screen and (max-width:550px){.console .dashboard .chart-metric{width:100%}}.console .dashboard .chart{width:80%;position:relative;height:0;padding-top:20px;padding-bottom:26%;margin-right:-2px;overflow:hidden;background-color:var(--config-color-background-fade);background-image:linear-gradient(transparent 1px,transparent 1px),linear-gradient(90deg,transparent 1px,transparent 1px),linear-gradient(var(--config-border-color) 1px,transparent 1px),linear-gradient(90deg,var(--config-border-color) 1px,transparent 1px);background-size:100px 100px,100px 100px,20px 20px,20px 20px;background-position:-2px -2px,-2px -2px,-1px -1px,-1px -1px;background-repeat:round;border:solid 1px var(--config-border-color);border-right:solid 1px transparent;border-bottom:solid 1px transparent}@media only screen and (min-width:551px) and (max-width:1198px),only screen and (max-width:550px){.console .dashboard .chart{width:100%;padding-bottom:32%;float:none;margin-bottom:20px}}.console .dashboard .chart canvas{position:absolute;bottom:0;display:block;height:100%;width:100%}.console .community a{padding:0 10px;display:inline-block}.console .link-list li{margin-bottom:15px}.console .link-list i{display:inline-block;width:30px;height:30px;line-height:30px;text-align:center;background:var(--config-color-fade);color:var(--config-color-fade-super);border-radius:50%;margin-right:15px}.console .link-list i.fade{background:0 0;color:var(--config-color-fade)}.console .provider{width:50px;height:50px;background:#f5f5f5;color:#868686;line-height:50px;text-align:center;font-size:25px;border-radius:50%}.console .provider.facebook{color:#fff;background:#3b5998}.console .provider.twitter{color:#fff;background:#55beff}.console .provider.telegram{color:#fff;background:#3ba9e1}.console .provider.github{color:#fff;background:#24292e}.console .provider.whatsapp{color:#fff;background:#25d366}.console .provider.linkedin{color:#fff;background:#1074af}.console .provider.microsoft{color:#fff;background:#137ad4}.console .provider.google{color:#fff;background:#4489f1}.console .provider.bitbucket{color:#fff;background:#2a88fb}.console .provider.gitlab{color:#faa238;background:#30353e}.console .provider.instagram{color:#fff;background:radial-gradient(circle at 30% 107%,#fdf497 0,#fdf497 5%,#fd5949 45%,#d6249f 60%,#285aeb 90%)}.console .premium{z-index:3;margin-top:320px}.console .premium .message{height:190px;overflow:hidden;position:absolute;top:-280px}.console .premium:after{content:'';position:absolute;top:0;left:-20px;right:-20px;bottom:-20px;background:var(--config-color-background);opacity:.7;z-index:300}.console .app-section{height:90px}.console .confirm{background:var(--config-color-link);color:#fff;border-radius:25px;padding:12px;line-height:28px;text-align:center}.console .confirm .action{font-weight:500;cursor:pointer}.console .platforms{overflow:hidden}.console .platforms .box{overflow:hidden}.console .platforms .box img{width:50px;margin:0 auto;margin-bottom:20px}.console .platforms .box .cover{margin:-30px -30px 30px -30px;padding:30px}.console .platforms .box .cover.android{background:#a4ca24}.console .platforms .box .cover.android h1{color:#fff;font-size:18px;margin-top:20px}.console .platforms .col{text-align:center;line-height:30px}.console .platforms a{display:block;margin:-20px;padding:20px}.console .platforms a:hover{background:#fbfeff}.console .platforms img{display:block;margin:0 30px;width:calc(100% - 60px);border-radius:50%;margin-bottom:20px}.console .document-nav{display:none;position:sticky;top:90px}@media only screen and (min-width:1380px){.console .document-nav{display:block}}.console .document-nav ul{position:absolute;width:200px;left:-260px}.console .document-nav ul li{margin-bottom:20px}.console .document-nav ul li .selected{font-weight:500}.console .scroll-to{display:none}@media only screen and (min-width:1199px){.console .logo .top{display:none!important}}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console>header{width:calc(100% - 30px)!important;margin:0 -30px;padding:15px}.console>header nav{width:100%;height:70px;overflow:hidden}.console>header nav.close{background:0 0}.console>header nav.close .logo .nav{display:none!important}.console>header nav.open{height:100%}.console>header nav.open .logo .top{display:none!important}.console>header nav.open .bottom{display:block!important}.console>header nav.open button{color:#87a5b9}.console>header nav button{margin:9px;background:0 0;color:var(--config-color-normal)}.console>header nav button:focus,.console>header nav button:hover{background:0 0}.console>header nav .logo{display:block!important;position:absolute;top:0;left:50%;margin:auto;transform:translateX(-50%)}.console>header nav .bottom{display:none!important}.console>footer{width:auto;margin:50px -30px 0 -30px!important;padding:0 30px 30px 30px}.console body{height:"calc(100% - 70px)"!important;width:calc(100% - 60px)!important;padding:70px 30px 0 30px!important}.console .cover{padding:25px 30px;margin:0 -30px}}@media only screen and (max-width:550px){.console body{height:"calc(100% - 70px)"!important;width:calc(100% - 40px)!important;padding:70px 20px 0 20px!important}.console .cover{padding:20px 20px;margin:0 -20px}.console>header{margin:0 -20px}.console>header .list{width:175px;font-size:14px}.console>footer{margin:50px -20px 0 -20px!important;padding:0 20px 20px 20px}}.dev-feature{display:none}.prod-feature{display:none}.development .dev-feature{display:block;opacity:.6!important;outline:solid #ff0 3px;outline-offset:3px}.development .dev-feature.dev-inline{display:inline-block}.development .prod-feature{display:none}.production .dev-feature{display:none}.production .prod-feature{display:block}.search{opacity:1!important}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.search button{margin-top:20px}}html.home body{padding:0 50px;color:var(--config-color-normal)}html.home .logo a{display:block}html.home .logo a:hover{opacity:.8}html.home .logo img{max-height:35px;width:198px;margin:45px auto 25px auto}html.home footer{background:0 0;text-align:center}html.home main{min-height:400px}.alerts ul{width:100%;visibility:hidden;position:fixed;padding:0;right:0;left:0;color:var(--config-color-normal);z-index:1002;margin:0 auto;bottom:15px;max-width:560px}.alerts ul li{margin:10px 0 0 0;padding:0}.alerts ul li div.message{position:relative;padding:12px 35px;margin:0 auto;list-style:none;background:var(--config-color-background-dark);text-align:center;font-size:14px;border-radius:10px;line-height:16px;min-height:16px;box-shadow:0 0 10px rgba(0,0,0,.05);opacity:.95}.alerts ul li div.message a,.alerts ul li div.message span{font-weight:600}.alerts ul li div.message i{cursor:pointer;position:absolute;font-size:14px;line-height:22px;top:8px;left:8px}.alerts ul li div.message.error{color:#fff;background:var(--config-color-danger)}.alerts ul li div.message.success{color:#fff;background:var(--config-color-success)}.alerts ul li div.message.warning{color:#fff;background:var(--config-color-success)}.alerts ul li div.message.open{display:block}.alerts ul li div.message.close{display:none}.alerts .cookie-alert{background:var(--config-color-focus-fade)!important;color:var(--config-color-focus)}.alerts .cookie-alert a{color:var(--config-color-focus);font-weight:400;border-bottom:dotted 1px var(--config-color-focus)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.alerts ul{top:auto;bottom:0;max-width:100%}.alerts ul li{margin:5px 0 0 0}.alerts ul li div.message{border-radius:0}}article{overflow-wrap:break-word;word-wrap:break-word}article h1{font-size:36px}article h2{font-size:24px}article h3{font-size:20px}article h4{font-size:20px}article h5{font-size:18px}article h6{font-size:16px}article h1,article h2,article h3,article h4,article h5,article h6{margin-top:30px!important;margin-bottom:30px!important}article p{line-height:32px;font-size:16px}article .update{display:block;margin-top:50px!important}article table{width:100%;margin:0;margin-bottom:30px!important;border-radius:0;border-bottom:solid 1px var(--config-border-color)}article table thead td{font-weight:500;padding:5px 15px}article table td,article table th{padding:15px;height:auto}article table td:first-child,article table th:first-child{padding-left:10px}article table td:last-child,article table th:last-child{padding-right:10px}article table td p,article table th p{font-size:inherit;line-height:inherit}article table td p:last-child,article table th p:last-child{margin:0}.avatar-container{position:relative}.avatar-container .corner{position:absolute;bottom:-3px;right:-3px}.avatar{width:60px;height:60px;border-radius:50%;background:var(--config-color-background-focus);display:inline-block;overflow:hidden;box-shadow:0 0 6px rgba(0,0,0,.09);position:relative;z-index:1;opacity:1!important}.avatar:before{content:"";position:absolute;width:100%;height:100%;z-index:0;background:var(--config-color-background-focus)}.avatar.inline{display:inline-block;vertical-align:middle}.avatar.trans{background:0 0}.avatar .no-shadow{box-shadow:none}.avatar.xs{width:30px;height:30px}.avatar.xxs{width:20px;height:20px}.avatar.small{width:50px;height:50px}.avatar.big{width:100px;height:100px}.avatar.huge{width:150px;height:150px}.box{position:relative;background:var(--config-color-background-fade);border-radius:10px;box-shadow:0 0 3px rgba(0,0,0,.05);padding:30px;display:block;border-bottom:none}.box.padding-small{padding:15px}.box.y-scroll{overflow-y:auto}.box.danger{background:var(--config-color-danger);color:#fff}.box.danger .box{color:var(--config-color-normal);background:var(--config-color-background-fade)}.box.danger>.button,.box.danger>button{background:#fff;color:var(--config-color-danger)}.box.note{background:var(--config-note-background)}.box.focus{background:var(--config-color-focus);color:var(--config-color-background-fade)}.box.focus .button,.box.focus button{background:var(--config-color-background-fade);color:var(--config-color-focus)}.box.line{background:0 0;border:solid 1px var(--config-color-background-dark);box-shadow:none}.box.warning{background:var(--config-color-success);color:#2d2d2d}.box.warning .button,.box.warning button{background:rgba(45,45,45,.8);color:var(--config-color-success)}.box .tabs{border-bottom:solid 1px var(--config-border-color);margin:0 -30px;padding:0 30px!important}.box>footer{margin:0 -30px -30px -30px;padding:15px 30px;background:var(--config-color-background-fade);border:solid 1px var(--config-border-color);border-radius:0 0 10px 10px}.box hr{height:1px;background:var(--config-console-background);border:none;margin:30px -30px}.box .label{position:absolute;top:10px;z-index:2;right:10px}.box.fade-bottom{position:relative;overflow:hidden}.box.fade-bottom:after{content:"";position:absolute;display:block;bottom:15px;width:100%;background:#000;background:linear-gradient(180deg,rgba(0,0,0,0) 0,var(--config-color-background-fade) 80%);height:100px;margin:0 -15px}.box .header{position:static;height:40px;padding:20px 30px 20px 30px;margin-bottom:30px;margin:-30px -30px 20px -30px;background:var(--config-color-background-fade);border-bottom:solid 1px #efefef}.box ul.numbers>li{position:relative;margin-left:30px;margin-right:50px}.box ul.numbers>li hr{margin-left:-60px;margin-right:-80px}.box ul.numbers>li .settings{position:absolute;top:3px;right:-50px}.box ul.numbers>li::after{display:block;width:25px;height:25px;line-height:25px;font-size:13px;font-weight:500;border-radius:50%;background:var(--config-color-focus);color:var(--config-color-background);counter-increment:section;content:counter(section);text-align:center;position:absolute;top:3px;left:-45px}.box .scroll{margin:0 -30px;overflow-y:scroll}.box .scroll table{width:100%;margin:0}.box ul.sortable{counter-reset:section}.box ul.sortable>li [data-move-down].round,.box ul.sortable>li [data-move-up].round,.box ul.sortable>li [data-remove].round{background:var(--config-color-focus);color:var(--config-color-background-fade);width:25px;height:25px;line-height:25px;display:inline-block;text-align:center;padding:0;margin-right:5px}.box ul.sortable>li [data-move-down].round:disabled,.box ul.sortable>li [data-move-up].round:disabled,.box ul.sortable>li [data-remove].round:disabled{display:none}.box ul.sortable>li:first-child [data-move-up]{display:none}.box ul.sortable>li:first-child [data-move-up]:disabled{display:inline-block;background:var(--config-color-background)}.box ul.sortable>li:last-child [data-move-down]{display:none}.box ul.sortable>li:last-child [data-move-down]:disabled{display:inline-block;background:var(--config-color-background)}.box .toggle{position:relative;border-top:1px solid var(--config-console-background);border-bottom:1px solid var(--config-console-background);margin:0 -30px;padding:30px 30px 0 30px;height:65px;overflow:hidden}.box .toggle.list{border-bottom:none}.box .toggle.sorts button.ls-ui-open{width:calc(100% - 100px)}.box .toggle button.ls-ui-open{right:0;position:absolute;top:0;width:100%;height:95px;background:0 0;opacity:.5;border-radius:0}.box .toggle .icon-minus,.box .toggle .icon-up-open{display:none}.box .toggle .content{display:none}.box .toggle.open{height:auto}.box .toggle.open .icon-minus,.box .toggle.open .icon-up-open{display:block}.box .toggle.open .icon-down-open,.box .toggle.open .icon-plus{display:none}.box .toggle.open .content{display:block}.box .list li{border-bottom:solid 2px var(--config-border-color);margin:0 -30px 30px -30px;padding:0 30px 30px 30px}.box .list li:last-child{padding-bottom:0;margin-bottom:0;border-bottom:none}@media only screen and (max-width:550px){.box .list li .actions{float:none}}.box.new{text-align:center}.box.new i{font-size:80px;line-height:80px;font-family:Poppins,sans-serif;font-style:normal;font-weight:300}.box.new b{margin-top:20px;display:block}.box .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.box .info hr{background:var(--config-modal-note-border)!important}.box .table-wrap{margin:0 -30px;overflow-y:scroll}.box .table-wrap table{margin:0}a.box{border-right:none;border-left:none}a.box:hover{box-shadow:0 0 1px rgba(0,0,0,.2);opacity:.7}.box-asidex{padding-right:25px!important;padding-left:70px;right:0;background:#f9f9f9;border-radius:0 10px 10px 0;height:calc(100% - 30px);position:absolute;padding-top:30px}.box-asidex:after{content:"";display:block;position:absolute;height:100%;width:51px;background:#fff;top:0;bottom:0;left:-6px}.cover{background:var(--config-color-focus-fade);padding:30px 50px;margin:0 -50px;position:relative}.cover .title,.cover h1,.cover h2,.cover h3,.cover h4{color:var(--config-color-focus);font-weight:600;margin-bottom:50px!important;font-size:28px;line-height:42px}.cover .title span,.cover h1 span,.cover h2 span,.cover h3 span,.cover h4 span{font-weight:600}.cover i:before{margin:0!important}.cover p{color:var(--config-color-fade)}.cover .button{color:#fff}.cover .link,.cover a{color:var(--config-color-focus);border-left:none;border-right:none;cursor:pointer}.cover .link:hover,.cover a:hover{border-bottom-color:var(--config-color-focus)}.console .database .row .col{height:452px}.console .database .row .col:after{width:2px;right:20px}.console .database hr{margin:0 -20px;background:var(--config-color-background);height:1px}.console .database h3{font-size:13px;line-height:20px;height:20px;background-color:var(--config-color-fade-super);margin:-20px -20px 0 -20px;padding:10px 20px;border-bottom:solid 1px var(--config-color-background);font-weight:600}.console .database .empty{height:162px;font-size:12px;text-align:center;margin:50px 0}.console .database .empty h4{font-size:13px;font-weight:600;line-height:120px}.console .database .search{background-color:var(--config-color-fade-super);margin:0 -20px 0 -20px;padding:10px 15px}.console .database .search input{height:40px;background-color:#fff;border-radius:25px;padding-top:0;padding-bottom:0}.console .database .code{height:411px;background:var(--config-color-fade-super);margin:0 -20px -20px -20px;padding:20px;width:calc(100% - 10px)}.console .database .code .ide{overflow:scroll;height:451px;margin:-20px;box-shadow:none;border-radius:0}.console .database .paging{background:var(--config-color-fade-super);margin:0 -20px -20px -20px;padding:20px}.console .database .button{margin:0 -20px;padding:0 20px!important;text-align:inherit;color:var(--config-color-focus);width:100%;font-size:15px;line-height:55px;box-sizing:content-box}.console .database .button i{margin-right:8px}.console .database .button:hover{border:none;background:var(--config-color-focus-fade)}.console .database .items{margin:0 -20px;height:262px;overflow-x:hidden;overflow-y:scroll}.console .database .items form{opacity:0;position:relative}.console .database .items form button{position:absolute;top:0;bottom:0;left:0;right:0;width:100%;height:45px;border-radius:0;cursor:pointer}.console .database .items li{padding:0;margin:0 0;line-height:45px;font-size:15px;padding-left:50px;padding-right:30px;position:relative}.console .database .items li i{position:absolute;display:none;right:10px}.console .database .items li .name{display:inline-block;width:100%;height:28px}.console .database .items li.selected,.console .database .items li:hover{background:#f5f5f5}.console .database .items li.selected i,.console .database .items li:hover i{display:block}.console .database .items li:last-child{border-bottom:none}body>footer{color:var(--config-color-fade);line-height:40px;margin:0 -50px;padding:12px 50px;font-size:13px;width:100%;background:#f1f1f1;position:relative;margin-top:80px!important}body>footer:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}body>footer .logo img{height:22px;padding-top:12px}body>footer a{color:var(--config-color-fade);font-size:13px}body>footer a:hover{border-bottom-color:var(--config-color-fade)}body>footer ul:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}body>footer ul li{font-size:13px;float:left;margin-right:20px!important}body>footer .copyright{padding-left:2px}[data-ls-if]{display:none}[data-service]{opacity:0}.load-service-start{opacity:0}.load-service-end{opacity:1;transition:opacity .5s ease-out;-moz-transition:opacity .5s ease-out;-webkit-transition:opacity .5s ease-out;-o-transition:opacity .5s ease-out}.load-screen{z-index:100000;position:fixed;height:100%;width:100%;background-color:var(--config-color-background-focus);top:0;left:0}.load-screen.loaded{transition:opacity 1s ease-in-out,top 1s .7s;opacity:0;top:-100%}.load-screen .animation{position:absolute;top:45%;left:50%;transform:translate(-50%,-50%) translateZ(1px);width:140px;height:140px}.load-screen .animation div{box-sizing:border-box;display:block;position:absolute;width:124px;height:124px;margin:10px;border:10px solid var(--config-color-focus);border-radius:50%;animation:animation 1.2s cubic-bezier(.5,0,.5,1) infinite;border-color:var(--config-color-focus) transparent transparent transparent}.load-screen .animation div:nth-child(1){animation-delay:-.45s}.load-screen .animation div:nth-child(2){animation-delay:-.3s}.load-screen .animation div:nth-child(3){animation-delay:-.15s}@keyframes animation{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.load-screen img{position:absolute;height:20px;bottom:60px;left:50%;transform:translate(-50%,-50%)}.modal-open .modal-bg,.modal-open body .modal-bg{position:fixed;content:'';display:block;width:100%;height:100%;left:0;right:0;top:0;bottom:0;background:#0c0c0c;opacity:.5;z-index:4}.modal{overflow:auto;display:none;position:fixed;transform:translate3d(0,0,0);width:100%;max-height:90%;max-width:640px;background:var(--config-color-background-fade);z-index:1000;box-shadow:0 0 4px rgba(0,0,0,.25);padding:30px;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:10px;box-sizing:border-box;text-align:left;white-space:initial;line-height:normal}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.modal{width:calc(100% - 20px)}}.modal.full{max-width:none;max-height:none;height:100%;border-radius:0;padding:80px 120px}.modal.full h1{font-weight:700}.modal.padding-small{padding:15px}.modal.height-tiny>form{height:100px}.modal.height-small>form{height:220px}.modal.width-small{max-width:400px}.modal.width-medium{max-width:500px}.modal.width-large{max-width:800px}.modal.open{display:block}.modalbutton.close{display:none}.modal.fill{height:95%;max-height:95%;max-width:75%}.modal h1,.modal h2{margin-bottom:25px;margin-top:0;font-size:20px;text-align:left}.modal h1,.modal h2,.modal h3,.modal h4,.modal h5,.modal h6{color:inherit!important;line-height:35px}.modal .main,.modal>form{position:relative;border-top:solid 1px var(--config-border-color);padding:30px 30px 0 30px;margin:0 -30px}.modal .main.strip,.modal>form.strip{border:none;padding:0;margin:0}.modal .separator{margin:20px -30px}.modal .bullets{padding-left:40px}.modal .bullets li{margin-bottom:30px!important}.modal .bullets li:before{position:absolute}.modal .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.modal .ide.strech{box-shadow:none;border-radius:0;margin:0 -30px}.modal button.close{width:30px;height:30px;line-height:30px;padding:0;margin:0;background:var(--config-color-normal);color:var(--config-color-background-fade);border-radius:50%}.modal .paging form{padding:0;margin:0;border-top:none}.modal.sticky-footer form footer{margin:-30px}.modal.sticky-footer footer{position:sticky;bottom:-30px;background:var(--config-color-background-fade-super);height:50px;z-index:1;padding:30px;box-shadow:0 0 1px rgba(0,0,0,.15)}.modal.sticky-footer footer form{display:inline-block}[data-views-current="0"] .scroll-to,[data-views-current="1"] .scroll-to{opacity:0!important}.scroll-to-bottom .scroll-to,.scroll-to-top .scroll-to{opacity:1}.scroll-to{opacity:0;display:block;width:40px;height:40px;line-height:40px;border-radius:50%;position:fixed;transform:translateZ(0);margin:30px;padding:0;bottom:0;font-size:18px;z-index:100000;transition:opacity .15s ease-in-out;right:0}.phases{list-style:none;margin:0;padding:0;position:relative}.phases li{display:none}.phases li li{display:block}.phases li.selected{display:block}.phases .number{display:none}.phases h2,.phases h3,.phases h4,.phases h5,.phases h6{margin:0 0 30px 0;text-align:inherit}.container{position:relative}.container .tabs{height:55px;line-height:55px;list-style:none;padding:0;margin-bottom:50px!important;margin-top:-55px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.container .tabs:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.container .tabs .selected{font-weight:400;color:var(--config-color-focus);position:relative;opacity:1}.container .tabs .selected:after{content:"";display:block;height:2px;background:var(--config-color-focus);width:calc(100% + 6px);margin:0 -3px;position:absolute;bottom:0;border-radius:2px}.container .tabs .number{display:none}.container .tabs li{float:left;margin-right:50px;color:var(--config-color-focus);opacity:.9;cursor:pointer}.container .tabs li:focus{outline:0}@media only screen and (max-width:550px){.container .tabs li{margin-right:25px}}.container .icon{display:none}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.container .tabs{width:auto;overflow-x:scroll;overflow-y:hidden;white-space:nowrap}.container .tabs li{display:inline-block;float:none}}.ide{background-color:var(--config-prism-background);overflow:hidden;position:relative;z-index:1;box-shadow:0 2px 4px 0 rgba(50,50,93,.3);border-radius:10px;margin-bottom:30px}.ide *{font-family:'Source Code Pro',monospace}.ide[data-lang]::after{content:attr(data-lang-label);display:inline-block;background:#fff;color:#000;position:absolute;top:15px;padding:5px 10px;border-radius:15px;font-size:10px;right:10px;opacity:.95}.ide[data-lang=bash]::after{background:var(--config-language-bash);color:var(--config-language-bash-contrast)}.ide[data-lang=javascript]::after{background:var(--config-language-javascript);color:var(--config-language-javascript-contrast)}.ide[data-lang=web]::after{background:var(--config-language-web);color:var(--config-language-web-contrast)}.ide[data-lang=html]::after{background:var(--config-language-html);color:var(--config-language-html-contrast)}.ide[data-lang=php]::after{background:var(--config-language-php);color:var(--config-language-php-contrast)}.ide[data-lang=nodejs]::after{background:var(--config-language-nodejs);color:var(--config-language-nodejs-contrast)}.ide[data-lang=ruby]::after{background:var(--config-language-ruby);color:var(--config-language-ruby-contrast)}.ide[data-lang=python]::after{background:var(--config-language-python);color:var(--config-language-python-contrast)}.ide[data-lang=go]::after{background:var(--config-language-go);color:var(--config-language-go-contrast)}.ide[data-lang=dart]::after{background:var(--config-language-dart);color:var(--config-language-dart-contrast)}.ide[data-lang=flutter]::after{background:var(--config-language-flutter);color:var(--config-language-flutter-contrast)}.ide[data-lang=yaml]::after{background:var(--config-language-yaml);color:var(--config-language-yaml-contrast)}.ide .tag{color:inherit!important;background:0 0!important;padding:inherit!important;font-size:inherit!important;line-height:14px}.ide .copy{cursor:pointer;content:attr(data-lang);display:inline-block;background:#fff;color:#000;position:absolute;transform:translateX(-50%);bottom:-20px;padding:5px 10px;border-radius:15px;font-size:10px;font-style:normal;left:50%;opacity:0;transition:bottom .3s,opacity .3s;line-height:normal;font-family:Poppins,sans-serif}.ide .copy::before{padding-right:5px}.ide:hover .copy{transition:bottom .3s,opacity .3s;opacity:.9;bottom:16px}.ide pre{-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;color:#e6ebf1;font-weight:400;line-height:20px;font-size:13px;margin:0;padding:20px;padding-left:60px}.ide.light{box-shadow:0 2px 4px 0 rgba(50,50,93,.1);background-color:#fff}.ide.light pre{color:#414770}.ide.light .token.cdata,.ide.light .token.comment,.ide.light .token.doctype,.ide.light .token.prolog{color:#91a2b0}.ide.light .token.attr-name,.ide.light .token.builtin,.ide.light .token.char,.ide.light .token.inserted,.ide.light .token.selector,.ide.light .token.string{color:#149570}.ide.light .token.punctuation{color:#414770}.ide.light .language-css .token.string,.ide.light .style .token.string,.ide.light .token.entity,.ide.light .token.operator,.ide.light .token.url,.ide.light .token.variable{color:#414770}.ide.light .line-numbers .line-numbers-rows{background:#f2feef}.ide.light .line-numbers-rows>span:before{color:#5dc79e}.ide.light .token.keyword{color:#6772e4;font-weight:500}code[class*=language-],pre[class*=language-]{text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4}pre[class*=language-]{overflow:auto}:not(pre)>code[class*=language-]{padding:.1em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#6b7c93}.token.punctuation{color:#f8f8f2}.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#f79a59}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#3ecf8e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#45b2e8}.token.keyword{color:#7795f8}.token.important,.token.regex{color:#fd971f}.token.italic{font-style:italic}.token.entity{cursor:help}pre[class*=language-].line-numbers{position:relative;padding-left:60px;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{background:var(--config-prism-numbers);position:absolute;pointer-events:none;top:-20px;bottom:-21px;padding:20px 0;font-size:100%;left:-60px;width:40px;letter-spacing:-1px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{padding-right:5px;pointer-events:none;display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#636365;display:block;padding-right:.8em;text-align:right}html{padding:0;margin:0;direction:ltr}body{margin:0;background:var(--config-console-background) no-repeat fixed;min-width:300px}ul{padding:0;margin:0}ul li{margin:0;list-style:none} \ No newline at end of file +.pull-start{float:left}.pull-end{float:right}img[src=""]{visibility:hidden;display:inline-block}:root{--config-width:910px;--config-width-xxl:1000px;--config-width-xl:910px;--config-width-large:700px;--config-width-medium:550px;--config-width-small:320px;--config-color-link:#1e849e;--config-color-background:#eceff1;--config-color-background-dark:#dfe2e4;--config-color-background-fade:#ffffff;--config-color-background-fade-super:#fdfdfd;--config-color-background-focus:#f5f5f5;--config-color-background-input:#ffffff;--config-color-placeholder:#868686;--config-color-tooltip-text:#dce8f5;--config-color-tooltip-background:#333333;--config-color-focus:#f02e65;--config-color-focus-fade:#fef8fa;--config-color-focus-hover:#ff729b;--config-color-focus-glow:#fce5ec;--config-color-focus-dark:#c52653;--config-color-normal:#40404c;--config-color-dark:#313131;--config-color-fade:#8f8f8f;--config-color-fade-light:#e2e2e2;--config-color-fade-super:#f1f3f5;--config-color-danger:#f53d3d;--config-color-success:#1bbf61;--config-color-warning:#ffed4d;--config-color-info:#386fd2;--config-border-color:#f5f5f5;--config-border-fade:#e0e3e4;--config-border-radius:10px;--config-prism-background:#373738;--config-prism-numbers:#39393c;--config-note-background:#f1fbff;--config-note-border:#5bceff;--config-warning-background:#fdf7d9;--config-warning-border:#f8e380;--config-social-twitter:#1da1f2;--config-social-github:#000000;--config-social-discord:#7189dc;--config-social-facebook:#4070b4;--config-language-bash:#2b2626;--config-language-bash-contrast:#fff;--config-language-javascript:#fff054;--config-language-javascript-contrast:#333232;--config-language-web:#fff054;--config-language-web-contrast:#333232;--config-language-html:#ff895b;--config-language-html-contrast:#ffffff;--config-language-yaml:#ca3333;--config-language-yaml-contrast:#ffffff;--config-language-php:#6182bb;--config-language-php-contrast:#ffffff;--config-language-nodejs:#8cc500;--config-language-nodejs-contrast:#ffffff;--config-language-ruby:#fc3f48;--config-language-ruby-contrast:#ffffff;--config-language-python:#3873a2;--config-language-python-contrast:#ffffff;--config-language-go:#00add8;--config-language-go-contrast:#ffffff;--config-language-dart:#035698;--config-language-dart-contrast:#ffffff;--config-language-flutter:#035698;--config-language-flutter-contrast:#ffffff;--config-modal-note-background:#f5fbff;--config-modal-note-border:#eaf2f7;--config-modal-note-color:#3b5d73;--config-switch-background:#e2e2e2;--config-console-background:#eceff1;--config-console-nav-start:#143650;--config-console-nav-end:#302839;--config-console-nav-border:#2a253a;--config-console-nav-switch-background:#ececec;--config-console-nav-switch-color:#868686;--config-console-nav-switch-arrow:url("data:image/svg+xml;utf8,")}:root .theme-dark{--config-color-background:#061F2F;--config-color-background-dark:#262d50;--config-color-background-fade:#1c223a;--config-color-background-fade-super:#1a1f35;--config-color-background-focus:#1a1f35;--config-color-background-input:#dce8f5;--config-color-tooltip-text:#061F2F;--config-color-tooltip-background:#dce8f5;--config-color-link:#4caedb;--config-color-placeholder:#9ea1af;--config-color-focus:#c7d8eb;--config-color-focus-fade:#1e233e;--config-color-focus-hover:#d3deea;--config-color-focus-glow:#d3deea;--config-color-focus-dark:#657586;--config-color-normal:#c7d8eb;--config-color-dark:#c7d8eb;--config-color-fade:#bec3e0;--config-color-fade-light:#181818;--config-color-fade-super:#262D50;--config-color-danger:#d84a4a;--config-color-success:#34b86d;--config-color-warning:#e0d56d;--config-color-info:#386fd2;--config-border-color:#262D50;--config-border-fade:#19203a;--config-prism-background:#1F253F;--config-prism-numbers:#1F253F;--config-note-background:#171e33;--config-note-border:#262D50;--config-warning-background:#1F253F;--config-warning-border:#262D50;--config-social-twitter:var(--config-color-normal);--config-social-github:var(--config-color-normal);--config-social-discord:var(--config-color-normal);--config-social-facebook:var(--config-color-normal);--config-language-bash:var(--config-color-normal);--config-language-bash-contrast:var(--config-color-background);--config-language-javascript:var(--config-color-normal);--config-language-javascript-contrast:var(--config-color-background);--config-language-web:var(--config-color-normal);--config-language-web-contrast:var(--config-color-background);--config-language-yaml:var(--config-color-normal);--config-language-yaml-contrast:var(--config-color-background);--config-language-html:var(--config-color-normal);--config-language-html-contrast:var(--config-color-background);--config-language-php:var(--config-color-normal);--config-language-php-contrast:var(--config-color-background);--config-language-nodejs:var(--config-color-normal);--config-language-nodejs-contrast:var(--config-color-background);--config-language-ruby:var(--config-color-normal);--config-language-ruby-contrast:var(--config-color-background);--config-language-python:var(--config-color-normal);--config-language-python-contrast:var(--config-color-background);--config-language-go:var(--config-color-normal);--config-language-go-contrast:var(--config-color-background);--config-language-dart:var(--config-color-normal);--config-language-dart-contrast:var(--config-color-background);--config-language-flutter:var(--config-color-normal);--config-language-flutter-contrast:var(--config-color-background);--config-modal-note-background:#15192b;--config-modal-note-border:#161b31;--config-modal-note-color:var(--config-color-normal);--config-switch-background:var(--config-color-normal);--config-console-background:#20263f;--config-console-nav-start:#1c2139;--config-console-nav-end:#151929;--config-console-nav-border:#171b30;--config-console-nav-switch-background:var(--config-color-focus);--config-console-nav-switch-color:var(--config-color-background);--config-console-nav-switch-arrow:url("data:image/svg+xml;utf8,")}.theme-light .force-light{display:block!important}.theme-dark .force-dark{display:block!important}.force-dark{display:none!important}.force-light{display:none!important}@font-face{font-family:Poppins;font-style:normal;font-weight:100;src:url(/fonts/poppins-v9-latin-100.eot);src:local('Poppins Thin'),local('Poppins-Thin'),url(/fonts/poppins-v9-latin-100.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-100.woff2) format('woff2'),url(/fonts/poppins-v9-latin-100.woff) format('woff'),url(/fonts/poppins-v9-latin-100.ttf) format('truetype'),url(/fonts/poppins-v9-latin-100.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:300;src:url(/fonts/poppins-v9-latin-300.eot);src:local('Poppins Light'),local('Poppins-Light'),url(/fonts/poppins-v9-latin-300.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-300.woff2) format('woff2'),url(/fonts/poppins-v9-latin-300.woff) format('woff'),url(/fonts/poppins-v9-latin-300.ttf) format('truetype'),url(/fonts/poppins-v9-latin-300.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:400;src:url(/fonts/poppins-v9-latin-regular.eot);src:local('Poppins Regular'),local('Poppins-Regular'),url(/fonts/poppins-v9-latin-regular.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-regular.woff2) format('woff2'),url(/fonts/poppins-v9-latin-regular.woff) format('woff'),url(/fonts/poppins-v9-latin-regular.ttf) format('truetype'),url(/fonts/poppins-v9-latin-regular.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:500;src:url(/fonts/poppins-v9-latin-500.eot);src:local('Poppins Medium'),local('Poppins-Medium'),url(/fonts/poppins-v9-latin-500.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-500.woff2) format('woff2'),url(/fonts/poppins-v9-latin-500.woff) format('woff'),url(/fonts/poppins-v9-latin-500.ttf) format('truetype'),url(/fonts/poppins-v9-latin-500.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:600;src:url(/fonts/poppins-v9-latin-600.eot);src:local('Poppins SemiBold'),local('Poppins-SemiBold'),url(/fonts/poppins-v9-latin-600.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-600.woff2) format('woff2'),url(/fonts/poppins-v9-latin-600.woff) format('woff'),url(/fonts/poppins-v9-latin-600.ttf) format('truetype'),url(/fonts/poppins-v9-latin-600.svg#Poppins) format('svg')}@font-face{font-family:'Source Code Pro';font-style:normal;font-weight:400;src:url(/fonts/source-code-pro-v11-latin-regular.eot);src:local('Source Code Pro Regular'),local('SourceCodePro-Regular'),url(/fonts/source-code-pro-v11-latin-regular.eot?#iefix) format('embedded-opentype'),url(/fonts/source-code-pro-v11-latin-regular.woff2) format('woff2'),url(/fonts/source-code-pro-v11-latin-regular.woff) format('woff'),url(/fonts/source-code-pro-v11-latin-regular.ttf) format('truetype'),url(/fonts/source-code-pro-v11-latin-regular.svg#SourceCodePro) format('svg')}.padding{padding:30px}.padding-top{padding-top:30px!important}.padding-top-large{padding-top:50px!important}.padding-top-xl{padding-top:80px!important}.padding-bottom{padding-bottom:30px!important}.padding-bottom-large{padding-bottom:50px!important}.padding-bottom-xl{padding-bottom:80px!important}.margin-end{margin-right:20px!important}.margin-start{margin-left:20px!important}.margin-end-small{margin-right:10px!important}.margin-start-small{margin-left:10px!important}.margin-end-large{margin-right:50px!important}.margin-start-large{margin-left:50px!important}.margin-end-no{margin-right:0!important}.margin-start-no{margin-left:0!important}.margin-end-negative{margin-right:-30px!important}.margin-start-negative{margin-left:-30px!important}.margin-end-negative-small{margin-right:-15px!important}.margin-start-negative-small{margin-left:-15px!important}.margin-end-negative-tiny{margin-right:-5px!important}.margin-start-negative-tiny{margin-left:-5px!important}.margin-top{margin-top:30px!important}.margin-bottom{margin-bottom:30px!important}.margin-top-no{margin-top:0!important}.margin-bottom-no{margin-bottom:0!important}.margin-top-xxl{margin-top:140px!important}.margin-top-xl{margin-top:80px!important}.margin-top-large{margin-top:50px!important}.margin-top-small{margin-top:15px!important}.margin-top-tiny{margin-top:5px!important}.margin-top-negative{margin-top:-30px!important}.margin-top-negative-tiny{margin-top:-5px!important}.margin-top-negative-small{margin-top:-15px!important}.margin-top-negative-large{margin-top:-50px!important}.margin-top-negative-xl{margin-top:-80px!important}.margin-top-negative-xxl{margin-top:-100px!important}.margin-bottom-xxl{margin-bottom:140px!important}.margin-bottom-xl{margin-bottom:80px!important}.margin-bottom-large{margin-bottom:50px!important}.margin-bottom-small{margin-bottom:15px!important}.margin-bottom-tiny{margin-bottom:5px!important}.margin-bottom-negative{margin-bottom:-30px!important}.margin-bottom-negative-tiny{margin-bottom:-5px!important}.margin-bottom-negative-small{margin-bottom:-15px!important}.margin-bottom-negative-large{margin-bottom:-50px!important}.margin-bottom-negative-xl{margin-bottom:-80px!important}.margin-bottom-negative-xl{margin-bottom:-100px!important}.force-left,.ide{direction:ltr;text-align:left}.force-right{direction:rtl;text-align:right}.pull-left{float:left}.pull-right{float:right}.ratio-wide{height:0;overflow:hidden;padding-top:56%;position:relative;width:100%}.ratio-wide>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-square{height:0;overflow:hidden;padding-top:56%;position:relative;width:100%}.ratio-square>*{position:absolute;top:0;left:0;width:100%;height:100%}.clear:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.phones-only{display:none}@media only screen and (max-width:550px){.phones-only{display:inherit!important}}.tablets-only{display:none}@media only screen and (min-width:551px) and (max-width:1198px){.tablets-only{display:inherit!important}}.desktops-only{display:none}@media only screen and (min-width:1199px){.desktops-only{display:inherit!important}}.phones-only-inline{display:none}@media only screen and (max-width:550px){.phones-only-inline{display:inline-block!important}}.tablets-only-inline{display:none}@media only screen and (min-width:551px) and (max-width:1198px){.tablets-only-inline{display:inline-block!important}}.desktops-only-inline{display:none}@media only screen and (min-width:1199px){.desktops-only-inline{display:inline-block!important}}*{font-family:Poppins,sans-serif;-webkit-font-smoothing:antialiased;font-weight:300}h1,h2,h3,h4,h5,h6{margin:0}h4,h5,h6{font-weight:400}.link,a{color:var(--config-color-link);text-decoration:none;border-left:2px solid transparent;border-right:2px solid transparent;transition:.2s;cursor:pointer}.link.disabled,a.disabled{opacity:.5}.link.tag:hover,a.tag:hover{opacity:.9}.link.danger,a.danger{color:var(--config-color-danger)}.link.link-animation-enabled,a.link-animation-enabled{display:inline-block}.link.link-animation-enabled:hover,a.link-animation-enabled:hover{transform:translateY(-2px)}.link-return-animation--start>i{display:inline-block;transition:.2s}.link-return-animation--start:hover>i{transform:translateX(-2px)}.link-return-animation--end>i{display:inline-block;transition:.2s}.link-return-animation--end:hover>i{transform:translateX(2px)}b,strong{font-weight:500}p{margin:0 0 20px 0;line-height:26px}small{font-size:16px;color:var(--config-color-fade)}.text-size-small{font-size:13px}.text-size-xs{font-size:10px}.text-size-normal{font-size:16px}.text-height-large{height:30px;line-height:30px}.text-height-small{line-height:13px}.text-one-liner{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.text-bold{font-weight:400!important}.text-bold-large{font-weight:500!important}.text-bold-xl{font-weight:600!important}.text-danger{color:var(--config-color-danger)!important}.text-success{color:var(--config-color-success)!important}.text-upper{text-transform:uppercase}.text-warning{color:var(--config-color-warning)}.text-focus{color:var(--config-color-focus)}.text-fade{color:var(--config-color-fade)}.text-green{color:var(--config-color-success)}.text-red{color:var(--config-color-danger)}.text-info{color:var(--config-color-info)}.text-yellow{color:#ffe28b}.text-disclaimer{font-size:11px;color:var(--config-color-fade)}.text-fade-extra{color:var(--config-color-fade);opacity:.5}.text-line-high-large{line-height:30px}.text-line-high-xl{line-height:40px}.text-sign{margin:5px 0;font-size:25px;width:25px;height:25px;line-height:25px;display:inline-block}.text-align-center{text-align:center}.text-align-start{text-align:left}.text-align-end{text-align:right}.text-align-left{text-align:left}.text-align-right{text-align:right}.text-dir-ltr{direction:ltr;display:inline-block}.text-dir-rtl{direction:rtl;display:inline-block}.icon-dot-3:before{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-o-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}i[class*=' icon-']:before,i[class^=icon-]:before{display:inline;line-height:unset}table{width:calc(100% + 60px);border-collapse:collapse;margin:-30px;border-radius:10px;overflow:hidden;position:relative;table-layout:fixed}table.y-scroll{overflow-y:auto}table thead{box-shadow:0 0 2px rgba(0,0,0,.25);border-bottom:solid 1px var(--config-color-fade-super);font-size:14px}table.small{font-size:14px}table.open-end tbody tr:last-child{border-bottom:none;font-weight:700;background:#f7fbf7}table.full tbody td,table.full tbody th{vertical-align:top;white-space:normal;overflow:auto;line-height:24px;padding-top:20px;padding-bottom:20px;height:auto}table .avatar{width:30px;height:30px}table tr{border-bottom:solid 1px var(--config-color-fade-super)}table tr:last-child{border-bottom:none}table tr:nth-child(even){background:var(--config-color-background-fade-super)}table tr.selected{background:var(--config-note-background)}table tr.selected td,table tr.selected td span{font-weight:500}table th{text-align:left;font-weight:400}table th i{color:var(--config-color-fade);font-size:10px;display:inline-block;vertical-align:top;line-height:16px;padding:0 3px}table td,table th{height:65px;padding:0 15px;line-height:50px}table td:first-child,table th:first-child{padding-left:30px}table td:last-child,table th:last-child{padding-right:30px}table td,table th{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){table.vertical{border-top:solid 1px var(--config-color-fade-super);display:block;overflow:hidden;padding-top:12px}table.vertical .hide{display:none}table.vertical tbody,table.vertical td,table.vertical th,table.vertical thead,table.vertical tr{width:100%;display:block}table.vertical th,table.vertical tr{padding-top:12px;padding-bottom:12px}table.vertical th:first-child,table.vertical tr:first-child{padding-top:0}table.vertical td,table.vertical th{padding:5px 20px!important;text-overflow:ellipsis;white-space:normal;height:40px;line-height:40px;width:calc(100% - 40px)}table.vertical td:first-child,table.vertical td:last-child,table.vertical th:first-child,table.vertical th:last-child{padding:0 10px}table.vertical td:last-child,table.vertical th:last-child{padding-bottom:0}table.vertical td p,table.vertical th p{display:inline-block;width:calc(100% - 40px)}table.vertical td:not([data-title=""]):before{content:attr(data-title);margin-right:4px;font-weight:400}table.vertical thead{display:none}}.zone{max-width:var(--config-width-xl);margin:0 auto 40px auto}.zone.xxxl{max-width:calc(1400px - 100px)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.zone.xxxl{max-width:100%}}.zone.xxl{max-width:var(--config-width-xxl)}.zone.xl{max-width:var(--config-width-xl)}.zone.large{max-width:var(--config-width-large)}.zone.medium{max-width:var(--config-width-medium)}.zone.small{max-width:var(--config-width-small)}.row{position:relative;margin:0 -50px;padding-left:50px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.row{margin:0 -30px;padding-left:30px}}.row.force-ltr>.col{float:left}.row.force-rtl>.col{float:right}.row.force-reverse>.col{float:right}.row.wide{margin:0 -100px;padding-left:100px}.row.wide>.span-1{width:calc(8.33333333% * 1 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-2{width:calc(8.33333333% * 2 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-3{width:calc(8.33333333% * 3 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-4{width:calc(8.33333333% * 4 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-5{width:calc(8.33333333% * 5 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-6{width:calc(8.33333333% * 6 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-7{width:calc(8.33333333% * 7 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-8{width:calc(8.33333333% * 8 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-9{width:calc(8.33333333% * 9 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-10{width:calc(8.33333333% * 10 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-11{width:calc(8.33333333% * 11 - 100px);box-sizing:content-box;padding-right:100px}.row.wide>.span-12{width:calc(8.33333333% * 12 - 100px);box-sizing:content-box;padding-right:100px}.row.thin{margin:0 -20px;padding-left:20px}.row.thin>.span-1{width:calc(8.33333333% * 1 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-2{width:calc(8.33333333% * 2 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-3{width:calc(8.33333333% * 3 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-4{width:calc(8.33333333% * 4 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-5{width:calc(8.33333333% * 5 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-6{width:calc(8.33333333% * 6 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-7{width:calc(8.33333333% * 7 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-8{width:calc(8.33333333% * 8 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-9{width:calc(8.33333333% * 9 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-10{width:calc(8.33333333% * 10 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-11{width:calc(8.33333333% * 11 - 20px);box-sizing:content-box;padding-right:20px}.row.thin>.span-12{width:calc(8.33333333% * 12 - 20px);box-sizing:content-box;padding-right:20px}.row.modalize{margin:0 -30px;padding-left:30px}.row.modalize>.span-1{width:calc(8.33333333% * 1 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-2{width:calc(8.33333333% * 2 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-3{width:calc(8.33333333% * 3 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-4{width:calc(8.33333333% * 4 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-5{width:calc(8.33333333% * 5 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-6{width:calc(8.33333333% * 6 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-7{width:calc(8.33333333% * 7 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-8{width:calc(8.33333333% * 8 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-9{width:calc(8.33333333% * 9 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-10{width:calc(8.33333333% * 10 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-11{width:calc(8.33333333% * 11 - 30px);box-sizing:content-box;padding-right:30px}.row.modalize>.span-12{width:calc(8.33333333% * 12 - 30px);box-sizing:content-box;padding-right:30px}.row:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.row .col{float:left;box-sizing:border-box}.row .col.sticky-top{position:sticky;top:90px}.row .col.sticky-bottom{position:sticky;bottom:0}.row .span-1{width:calc(8.33333333% * 1 - 40px);box-sizing:content-box;padding-right:40px}.row .span-2{width:calc(8.33333333% * 2 - 40px);box-sizing:content-box;padding-right:40px}.row .span-3{width:calc(8.33333333% * 3 - 40px);box-sizing:content-box;padding-right:40px}.row .span-4{width:calc(8.33333333% * 4 - 40px);box-sizing:content-box;padding-right:40px}.row .span-5{width:calc(8.33333333% * 5 - 40px);box-sizing:content-box;padding-right:40px}.row .span-6{width:calc(8.33333333% * 6 - 40px);box-sizing:content-box;padding-right:40px}.row .span-7{width:calc(8.33333333% * 7 - 40px);box-sizing:content-box;padding-right:40px}.row .span-8{width:calc(8.33333333% * 8 - 40px);box-sizing:content-box;padding-right:40px}.row .span-9{width:calc(8.33333333% * 9 - 40px);box-sizing:content-box;padding-right:40px}.row .span-10{width:calc(8.33333333% * 10 - 40px);box-sizing:content-box;padding-right:40px}.row .span-11{width:calc(8.33333333% * 11 - 40px);box-sizing:content-box;padding-right:40px}.row .span-12{width:calc(8.33333333% * 12 - 40px);box-sizing:content-box;padding-right:40px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.row.responsive{width:100%;padding:0;margin:0}.row.responsive>.span-1,.row.responsive>.span-10,.row.responsive>.span-11,.row.responsive>.span-12,.row.responsive>.span-2,.row.responsive>.span-3,.row.responsive>.span-4,.row.responsive>.span-5,.row.responsive>.span-6,.row.responsive>.span-7,.row.responsive>.span-8,.row.responsive>.span-9{width:calc(8.33333333% * 12 - 0px)!important;box-sizing:content-box!important;padding-right:0!important;width:100%!important}}.tiles{position:relative}.tiles:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.tiles>*{margin-right:50px!important;float:left;width:calc(33.3333% - 33.3333px)}.tiles>* .photo-title{width:calc(100% + 30px);height:15px;margin:-15px -15px 10px -15px;border-radius:10px 10px 0 0;background:var(--config-color-fade-super);border-bottom:solid 1px var(--config-color-fade-super)}.tiles>:nth-child(3n){margin-right:0!important}@media only screen and (min-width:551px) and (max-width:1198px){.tiles>li{width:calc(50% - 25px)}.tiles>li:nth-child(3n){margin-right:50px!important}.tiles>li:nth-child(2n){margin-right:0!important}}@media only screen and (max-width:550px){.tiles>li{width:100%;margin-right:0!important}}@font-face{font-family:fontello;src:url(data:application/octet-stream;base64,d09GRgABAAAAAFqcAA8AAAAAjUAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABWAAAADsAAABUIIslek9TLzIAAAGUAAAAQwAAAFY+UFR4Y21hcAAAAdgAAAMCAAAIVGsHIX1jdnQgAAAE3AAAABMAAAAgBzP+pGZwZ20AAATwAAAFkAAAC3CKkZBZZ2FzcAAACoAAAAAIAAAACAAAABBnbHlmAAAKiAAASOkAAGyevKUl9mhlYWQAAFN0AAAAMgAAADYauqkaaGhlYQAAU6gAAAAgAAAAJAgaBKBobXR4AABTyAAAANQAAAHAgvP/gWxvY2EAAFScAAAA4gAAAOKlpIsybWF4cAAAVYAAAAAgAAAAIAJcDRRuYW1lAABVoAAAAXQAAALNzZ0XGHBvc3QAAFcUAAADCwAABJSPrOZacHJlcAAAWiAAAAB6AAAAhuVBK7x4nGNgZGBg4GIwYLBjYHJx8wlh4MtJLMljkGJgYYAAkDwymzEnMz2RgQPGA8qxgGkOIGaDiAIAJjsFSAB4nGNgZC5hnMDAysDAVMW0h4GBoQdCMz5gMGRkAooysDIzYAUBaa4pDAdeMHw6xhz0P4shirmR4RhQmBEkBwADgA1QAHic3dXJjlVVGMXxf0GBinQ2aIllU6jYN1UIVZS9NEoj2HfYd4CiFaY8hANImMCAFyDhEZjAAFIvYG6IDEjW8Hz3AYB1aq8wINGBzrgnv5t7T+5Ozt351trAEmCxPWej/riaEX9i0TLfHVm4v5hlC/dHRy75+6s86t+Na2/3Z3eiO9Od6y52892gu1xjNV4TNVUzdbiO1LE6WafqdJ2t+RrUlbo6nBzODo8Pz1+7BuLG6gs3rZ5eWH3031b/59eIn/6vG9fgpuvSwvX3P1z96kXei1Hv2FJu43bu8L7cyXJWsJJVrOYu7uYe7mUN93E/YzzAWh5knId4mEe8axOs4zEe5wnW8yRP8TTP8Kz393le4EVeYpIpNvAyG9nENDNsZpZX/MSv8Tpv8CZv8TZb2Mo2tvMO77KDnexiN++xh728zwd8yEd8zCd8ymd8zhd8yT6+4mu+4Vu+43t+4Ed+4md+YT8HOMiv/MYhfucP5vz3lv6Pnb1VXsv7tyVb822un9Smz4LCc4Ciz46iz4+iz5XC84LCk4PCM4TC04Siz5vCE4aifzqFpw6F5w+FJxGFZxKFpxOF5xSFJxaFZxeFpxiF5xmFJxuFZxyFpx2F5x6FE4DCWUDhVKBwPlA4KSicGRRODwrnCIUThcLZQuGUoXDeUDh5KJxBFE4jCucShROKou9IhVOLwvlF4SSjcKZRON0onHMUTjwKZx+FWwCF+wCFmwGFOwKF2wKFewOFGwSFuwSFWwWF+wWFmwaFOweF2weFewiFGwmFuwmFWwqF+wqFmwuFOwyF2wyFew2FGw6Fuw6FWw+F+w+FmxCFO9HnTON2pDvRuCfpzjRuTLpzjbuT7kLjFqW72LhP6eYbNyvdoHHH0l1u3LbUWOPepcYbNzA10biLqanGrUxNN+5naqZxU1OHG3c2daRxe1NHG/c4daxxo1MnG3c7dapxy1OnG/c9dbZx81Pzjc8AatD4NKCuND4XqKuNTwiGk43PCoazjU8Nhscbnx8MzzfMXQcZJK6NAAB4nGNgQAMSEMjc+N8KhAETIgPbAHicrVZpd9NGFB15SZyELCULLWphxMRpsEYmbMGACUGyYyBdnK2VoIsUO+m+8Ynf4F/zZNpz6Dd+Wu8bLySQtOdwmpOjd+fN1czbZRJaktgL65GUmy/F1NYmjew8CemGTctRfCg7eyFlisnfBVEQrZbatx2HREQiULWusEQQ+x5ZmmR86FFGy7akV03KLT3pLlvjQb1V334aOsqxO6GkZjN0aD2yJVUYVaJIpj1S0qZlqPorSSu8v8LMV81QwohOImm8GcbQSN4bZ7TKaDW24yiKbLLcKFIkmuFBFHmU1RLn5IoJDMoHzZDyyqcR5cP8iKzYo5xWsEu20/y+L3mndzk/sV9vUbbkQB/Ijuzg7HQlX4RbW2HctJPtKFQRdtd3QmzZ7FT/Zo/ymkYDtysyvdCMYKl8hRArP6HM/iFZLZxP+ZJHo1qykRNB62VO7Es+gdbjiClxzRhZ0N3RCRHU/ZIzDPaYPh788d4plgsTAngcy3pHJZwIEylhczRJ2jByYCVliyqp9a6YOOV1WsRbwn7t2tGXzmjjUHdiPFsPHVs5UcnxaFKnmUyd2knNoykNopR0JnjMrwMoP6JJXm1jNYmVR9M4ZsaERCICLdxLU0EsO7GkKQTNoxm9uRumuXYtWqTJA/Xco/f05la4udNT2g70s0Z/VqdiOtgL0+lp5C/xadrlIkXp+ukZfkziQdYCMpEtNsOUgwdv/Q7Sy9eWHIXXBtju7fMrqH3WRPCkAfsb0B5P1SkJTIWYVYhWQGKta1mWydWsFqnI1HdDmla+rNMEinIcF8e+jHH9XzMzlpgSvt+J07MjLj1z7UsI0xx8m3U9mtepxXIBcWZ5TqdZlu/rNMfyA53mWZ7X6QhLW6ejLD/UaYHlRzodY3lBC5p038GQizDkAg6QMISlA0NYXoIhLBUMYbkIQ1gWYQjLJRjC8mMYwnIZhrC8rGXV1FNJ49qZWAZsQmBijh65zEXlaiq5VEK7aFRqQ54SbpVUFM+qf2WgXjzyhjmwFkiXyJpfMc6Vj0bl+NYVLW8aO1fAsepvH472OfFS1ouFPwX/1dZUJb1izcOTq/Abhp5sJ6o2qXh0TZfPVT26/l9UVFgL9BtIhVgoyrJscGcihI86nYZqoJVDzGzMPLTrdcuan8P9NzFCFlD9+DcUGgvcg05ZSVnt4KzV19uy3DuDcjgTLEkxN/P6VvgiI7PSfpFZyp6PfB5wBYxKZdhqA60VvNknMQ+Z3iTPBHFbUTZI2tjOBIkNHPOAefOdBCZh6qoN5E7hhg34BWFuwXknXKJ6oyyH7kXs8yik/Fun4kT2qGiMwLPZG2Gv70LKb3EMJDT5pX4MVBWhqRg1FdA0Um6oBl/G2bptQsYO9CMqdsOyrOLDxxb3lZJtGYR8pIjVo6Of1l6iTqrcfmYUl++dvgXBIDUxf3vfdHGQyrtayTJHbQNTtxqVU9eaQ+NVh+rmUfW94+wTOWuabronHnpf06rbwcVcLLD2bQ7SUiYX1PVhhQ2iy8WlUOplNEnvuAcYFhjQ71CKjf+r+th8nitVhdFxJN9O1LfR52AM/A/Yf0f1A9D3Y+hyDS7P95oTn2704WyZrqIX66foNzBrrblZugbc0HQD4iFHrY64yg18pwZxeqS5HOkh4GPdFeIBwCaAxeAT3bWM5lMAo/mMOT7A58xh0GQOgy3mMNhmzhrADnMY7DKHwR5zGHzBnHWAL5nDIGQOg4g5DJ4wJwB4yhwGXzGHwdfMYfANc+4DfMscBjFzGCTMYbCv6dYwzC1e0F2gtkFVoANTT1jcw+JQU2XI/o4Xhv29Qcz+wSCm/qjp9pD6Ey8M9WeDmPqLQUz9VdOdIfU3Xhjq7wYx9Q+DmPpMvxjLZQa/jHyXCgeUXWw+5++J9w/bxUC5AAEAAf//AA94nLS9C2Ac1XU3fs+d587uzs7uzs6uVrurfa+0Wq2kfcqSLK9l2ZJlWQghjGyELIztGMsvwJhHDAHjEEgopi5xKKTEToFQCCkYklJKHk1ImiY0JWlqkqb959l8JE1ImpJ+CbHG37mzK1kmSZP2/33anfe9M7P3nsfvnHvOFaGEnHuGfoFzEB+JkFQtTgQQ9nMAPOwnPOX3E0rofkLITq/f7fYXRCHYntLFRCyeLpcGOL9RqBYiHKeL8TxUIkC/sK7bTHavUwLZgY71XxjNDqZD8rHDz93MH/nw0bX909P9XVOb+lthZCQ9MLUJPjV9661P30YPE0LPmee+xP2Qvk5UfI/tG552TkzXEoTjee4qAQgFSmAvAdiHL0X5KcLz9DJCebqhGV+Z47n9/2WhzTU3kHDQr7s1m0hUcIqC3g4FQ+JUwJ+S4dKVYsofqw5AHxRawCjGCgb3QlSjWapHz/5Tic9SLcrtVs6eVvmo/kgpHolWYEpPwhuBgNkfCBbgpUBgX+tRPRSPJAPYWkQ+d+7cL7gfcHbiIi2kk6wiF5GtZD95BzlUu+6mG69ZP7JalG1XXznXFouKvLB5+uKJpoBbkym3oqfbJoNIgB92gU0FWbDJc04QHMBzAj9rB04BChydlQAIgSncANkkAgEyesvbr79u7/yO7VdcfunUxrF02p/245+uCeH2qk8XM4l4ulouVarFgpFZduxvHEuNY2wEA9h17OUMKx+rH7fD+frLr/tjjfqN42LjOLGs/jFFnpcc8F+uuWlFXvBICjhE+rrkOPvYb78G/8XRocVKeOKVZY/4knVGnJcVxRxbVobewk7V982Nv7nC358vQhjP/JLbTF8iAVIjE7WNDsAeGY5hF6wjnCRKnLiXUBFEyqhREEGYJ0TgBcLvJRIRqSTOETwQpogg8NO4w4/lUtmM10joshBqB10CMZ5eCYlG6xVxZfjBqLobXZBJr4IBwF6rZNylNPZaKV3phMWTBn2v3fyyPaK/6dLBUN/UI3botL80u+Y0/iZgryrLp1MVKKWfExVKF8+smYVZ1W6+ouiuU4Z6BuueUg1qwxMfNW9cM6vIdpvolGSoJOEurM1TmW+cmUV6t+QItwXpXSa7ydra4I7NY6t5wvcpFEiptVnjOeCGWSvsFwHPo0gBZAfgUMxwlEMxQ3fOXH7JxetH2rPxqNcjCUY7vmFcBaNQSSEtuUCUDL+hSypk4p14gB/WGOVMOiOJcVynS8i/1XQn5IE12yqoVBsni8jTlSp+GGEje/urlYK/cTMJT6Dc6pu8cZJuOrgJQrK0S7F7W0XBNeGUpI1NQZvEa4dlh9bsv0jUxHUGL8itikveib9cEXbJqj9VLytvDARtMuc+jM3sCvkvElzSiM7ztnphBWb7pqYOTU3dyK5rEV9zQVRF3wQI/U55LKQp0ttsjn5BrEUEVXQUXKFmFzgkq2xTMNohOSR9YllRe58grAk1igY1pErWoOTc/+buop9BebOxNprPZaNUEMQm4AXDQznejj3NDxNREPdbHUF4jl9sfUAhb4kTFJ9kmt1rLGkYCXe8VRKa27G5fbrKZeIZo1ioYvvWNyuxKfNQqkbAn2aEWvBXqqJU4e5KlbObbnz8yvfd4g3cuq1vxuN1BQKrJtO5VK5p6FMHhN2jF5VXVnx9JbqvkvGvv+/O7TV6Md0I6yqc6Nw2SH20aXwue8l2wadv2AUrHNFaUsTfh8u5h7ljXJhwyEd2opEm1BWjtWH25hyQWTxPBZSMjLtElJWKQ+ZESRKnrB1RmrbbkPmkMY87Eg41Gz53k6fJ7XGzP83JOC9Wji0tpYIeTxUMFfK04mcH3LGzd3C3mM8sfIXm4SK2f/aOAwcgYMRppCtKky8eOECfOmA+c8D88/3m4Z5rronnkhDPR6s919T75if0q/ROkiSRWnO8SZN4bPVhDph+ZZJ8px7SdV4IoHpFWkcSl9gqXVoFGbaqIA1X2crAy36DftU1puW0Rx7B1ZjGttr5Y5frkUdcBwy28+ijrl8v6MqzAg05dpx+Fluym+Rr7fl0KuBzqU4ZOAcKMzrMI39yjDBgP2WvmG1NxGNuXRD87eBm+qKc8DEp5bahdJKY/sjYQEQWTFeq1hs3lEkY31jnPChWjjHBAqvxczguSFQSzFvNWyWnkOCpAH/o6fLehVJWVm4WodX8ORY9cwZ7wAWy+QtItgn4OoPmJ7Bom6gKMO5yXbOHieXv7uS1Ov2jIOqHHyOG8Nd09towbL08wy9lnTKZ0mjcdF1L0T7lTXvI/qYdn/KGasDfqriP35Bh1PvsGernJolOirUuPBbxfsgvBxgpooqeE5CjKEwKTIZu4lGO0tEEoyZvzOIabBsxDEUp4cZPmYnxYoH6L+uKP2Lc++S99x68cpLb8EetrbsfNafh8Udv3nVt45ncdpSjZRKuBcu5RNAtixf8kLZ0HxV87Si1XKBCJywpbqne8kgweWC6wGp+pqvDSDlM7qEQ5fqCqppw9Ta9NxseDufg/mCvmlTV5vvvD2qupKun+f5sZDicfW+wR0u4tKb7QVZ7gyuxzqVPhrOQCz15KZ5diZU2bfptFwhv/YZTXB51gZu0oY5cV1tTRnq2oZQndJjYRNt+mYnw/UTiJJRDCNKmWOcihuPIHMNt0wy3ja3sTxQT8UJqRcCjIIZJldIqjaAoX9wuwxj+YoSBN+v3cqgZGq2SKeOqzj0G/YIe0WkgGPgDPeqhRiiwLmqc/aI/AlHjdUclcSxecb5uRD9mCxzTXceQ9o75ve43lYjypieMmtAT9fBBx+LO3c8a0aiBK2hpbW2JwKTR0LY5rKK86WakKGA79HJPcLeTVuSvQbKBTJHbarc0K5THvlGwV50K6kQeUCMiYhAVEJV5YnNKTpu01wUKcdoU5xyKOtkhyXNMxtsFh32OY9SG7UMdIyoIApPWxEnWbxwbWpNtu3hibGrj1Mjwmg1DG2qrequlQr6jrTvbHUgUW3W30NQOhk/HBi+XPNWy1TioM4uFCCAY9Ek+w8/aihGKiG2ppXlRM7zYonWsuBJKeFHKFAwPAhRPxVuo5EGlBnfZJeaPkJF/9NPTtOiL0pD/FV+Exr0leP+1YrML1ZAaFMyD7ztzxvzKmTNf3OWLRn3vx1U2CqXbP0ZfMF/kn7tj/kF64p9O0Adp0/X3f9p8nYLxqedA8QJXjGaxi1rjUATOvccWdEVyrqCw8M4zkD9Dv2J+6SvwcATvFDHeb0Qixp88b5rPPw/0+YVPP3jGuqUl577GvUa/hzZOktFiAtFLsokioObpcBxgLfIW9kldcSDInhOBQ40yiRuONS9HxgiJRf0G3sCnp8oSyr9UeZH6ym4mS5iUSrghLkqlAaFYFoC+vj2iL3j0SMiAaLTiUI7QnscemKJaqH/izkkYM5/r75l8UY8gSaaRFguqwxyAmcl75NDU6Mxg/v1vmmfJojx4De2zEInVIm5LpiFmQ1nAMYkMxO9z2kkIQjyaNEI8g8zPWbo4Qv1on2HXIZpE2WY3H3Ep2Xjw3lviIwN5r56rrUvccu9t5qPKRQpMuJRKZSL19ndDIBv36cnWINzz89vMZxTr+b/kPCiPEqStlq5DBnz6AcFqIsa12ESoHLCNEsUUsiuzEC3jILEkmJYDWaYO/D7GpJ6o8VrUmEcWes3iwdci/nncYQd/wc7+wMCzvh80zjJ2ey1K4Nw5lPF98Lco4901dUko+gpMuqcsm4RJvzrD9+ku0+HSqcNiY+VJlPCIhhmD2gnrVvbbtiCOr9to1VqpFbGSjASDum+RFhioQHNr7q3wKF3GT9Gyh5eZPszU4d5iGnHLxRQ73qxrZ39iiQvO7cJX+u1H24etXWsN2gh7f5e1BnVYNdgF3CcNOvkUfYAbQl2Fug8u1H1+P2sdGzUs7ZdB7VdXz4ZO7zVfhTZFuQK1oLnNboeHsaWuUOgT5tfMV61dBT6AW3jYbr9CiTSec3DxObYLnxPyWM9paNiqDerIxc8etBtvH7JfgY9oNb9m3QxvqsDV5pX1p0MbtNYLsIKkYVPU7SwnUh9qYAM5llE/MxsItxd7CIiA5gfzEkwSjmPGFMePtZXcCV2z6LBYSme4GFr5vqK/mHI3+sldqnrRVMKu8HuRSJ6mcMi8/hDQp8vPwiWN9lffKURU+LIaEd6pQi9ac+Az/w1NuvnLzf+w+gSc8TiUVB08LvPlOGnQ0yHupGUD6STP3jiNb9ya8XCUZ4SDwp65Kpb7VSwjCHaGEh0Bj4DieVG1pRbbENV8qm70pBlXM6vFBT5/xZLTZxoKDXaad/h6jT6fDw4bU/DHzuYjF+06fnxXdF2Tzfane2h2Q8ylLCmx/zTv0PWVep8Bh6tT/2KkNszC8Vfuo9hkHmn2ln7a1KErizR1NcrNz5A0CdWamq2+dgIZWurvZDxhQRFdRWM1Y9lkSOXpCtO4iEDoKmaLlQYoUzDca3d+/65Mdt8fNScVFRuEcg7eqUu6Jrmmt8PE0Re33/X9O2H2ykfm+CvTMg8OBdDE5FyCasihkDdbOD61/uhcz9wjqBDJud2chl3OrAAX8RI/aUYORsnudTk5bOrhSHOwyW/4PG4bWYv0whMGMgRk7lmkHQ4m8Zdwm0SGbkcJsSs2GW8lIXBjkt3n9nMxd9Gdivliq0CQUlVBAly4qlfy4jLGwc8vWngd7jJvBBnuls17/HA4YL6cg8mOJ9Z+f51/avXUCTgF5jMwbl75jUvuvzR96T9Mzk9Caf3L6+GPC+YnC/Ciar5dXaSZz3Md9Of4G6KI+U7WFBVfy410Q4c3PG1MTNcKTB4B3WEHCdmNSoSJIg6h+ZWEV0AQeWGOiLIsThFRlKeJLMpjzbUiq4SCa+9/o9bmWiweawoAybXHyvFyOBSINkU1l9MuCTxH/OB3MK9ZXPQVSyi/mVCLZ3zF8gDCzDxwcVFHU72U5hqmtbhklaPhDe8dnzwSVp3xpN12pKs7O5pv6egYyOdb9m2dqfT0VGa2/svsTKVarczM0t1TF/WGIokWuLnoWFvtGm0z5/Or8vmBThrtKdcLshqz/7J1ptxjyYlzC6gjtyLf+UicdNU6JETiKJWWuRko0ivlODqFsImbZhh+zOf36zpjOSgho8VdaGbhStRRKhZQOvqRpuN5OgBIuvTvlCKKpu/diquionxBYR4V5Z5rP/fa5/eJN7/4xgu3wvOaUrDbv3ur3V5QWrCEggU2XP/SwYMv/ZCtCHfuu+dO8C1cmjgsFFLBHkK6RJJkviAOzVYEHSjw6BRuKFMxlIxhe8Wagx6306f6fAXmEkkZTJNk0kx8+znW3HExAt4K6wHRX3/3CvfN2q5bFo7dWizXoDxwZtWfxfP5oS6au0kojVVAHBE0bvSF+b+8Ymaewvz8wjG82DWUhzuc3nSJ9iTd7tM222mrXU0k0PvobYjiEyRZi13gs6HcFIpcBkQ5OuZf4S8y29WbrjK7inV+w84SKj73Ek7yC/h+kShPA9x42iNr6umPab2ej53mhvS4dvZbWlyH2zw9HjqKpmJUlcW5bYqybW5W0c37tGhUgwO68nlFWeSbF7gHuPWIi/rIDHmsFuwFm3DZMCW8105B4oY3tVPbOpFKdN2GpxXkowoROBsv7CAgIW/sQCHNCTI3R2yESDZmdRAJeOlKQkXRohNxmohURF7qYRU5wbaX1UTa2vv7Vt1cUzdP+/2hVj/KaL1uwTBakxDQtAAqI4QqnQ2SQ4m5SIgNGszkKQMMlseqBa8uWa7VzKJnoK7DJb+15Tq0g1peM9B2p1FDVlXe8GrXuTq0Sc28wXUd2v+TmnbQlUfxpvARv02V7LyseOCNrsn8u/LX5bu7u97VebCzc7Lzzs6loyf9roOax+A00YUvzSu84c67DrpcF2vwAUO7TnNNunJ4U7ynqtoQtCF84mx2t3nvYOfFnV3Xdb6rq7sbb3NnfjLfeTB/V/2oods/wN3HhVFTtpDp2qUSgADDMiCMpnQdg1xAUcUzicXPo0W4T0J1JApEnJOxIAhTuBFgmuDBmOED0hz0tRgtqtOB0lzkiQ66re7jt1ST5EvgJ1YG5qeyzJYMyjHdoDdd9wg9eSjkFw7sR3k9JTxykLlzmgLROBd+5PuPCHj+mm/BlD983QfN09FCiMZ9AabsPfj+X+fy9Luof3QSQCqMIle3kg609CqklwyQNWQELb5xcjFafXtqV6NJNri6NrCyv3dFT6XY3dXZkcu2ZtLJRDzaEgk1B1Fb6d4U/rJhOxBFEhF7AuKbWZtMBUqFSbYV6CaCR6NApi65eGJ844bRdWvRkHPbbTKKaOIC1VH/zcxPjLAnwTBDUcgkJL/kr2aquFg7+JXwm5FaoMpO4FJcRTPSKqgv/k7AKqlqwougSSp6qwkOPM3NHvFd5uiddm8w6IUR7yHPxoNN/ePRjePje8fGNnZs3Lhx78aNY/e2u4PjLRs3jreMrUj3RvHsM03usYPO8thYi/d6z0bzaGvXLvcG0OYvvUbpo98NpoMLE/Rp3My73ePP3d6/EeuM7WncrWN8bGwse2nz+Ju407Kxtzc6Nj6evc298dlacWz8r7BGtXXh36+4+mq6shPl1S/OfYz7ESdjbyTIDR+NWGpgw9N2ZP42gjJpPzaiCOJ+5raB/Wjwofy6ilimHooyspmJlY3NtdbfXBbh2luKbq65w+FwIpxwe91xr9uoKEKoPVX3yzCJZ/nkixJzP8eY8zlTzAipIkIL7vMejxjik8bZV4wkH1KyD2974rTMt0KuVeZPP7Gtyzxrnn3sE59Tcp4H9WBQf7ArsOeIvG+ffOTMG28AQROIs2Tza2iW1OmO6RNU4hLiS8LPWr4UbpIZZ4DGGR6NpVKxWKo1hSQXS8aSeiYbsKG8dqerqYrfcEEMdUoFxbPEoaxmL47CBneQZmLVIi5UgV1hh73pu/T495rsahh222mz3GT+TZPYnSkXxKD5+WY+4wW+/V/agNPhiNPxgr055ti1Sws3219wOM+RCA18synwrSANfefj+EdQEy7altxbkFwCrbEiqZJP1T6ebE5wTXawcU222YgQRpCPsJCjswrIQQg45cAskQynNEkMLzEm435fC+8kXnB6Z0MAHuayI7Mxd9TFiw6HOFnfEx2bdE3lHKJjtFTq6komo9FQKBCQZZ4npFRFxFLuKnYVC93JzmRnviPXnm3DdktFE9FEPBZqCbVEwoHmQB1i6l6PW3Oh3JHtMooeXuIllFIc4dwpRMZVX6LsxSWGCxTLzAmYEHDh3DE34Lli4zpKJ3cRsRTgefY5Mzw8DG+MmI5X8Q+eOnPmmPkQvWPk1ZGRV4eHzwybD5kPcR7zob/HUk+N4N/CZ8+wP3YedpjfGWbVI2eGz8AOVsJ8EA0EXBp68zPca1wNJViVXEN21XbMA9gR1UMbihmxGyX4pgzleGGY2MG+n4gIT0QBbS3EzyiIkQ0OKACS1bBzNoSpHD+FG56bllFSc2P79u6++vLpyYnB2sr+UrGrs+irBBxIaSBKmTy36EXqg0Jdn1nDXGIn5IVMpRrhLRcBU2bu81dV5rNXKTIVXVZ9gFaZiSEs1YCD8XZdjgTzxi3MJ3zLe+iXhGcFl9IViQQzznygNZh0xtodWiSYCxxTVOm0YF0+1pyLBh2eJi2Q9LT5K4Ppeu3mbFLX3MGQI5nMV2qt9Qp0XWlHm5YOOqjlpF74rIy3cIjziIVAUQwjrZe2N0Wyuk7xqnBagB81CmjxeHN8Zaow5OsMBgywanuiyabEyoFgrSsXd3D1Clb/WHx9CLVKmOTImlotjl0CwyIb40DCsoYOqcDPWSCXTknMAz3NEK6lBrOtqWSwyRc2wnaFKUHZUggRTle5eJ4rDXDeOmjUjaqBJrAuWiqioAK8MfPEgU2trZsOPPH84s7MzJEjzx85MiP3ZvnBmVotr8oaPVQYnRgI9U+OFgqjk/2hgYnRgum59fSt+D0jOxQ1NzCwZSDftWi/0zuQvyXk6WwtI3DMs4LWyF7ESPsID2AN0rPX52Es4U1VvJbZ7o2V0QwGv3DeYK/4rX42JOY6ehbCU4emAF6JGgs/sHxH7vtfPkE9uPvonr4pOrHylPkJy3sEg4hJ9uy8//6deyKoH86h3XiKcyJ+i5FCrbMFbXJmbdFhi66Z4xp4Ziih2GSeHmYpADfmTyCT1y0FRNk6thtiNzYGmWAGAh+hfoRgBr3j+CvH8QuRXK/+ye1vnzi+q0b799z7yL17+mHtJ31w5G3H6YkvPCDeYz4Yzvo+uXZg930fvHdfLz+488TGt2//pM/izd3cS9wlKBmDZJC8s+YgLKRhuLUZCQxRrIqKLC8h4kHUswPbF8hV+FN4Tra8VCBwMPfr6LXzggp0/nfV2FxztMQ9rYY74fXYELIKJebDK8URLVULqVjaXcpT5EmfJjCUwQiI+ffKA3y1VKmyoBDmYZYiSFsRDhZssS7w9LbazHvomfcGS5N7JktB+lg2/CZ24JvhbCjflfTQo1cL0VxU2H0EjHhX15zcFbPZ2nrhzx6HtlB/Tzze0x8yX308nO2bmurLhgOFqdm7Nk4d1xS7P4J4zK5ox6fG75ybLLExQtbHfCdqaTcpoGUwRDaxuIvanotU7GIYdoPNbttPUFbv10Cm8n6XpdklqPuS9joUymIvxHmCIFKsI0+17upULVen03J1zl6xZfOmqcmJDaPrEdetGuhd0eTTmxAwxTQPthnUHZ8l5veoFqqINKHhAmTO4UIEzUo2PF2I8H6os2DBwJ08DFBDYK7TdAZNOjbKijxayCwV2bCld0NHDdby2cFYOsXROybXmIGhCeAdWjTdGxOT+ZGJdU1tmhzvSUc1FRY+x0ackVXutsJsnrsZ1gzkN6zY0sEhHBhq5dcON67PckO53GeNIDh92rh5+eD4+GCkZ7CnlDYCoSA1tKBCjXSpZzBEj9VHsc2fbL6V3vyxG8Wj/9CehyFu9bjmcwYC0Lhcjweg/ZYvLEDStQTzRhBocL7VnmTJue73ez3MeoS6ZCpUqjqLzMHm4tIW18NT9fe81/NFWzKdll72PP6jx+HH9Wf1rcyYEx7D8MDTmZWHHn/c4qOPoY5jT44iIn/ioynsdYYEWVBRn4h0IAnY43sd2OuSQOZ5ILJE5HnEFHa7fBW+KEpVac4JdpvNPokbu20zQbpBbNj/X9eWZPv8b6++uRaJoREd64ghpmhLI/QPNwfxJQOeZDnuVpmE8cW8qN2YZgPLU274iwU/ykNOQoTWDnFvOYa2NZJSORPzwefNHoWfO6n5/Kon9P8FdYcne3KOV8zeR0KQDKmhB0In4CMiL04teLYo8BFYtx2Se4JtHUGHGohO7DH/eTtsq1SCC+/ePj0+NfXY9vqY1TPcZqvf3CiFpsheclVtzgNojgwThUqSsoM47SoVBWc9jIXUfeQoNC0XEwJFOofYWXI4pCm2lRybiUNybIw2794xu3nyYssCQoTVPBW9xKvjx+NintrFWI9qgw2q9ZCygpGK489WgYVqqNQQ8aOziA2U9QWjgp8BYGEfAzRdwU/Jiv7II+mIYEV3iHE8yqQ1VqYSxUooqBdjPTbc9rEj9NYXDq+K5EI02ho3v89n+vShvDuUdYgyT9mfqDiyIa1rMDumjOZqJWcop5y/ZM+Gtfy69AQ8wQm3XVcwCwdvEzjcPdgNpPvgrSK3GPPRdzl70K30yLt9kUg2EtnJi7Kd3XcoPSFOpIcCraGsXVR49idT5TdeMj94W7d1f/HW6637L+5b9H6ce4DLWn3mJ+tr6xgu5SkPwxJzJHI83Ssw/MATy83EcIN4Hjd4PIoNiMfv8atOm1txI9STQa4jB6IhC2nEywYDo2ww0OeXMm54+ROgmj8zj5o/A/UTJ77yFfPMV7/6/InCE1x28SwcBvXsy1+FrHWR7nkDr4KHjd80MP84SdSiBQYHkGhQA9d9dI1gw8FaLJrmEECndMurjNK0k6atrmQ9jPYJQ+lWaI/fqJMD9emIFC3aKOOx5TapIkz0F5izBTmJ8zhE+4pS0RVxB/sq7cMPtjd7FcT8EgfhlpDa5ZJ5RdcUXaKaHE1HEM2Cmt03Bh7BLkpKJBJ1SFqAzmU4+rCrSw1FQ7wg+/Tm3AMj7ZWQX/NEVVeptMIuOiiXadEjLhrQZHs0ElEE2c4ZsHFf1kFR4UaTcUAYVfEqCDfQ5iEWJpn8DTYQs4D6rMjB68jDtYdWp2jYs74jyXnDdDgKkSB4whHPXKyFhr228FQzeJv8Ts4me21bA4aDk31uiRN4WZjVNZHjXXYENQi5ZlWFciQUIpPWDglNo7UcGrv2wN7db9t51dwVl2+6ZOPY2qFVAyv7++qMWurubEPgHYu2sKiZYFOgYfU0/rQ4KjxEZwm2ZJZt4S1bDkEd3oeFEhnVYqECy8pXG9f8jWvV8z5DFIdLUSSLA4gWChw+deqzp09/dnENDz377JnTp+HDp06defbZlxxi0greY+uHrFNnTp3yKHLCCu9LyMqrudDZ18PZbHiklEqmSmcqyUSqAsPh7EWnTp1Knj59Onlq4aVTb7JV8jR0nbLudorVNtN47dSp+WWncgtldiv6xXC2kiqVUpX6OmvFLB3nnkLeZH3aioi+i5TIPbW7UYPYiE1A8Ypmo11W9uqgOO2Kc544XXanax4NeNWhgmMvEb1gE0TbLO9BpWqXiX3ODS4NOcylzhIHpY5J4nAwz5CDjsbQkAWCuKO7q2G4ppLR1lhr3Wht9J3HyUY8RNIMzb66v8hnRRaimZ9YavIi2qxFX4rZqYsWgzeWifnRZmULty3S0REZiy9sbJ6MdXTE5uLUFV/4Gfztwuf80VguGr2UVjvN0Nfe857Pvec9tJSPmsdb8n/8xx1R2BfruP7226//G/OfIWm+Pdoexa85+rPbE4lEI2bil9z3UD6IyAsryAAZAanmqw30aUzxEr7EgkSGV6PB0HDrdhHm9JD4vYQZFGQHMhKKOX5OxgNRAHErYf4zFmvJfNUCHWte9AjVy0vwe1Tw/48eVOteXoVnwcm/q87mzZtrBiFDa1b2d+fb0pFmw4stIeo2JgurGSR8n+WbMRA/epcFIFRjBT9zPImZNJ4SJbdu+GOFChrLWNDg/JAog5RpBJDBT2uX1srgs9lesnlwSc6uMbvWzM6ugVcSERsnNcuK02F2pUos2POVVElIyoHqKfPuU3R/8VRRy2mXah9ffenqlgocX7yF+Ynd9RsMzoLKe8WQzHOlVOMe6yS8gwzHTpp3n4R86VTJ5bpUy1l66xnOi32tkyyi9I210bYE2tIXo35SUS3kEInzw41QRcJRHo3f+qjWHAJ9EbEJbqYlLCWOtbe3b23fvA4FV2tbNSazMDpEz4ih83wijhKHCR2O4WmaQfslE5d0g53V6+CBS1fLGiuaqSSrBYOFtLN4R7RiJLaJp6W4BJW21Ff8rnS6lI22aqhW/en8UAWNzN6BdKhDBDD/AWzjOo82FI+sqEeCIY5WQV3vLDgzo+sKUU8wnwxvOgittS0zpaZtTX0HwPNXtVQg7lHQ9uoLz/hKcIJXk7XOTC8DPKED4Y6IGjS5MlVlSTPUkJCkyYCA5hqfBxYErsZzo5lwIRLXVH3fJT1bKqhheBAXx26foX3Ytr2kp1bWsDnz7Q5E3HS4iQW84A62aB2NvyWwYUW51LUtwgtGu69a97JUPCsBG62KFIayeRVaNwxZMS+nJFIXFX0IU0UpzrR0AlGtpDhySZeGJlCvM55TlDVdIy3NPa0VKrsndIETKE9dQ0g1PNhmqSQGRG3QtTEdKo538YpD8qY++DDEdVUWKe3l2TgTb4SQARSnX4nYJs1X8pfkDUXhXIEWyiA4G6f65bmPcFdxedKGNl9LLeSwUzT8GjGt9ZHqrs5ce5OfQ3GXigBzJolp5ukWF0eq+8A/wPutoUtEEV6jIfNoi8jrYs35UDSotB7ZHYo4WryyoRpJf2lYSR+88YkJGXtb6ZnbnlSNWi5Xy/2oMNATaBWHHNHQvqOtjlD0om6tI6wGRbVw4/SAQ+SVyQ/jjcBh5Gv5fG1xLOsj3BZORS5YSS4mc7UretHK3NDfh4BtPUg8NzwK0hDCNmw8YIMiRODIPMoUhN/zNuxwlCQCyhRekvgp3PDSNJF4aWzVQK59ZN3AxasuLhXbV+ZWtsSDrQqzs5hh4asP2jNIVS1XqhXUqPjFLmbRLDprCD+2AoccgwyDTKD7parhRdvVigOWLFOf0rQ0y4FnIBgdrU5k24BmVBfvEOwyL4TSwHnTLWVJzc0PH5nt6Zk9cu9tcxVYk3vX9K5N7z8wRGsHH5jes/UHY/2D1z6IhCVQ0ZOPJJomB/q7hELaQUWnfYxzZPFHtyUizXzF/HrPzNF7js700srcbSPXzBxp7+W4oX0nHzu5Z4SWN3znqkObHjw4sOhDewBebtgwvbUqojy0WBiNTzV2CUwLHAOfY14L/voNT9AbXA6A2fiRl42lMOM79ms7+/K12kytBvfka4PTg9bWOn65tmVwcEtt+Zq9zrmz5+7jHuRy+E5epNNtta0O4GgiHvDzgoTwVxIFUbL8rKLAz8tAbIDwjDLTiiUUzClguctws+gv8+nsvdPJaKS5SW/ztWmqzat46+9uZ3q9QjwstJL446JPxM5FKMchtMognyLVMyTmx15noX6f+wRoovmfiNxfF8Hxs2yexqPp/HORPSVPV0BVssGuyKEerWQ4HMkgl6XmL+olFQT5nhNdwVA0F+1d+EipFEimT8715oPx+AO7SCN2jMWPtLDR4yYZRQ5YrjY2ksKdj37xlctJy/536wzqWyGt5yPr6vGrDOhzrxmS5FK+PTto3mBpm7sHZ7+tuCTJoPcunPTbOOXbM9b5u9h65tsKVRpxRNgBx+hTRCE+0lHL8syHsx8VP+X2X2AOWYM9Yx63142LxpIxvDGERplyoZJJwPldD1z00ntmjsI3WSR4Y+8p8/Q9L8H+I7Mwvrhn9fsvzh3lXuMGrbymNPPnppKJWJSJDSuUaS3DM2S/ACgVWWoEx/GTouVPZ7FCYy0tmqsl3ZIO+FwRLRzXrLyIREayIos5a/TGW8dvSw0G5YwLRZuPm95++MxzpbE9SpQ+nlRf0QxDW8ixNX3/yGNH3j1DxRMnxksnoTWt/kKJm/cENLNVCwQ0+JoWMPs/eLL/6BNnNlny9X+f+w73Dc5jxagwXw4TrVw9/YhFozYUR1OgHjLSydfDk1k0EC4RZrgzdxhq2iS3CCa5zSO9nqA2NpWOeAqUDj1/+IWvKPKnboT4SCSXG8jl6L6uw1MiH5Kz+d4RNTg6+oX7Dv0wPrXw7lwtm63lrLb91bld9JNoO4n4ZgXG5x4n5QXOcnQOMzHJc8KcxQM8xd7AN51kunGaqYWxQnc65XfH3SJSnh/RrpgRqzGUdfieUaYGOGDOBpGNvlPDX6w0NEUmHf0yBNeNrPuS+bORqZoofxgmn1D41tpgl3knL/MqtdmpXW2ZDkwGppt8vNuJClsze/ft2pWl9Esjd225c+TGD3/4xv7dm6b2wHN8VI6ILi/v8uZu3jJ7KBGWwoYR97zU0AuvIt3+K2lCDkqxlmekEwk3B1E+sFAsGOKY+byfJfHsdAfdPouLBviqO850HBtwcbNAUMFt+N1pcCMurMArgj4688HZrR+cXedAeYf7c7Mn59Y5zU9+eH4fvPH4vj30ekFNRw1Y2BaIpBXFISfjOqUPByJJu90cUFfAX/eaY/BptddctWIx7p57lD6A3BWuBV31mMAL1K/u1zmWNGClNmWWYhfqAQzco5oZRVRp/rCRqwD7rGQFiqRqRl0uMOq5C4/Cfpa8YLXLAf473Ea0qTpQYz5cj5la57JRGNrwdIDFTCmAElVEkM6jWc1Lc0gisiDKV9oRMXA2gcVuANimiM1mDZDagMVMWZUkIu79/WttrrWE0eLqX1Hqbk3Fo6GOcEeT4dYcSt2+aozH+61kCR8LCq1K5fqABhNsPsuirbLhDrwmYjFDcJcsBM/2EdRX+HWrt8BMrfx+c271DPyJdUCvWT1z9mdfHqvAJVHfwjFfFCLcGxFj4c+iXRD10Wt8Ufr0lkHzfiz8/vfP1HBZDfOrZ2ZWm3M/rIxB0RoaMd9rRK6GPb5oV4v5EXYLq12v5j/NXWzFw+ZYJBVyEDLUeZyI5DZnhVtOnQ+3LJbK5XJxMeyXjdewITorhdJ9oRwX6j9NZ/mVFf6vA95fvdcdDLr5eU8wF/T86seeYNDDez1B85V0yLypOZ1uhnc1t3LpOzwBCLrvwLLmJxY+wKrQK7HsE1iikk7X6Q/eoMcJWo3PKgJ0tIMV9mw92A/fV1VzKhCPB+BGJaKY/6npEUojurakG6iHvmhhhUQtiuiUYyzF5NpOpmCXQ4J62ihK2zjeOba4A89vec48u+U5+mJt4XODg7S3trit68D/xX2MtqGN01QznMvit+t84WF8YVvGFX5bI6yHe9Lcxt53WyNYtpXFxzpm7HDMvMpuhz+xR5QZ5Mev4Wn7DIuYrfPhx+i6+rOsjNhhspRf5NetZ6WYT3QpRLcRncs9OYO3M79mfq0Ro/swi8h92D4/oyjQZr6qKOw6PKwojeDc+rM88GOklUDN91Yboly0km3ekkPKQr4XtlgDdvSxxTDvenx39AI9LRInWVXrt6OGdqCeZJ4r4IbR5GHBnEMsVp9FB81aT2WxnGQTe+qoJElOyenRNZaglIr5MlVfLFWOlf1SmTu2sO2b36QPn72DPvzNb75j/+MfOvDNA/sfe5z1grzkP3WhRMmQKhkk4+QKcjW5vXZrMiris2a60pGAwlyFG/NBDxVEcml/heOFXcNrB3qQ6NlwufWSZK9sDY2zASSGyueRg4ANnPMsQmyeSNI+YgEOwvCGzcIbTIC8bcdlm8Y29PUWuxOxUCacIS5wKQyAxqV0plJF+8qno42Vlqx1ucTOgHUNgRGe8dZHjLBkscCu+Vm+oyhVmGQR67dgF6yxOXaMZ1ZChbPsAIa5MpVvGXpXrjI60y/yAxVtn96vDxbSeRkmQkZvz9T4jfvGNgV3nzzCq+n+YEQNzLZqh9JabyF/o0hPfOLaLUOOIVGNGPfC3Am+NhiodW1XtgdVD1U37emt7INfKKWx0Vw6p2mi1tXDb4sHrz20+8i+uYFCALrUbCgyoCaDZikwrSuBUC6vy3uOqifUPK8e39RVUJJjc08nR+87QtVt8JW7XjZyHrGHP35IN5Tswo8U2ROfqGUdJxn5sNyjj3Pz9JzFz3FyPbmpdmgbyNIVU5TI+wd7y9mUKEKIRcAOt4AwxMZuJHmHE3gbSGgbzDqoHcEpsFHAORUtc0EQp9hWFKaJIApjiYRuGQyJ6xPXb7/qsk3jG/v7mps8cT2+XFK46qGy9ZDY4uKOF6UfCg2Uf5kWioKeYx1jGdT4YcGynWAZXlYH4YEVO8tGCnHlt2Iq2YliBYuwAmiRG34roZX1o2TdBZ71BJoSTU131jd/ufDlRKGQgK8b5cKWwkuqGtBVu6SHouFyNdLicrklRfUEQtFmn8dpk2yy0+awNUcQUPKxsK45mgo5TmhvW9tjd3mbo6LbZ0SasYRTxrJOj68ZTjWnm5e+MFtMLPxdsrCxkJ+gP0oUF77l1iR2R6fDIdoUwSnYFJvd5pTioMhOxa7ku9OZ5iavbrdxICiq4rA5/LjH21qwmKogQrfZdW9TcybdncfiTnkpLv0DVh6oi+ENO6VWkHfDmAA2covcY9n6hSozcgmTP1a436ev3p796UMvmm+8cF/PG6d7/vAFcHz8wZ+2b7/6wz8jLJqzkV8qkzrWTZFO0kNWk1FyuHaTG1j/M3SAhCKBzOxwVBZ0TgPeBSyhew7pXfQ6Kaoeac4BMrEpsm2OKHa7MkUUxT5N7Ip9rHcFi7EeGR5aM7Byxere1aVivoOFbSTidfjMWBIRhL1OSPpiFGOe1sfGrCwAgSWkSuw0GyTzsiOBxXGwcpBKs0F4FdLe+giaLnJsLA3PifDlmaP0thdv4+86Hs/FWSyjeeP+/T3+OI0UUAhP7N8PfnaWRvNRyGjRfJwPVt4TiLI81uhWrStC47mkSAeOPm/d5gksGSkFuei7qvs/FO2K4td8prq/x4hjMRqd0/JRmswmRfDWL7JbslTZ3LKY7STpZkgj4nNwPAt2IAKziAQyh2KX5ykaGJQypEH5MZ9fD+lNVspNqRNZ4a2B29JiKK3fiqetLg/hvl3ZsUNRiqhId+xAVVaw23FrLygR3OLJovK1ZeHc31LZ1bB9sRTuXnj8B8tCuy16nOfuRdwk1PUu1NXUopL3aEwZemMcpLyDYBujx8x/mwK7OUdnKNxlySzUe5+n6xE/Z8gw2Um+vuFp28T0RzstDd5s+bnZgYAHjZObN1tFaq0OxjsycMgrc43ZBWaFeiS5YEWSK9iqlIzV71IrElSqYOf2OhAsv6Uaa3VUTAIhwiRhiYWsqoBVf/+nbN5cC+3cfvnmyYm1q2v1MaSeajGTSsR83pgTzUKvj43usMFYS4ClM3naXpd5LBufa4dOQD1lSbJw43wflDuhXK0Hz8XZFaEx5Me8kdWKB3Wci6pIBhT32iEwOTToUx2oXymL8LSpid7SZR+ycQzRgej2exIUErHnZYonPDbZ5vMUrxwd3XLF+7b3uAxk7BZRTbi9quKX3WlJ2BVyd7R6mii/PuzOw7bv682e1nR8eNAfVh1Of8B2hSwpTb63+4K6Wzrx3n7F6crGr/q028bxAOMc3cNftOciQLP+CqN6x2CnaneGvSq8U2QY4KGRDZITAiwFvIFhV9NbiEqUv7CgIViZNVYYHIJMls5rwGrFyk0O2d9U2GwN9bzleoLb4j3eRg8jevr1/Ge/14Jkbv188LgBO/SozUqHCwXotOlQDXsjHdq61bl/P/cw/QkXIVnE1QEJcTXrOcPPIvX8A5CJ8CpIFZoxEGikOy23MJsQQFIm+Ix4v6hENg308y7Z6Sr1l5TQwdna+HVKNiehKnB0BYNy8F0zr7c91zOy/vk3RGNo4sBAclPKUZnbeeeNd5Zvh+xE+0ulVeO6e9269MC2nrH3N/LBXkN7P0NWkbHa+lbWn9jL1TzlaQz1BpvdAESWYIUwnjnTWGLFfCO7c+6tYSe9zalyJVWsp2pfMJRihOvxOsvHUayBR69bZynXS+g508gj5zxLQyh3S4oimTcsDZ4oclJWzmCXbTPvFzS+huhjzzbWb2GXDmOnl0ZNrHJLQyanZQTe/2R+l/UtVhTFmqBaFa3M9DpmfcmK07iEbCFbyXZEq/vIdeRGsrv2tmSk2cfzsMNJObobpeIwCBLPEo0p8MyWY+B5nnAicAymMm2FwFQAiaFVYR/zLTA/KSx5SoBcd+01B9YN9VS7u3Lt4RC5BC6pR9fXvcUis/gyA3wGAWgmjSglHZfyKCIYaI1wfkYXcTZ43wksW4XxLi60UqwipkULIQ8saozDqpKIxJX+tVW1JOqVKu5U4KmDn7v2kdtdajha6IkHac7XpPX7fKV9ZTlSczXpuUC8pyvmEx3BdFy1Rx12h0xljncERFGKt6YdTtDU2x85+On7EcJTUHTeLim6qChKiHfanClw8+601xsFD/Vw9oMvXX/Pt9o5VTlUaeLUSG6ka01XcZVgqE6XS/QExVXFrjWdI/mQRvW0IAb8HoMDXhE5ToyoTl8QBUwhRBUUat+6B9XFwgM802iii7OLDh+vSqrOOxHxiIJDROPACXaJU+tjN+d+iboxQD/DMgtqPpVFTS5j5J6MxciGtOQuRbKFNKNDzmVzmz+2nKTHFPPHHm+AfsFPnzLwpLnNr3D2Y3aWp+x15xbz03db/NSCSDlFiqSfHKjtTSCCCoONZKJUEljKIppjnDjrQHqwCcTGkqTR3pGYX02ReWUOaZHaZcrinQDq8U6MbuwwVioBQdbv71tRKXd15nNtiHLisaBfxV9NkATUZd4Ry0xsxHouRT/5KkUXFEEq+lH8W+nE7hhiOXAnrARjN3faMiatIFDzy7gytO+5KgZLIU7Hhj/0IfOhD31o79NnIv4fQMSg6Vcj/tfo4SUb9GTUgBuMiut7mhE1/sJ/w4fglg99+ukfsLxj86FjRsUcp3e8hhaq+WB97pGvcj+g/8uyK2IoEXsRFW6tzQz193GKvZSjsq3Zy5yPw8ROFNGuzDI9ybG5fmxEpjZ5WSKxwGzleujU7HmbeWRdbVVPJeX1+Q0WFeVgkzZZgA+BW3kpBLBhUjOBZE3LtPxCYrnv5S2+mDMDWwaWvpxHkRe2sEBn+pjkOPvj33QWlh/UcjXaP9NPaz/rsG5h/tvy+ZPI4sk9TPaxFRjnp06y2m439wNuI9LZSrIRLeu95Bbqr1W3bL6U01037KOqvh+cahfi6rd1ZjnFMxyinLAqrHJ2XkRFyeJXbcNbQUEdxrF0LgcimauIS3ft94HicduUHYTn7By/AykRm3gH0VWnPmVoVPWCU1ads9hrkkdGmvUQt83jZn0iCjZxK0Ebnptiw4h2Kthn3+rGQNCUwkdt/22PsrMZnv7vPSuLz9px/lkexb33/9XDajt/03Pse/9vP4jFM+TGx6PRt9903bXzV1915fgV41fMXD558cRFG9avGYyujCJGTDYbbk/Am4j7WSgRy4vLVOvjVVLGMn/LcTFjZUtIEfD6EtbUc9USG5XPsBAwwwr9KpaFZcxRtOYss2ZdQdkhSplq0Sf8Dh75x4Fcf7IlFNUCfSqvBhTFlrT1vVAOxeELfCjeilre2aR3OivR1p50Pg3D3MYL2eiv+gEEf+tYHzf+Wzmp0lGAVHeTpiX4FtHT4pRRKUcu7V4tZmFws66E85FAwKlqEI2FovnWUD4YccVPN5hNVhzw056eNR1+1FrZ4W9/7TfzG12K4Zwhe2BL3VBoKqEVAsyGzXdQnuycpYqNH16BtkrDMrAK0N9Y4L+ovGiH5IlNRmN3B5q5+1hevjAlUpaFaecQdrAMSVm2Yv1ZqjEvn7dGltVioe5MLFKBjcbVq9LfWNXzP3hgrbNeQdn7e9ZglBsGsnvXtrlLLkbJXC52RyO6xyWJHJmBGebZ9yLotQYm0WRBq7wq+UVmoPgaHhnrrPVh6bmopjNpNFarRb9Rrbvd6laONdZuHTBfBRuPZ2gqjaDI8pZjActfjpCOSq54eNTbmV/tbnEDxJIxuwQy16THu7v7uloCzYpuc/AyTznFG+iRofPa9jUKR5uMNk4GQeYcssvXlr3p4iuOrnHabCp9U5HPfpERJleWFXoGoBvQBkfTRG4TVGnw5J+sbY95g5ri0bWWaNtluZ6J7ljKoaNE7hKpjAiEdyJ45GSXyy7av7J3ZTaYjLWkSlNrOi574WpVP/uTJLt50qLJc+fO/SPtR/2pkjDzszesk+WzjzVm8iilL5gGK11XdBmLdZdN7XHhxE0XTvTxHy6FTeOkRHQwPq9a9hI7bIypvcxtpudwj8VbjNaG4yyfdFhi9rCAzbRXtKzeA0xdy0xds6abIrLM/H+yMEZIWyYWDTUbultzMiDDcr0UZtq6E+5qfYY+SyHjIfYg2hCiz51ACyJT9CVQbBWMzzz/Um8e8j29Mz30PX+R70rkneLzAM+DPZDuj289CD9feJW2PdlWqUxWKmbN/Ay09g2mw+6Q+cVvvvvR5glPMKrBrYjflnwoXhJC/FYha8hgbVVYZ/lIw0xAczZxTmCBrEszmEiM0iVZlqaIJDFKl+Sx1TW/BTmamuqzLDEfUiaNqIPFWDNjnU1BgTCjnpxQ9yT561kgEQ5/0XI3y02+A/deq/NqKMirY7OjLj4U1MzvZ3tztHUgDdFsfyvN9eT/oHf2znvvQjzRM3f0+JFtlfXLfC9vrpmiE2vVoM2h5Xp6ci5VCf5rJGtVDDW2T7KJIFjVo7P9fGnn4Y3LvTH1mNpf8l1oB9uJ38ol7CPDaCdtJfPkBnIHOUY+QP6UPMuiMEfAaqoIaRYizbNBv89lFwRDVxXemvulye208VxAc8hsFIJe6ZUoeERKQtiKLRAKh0OTuAmFp0k4FB47efL0nz/xoZN/evJPP3jqA+9/8IH3Hj92z9133nHbLTffcPCaffO7dmzbOrN50+TExtHhodUDfT3Fxl8hWp+fEykHOR+B7/L9zLJ9xHjIA7iP8iD1W8r4f4/z/gvv+bvKlNmxl0WCvmU874ytbMPviO0WG35H6ke032ZGbDb4js18yFax4bdx4YzMjuThxlF982q9iPmd+vbY4gbvOIA7w2e3cq+1tpzdyqJruVOR7OesWnfW1/WqX/+1Uw/82n59DT7rVOP7klXIZl4Cb5gOtrBnwBtIYw058Ze8QV9AGyxJVrA5+/Juyw7jYGk6vHKpNeP3sEg5a1Bt2bQ6bIo+HTjmK8gDm2yGGb2Lg25LM/qxkTeYZrM4mmdFEXhB40Oi+I1viDD0ClXFuCzCl6lDSkgy7MESqhgShG98QxBCuIul1+IhCMwvwIt4SeP/+RuiSscXOiWZs6Gwp6/gDRSqmsfMX9UrfeOfsTQ+wjwraI05O+kXrVjbGGmtpSQ2/5oVX2tN8UQs/x6bs20TKzzaH3Fbg/go2FCnMVcI+z15YZEc6NY315TumE/2jg6ui7g0MeBaVxuvpA2V3oESuhd2LNyZaHsbdZmXdM2ODuVbNQnN72xu3chcJzyl1v0a5/3Cev2dmDeY6YnFBBNUTwxesnBZoGM+3d9sKQvLGbzkEa77gWGZYIJfrbDbn3zSbl+BCmLBbl+wh+DqZVJnhJ3H6yHHCkUxrTIQWyZWfu29nL/jvULG+fd6i3/6gve6z6w/b/H18L2+uvy9FtjETAperr8eFrjgvZgNf4LzcEUrtqmNDJLtJF5r2bSqP0oEa2KzC+D4wMpCN2VWt8HcLtiLEWAOO5U5UwfAa6EUCbG0lTLBzHCvyuHVNIskSWdWgRGtRkD0ihxLyrW8t50UlcUAZw1SWV5c+Bu+1uLTVEPPRgbkfdVEFrjJnUPvsLc6Wj/4YCAYt2slV/c73l5wFbp5hQ95uw7u6WtqzSftqptTDsPAYQeVd7g6uUDAe8/n33Ox7JBBbBFVgWqeuNy87up/eGxT3AbXJXJeWz5vs8tRpb/W5BpKF0dsm+BKBAFSsJkXPS5Np6Lmkqi+8CW1J93qaA5Sm6DoEW9lr0245BJOj1DvrPlTd8gjr/O5VolaQMo+Fd+WUYOah1OCiq+7qd0eb4wpshyN83nq3bW8yyHxPKxVEDbwZKguFKzYAbJTlglRnSwfvJ4JbkUNuFNusERoY3uLeQdbuFOmAL/CJWbeX4YqPGz+IfSZJ8z16+Aa+KG5Fq5ncU6LtCdbM9z011YwsIwiaC9epISbJwILYxPor8VmKLZoS7BJc9l8iq8xqe5iXlWGqW8oDECeLifIpeyo+y65Z4JO3fXEnZv48Xvh8uWT3TSymn4+ec8j90xaK/OV5bxitdcfcKvxbSvMT9sBAm8HUaj4ASE9x+Y8FNgMWBzqVGDTeImSIDJ/CYew/9cmWou58V3TuQQLx0v56xiZDejhrk83/PgLUn5rCJuLWRmDVZYzaUhoCvoRcfnZ7Au0b0Xs8d2X3b/p4JxtaPL+ncOHBqEjekyyGco7zV84dVoBQeqKpCrQm9Z/9F2b6be948Vrtx/edP9lOx6PrY8c3vC24zB6s6s2zHvB7nHCs4oaSkIplanwp0aGvWY9d9fKO3WQNOkil9YmHbBsXJKD/TZJ4Nj0Xyw2iOeJ3cnbZ13gVFXnFG6c6iYkGnW0NZNrz3S1dqaTMZYd2GTl2Hg8BQ+bxBsEX6IRUpEo+9lMEss+LC0mZs2EKTAHNtut0gD1Dxva2Q9rBgwfOzaMn5FjMVcAzm4NuGLczZrxqy8bbm7yTGNquWOoDZ86v+iuZBIvzBvqMJuCzvJNnkNaHLHmA6zUih7sQS9gv7E4EwTM2HvEmipg9q3Z3b5UIpOqT8oWa8Qe5zk2M5hfQpCsgs5GcxBMJuApuF/a+eJN0w8fHKTrDnxw04duvH5o58jNI/jtmiy1asJ/wEOp+E2f2DN07cmPnLx26JoDa0ZvPnHzaChdyfmYDtEt+vNgX7x17nWG/P6cfJL8Pfkm+Qk5By4Uc52witpXr2Pp0JY0XwE90E1+QL5N/pj8IWkibgTSLHC1DVohRr5OvkzuJrejpI3heTanWBO4yd+SvyY3kWsQJ3Qjj4qIsm3AUjz+kjxDriZXkvVkNfMC4vJL8gvy72QzYWOFOsrsPyOn8O4+lCp2pnNxTybDdZMaEaZd9/nsO1qBpEMejnH6XCbs5WgyqCE6pcJcqtnN8fEAymiRl+YSTS5OjBoOziaLtrkIKLqsTMX8Tk4mul3W54gPwDdFfD7YTMAHG5trV1qP0O2+vf+vnrF59WUWP68ExJFQhA5ohxQkIAzNeNnL3N7AppJcIL8iPyf/QX5M/o38K/ku+RfyDfKP5B/I35Evks+Sz5CPk78iH0XM/hR5kjyK6P1PyEPkveSPyB+Q95B3Iqa/hbydXE+uQ4k4T3aQq8gV5HJyKWL+jWQDWYt20Uq0AcqkSDpIO1pKCbRBm7GtvdgjkmUxAC5trE8ucEiz0SCWksYmlkUozBJwLXv8v3Mslf9n9X7bMbzlfu7/n/f3NupLb/md/9Nj+ro1PeJCpT4frpVr93usRn7fgudXEFFkNk5WXyWZLyz5u/aOWbssL7Bx9vfc5SYN9excfV7Ok2xu/KU3ed/S3omlJ51Ycnz81dLe+5btLb7W+37DXRZW1v/VABsATP++dV7+bz9mCddcjnJSteYaWUmure1f4eCQ07MxVFzNHirwHJuPqg34IFrK6+yNSZuJtN8aHuQJsMQEwkZb52zWfEpTuFmcYi/f0RLRXEAq5Y6V+ZXpVCTXkvMbrrAWliWiLs5WxaZxtuJYMuUIeC88rNYP6eLFCEhLV0sDAE8d/f7gtZ//3hf2cYPff+dv2z/40iFaPzj4Ejza1bk1PZjG79bOLnMKjzLsKINHZ+qXaHagFY/oHXh2uH6RbdjcEia21w+xvdjM82wMqIhSBdsswIYl/Ag8uRU95ZZIsyhKrSkqiG1JbBdh+IJkDknkpDk7y9nhReE3ZHXk2hNxjxtIV2d7MVfMpOPZRLY+U71NJhpojsaELipliI3zx9nsb/5qGqrWxHA6FNj0cBnJqDYmZ075KyU81OGNnc/fNcZP3faplz512xQ/dtfzO3dv3ZOfze+dM0N7NG1PEZ7aPbcXT+zZuntxcge4ak+xuEfjJieOvvj5F49ONDbDRavCwrvr9b9qfrx+4tVbn76Nv/H5Q9fv2fpVdqvF+Ys/i3jhVjJe27B5qoRw2M/+4wSzNbFpeJtEG7NRIqgVGT1ZZqdihZVwLDli7OabDuyf3zG3dfrSi8ZHhw4aK+fsaFsJ8bSVclYtsSw1y2eaiKGlpVtTA1jDCPUCZatAOeOunp8gQExXrUCqQqU+laXlpvMzR2v9Vtbce2+97l26Nfdk07bCtianBp5g1OZBqjTvMiTw2eIRHRyu4K7u2YBT9RgRvMbmiOepLMjxgAEONbira85QVU8wpOggueBOVQKvkg+pamB751xAVfVAXNLBa4uGPOC4iOcDTo9GRdG+5p8cCCC/dZFD5DWPGgB2xa3xvORc0wKfczlVvKGiIcvSxuUguyw4xl+iVHScHneI1GEsu+DcuPCzQbyZR1MDy8aCNcQlLO5tBZu1rthGJTmGlkXY53Qgn3PDPKrrdaKDs/OLkyFSAXtwnixNhkH2KSBLkoxdKMvSJhZWKY263YRUy4XujvbWTDKOPNLk1t2614OPc1VZYJD1/wSW9K63Uoy5E5bDqX6CLYgVU75EIwVcWNqDuw1V11ioBLyHd8IH7qV9hmod4vfHTt4c+Kwin0J1cUN9Sz9gTuEV85P1aXzD8JrDvBHuMh1WHTyzGpcPO566pT4T1C2WsLR8JfwNSMcCIrVSrduuWBFFw+f/hQlvEe4kD0sOE8KCSwUiuN0CWl6Cj80qC96Y2wv8Db+6e5jbcu/ZuTdhtTTCbTn7Ou0zHTAIq81Pns/f/AJ3Cd5lbW0wgV3gBQTbwyEAFgDNoBmLb+FZfAthAyVsFg4swYw/Jko4GFs71LeC5e63pkR8vJv9uyYrYIMl8KUzkiqw0DnrP6XU/3uHBctZjixftRzXPp1NqiAm0PjPjs7t651+YFbvqs31pkfdLkVW0RxHi9IWaAp7ejvi6S4oJFtKCBtFuPmq/SdUTdWctnAu4KBC/+6J3giM3ru5snNuXZ4mW1a1BXqMrtYQJ067i7ddeU2yt3MC8vHUu4fzoa7eWn9wbs99+5vywSDvKoAtN9Br6aq/QPoUiAdxW4i0stnQmR+ezUXgVpULIiSSCZ0uklQ8k0Jz0W39dxLB8CP4bITw6ETivGz0RjBgo+pUbOaCg+VtaBCBJGTNM/QGLej8T2g3vxP4p4/adFlES5m+beEa1QP8HULQSQ87VJCP0RVm6L2wduFZVaPy22kHNbta4UpIa9z7gPnozXPH/8+AdYYWMB1wAOtZEQYpkMtFOIBtXhfwjRH5zPDdAELiQsJioIOmhDlBw4aqiszA1CJsZsQoLsbGzskIYopbMe76J8PFwcJoxViw49/N/H//8llkmJ5asHBw/ZMByoG4rIwn/p5ijGeX4Fnz9xRzz79VjGE8/+at4ZFg/7cQ2NUN44HcYZQBDM8QBjEGddCIkYgAFzN8vwB8Z56Ksoy0kCB4Ka2wmRo7Kx/oKFLwiYFM4GWWjMDEpMcELF/lmJlmBLGKCf2TE+Dh4KthlAMtannOyctjU6vCGIVae1QyNkmKcexkE+BjUdvCwcrMeqme1yoWmF3RKygA1jHdzAAAAHicY2BkYGAA4mxbrcXx/DZfGbiZXwBFGG5nG+2F0f8f/7diecTcCORyMDCBRAFWVA0GAAB4nGNgZGBgDvqfxcDA8uj/4/+PWR4xAEVQQAEAsWAHwXicVZG7DQIxDIZzSQYg7AE3AJOcRMsKNwBiiCuvRmIDGlpqJoCCINEgIQGCw/zOm+KXLT8+24myQsi9EOpCX2WJVAOf1QcZb+XMK8XXkA0ynPO9zHK8yOD8CJpwjAYt6aRWmBU5U66hpzojZjJTLjkPJnxdE7nZNnA7nokc92/KHWDn9Eo1HX1crg8zDvSQW3rHvfQR2glR3dE7/r/H1TBjKG7G/rGmWnjJK93SXgazbcFp8ru422u+Jb4PWCxTsGF16/z8D5b/Jezbhl6b4z+jM3aDAAAAAABEAKwBmgIkAuYDVgO0A/4EZgSOBMgFKgWuBnIG0AcQB1gHfgfkCBgITgimCQ4JWgnACmIKtAsOC1wMPAycDWYN3A4+DvgPyBAuEHYQxhFoEiwSahMIE+IUOBTAFbAWSBc+F+wYYhjCGWoZtBouGnIasBsSG14bzhwiHFodBh1iHYAdsB3mHhweRh6CH2ggWiCGITwhoiHCIsQi5iMOI1Yj3CTKJP4llCYyJ+opNCl4Kd4qaiuMK/4sSCyULOAtki3SLioupC8YL2ox/jKWMzA0BDSUNMw1VDWwNfw2TwAAAAEAAABwAUAAFAAAAAAAAgBSAGIAcwAAARILcAAAAAB4nHWQzUrDQBRGv9H614KKglvvSlrENAbcFAqFim50I9KtpGmapKSZMpkW+hq+gw/jS/gsfk2nIhYTJnPumTt3JhfAGb6gsH7uONascMhozTs4QNfxLv294xr5yfEeGnh1vE//5riOaySOGzjHOyuo2hGjCT4cK5yqE8c7OFaXjnfpbxzXyF3He7hQz4736SPHdQxU6biBK/XZ17OlyZLUSrPfksAPfBkuRVNlRZhLOLepNqX0ZKwLG+e59iI93fBLnMzz0GzCzTyITZnpQm49f6Me4yI2oY1Hq+rlIgmsHcvY6Kk8uAyZGT2JI+ul1s467fbv89CHxgxLGGRsVQoLQZO2xTmAXw3BkBnCzHVWhgIhcpoQc+5Iq5WScY9jzKigjZmRkz1E/E63/Asp4f6cVczW6t94QFqdkVVecMu6/lbWI6moMsPKjn7uXmLB0wJay12rW5rqVoKHPzWE/VitTWgieq/qiqXtoM33n//7BtRThEV4nG1SVXPlNhS+X2K8d5Ntd8u8ZXJpy7SFLTMzyPKxrVqWHEmOk3/fI2fz0JnqRRrNgY82e5uzs938/5mwh30kSJEhR4ESW+xwAQc4xEXchJtxCZdxC27Fbbgdd+BO3IW7cQ/uxX24Hw/gCh7EQ3gYj+BRPIbH8QSexFN4GhWewbN4Ds/jBVzFi3gJL+MVvIrX8DrewJt4C2/jGt7Bu3gP7+M6PsCH+Agf4xN8is/wOb7Al/gKX+MbfIvv8D1+wI/4CT/jF/yK3/A7/sCf+At/Q6CGRANCiw49FP7BAI0RBhYTjjbJ7MllrdUNuUQrH1JtO2X2pe3ysKgQyO2EC0pqqoQOmRRGkk56O1LR2MVUjXJFKyTV1g6F8Fyv/JDNk7aiKX2w0yKC7DM6mawLaU88LJn07Lkk9iYjmbnU1IbKTmS2TnX92TNVprYn2eLIyD6XduTKcOCDkIM9JtdquxRHM/mgrNnaoZLKMcgmX4QzynTJKJRmRmbIBzqtlDlOgxO+X1FHcHmnhffksyMnbUO57+e21bRPp5RoK4fMM1bZJzVpnUaVfMHfIq4ra6eolcJTqQwj6pwYUxmb0qm3hgopNJlGuHRyyoSEGhWSmjXOVBBaycxxKYVi6UXwYprSxobq6gVlWntOozhWDfG6cXc020DVqks+KRlmR/nE89maRItxKqPyq2J77IDkkdGoPGJXx8Rai9N0EkygXP2KhTlrv0o8KjP7gk64y3SUGwqLdUPeKC+ta4rRWhOVy/283odn7p9jLHUEFcXeb+i4PLMu5mG1M5rbKkdF9KCik3DYqdDP9Xl31irNaUkaK/0uJq+qZ820Dta3mceaHO+YDSeTR1DiOUDrMJbTpH5Umgq2trbCNUm0MPO9It1cYi6c1htrqgjm8n+/VqRlrUI9Rx/yRRnWxu+iDTdqtktPpFkXZlELM2SdtTziYj0rzeu7itFHmxoRRM1BYGAt1bM9zTjijHhbK2PlrIXzW57i2BRHopw4DkzLh2xkFvOYsSZa1EUgTTFFm82/01ZkeQB4nGPw3sFwIihiIyNjX+QGxp0cDBwMyQUbGVidNjEwMmiBGJu5mBg5ICw+BjCLzWkX0wGgNCeQze60i8EBwmZmcNmowtgRGLHBoSNiI3OKy0Y1EG8XRwMDI4tDR3JIBEhJJBBs5mFi5NHawfi/dQNL70YmBhcADHYj9AAA) format('woff'),url(data:application/octet-stream;base64,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AADoMAAA6DAAAAAxAADoMQAA6DEAAAAyAADoMgAA6DIAAAAzAADoMwAA6DMAAAA0AADoNAAA6DQAAAA1AADoNQAA6DUAAAA2AADoNgAA6DYAAAA3AADoNwAA6DcAAAA4AADoOAAA6DgAAAA5AADoOQAA6DkAAAA6AADoOgAA6DoAAAA7AADoOwAA6DsAAAA8AADoPAAA6DwAAAA9AADoPQAA6D0AAAA+AADoPgAA6D4AAAA/AADoPwAA6D8AAABAAADoQAAA6EAAAABBAADoQQAA6EEAAABCAADoQgAA6EIAAABDAADoQwAA6EMAAABEAADoRAAA6EQAAABFAADoRQAA6EUAAABGAADoRgAA6EYAAABHAADoRwAA6EcAAABIAADoSAAA6EgAAABJAADoSQAA6EkAAABKAADoSgAA6EoAAABLAADoSwAA6EsAAABMAADoTAAA6EwAAABNAADoTQAA6E0AAABOAADoTgAA6E4AAABPAADoTwAA6E8AAABQAADwjgAA8I4AAABRAADwmwAA8JsAAABSAADwsAAA8LAAAABTAADwxQAA8MUAAABUAADwygAA8MoAAABVAADwywAA8MsAAABWAADwzQAA8M0AAABXAADw3AAA8NwAAABYAADw4QAA8OEAAABZAADxGAAA8RgAAABaAADxHAAA8RwAAABbAADxIQAA8SEAAABcAADxMgAA8TIAAABdAADxNwAA8TcAAABeAADxOAAA8TgAAABfAADxcQAA8XEAAABgAADxegAA8XoAAABhAADxkgAA8ZIAAABiAADxkwAA8ZMAAABjAADxnAAA8ZwAAABkAADxoAAA8aAAAABlAADxrQAA8a0AAABmAADxwAAA8cAAAABnAADxzQAA8c0AAABoAADx3AAA8dwAAABpAADx5QAA8eUAAABqAADx/gAA8f4AAABrAADyMQAA8jEAAABsAADyOgAA8joAAABtAADylgAA8pYAAABuAADyxgAA8sYAAABvAAIAAP+xAsoDDAAVAB4AJUAiAAUBBW8DAQEEAW8ABAIEbwACAAJvAAAAZhMXEREXMgYFGislFAYjISImNTQ+AxcWMjcyHgMDFAYiLgE2HgECykYx/iQxRgoYKj4tScpKKkImHAiPfLR6BIKshEU8WFg8MFRWPCgBSEgmPlRWAcBYfn6wgAJ8AAAC//7/zgPq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eSBzdmcydHRmIGZyb20gRm9udGVsbG8gcHJvamVjdC5odHRwOi8vZm9udGVsbG8uY29tAEMAbwBwAHkAcgBpAGcAaAB0ACAAKABDACkAIAAyADAAMgAwACAAYgB5ACAAbwByAGkAZwBpAG4AYQBsACAAYQB1AHQAaABvAHIAcwAgAEAAIABmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQBmAG8AbgB0AGUAbABsAG8AUgBlAGcAdQBsAGEAcgBmAG8AbgB0AGUAbABsAG8AZgBvAG4AdABlAGwAbABvAFYAZQByAHMAaQBvAG4AIAAxAC4AMABmAG8AbgB0AGUAbABsAG8ARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABzAHYAZwAyAHQAdABmACAAZgByAG8AbQAgAEYAbwBuAHQAZQBsAGwAbwAgAHAAcgBvAGoAZQBjAHQALgBoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAAAAAAIAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQEOAQ8BEAERARIBEwEUARUBFgEXARgBGQEaARsBHAEdAR4BHwEgASEBIgEjASQBJQEmAScBKAEpASoBKwEsAS0BLgEvATABMQEyATMBNAE1ATYBNwE4ATkBOgE7ATwBPQE+AT8BQAFBAUIBQwFEAUUBRgFHAUgBSQFKAUsBTAFNAU4BTwFQAVEBUgFTAVQBVQFWAVcBWAFZAVoBWwFcAV0BXgFfAWABYQFiAWMBZAFlAWYBZwFoAWkBagFrAWwBbQFuAW8BcAFxAAR1c2VyBmZvbGRlcgRsaXN0BWxvZ2luA2NvZwd0d2l0dGVyC2FydGljbGUtYWx0BmNhbmNlbARob21lCGRvd24tZGlyCGZhY2Vib29rCGFzdGVyaXNrBnVwbG9hZAlzdG9wd2F0Y2gGZXhwb3J0BWhlYXJ0BHBsdXMGdXAtZGlyBG1lbnUJbGVmdC1vcGVuCnJpZ2h0LW9wZW4FaW5ib3gGd3JlbmNoB2NvbW1lbnQNc3RhY2tvdmVyZmxvdwhxdWVzdGlvbgpvay1jaXJjbGVkB3dhcm5pbmcEbWFpbARsaW5rB2tleS1pbnYFdHJhc2gIZG93bmxvYWQHZ2xhc3NlcwZxcmNvZGUHc2h1ZmZsZQNleWUEbG9jawZzZWFyY2gEYmVsbAV1c2Vycwhsb2NhdGlvbglicmllZmNhc2UJaW5zdGFncmFtBWNsb2NrBXBob25lCGNhbGVuZGFyBXByaW50BGVkaXQEYm9sZAZpdGFsaWMGcm9ja2V0CHdoYXRzYXBwBWRvdC0zDGluZm8tY2lyY2xlZAh2aWRlb2NhbQtxdW90ZS1yaWdodAdwaWN0dXJlB3BhbGV0dGUEbGFtcAlib29rLW9wZW4Cb2sIY2hhdC1hbHQHYXJjaGl2ZQRwbGF5BXBhdXNlCWRvd24tb3Blbgd1cC1vcGVuBW1pbnVzCGV4Y2hhbmdlB25ldHdvcmsHZGlzY29yZAhtb29uLWludgdzdW4taW52DmNhbmNlbC1jaXJjbGVkCWxpZ2h0bmluZwNkZXYJcmlnaHQtZGlyCGxlZnQtZGlyBGZpcmUIbGluay1leHQOZ2l0aHViLWNpcmNsZWQGZmlsdGVyBGRvY3MLbGlzdC1idWxsZXQNbGlzdC1udW1iZXJlZAl1bmRlcmxpbmUEc29ydAhsaW5rZWRpbgVzbWlsZQhrZXlib2FyZARjb2RlBnNoaWVsZBJhbmdsZS1jaXJjbGVkLWxlZnQTYW5nbGUtY2lyY2xlZC1yaWdodAliaXRidWNrZXQHd2luZG93cwtkb3QtY2lyY2xlZAp3aGVlbGNoYWlyBGJhbmsGZ29vZ2xlD2J1aWxkaW5nLWZpbGxlZAhkYXRhYmFzZQhsaWZlYnVveQZoZWFkZXIKYmlub2N1bGFycwpjaGFydC1hcmVhCXBpbnRlcmVzdAZtZWRpdW0GZ2l0bGFiCHRlbGVncmFtAAAAAQAB//8ADwAAAAAAAAAAAAAAAAAAAAAAGAAYABgAGAOB/zoDgf86sAAsILAAVVhFWSAgS7gADlFLsAZTWliwNBuwKFlgZiCKVViwAiVhuQgACABjYyNiGyEhsABZsABDI0SyAAEAQ2BCLbABLLAgYGYtsAIsIGQgsMBQsAQmWrIoAQpDRWNFUltYISMhG4pYILBQUFghsEBZGyCwOFBYIbA4WVkgsQEKQ0VjRWFksChQWCGxAQpDRWNFILAwUFghsDBZGyCwwFBYIGYgiophILAKUFhgGyCwIFBYIbAKYBsgsDZQWCGwNmAbYFlZWRuwAStZ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) format('truetype')}[class*=" icon-"]:before,[class^=icon-]:before{font-family:fontello;font-style:normal;font-weight:400;speak:never;display:inline-block;text-decoration:inherit;width:1em;margin-right:.2em;text-align:center;font-variant:normal;text-transform:none;line-height:1em;margin-left:.2em}.icon-user:before{content:'\e800'}.icon-folder:before{content:'\e801'}.icon-list:before{content:'\e802'}.icon-login:before{content:'\e803'}.icon-cog:before{content:'\e804'}.icon-twitter:before{content:'\e805'}.icon-article-alt:before{content:'\e806'}.icon-cancel:before{content:'\e807'}.icon-home:before{content:'\e808'}.icon-down-dir:before{content:'\e809'}.icon-facebook:before{content:'\e80a'}.icon-asterisk:before{content:'\e80b'}.icon-upload:before{content:'\e80c'}.icon-stopwatch:before{content:'\e80d'}.icon-export:before{content:'\e80e'}.icon-heart:before{content:'\e80f'}.icon-plus:before{content:'\e810'}.icon-up-dir:before{content:'\e811'}.icon-menu:before{content:'\e812'}.icon-left-open:before{content:'\e813'}.icon-right-open:before{content:'\e814'}.icon-inbox:before{content:'\e815'}.icon-wrench:before{content:'\e816'}.icon-comment:before{content:'\e817'}.icon-stackoverflow:before{content:'\e818'}.icon-question:before{content:'\e819'}.icon-ok-circled:before{content:'\e81a'}.icon-warning:before{content:'\e81b'}.icon-mail:before{content:'\e81c'}.icon-link:before{content:'\e81d'}.icon-key-inv:before{content:'\e81e'}.icon-trash:before{content:'\e81f'}.icon-download:before{content:'\e820'}.icon-glasses:before{content:'\e821'}.icon-qrcode:before{content:'\e822'}.icon-shuffle:before{content:'\e823'}.icon-eye:before{content:'\e824'}.icon-lock:before{content:'\e825'}.icon-search:before{content:'\e826'}.icon-bell:before{content:'\e827'}.icon-users:before{content:'\e828'}.icon-location:before{content:'\e829'}.icon-briefcase:before{content:'\e82a'}.icon-instagram:before{content:'\e82b'}.icon-clock:before{content:'\e82c'}.icon-phone:before{content:'\e82d'}.icon-calendar:before{content:'\e82e'}.icon-print:before{content:'\e82f'}.icon-edit:before{content:'\e830'}.icon-bold:before{content:'\e831'}.icon-italic:before{content:'\e832'}.icon-rocket:before{content:'\e833'}.icon-whatsapp:before{content:'\e834'}.icon-dot-3:before{content:'\e835'}.icon-info-circled:before{content:'\e836'}.icon-videocam:before{content:'\e837'}.icon-quote-right:before{content:'\e838'}.icon-picture:before{content:'\e839'}.icon-palette:before{content:'\e83a'}.icon-lamp:before{content:'\e83b'}.icon-book-open:before{content:'\e83c'}.icon-ok:before{content:'\e83d'}.icon-chat-alt:before{content:'\e83e'}.icon-archive:before{content:'\e83f'}.icon-play:before{content:'\e840'}.icon-pause:before{content:'\e841'}.icon-down-open:before{content:'\e842'}.icon-up-open:before{content:'\e843'}.icon-minus:before{content:'\e844'}.icon-exchange:before{content:'\e845'}.icon-network:before{content:'\e846'}.icon-discord:before{content:'\e847'}.icon-moon-inv:before{content:'\e848'}.icon-sun-inv:before{content:'\e849'}.icon-cancel-circled:before{content:'\e84a'}.icon-lightning:before{content:'\e84b'}.icon-dev:before{content:'\e84c'}.icon-right-dir:before{content:'\e84d'}.icon-left-dir:before{content:'\e84e'}.icon-fire:before{content:'\e84f'}.icon-link-ext:before{content:'\f08e'}.icon-github-circled:before{content:'\f09b'}.icon-filter:before{content:'\f0b0'}.icon-docs:before{content:'\f0c5'}.icon-list-bullet:before{content:'\f0ca'}.icon-list-numbered:before{content:'\f0cb'}.icon-underline:before{content:'\f0cd'}.icon-sort:before{content:'\f0dc'}.icon-linkedin:before{content:'\f0e1'}.icon-smile:before{content:'\f118'}.icon-keyboard:before{content:'\f11c'}.icon-code:before{content:'\f121'}.icon-shield:before{content:'\f132'}.icon-angle-circled-left:before{content:'\f137'}.icon-angle-circled-right:before{content:'\f138'}.icon-bitbucket:before{content:'\f171'}.icon-windows:before{content:'\f17a'}.icon-dot-circled:before{content:'\f192'}.icon-wheelchair:before{content:'\f193'}.icon-bank:before{content:'\f19c'}.icon-google:before{content:'\f1a0'}.icon-building-filled:before{content:'\f1ad'}.icon-database:before{content:'\f1c0'}.icon-lifebuoy:before{content:'\f1cd'}.icon-header:before{content:'\f1dc'}.icon-binoculars:before{content:'\f1e5'}.icon-chart-area:before{content:'\f1fe'}.icon-pinterest:before{content:'\f231'}.icon-medium:before{content:'\f23a'}.icon-gitlab:before{content:'\f296'}.icon-telegram:before{content:'\f2c6'}.datalist-polyfill{list-style:none;display:none;background:#fff;box-shadow:0 2px 2px #999;position:absolute;left:0;top:0;margin:0;padding:0;max-height:300px;overflow-y:auto}.datalist-polyfill:empty{display:none!important}.datalist-polyfill>li{padding:3px;font:13px "Lucida Grande",Sans-Serif}.datalist-polyfill__active{background:#3875d7;color:#fff}date-input-polyfill{z-index:1000!important;max-width:320px!important;width:320px!important}date-input-polyfill .monthSelect-wrapper,date-input-polyfill .yearSelect-wrapper{height:50px;line-height:50px;padding:0;width:40%!important;margin-bottom:10px!important}date-input-polyfill .monthSelect-wrapper select,date-input-polyfill .yearSelect-wrapper select{padding:0 12px;height:50px;line-height:50px;box-sizing:border-box}date-input-polyfill .yearSelect-wrapper{width:35%!important}date-input-polyfill table{width:100%!important;max-width:100%!important;padding:0 12px 12px 12px!important;box-sizing:border-box;margin:0}date-input-polyfill table td:first-child,date-input-polyfill table td:last-child,date-input-polyfill table th:first-child,date-input-polyfill table th:last-child{width:32px!important;padding:4px!important}date-input-polyfill select{margin-bottom:10px}date-input-polyfill button{width:25%!important;height:50px!important;line-height:50px!important;margin-bottom:10px!important;background:inherit;position:relative;color:inherit;padding:inherit;box-sizing:inherit;border-radius:inherit;font-size:inherit;box-shadow:none;border:none;border-bottom:none!important}::placeholder{color:var(--config-color-placeholder);text-align:left}::-webkit-input-placeholder{text-align:left}input:-moz-placeholder{text-align:left}input,textarea{background:var(--config-color-background-input)}input[type=file],input[type=file]::-webkit-file-upload-button{cursor:pointer}.button,button{display:inline-block;background:var(--config-color-focus);border-radius:26px;border:none;color:var(--config-color-background-fade);height:52px;line-height:52px;padding:0 25px;cursor:pointer;font-size:16px;box-sizing:border-box;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.button:focus,.button:hover,button:focus,button:hover{background:var(--config-color-focus-hover)}.button.fly,button.fly{position:fixed;z-index:2;bottom:30px;right:30px}@media only screen and (max-width:550px){.button.fly,button.fly{right:15px}}.button.fill,button.fill{display:block;width:100%;text-align:center;padding:0 10px!important}.button.fill-aligned,button.fill-aligned{display:block;width:100%;text-align:left;padding:0 20px!important}.button.icon,button.icon{padding-right:30px!important}.button.icon-reduce,button.icon-reduce{padding-left:15px!important}.button.reverse,button.reverse{background:0 0;height:50px;line-height:48px;padding:0 23px;color:var(--config-color-focus);border:solid 2px var(--config-color-focus)}.button.reverse:focus,.button.reverse:hover,button.reverse:focus,button.reverse:hover{color:var(--config-color-focus-hover);border-color:var(--config-color-focus-hover)}.button.small,button.small{padding:0 15px;height:40px;line-height:36px;font-size:13px}.button.round,button.round{width:52px;padding:0}.button.round.small,button.round.small{font-size:12px;width:30px;height:30px;line-height:30px}.button.white,button.white{background:#fff;color:var(--config-color-focus)}.button.white.reverse,button.white.reverse{color:#fff;background:0 0;border:solid 2px #fff}.button.trans,button.trans{background:0 0!important}.button.trans.reverse,button.trans.reverse{background:0 0!important}.button.success,button.success{background:var(--config-color-success)}.button.success.reverse,button.success.reverse{color:var(--config-color-success);background:#fff;border:solid 2px var(--config-color-success)}.button.danger,button.danger{background:var(--config-color-danger);color:#fff}.button.danger.reverse,button.danger.reverse{color:var(--config-color-danger);background:var(--config-color-background-fade);border:solid 2px var(--config-color-danger)}.button.dark,button.dark{background:var(--config-color-dark);color:var(--config-color-background-fade)}.button.dark.reverse,button.dark.reverse{color:var(--config-color-dark);background:var(--config-color-background-fade);border:solid 2px var(--config-color-dark)}.button .disabled,.button.disabled,.button:disabled,button .disabled,button.disabled,button:disabled{color:var(--config-color-normal);background:var(--config-color-background-dark);opacity:.6;cursor:default}.button.link,button.link{background:0 0;border-radius:0;color:var(--config-color-link);height:auto;line-height:normal;padding:0;padding-right:0!important}.button.link:focus,button.link:focus{box-shadow:inherit}.button.strip,button.strip{background:0 0;height:auto;line-height:16px;color:inherit;padding:0 5px}.button.facebook,button.facebook{color:#fff!important;background:#4070b4!important}.button.twitter,button.twitter{color:#fff!important;background:#56c2ea!important}.button.linkedin,button.linkedin{color:#fff!important;background:#0076b5!important}.button.github,button.github{color:#fff!important;background:#7e7c7c!important}.button:focus,button:focus{outline:0}label{margin-bottom:15px;display:block;line-height:normal}.input,input[type=date],input[type=datetime-local],input[type=email],input[type=file],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=url],select,textarea{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px}.input[type=file],input[type=date][type=file],input[type=datetime-local][type=file],input[type=email][type=file],input[type=file][type=file],input[type=number][type=file],input[type=password][type=file],input[type=search][type=file],input[type=tel][type=file],input[type=text][type=file],input[type=url][type=file],select[type=file],textarea[type=file]{line-height:0;padding:15px;height:auto}.input:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=email]:focus,input[type=file]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus,select:focus,textarea:focus{outline:0;border-color:#b3d7fd}.input:disabled,input[type=date]:disabled,input[type=datetime-local]:disabled,input[type=email]:disabled,input[type=file]:disabled,input[type=number]:disabled,input[type=password]:disabled,input[type=search]:disabled,input[type=tel]:disabled,input[type=text]:disabled,input[type=url]:disabled,select:disabled,textarea:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.input.strip,input[type=date].strip,input[type=datetime-local].strip,input[type=email].strip,input[type=file].strip,input[type=number].strip,input[type=password].strip,input[type=search].strip,input[type=tel].strip,input[type=text].strip,input[type=url].strip,select.strip,textarea.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:right 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.input.strip:focus,input[type=date].strip:focus,input[type=datetime-local].strip:focus,input[type=email].strip:focus,input[type=file].strip:focus,input[type=number].strip:focus,input[type=password].strip:focus,input[type=search].strip:focus,input[type=tel].strip:focus,input[type=text].strip:focus,input[type=url].strip:focus,select.strip:focus,textarea.strip:focus{border-color:#b3d7fd}.input:-webkit-autofill::first-line,input[type=date]:-webkit-autofill::first-line,input[type=datetime-local]:-webkit-autofill::first-line,input[type=email]:-webkit-autofill::first-line,input[type=file]:-webkit-autofill::first-line,input[type=number]:-webkit-autofill::first-line,input[type=password]:-webkit-autofill::first-line,input[type=search]:-webkit-autofill::first-line,input[type=tel]:-webkit-autofill::first-line,input[type=text]:-webkit-autofill::first-line,input[type=url]:-webkit-autofill::first-line,select:-webkit-autofill::first-line,textarea:-webkit-autofill::first-line{font-weight:300;font-size:16px}input[type=email],input[type=url]{direction:ltr}input[type=email]::placeholder,input[type=url]::placeholder{text-align:left;direction:ltr}select{background:0 0;-webkit-appearance:none;background-image:var(--config-console-nav-switch-arrow);background-position:right 15px top 50%;background-repeat:no-repeat;background-color:var(--config-color-background-input);width:calc(100% - 62px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:45px}select:-webkit-autofill{background-image:url("data:image/svg+xml;utf8,")!important;background-position:100% 50%!important;background-repeat:no-repeat!important}input[type=search],input[type=search].strip{background:0 0;-webkit-appearance:none;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAdZJREFUWIXt1s2LjWEYBvDfnDMzFpNIamZIFrMiJYMyFmKhZKfOwoiFr2LFn2BByG6WVrKwMcjWxgoLIlKIUk6RrzAjZWZ8LO731FlwvB+PUbjq6X0X7/VeV/d9P9fz8IdRL8Hpw3x8w0xaOz9GNxq4gJeZcGs1cRab0fU7xLfgMSYzoT3YgNXYhIO4iM+4iTWphGs4jikcFSXvhEGczr4/UFW8C2N4jXUFudvwCYeqGNgnSr6yJH8rpkWLCqMfE9hdUryFE3iC3qLEk7ij+kT34Q32FiHV8Qr7K4q3cArXihCGxd5elMjARnzBvE4f1dreV+AtnicycC/7/7K8BhaIvqXCO3zFwrwGZtCT0EAtW9N5DTSxWGR/CizNns/yEgbFEK5NZGCnaEPHE7e9Ai9wA6OJDIzistgJubFdxHB/RfFVYgCHixJruI5x5dNwDm6J47sUhkTvjpUw0Y1zeOrXR3hHjOA9zmBuTs4Arog4/yhuUZWwHPdFMh7280BZgiP4ILJ/UuymqRQmejPxphiquzgvKnMJDzOxB9glZqiRiecykbfHdawX98EhcdxO4BGu4nYm2EJDzEKPSMIdYrBnFYUq8d/EP2di1gey3cS4ErflvxffASbhcakIINaMAAAAAElFTkSuQmCC);background-color:var(--config-color-background-input);background-position:left 15px top 50%;background-repeat:no-repeat;background-size:20px 20px;width:calc(100% - 60px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-left:45px}select[multiple]{min-height:75px;padding:5px 10px!important;padding-right:50px!important}select[multiple] option{padding:10px 4px;border-bottom:solid 1px #f1f1f1}select[multiple] option:last-child{border-bottom:none}textarea{min-height:75px;resize:vertical;line-height:32px;padding:5px 15px}textarea.tall{min-height:180px}fieldset{border:none;margin:0;padding:0}.counter{font-size:13px;text-align:right;color:var(--config-color-fade);margin-top:-20px;margin-bottom:20px}.file-preview{background:var(--config-color-background-input) url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAIElEQVQoU2NkYGAwZsAEZ9GFGIeIQix+wfQgyDODXSEAcUwGCrDSHgkAAAAASUVORK5CYII=)!important;border:solid 1px #e2e2e2;box-shadow:inset 0 0 3px #a0a0a0;border-radius:8px;width:calc(100% - 2px);max-height:180px;visibility:visible!important}.video-preview{padding-top:56%;position:relative;border-radius:10px;background:#e7e7e7;overflow:hidden;margin:0}.video-preview iframe{position:absolute;top:0;width:100%;height:100%;border:none}.map-preview{padding-top:50%;position:relative;margin-bottom:10px;border-radius:10px;background:#e7e7e7;overflow:hidden;box-shadow:0 0 30px rgba(218,218,218,.5)}.map-preview iframe{position:absolute;top:0;width:100%;height:100%;border:none}.tooltip{position:relative}.tooltip.large:hover:after{white-space:normal;width:280px}.tooltip.small:hover:after{white-space:normal;width:180px}.tooltip:hover:after{white-space:nowrap;background:var(--config-color-tooltip-background);border-radius:5px;bottom:26px;color:var(--config-color-tooltip-text);content:attr(data-tooltip);padding:5px 15px;position:absolute;font-size:13px;line-height:20px;z-index:98;left:20%;margin-left:-30px;word-break:break-word}.tooltip:hover:before{border:solid;border-color:var(--config-color-tooltip-background) transparent;border-width:6px 6px 0 6px;bottom:20px;content:"";position:absolute;z-index:99;left:5px}.tooltip.down:hover:after{top:26px;bottom:inherit}.tooltip.down:hover:before{top:20px;border-width:0 6px 6px 6px;bottom:inherit}.tag{display:inline-block;background:var(--config-color-fade-light);color:var(--config-color-fade);border-radius:12px;line-height:24px;padding:0 8px;font-size:12px;box-shadow:none!important;border:none;height:auto;width:auto;white-space:nowrap;text-overflow:ellipsis}.tag:hover{border:none}.tag.green{background:var(--config-color-success);color:#fff}.tag.red{background:var(--config-color-danger);color:#fff}.tag.yellow{background:#ffe28b;color:#494949}.tag.focus{background:var(--config-color-focus);color:#fff}.tag.dark{background:var(--config-color-dark);color:#e7e7e7}.tag.blue{background:var(--config-color-info);color:#fff}.tag.link{background:var(--config-color-link);color:#fff}input[type=checkbox],input[type=radio]{width:26px;height:16px;position:relative;-webkit-appearance:none;border-radius:0;border:none;background:0 0;vertical-align:middle;margin:0}input[type=checkbox]:after,input[type=radio]:after{content:"";display:block;width:20px;height:20px;background:var(--config-color-background-fade);top:-5px;border-radius:50%;position:absolute;border:solid 3px var(--config-color-focus);vertical-align:middle}input[type=checkbox]:checked:after,input[type=radio]:checked:after{text-align:center;font-family:fontello;content:'\e83d';font-size:16px;line-height:20px;color:var(--config-color-background-fade);background:var(--config-color-focus)}input[type=checkbox][type=radio]:checked:after,input[type=radio][type=radio]:checked:after{content:'';display:block;width:10px;height:10px;border-radius:50%;background:var(--config-color-background-fade);border:solid 8px var(--config-color-focus)}input[type=checkbox]:focus,input[type=radio]:focus{outline:0}input[type=checkbox]:focus:after,input[type=checkbox]:hover:after,input[type=radio]:focus:after,input[type=radio]:hover:after{outline:0;border-color:#000}input[type=checkbox]:checked:focus:after,input[type=checkbox]:checked:hover:after,input[type=radio]:checked:focus:after,input[type=radio]:checked:hover:after{border-color:var(--config-color-focus)}.input-copy{position:relative}.input-copy input,.input-copy textarea{padding-right:65px;width:calc(100% - 82px);resize:none}.input-copy .copy{position:absolute;top:0;right:0;border-left:solid 1px var(--config-color-fade-light);height:calc(100% - 2px);width:50px;line-height:50px;text-align:center;background:var(--config-color-background-focus);margin:1px;border-radius:0 10px 10px 0}.paging{color:var(--config-color-fade);padding:5px 15px;font-size:12px}.paging form{display:inline-block}.paging button:disabled{color:var(--config-color-background-fade);opacity:.6}.blue-snap iframe{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px;float:none!important;height:40px!important;width:calc(100% - 32px)!important;border:solid 1px #e2e2e2!important;background:0 0!important;position:static!important}.blue-snap iframe[type=file]{line-height:0;padding:15px;height:auto}.blue-snap iframe:focus{outline:0;border-color:#b3d7fd}.blue-snap iframe:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.blue-snap iframe.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:right 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.blue-snap iframe.strip:focus{border-color:#b3d7fd}.blue-snap iframe:-webkit-autofill::first-line{font-weight:300;font-size:16px}.blue-snap .error{font-size:12px;margin-top:-25px;color:var(--config-color-danger);height:40px;padding-left:2px}.pell{height:auto;padding-bottom:0;margin-bottom:0;padding-top:0;background:var(--config-color-background-input);line-height:normal!important;position:relative}.pell.hide{padding:0!important;height:1px;min-height:1px;max-height:1px;border:none;box-shadow:none;margin-bottom:20px;opacity:0}.pell [contenteditable=true]:empty:before{content:attr(placeholder);display:block;color:var(--config-color-placeholder)}.pell .pell-actionbar{border-bottom:solid 1px var(--config-color-fade-light);margin:0 -15px 15px -15px;padding:10px 15px;position:sticky;top:70px;background:var(--config-color-background-input);border-radius:10px 10px 0 0}.pell .pell-content{min-height:100px;display:block;padding:10px;margin:-10px;cursor:text}.pell .pell-content:focus{outline:0}.pell button{background:inherit;color:inherit;margin:0;padding:0;padding-right:15px;height:40px;line-height:40px;box-shadow:none;cursor:pointer;font-size:13px;border-radius:0}.pell button.pell-button-selected,.pell button:focus,.pell button:hover{color:var(--config-color-link)}.pell h1,.pell h2,.pell h3,.pell h4,.pell h5,.pell h6{text-align:inherit;margin-bottom:30px}.pell b,.pell strong{font-weight:700}.pell ol,.pell ul{margin:0 0 20px 0}.pell ol li,.pell ul li{display:list-item!important;list-style:inherit;list-style-position:inside!important;margin:0 20px 2px 20px}.pell ol li p,.pell ul li p{margin:0;display:inline}.pell ol li{list-style:decimal}.pell ol li::before{content:'';display:none}label.switch{line-height:42px}.switch,input[type=checkbox].button.switch,input[type=checkbox].switch{width:52px;height:32px;line-height:32px;border-radius:21px;background:var(--config-color-fade);display:inline-block;margin:0;padding:5px;padding-left:5px;padding-right:30px}.switch.on,.switch:checked,input[type=checkbox].button.switch.on,input[type=checkbox].button.switch:checked,input[type=checkbox].switch.on,input[type=checkbox].switch:checked{background-color:var(--config-color-success);padding-left:25px;padding-right:5px}.switch.on:focus,.switch.on:hover,.switch:checked:focus,.switch:checked:hover,input[type=checkbox].button.switch.on:focus,input[type=checkbox].button.switch.on:hover,input[type=checkbox].button.switch:checked:focus,input[type=checkbox].button.switch:checked:hover,input[type=checkbox].switch.on:focus,input[type=checkbox].switch.on:hover,input[type=checkbox].switch:checked:focus,input[type=checkbox].switch:checked:hover{background:var(--config-color-success)}.switch:focus,.switch:hover,input[type=checkbox].button.switch:focus,input[type=checkbox].button.switch:hover,input[type=checkbox].switch:focus,input[type=checkbox].switch:hover{background:var(--config-color-fade)}.switch:focus:after,.switch:hover:after,input[type=checkbox].button.switch:focus:after,input[type=checkbox].button.switch:hover:after,input[type=checkbox].switch:focus:after,input[type=checkbox].switch:hover:after{background:#fff}.switch:after,input[type=checkbox].button.switch:after,input[type=checkbox].switch:after{content:"";display:block;width:22px;height:22px;background:#fff;border-radius:50%;border:none;position:static;top:0}.password-meter{margin:-41px 10px 30px 10px;height:2px;background:0 0;max-width:100%;z-index:2;position:relative}.password-meter.weak{background:var(--config-color-danger)}.password-meter.medium{background:var(--config-color-success)}.password-meter.strong{background:var(--config-color-success)}.color-input:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.color-input .color-preview{width:53px;height:53px;float:left;margin-right:10px;background:#000;border-radius:10px;box-shadow:inset 0 0 3px #a0a0a0;position:relative}.color-input .color-preview input{opacity:0;position:absolute;top:0;bottom:0;left:0;right:0;width:100%;height:100%;cursor:pointer}.color-input input{text-transform:uppercase;float:left;width:calc(100% - 95px)}.grecaptcha-badge{box-shadow:none!important;border-radius:10px!important;overflow:hidden!important;background:#4d92df!important;bottom:25px}.grecaptcha-badge:hover{width:256px!important}.back{font-size:15px;line-height:24px;height:24px;margin-left:-15px;margin-top:-25px;margin-bottom:20px}.back span{font-weight:inherit!important}@media only screen and (max-width:550px){.back{margin-left:-5px}}hr{height:1px;background:var(--config-border-color)!important;border:none}hr.fade{opacity:.7}.upload{position:relative}.upload:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.upload input{position:absolute;top:0;left:0;opacity:0;cursor:pointer}.upload.single .preview{height:0;position:relative;padding-top:100%;width:100%;margin-bottom:15px!important}.upload.single .preview li{position:absolute;top:0;width:calc(100% - 20px);height:calc(100% - 20px);margin-right:0!important;margin-bottom:0!important}.upload .button{float:left;margin-right:10px!important}.upload .button.disabled,.upload .button.disabled:hover{background:0 0;color:inherit;border-color:inherit}.upload .count{float:left;line-height:52px}.upload .progress{background:var(--config-color-success);height:6px;border-radius:3px;margin-bottom:15px!important}.upload .preview:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.upload .preview li{float:left;margin-right:20px!important;margin-bottom:15px!important;background:var(--config-color-background-fade-super);width:150px;height:150px;line-height:148px;text-align:center;border-radius:20px;overflow:hidden;position:relative;cursor:pointer;border:solid 1px var(--config-color-background-dark)}.upload .preview li:hover:before{background:var(--config-color-focus)}.upload .preview li:before{content:'\e807';font-family:fontello;font-size:12px;position:absolute;width:20px;height:20px;display:block;top:8px;right:8px;text-align:center;line-height:20px;vertical-align:middle;border-radius:50%;background:#484848;color:#fff;z-index:1}.upload .preview li img{vertical-align:middle;max-height:150px;max-width:150px;-webkit-filter:drop-shadow(0 0 6px rgba(0, 0, 0, .3));filter:drop-shadow(0 0 1px rgba(0, 0, 0, .3))}.upload.wide .preview li{height:0;width:100%;position:relative;padding-top:30.547%;background:#e7e7e7;border-radius:10px;overflow:hidden;border:solid 1px #f9f9f9;margin:0}.upload.wide .preview li img{border-radius:10px;position:absolute;top:0;width:100%;display:block;opacity:1;max-width:inherit;max-height:inherit}ol{list-style:none;counter-reset:x-counter;padding:0}ol li{counter-increment:x-counter;line-height:30px;margin-bottom:30px;margin-left:45px}ol li::before{display:inline-block;content:counter(x-counter);color:var(--config-color-background-fade);background:var(--config-color-focus);border:solid 2px var(--config-color-focus);margin-right:15px;margin-left:-45px;width:26px;height:26px;border-radius:50%;text-align:center;line-height:26px}.required{color:var(--config-color-danger);font-size:8px;position:relative;top:-8px}.drop-list{position:relative;outline:0}.drop-list.open ul{display:block}.drop-list ul{position:relative;background:var(--config-color-background-fade);border-radius:10px;box-shadow:0 0 3px rgba(0,0,0,.05);padding:30px;display:block;border-bottom:none;box-shadow:0 0 6px rgba(0,0,0,.1);display:none;position:absolute;bottom:calc(100% + 10px);z-index:2;padding:0;left:-10px;max-width:280px;min-width:240px}.drop-list ul.padding-small{padding:15px}.drop-list ul.y-scroll{overflow-y:auto}.drop-list ul.danger{background:var(--config-color-danger);color:#fff}.drop-list ul.danger .box{color:var(--config-color-normal);background:var(--config-color-background-fade)}.drop-list ul.danger>.button,.drop-list ul.danger>button{background:#fff;color:var(--config-color-danger)}.drop-list ul.note{background:var(--config-note-background)}.drop-list ul.focus{background:var(--config-color-focus);color:var(--config-color-background-fade)}.drop-list ul.focus .button,.drop-list ul.focus button{background:var(--config-color-background-fade);color:var(--config-color-focus)}.drop-list ul.line{background:0 0;border:solid 1px var(--config-color-background-dark);box-shadow:none}.drop-list ul.warning{background:var(--config-color-success);color:#2d2d2d}.drop-list ul.warning .button,.drop-list ul.warning button{background:rgba(45,45,45,.8);color:var(--config-color-success)}.drop-list ul .tabs{border-bottom:solid 1px var(--config-border-color);margin:0 -30px;padding:0 30px!important}.drop-list ul>footer{margin:0 -30px -30px -30px;padding:15px 30px;background:var(--config-color-background-fade);border:solid 1px var(--config-border-color);border-radius:0 0 10px 10px}.drop-list ul hr{height:1px;background:var(--config-console-background);border:none;margin:30px -30px}.drop-list ul .label{position:absolute;top:10px;z-index:2;right:10px}.drop-list ul.fade-bottom{position:relative;overflow:hidden}.drop-list ul.fade-bottom:after{content:"";position:absolute;display:block;bottom:15px;width:100%;background:#000;background:linear-gradient(180deg,rgba(0,0,0,0) 0,var(--config-color-background-fade) 80%);height:100px;margin:0 -15px}.drop-list ul .header{position:static;height:40px;padding:20px 30px 20px 30px;margin-bottom:30px;margin:-30px -30px 20px -30px;background:var(--config-color-background-fade);border-bottom:solid 1px #efefef}.drop-list ul ul.numbers>li{position:relative;margin-left:30px;margin-right:50px}.drop-list ul ul.numbers>li hr{margin-left:-60px;margin-right:-80px}.drop-list ul ul.numbers>li .settings{position:absolute;top:3px;right:-50px}.drop-list ul ul.numbers>li::after{display:block;width:25px;height:25px;line-height:25px;font-size:13px;font-weight:500;border-radius:50%;background:var(--config-color-focus);color:var(--config-color-background);counter-increment:section;content:counter(section);text-align:center;position:absolute;top:3px;left:-45px}.drop-list ul .scroll{margin:0 -30px;overflow-y:scroll}.drop-list ul .scroll table{width:100%;margin:0}.drop-list ul ul.sortable{counter-reset:section}.drop-list ul ul.sortable>li [data-move-down].round,.drop-list ul ul.sortable>li [data-move-up].round,.drop-list ul ul.sortable>li [data-remove].round{background:var(--config-color-focus);color:var(--config-color-background-fade);width:25px;height:25px;line-height:25px;display:inline-block;text-align:center;padding:0;margin-right:5px}.drop-list ul ul.sortable>li [data-move-down].round:disabled,.drop-list ul ul.sortable>li [data-move-up].round:disabled,.drop-list ul ul.sortable>li [data-remove].round:disabled{display:none}.drop-list ul ul.sortable>li:first-child [data-move-up]{display:none}.drop-list ul ul.sortable>li:first-child [data-move-up]:disabled{display:inline-block;background:var(--config-color-background)}.drop-list ul ul.sortable>li:last-child [data-move-down]{display:none}.drop-list ul ul.sortable>li:last-child [data-move-down]:disabled{display:inline-block;background:var(--config-color-background)}.drop-list ul .toggle{position:relative;border-top:1px solid var(--config-console-background);border-bottom:1px solid var(--config-console-background);margin:0 -30px;padding:30px 30px 0 30px;height:65px;overflow:hidden}.drop-list ul .toggle.list{border-bottom:none}.drop-list ul .toggle.sorts button.ls-ui-open{width:calc(100% - 100px)}.drop-list ul .toggle button.ls-ui-open{right:0;position:absolute;top:0;width:100%;height:95px;background:0 0;opacity:.5;border-radius:0}.drop-list ul .toggle .icon-minus,.drop-list ul .toggle .icon-up-open{display:none}.drop-list ul .toggle .content{display:none}.drop-list ul .toggle.open{height:auto}.drop-list ul .toggle.open .icon-minus,.drop-list ul .toggle.open .icon-up-open{display:block}.drop-list ul .toggle.open .icon-down-open,.drop-list ul .toggle.open .icon-plus{display:none}.drop-list ul .toggle.open .content{display:block}.drop-list ul .list li{border-bottom:solid 2px var(--config-border-color);margin:0 -30px 30px -30px;padding:0 30px 30px 30px}.drop-list ul .list li:last-child{padding-bottom:0;margin-bottom:0;border-bottom:none}@media only screen and (max-width:550px){.drop-list ul .list li .actions{float:none}}.drop-list ul .list li .avatar{display:block}.drop-list ul .list li .avatar.inline{display:inline-block}.drop-list ul.new{text-align:center}.drop-list ul.new i{font-size:80px;line-height:80px;font-family:Poppins,sans-serif;font-style:normal;font-weight:300}.drop-list ul.new b{margin-top:20px;display:block}.drop-list ul .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.drop-list ul .info hr{background:var(--config-modal-note-border)!important}.drop-list ul .table-wrap{margin:0 -30px;overflow-y:scroll}.drop-list ul .table-wrap table{margin:0}.drop-list ul:before{border:solid;border-color:var(--config-color-background-fade) transparent;border-width:8px 8px 0 8px;bottom:-8px;content:"";position:absolute;z-index:99;left:30px}.drop-list ul.arrow-end:before{right:30px;left:unset}.drop-list ul li{border-bottom:solid 1px var(--config-color-fade-super);margin:0;padding:0}.drop-list ul li:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.drop-list ul li:first-child{border-radius:10px 10px 0 0}.drop-list ul li:last-child{border-radius:0 0 10px 10px}.drop-list ul li:hover{background:var(--config-color-fade-super)}.drop-list ul li:first-child:hover,.drop-list ul li:last-child:hover{border-color:transparent}.drop-list ul li .link,.drop-list ul li a,.drop-list ul li button.link{display:block;vertical-align:middle;height:auto;line-height:30px;display:inline-block;padding:10px 15px!important;color:inherit;font-size:14px;border:none;cursor:pointer;width:calc(100% - 30px);text-align:left;box-sizing:content-box}.drop-list ul li.disabled .link:hover,.drop-list ul li.disabled a:hover{background:0 0}.drop-list ul li .avatar{width:30px;height:30px;margin-right:10px;float:left}.drop-list ul li:last-child{border-bottom:none}.drop-list.bottom ul{bottom:auto;margin-top:-2px}.drop-list.bottom ul:before{bottom:auto;top:-8px;border-width:0 8px 8px 8px}.drop-list.end ul{right:-10px;left:auto}.disabled{opacity:.2;cursor:default}.disabled .button,.disabled .link,.disabled a,.disabled button{cursor:default!important}.disabled .button:hover,.disabled .link:hover,.disabled a:hover,.disabled button:hover{background:0 0}.tags{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px;background:var(--config-color-background-input);min-height:42px;height:auto;cursor:text}.tags[type=file]{line-height:0;padding:15px;height:auto}.tags:focus{outline:0;border-color:#b3d7fd}.tags:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.tags.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:right 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.tags.strip:focus{border-color:#b3d7fd}.tags:-webkit-autofill::first-line{font-weight:300;font-size:16px}.tags .add{display:inline-block!important;border:none;padding:0;width:auto;margin:0;max-width:100%;min-width:200px}.tags ul.tags-list{display:inline;white-space:pre-line}.tags ul.tags-list li{display:inline-block!important;margin-right:10px;font-size:16px;padding:5px 10px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.tags ul.tags-list li::before{float:right;content:'\e807';font-family:fontello;font-style:normal;display:inline-block;text-align:center;line-height:16px;width:16px;height:16px;font-size:12px;background:#000;color:#fff;border-radius:50%;margin-top:4px;margin-bottom:4px;margin-left:6px;margin-right:0}.switch-theme{background:var(--config-switch-background);border-radius:19px;height:26px;width:44px;margin:9px 0}.switch-theme button{padding:3px;display:block;background:0 0;height:26px;width:100%}.switch-theme i{background:var(--config-color-background-fade);border-radius:50%;height:18px;width:18px;line-height:18px;font-size:12px;padding:0;margin:0;color:var(--config-color-fade)}.switch-theme i.force-light{float:right}.switch-theme i.force-dark{float:left}.dot{width:20px;height:20px;background:var(--config-color-fade);border-radius:50%;display:inline-block;vertical-align:middle;margin:0!important;padding:0!important}.dot.danger{background:var(--config-color-danger)!important}.dot.success{background:var(--config-color-success)!important}.dot.warning{background:var(--config-color-warning)!important}.dot.info{background:var(--config-color-info)!important}.console{width:100%;padding:0;overscroll-behavior:none}.console body{position:relative;width:calc(100% - 320px);padding-top:70px;padding-bottom:0;padding-right:50px;padding-left:270px;margin:0;color:var(--config-color-normal);background:var(--config-console-background)}.console body .project-only{display:none!important}.console body.show-nav .project-only{display:inline-block!important}.console body.hide-nav{padding-left:50px;width:calc(100% - 100px)}.console body.hide-nav header{width:calc(100% - 50px)}.console body.hide-nav header .logo{display:inline-block}.console body.hide-nav .console-back{display:block}.console body.hide-nav .console-index{display:none}.console body.hide-nav .account{display:none}.console body.index .console-back{display:none}.console body.index .console-index{display:block}.console body.index .account{display:block}.console body .console-index{display:block}.console body .console-back{display:none}.console main{min-height:480px}.console header{position:fixed;top:0;width:calc(100% - 280px);height:40px;line-height:40px;padding:15px 30px;background:var(--config-color-background-fade);box-shadow:0 0 2px rgba(0,0,0,.1);margin:0 -50px;z-index:2;font-size:14px}.console header .logo{display:none;border:none}.console header .logo:hover{border:none;opacity:.8}.console header .logo img{height:26px;margin:7px 0}.console header .setup-new{width:40px;height:40px;line-height:40px}.console header .list{width:240px}.console header .list select{height:40px;line-height:40px;padding-top:0;padding-bottom:0;border:none;border-radius:26px;background-color:var(--config-console-nav-switch-background);color:var(--config-console-nav-switch-color)}.console header .account{margin-left:25px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.console header .switch-theme{margin:2px 0}.console header .avatar{height:40px;width:40px}.console header .account-button{background:0 0;position:absolute;width:100%;height:40px;border-radius:0;z-index:1}.console header .notifications{position:relative;font-size:20px}.console header .notifications a{color:#1b3445}.console header .notifications:after{position:absolute;content:"";display:block;background:var(--config-color-danger);width:8px;height:8px;border-radius:50%;top:3px;right:3px}.console header nav{background:#1b3445;background:linear-gradient(var(--config-console-nav-start),var(--config-console-nav-end));color:#788c99;position:fixed;height:100%;width:220px;top:0;left:0}.console header nav .logo{height:39px;padding:15px 20px;display:block}.console header nav .logo img{display:inline-block;margin-top:7px;margin-bottom:14px}.console header nav .logo svg g{fill:var(--config-color-focus)}.console header nav .icon{display:block;border:none;margin:18px 10px 50px 10px}.console header nav .icon img{display:block}.console header nav .icon:hover{border-bottom:none}.console header nav .icon:hover svg g{fill:var(--config-color-focus)}.console header nav .container{overflow:auto;height:calc(100% - 133px);width:100%}.console header nav .project-box{padding:20px;text-align:center;display:block;border:none;line-height:100px;height:100px}.console header nav .project-box img{max-height:80px;max-width:80%;display:inline-block;vertical-align:middle}.console header nav .project{display:block;padding:85px 25px 20px 25px;color:#788c99;position:relative;border:none;height:20px}.console header nav .project:hover{border-bottom:none}.console header nav .project .name{height:20px;line-height:20px;margin:0;padding:0;display:inline-block;max-width:100%}.console header nav .project .arrow{display:block;position:absolute;right:5px;top:10px;width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #788c99;transform:rotate(225deg)}.console header nav .project img{position:absolute;bottom:40px;display:block;margin-bottom:10px;max-height:35px;max-width:40%}.console header nav .subtitle{padding:0 30px;display:block;font-size:12px;font-weight:300}.console header nav .links{margin-bottom:15px!important}.console header nav .links.top{border:none;padding-bottom:0;margin-bottom:5px!important}.console header nav .links.bottom{position:absolute;bottom:0;left:0;right:0;padding-bottom:0;border:none;margin-bottom:0!important;box-shadow:0 0 10px rgba(0,0,0,.1)}.console header nav .links.bottom a{border-top:solid 1px var(--config-console-nav-border);border-bottom:none}.console header nav .links .sub{display:inline-block;border:none;width:25px;height:25px;line-height:25px;border-radius:50%;padding:0;background:var(--config-color-focus);color:#fff;text-align:center;font-size:12px;margin:18px}.console header nav .links .sub i{width:auto;margin:0}.console header nav .links .sub:hover{border:none}.console header nav .links a{padding:8px 20px;border:none;display:block;color:#87a5b9;font-weight:400;border-left:solid 5px transparent;font-size:13px}.console header nav .links a i{margin-right:8px;width:22px;display:inline-block}.console header nav .links a.selected,.console header nav .links a:hover{color:#e4e4e4}.console header nav:after{content:'';display:block;position:absolute;background:#302839;height:100px;width:100%;bottom:-100px}.console>footer{width:calc(100% + 100px);margin:0 -50px;box-sizing:border-box;background:0 0;padding-right:30px;padding-left:30px}.console>footer ul{float:none;text-align:center}.console>footer ul li{float:none;display:inline-block}.console .projects{position:relative}.console .projects:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.console .projects li{float:left;margin-right:50px;margin-bottom:50px;width:270px}.console .projects li:nth-child(3n){margin-right:0}.console .dashboard{padding:20px;min-height:95px;overflow:hidden;position:relative;z-index:1}.console .dashboard .chart{width:80%}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console .dashboard .chart{width:100%}}.console .dashboard hr{margin:20px -20px;height:2px;background:var(--config-console-background)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console .dashboard hr{height:3px}}.console .dashboard footer{margin:-20px;padding:20px;background:#fcfeff;border:none;color:var(--config-color-link)}.console .dashboard .col{position:relative}.console .dashboard .col:last-child:after{display:none}.console .dashboard .col:after{content:"";display:block;width:2px;background:var(--config-console-background);height:calc(100% + 110px);position:absolute;top:-20px;bottom:-20px;right:24px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console .dashboard .col:after{width:calc(100% + 40px);height:3px;position:static;margin:20px -20px}}.console .dashboard .value{color:var(--config-color-focus);vertical-align:bottom;line-height:45px}.console .dashboard .value .sum{font-size:45px;line-height:45px;font-weight:700;vertical-align:bottom}.console .dashboard .unit{font-weight:500;line-height:20px;vertical-align:bottom;font-size:16px;display:inline-block;margin-bottom:5px;margin-left:5px;color:var(--config-color-focus)}.console .dashboard .metric{color:var(--config-color-focus);font-weight:400;font-size:13px;line-height:16px}.console .dashboard .range{color:var(--config-color-fade);font-weight:400;font-size:14px;line-height:16px}.console .dashboard a{display:block;font-weight:400;font-size:14px;line-height:16px;padding:0;border:none}.console .chart-metric{width:19%}@media only screen and (min-width:551px) and (max-width:1198px),only screen and (max-width:550px){.console .chart-metric{width:100%}}.console .chart{width:100%;position:relative;height:0;padding-top:20px;padding-bottom:26%;margin-right:-2px;overflow:hidden;background-color:var(--config-color-background-fade);background-image:linear-gradient(transparent 1px,transparent 1px),linear-gradient(90deg,transparent 1px,transparent 1px),linear-gradient(var(--config-border-color) 1px,transparent 1px),linear-gradient(90deg,var(--config-border-color) 1px,transparent 1px);background-size:100px 100px,100px 100px,20px 20px,20px 20px;background-position:-2px -2px,-2px -2px,-1px -1px,-1px -1px;background-repeat:round;border:solid 1px var(--config-border-color);border-right:solid 1px transparent;border-bottom:solid 1px transparent}@media only screen and (min-width:551px) and (max-width:1198px),only screen and (max-width:550px){.console .chart{width:100%;padding-bottom:32%;float:none;margin-bottom:20px}}.console .chart canvas{position:absolute;bottom:0;display:block;height:100%;width:100%}.console .chart-notes{font-size:12px}.console .chart-notes li{line-height:20px;display:inline-block;margin-right:15px}.console .chart-notes li::before{display:inline-block;content:'';width:14px;height:14px;background:var(--config-color-normal);border-radius:50%;margin-right:8px;vertical-align:middle}.console .chart-notes li:nth-child(1){color:#29b5d9}.console .chart-notes li:nth-child(1)::before{background:#29b5d9}.console .chart-notes li:nth-child(2){color:#4eb55b}.console .chart-notes li:nth-child(2)::before{background:#4eb55b}.console .chart-notes li:nth-child(3){color:#ec9323}.console .chart-notes li:nth-child(3)::before{background:#ec9323}.console .community a{padding:0 10px;display:inline-block}.console .link-list li{margin-bottom:15px}.console .link-list i{display:inline-block;width:30px;height:30px;line-height:30px;text-align:center;background:var(--config-color-fade);color:var(--config-color-fade-super);border-radius:50%;margin-right:15px}.console .link-list i.fade{background:0 0;color:var(--config-color-fade)}.console .provider{width:50px;height:50px;background:#f5f5f5;color:#868686;line-height:50px;text-align:center;font-size:25px;border-radius:50%}.console .provider.facebook{color:#fff;background:#3b5998}.console .provider.twitter{color:#fff;background:#55beff}.console .provider.telegram{color:#fff;background:#3ba9e1}.console .provider.github{color:#fff;background:#24292e}.console .provider.whatsapp{color:#fff;background:#25d366}.console .provider.linkedin{color:#fff;background:#1074af}.console .provider.microsoft{color:#fff;background:#137ad4}.console .provider.google{color:#fff;background:#4489f1}.console .provider.bitbucket{color:#fff;background:#2a88fb}.console .provider.gitlab{color:#faa238;background:#30353e}.console .provider.instagram{color:#fff;background:radial-gradient(circle at 30% 107%,#fdf497 0,#fdf497 5%,#fd5949 45%,#d6249f 60%,#285aeb 90%)}.console .premium{z-index:3;margin-top:320px}.console .premium .message{height:190px;overflow:hidden;position:absolute;top:-280px}.console .premium:after{content:'';position:absolute;top:0;left:-20px;right:-20px;bottom:-20px;background:var(--config-color-background);opacity:.7;z-index:300}.console .app-section{height:90px}.console .confirm{background:var(--config-color-link);color:#fff;border-radius:25px;padding:12px;line-height:28px;text-align:center}.console .confirm .action{font-weight:500;cursor:pointer}.console .platforms{overflow:hidden}.console .platforms .box{overflow:hidden}.console .platforms .box img{width:50px;margin:0 auto;margin-bottom:20px}.console .platforms .box .cover{margin:-30px -30px 30px -30px;padding:30px}.console .platforms .box .cover.android{background:#a4ca24}.console .platforms .box .cover.android h1{color:#fff;font-size:18px;margin-top:20px}.console .platforms .col{text-align:center;line-height:30px}.console .platforms a{display:block;margin:-20px;padding:20px}.console .platforms a:hover{background:#fbfeff}.console .platforms img{display:block;margin:0 30px;width:calc(100% - 60px);border-radius:50%;margin-bottom:20px}.console .document-nav{display:none;position:sticky;top:90px}@media only screen and (min-width:1380px){.console .document-nav{display:block}}.console .document-nav ul{position:absolute;width:200px;left:-260px}.console .document-nav ul li{margin-bottom:20px}.console .document-nav ul li .selected{font-weight:500}.console .scroll-to{display:none}@media only screen and (min-width:1199px){.console .logo .top{display:none!important}}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console>header{width:calc(100% - 30px)!important;margin:0 -30px;padding:15px}.console>header nav{width:100%;height:70px;overflow:hidden}.console>header nav.close{background:0 0}.console>header nav.close .logo .nav{display:none!important}.console>header nav.open{height:100%}.console>header nav.open .logo .top{display:none!important}.console>header nav.open .bottom{display:block!important}.console>header nav.open button{color:#87a5b9}.console>header nav button{margin:9px;background:0 0;color:var(--config-color-normal)}.console>header nav button:focus,.console>header nav button:hover{background:0 0}.console>header nav .logo{display:block!important;position:absolute;top:0;left:50%;margin:auto;transform:translateX(-50%)}.console>header nav .bottom{display:none!important}.console>footer{width:auto;margin:50px -30px 0 -30px!important;padding:0 30px 30px 30px}.console body{height:"calc(100% - 70px)"!important;width:calc(100% - 60px)!important;padding:70px 30px 0 30px!important}.console .cover{padding:25px 30px;margin:0 -30px}}@media only screen and (max-width:550px){.console body{height:"calc(100% - 70px)"!important;width:calc(100% - 40px)!important;padding:70px 20px 0 20px!important}.console .cover{padding:20px 20px;margin:0 -20px}.console>header{margin:0 -20px}.console>header .list{width:175px;font-size:14px}.console>footer{margin:50px -20px 0 -20px!important;padding:0 20px 20px 20px}}.dev-feature{display:none}.prod-feature{display:none}.development .dev-feature{display:block;opacity:.6!important;outline:solid #ff0 3px;outline-offset:3px}.development .dev-feature.dev-inline{display:inline-block}.development .prod-feature{display:none}.production .dev-feature{display:none}.production .prod-feature{display:block}.search{opacity:1!important}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.search button{margin-top:20px}}html.home body{padding:0 50px;color:var(--config-color-normal)}html.home .logo a{display:block}html.home .logo a:hover{opacity:.8}html.home .logo img{max-height:35px;width:198px;margin:45px auto 25px auto}html.home footer{background:0 0;text-align:center}html.home main{min-height:400px}.alerts ul{width:100%;visibility:hidden;position:fixed;padding:0;right:0;left:0;color:var(--config-color-normal);z-index:1002;margin:0 auto;bottom:15px;max-width:560px}.alerts ul li{margin:10px 0 0 0;padding:0}.alerts ul li div.message{position:relative;padding:12px 35px;margin:0 auto;list-style:none;background:var(--config-color-background-dark);text-align:center;font-size:14px;border-radius:10px;line-height:16px;min-height:16px;box-shadow:0 0 10px rgba(0,0,0,.05);opacity:.95}.alerts ul li div.message a,.alerts ul li div.message span{font-weight:600}.alerts ul li div.message i{cursor:pointer;position:absolute;font-size:14px;line-height:22px;top:8px;left:8px}.alerts ul li div.message.error{color:#fff;background:var(--config-color-danger)}.alerts ul li div.message.error a{color:#fff;border-bottom:dotted 1px #fff}.alerts ul li div.message.success{color:#fff;background:var(--config-color-success)}.alerts ul li div.message.success a{color:#fff;border-bottom:dotted 1px #fff}.alerts ul li div.message.warning{color:#fff;background:var(--config-color-success)}.alerts ul li div.message.warning a{color:#fff;border-bottom:dotted 1px #fff}.alerts ul li div.message.open{display:block}.alerts ul li div.message.close{display:none}.alerts .cookie-alert{background:var(--config-color-focus-fade)!important;color:var(--config-color-focus)}.alerts .cookie-alert a{color:var(--config-color-focus);font-weight:400;border-bottom:dotted 1px var(--config-color-focus)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.alerts ul{top:auto;bottom:0;max-width:100%}.alerts ul li{margin:5px 0 0 0}.alerts ul li div.message{border-radius:0}}article{overflow-wrap:break-word;word-wrap:break-word}article h1{font-size:36px}article h2{font-size:24px}article h3{font-size:20px}article h4{font-size:20px}article h5{font-size:18px}article h6{font-size:16px}article h1,article h2,article h3,article h4,article h5,article h6{margin-top:30px!important;margin-bottom:30px!important}article p{line-height:32px;font-size:16px}article .update{display:block;margin-top:50px!important}article table{width:100%;margin:0;margin-bottom:30px!important;border-radius:0;border-bottom:solid 1px var(--config-border-color)}article table thead td{font-weight:500;padding:5px 15px}article table td,article table th{padding:15px;height:auto}article table td:first-child,article table th:first-child{padding-left:10px}article table td:last-child,article table th:last-child{padding-right:10px}article table td p,article table th p{font-size:inherit;line-height:inherit}article table td p:last-child,article table th p:last-child{margin:0}.avatar-container{position:relative}.avatar-container .corner{position:absolute;bottom:-3px;right:-3px}.avatar{width:60px;height:60px;border-radius:50%;background:var(--config-color-background-focus);display:inline-block;overflow:hidden;box-shadow:0 0 6px rgba(0,0,0,.09);position:relative;z-index:1;opacity:1!important}.avatar:before{content:"";position:absolute;width:100%;height:100%;z-index:0;background:var(--config-color-background-focus)}.avatar.inline{display:inline-block;vertical-align:middle}.avatar.trans{background:0 0}.avatar .no-shadow{box-shadow:none}.avatar.xs{width:30px;height:30px}.avatar.xxs{width:20px;height:20px}.avatar.small{width:50px;height:50px}.avatar.big{width:100px;height:100px}.avatar.huge{width:150px;height:150px}.box{position:relative;background:var(--config-color-background-fade);border-radius:10px;box-shadow:0 0 3px rgba(0,0,0,.05);padding:30px;display:block;border-bottom:none}.box.padding-small{padding:15px}.box.y-scroll{overflow-y:auto}.box.danger{background:var(--config-color-danger);color:#fff}.box.danger .box{color:var(--config-color-normal);background:var(--config-color-background-fade)}.box.danger>.button,.box.danger>button{background:#fff;color:var(--config-color-danger)}.box.note{background:var(--config-note-background)}.box.focus{background:var(--config-color-focus);color:var(--config-color-background-fade)}.box.focus .button,.box.focus button{background:var(--config-color-background-fade);color:var(--config-color-focus)}.box.line{background:0 0;border:solid 1px var(--config-color-background-dark);box-shadow:none}.box.warning{background:var(--config-color-success);color:#2d2d2d}.box.warning .button,.box.warning button{background:rgba(45,45,45,.8);color:var(--config-color-success)}.box .tabs{border-bottom:solid 1px var(--config-border-color);margin:0 -30px;padding:0 30px!important}.box>footer{margin:0 -30px -30px -30px;padding:15px 30px;background:var(--config-color-background-fade);border:solid 1px var(--config-border-color);border-radius:0 0 10px 10px}.box hr{height:1px;background:var(--config-console-background);border:none;margin:30px -30px}.box .label{position:absolute;top:10px;z-index:2;right:10px}.box.fade-bottom{position:relative;overflow:hidden}.box.fade-bottom:after{content:"";position:absolute;display:block;bottom:15px;width:100%;background:#000;background:linear-gradient(180deg,rgba(0,0,0,0) 0,var(--config-color-background-fade) 80%);height:100px;margin:0 -15px}.box .header{position:static;height:40px;padding:20px 30px 20px 30px;margin-bottom:30px;margin:-30px -30px 20px -30px;background:var(--config-color-background-fade);border-bottom:solid 1px #efefef}.box ul.numbers>li{position:relative;margin-left:30px;margin-right:50px}.box ul.numbers>li hr{margin-left:-60px;margin-right:-80px}.box ul.numbers>li .settings{position:absolute;top:3px;right:-50px}.box ul.numbers>li::after{display:block;width:25px;height:25px;line-height:25px;font-size:13px;font-weight:500;border-radius:50%;background:var(--config-color-focus);color:var(--config-color-background);counter-increment:section;content:counter(section);text-align:center;position:absolute;top:3px;left:-45px}.box .scroll{margin:0 -30px;overflow-y:scroll}.box .scroll table{width:100%;margin:0}.box ul.sortable{counter-reset:section}.box ul.sortable>li [data-move-down].round,.box ul.sortable>li [data-move-up].round,.box ul.sortable>li [data-remove].round{background:var(--config-color-focus);color:var(--config-color-background-fade);width:25px;height:25px;line-height:25px;display:inline-block;text-align:center;padding:0;margin-right:5px}.box ul.sortable>li [data-move-down].round:disabled,.box ul.sortable>li [data-move-up].round:disabled,.box ul.sortable>li [data-remove].round:disabled{display:none}.box ul.sortable>li:first-child [data-move-up]{display:none}.box ul.sortable>li:first-child [data-move-up]:disabled{display:inline-block;background:var(--config-color-background)}.box ul.sortable>li:last-child [data-move-down]{display:none}.box ul.sortable>li:last-child [data-move-down]:disabled{display:inline-block;background:var(--config-color-background)}.box .toggle{position:relative;border-top:1px solid var(--config-console-background);border-bottom:1px solid var(--config-console-background);margin:0 -30px;padding:30px 30px 0 30px;height:65px;overflow:hidden}.box .toggle.list{border-bottom:none}.box .toggle.sorts button.ls-ui-open{width:calc(100% - 100px)}.box .toggle button.ls-ui-open{right:0;position:absolute;top:0;width:100%;height:95px;background:0 0;opacity:.5;border-radius:0}.box .toggle .icon-minus,.box .toggle .icon-up-open{display:none}.box .toggle .content{display:none}.box .toggle.open{height:auto}.box .toggle.open .icon-minus,.box .toggle.open .icon-up-open{display:block}.box .toggle.open .icon-down-open,.box .toggle.open .icon-plus{display:none}.box .toggle.open .content{display:block}.box .list li{border-bottom:solid 2px var(--config-border-color);margin:0 -30px 30px -30px;padding:0 30px 30px 30px}.box .list li:last-child{padding-bottom:0;margin-bottom:0;border-bottom:none}@media only screen and (max-width:550px){.box .list li .actions{float:none}}.box .list li .avatar{display:block}.box .list li .avatar.inline{display:inline-block}.box.new{text-align:center}.box.new i{font-size:80px;line-height:80px;font-family:Poppins,sans-serif;font-style:normal;font-weight:300}.box.new b{margin-top:20px;display:block}.box .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.box .info hr{background:var(--config-modal-note-border)!important}.box .table-wrap{margin:0 -30px;overflow-y:scroll}.box .table-wrap table{margin:0}a.box{border-right:none;border-left:none}a.box:hover{box-shadow:0 0 1px rgba(0,0,0,.2);opacity:.7}.box-asidex{padding-right:25px!important;padding-left:70px;right:0;background:#f9f9f9;border-radius:0 10px 10px 0;height:calc(100% - 30px);position:absolute;padding-top:30px}.box-asidex:after{content:"";display:block;position:absolute;height:100%;width:51px;background:#fff;top:0;bottom:0;left:-6px}.cover{background:var(--config-color-focus-fade);padding:30px 50px;margin:0 -50px;position:relative;border-bottom:solid 1px var(--config-border-fade)}.cover .title,.cover h1,.cover h2,.cover h3,.cover h4{color:var(--config-color-focus);font-weight:600;margin-bottom:50px!important;font-size:28px;line-height:42px}.cover .title span,.cover h1 span,.cover h2 span,.cover h3 span,.cover h4 span{font-weight:600}.cover i:before{margin:0!important}.cover p{color:var(--config-color-fade)}.cover .button{color:#fff}.cover .link,.cover a{color:var(--config-color-focus);border-left:none;border-right:none;cursor:pointer}.cover .link:hover,.cover a:hover{border-bottom-color:var(--config-color-focus)}.console .database .row .col{height:452px}.console .database .row .col:after{width:2px;right:20px}.console .database hr{margin:0 -20px;background:var(--config-color-background);height:1px}.console .database h3{font-size:13px;line-height:20px;height:20px;background-color:var(--config-color-fade-super);margin:-20px -20px 0 -20px;padding:10px 20px;border-bottom:solid 1px var(--config-color-background);font-weight:600}.console .database .empty{height:162px;font-size:12px;text-align:center;margin:50px 0}.console .database .empty h4{font-size:13px;font-weight:600;line-height:120px}.console .database .search{background-color:var(--config-color-fade-super);margin:0 -20px 0 -20px;padding:10px 15px}.console .database .search input{height:40px;background-color:#fff;border-radius:25px;padding-top:0;padding-bottom:0}.console .database .code{height:411px;background:var(--config-color-fade-super);margin:0 -20px -20px -20px;padding:20px;width:calc(100% - 10px)}.console .database .code .ide{overflow:scroll;height:451px;margin:-20px;box-shadow:none;border-radius:0}.console .database .paging{background:var(--config-color-fade-super);margin:0 -20px -20px -20px;padding:20px}.console .database .button{margin:0 -20px;padding:0 20px!important;text-align:inherit;color:var(--config-color-focus);width:100%;font-size:15px;line-height:55px;box-sizing:content-box}.console .database .button i{margin-right:8px}.console .database .button:hover{border:none;background:var(--config-color-focus-fade)}.console .database .items{margin:0 -20px;height:262px;overflow-x:hidden;overflow-y:scroll}.console .database .items form{opacity:0;position:relative}.console .database .items form button{position:absolute;top:0;bottom:0;left:0;right:0;width:100%;height:45px;border-radius:0;cursor:pointer}.console .database .items li{padding:0;margin:0 0;line-height:45px;font-size:15px;padding-left:50px;padding-right:30px;position:relative}.console .database .items li i{position:absolute;display:none;right:10px}.console .database .items li .name{display:inline-block;width:100%;height:28px}.console .database .items li.selected,.console .database .items li:hover{background:#f5f5f5}.console .database .items li.selected i,.console .database .items li:hover i{display:block}.console .database .items li:last-child{border-bottom:none}body>footer{color:var(--config-color-fade);line-height:40px;margin:0 -50px;padding:12px 50px;font-size:13px;width:100%;background:#f1f1f1;position:relative;margin-top:80px!important}body>footer:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}body>footer .logo img{height:22px;padding-top:12px}body>footer a{color:var(--config-color-fade);font-size:13px}body>footer a:hover{border-bottom-color:var(--config-color-fade)}body>footer ul:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}body>footer ul li{font-size:13px;float:left;margin-right:20px!important}body>footer .copyright{padding-left:2px}[data-ls-if]{display:none}[data-service]{opacity:0}.load-service-start{opacity:0}.load-service-end{opacity:1;transition:opacity .5s ease-out;-moz-transition:opacity .5s ease-out;-webkit-transition:opacity .5s ease-out;-o-transition:opacity .5s ease-out}.load-screen{z-index:100000;position:fixed;height:100%;width:100%;background-color:var(--config-color-background-focus);top:0;left:0}.load-screen.loaded{transition:opacity 1s ease-in-out,top 1s .7s;opacity:0;top:-100%}.load-screen .animation{position:absolute;top:45%;left:50%;transform:translate(-50%,-50%) translateZ(1px);width:140px;height:140px}.load-screen .animation div{box-sizing:border-box;display:block;position:absolute;width:124px;height:124px;margin:10px;border:10px solid var(--config-color-focus);border-radius:50%;animation:animation 1.2s cubic-bezier(.5,0,.5,1) infinite;border-color:var(--config-color-focus) transparent transparent transparent}.load-screen .animation div:nth-child(1){animation-delay:-.45s}.load-screen .animation div:nth-child(2){animation-delay:-.3s}.load-screen .animation div:nth-child(3){animation-delay:-.15s}@keyframes animation{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.load-screen img{position:absolute;height:20px;bottom:60px;left:50%;transform:translate(-50%,-50%)}.modal-open .modal-bg,.modal-open body .modal-bg{position:fixed;content:'';display:block;width:100%;height:100%;left:0;right:0;top:0;bottom:0;background:#0c0c0c;opacity:.5;z-index:4}.modal{overflow:auto;display:none;position:fixed;transform:translate3d(0,0,0);width:100%;max-height:90%;max-width:640px;background:var(--config-color-background-fade);z-index:1000;box-shadow:0 0 4px rgba(0,0,0,.25);padding:30px;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:10px;box-sizing:border-box;text-align:left;white-space:initial;line-height:normal}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.modal{width:calc(100% - 20px)}}.modal.full{max-width:none;max-height:none;height:100%;border-radius:0;padding:80px 120px}.modal.full h1{font-weight:700}.modal.padding-small{padding:15px}.modal.height-tiny>form{height:100px}.modal.height-small>form{height:220px}.modal.width-small{max-width:400px}.modal.width-medium{max-width:500px}.modal.width-large{max-width:800px}.modal.open{display:block}.modalbutton.close{display:none}.modal.fill{height:95%;max-height:95%;max-width:75%}.modal h1,.modal h2{margin-bottom:25px;margin-top:0;font-size:20px;text-align:left}.modal h1,.modal h2,.modal h3,.modal h4,.modal h5,.modal h6{color:inherit!important;line-height:35px}.modal .main,.modal>form{position:relative;border-top:solid 1px var(--config-border-color);padding:30px 30px 0 30px;margin:0 -30px}.modal .main.strip,.modal>form.strip{border:none;padding:0;margin:0}.modal .separator{margin:20px -30px}.modal .bullets{padding-left:40px}.modal .bullets li{margin-bottom:30px!important}.modal .bullets li:before{position:absolute}.modal .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.modal .ide.strech{box-shadow:none;border-radius:0;margin:0 -30px}.modal button.close{width:30px;height:30px;line-height:30px;padding:0;margin:0;background:var(--config-color-normal);color:var(--config-color-background-fade);border-radius:50%}.modal .paging form{padding:0;margin:0;border-top:none}.modal.sticky-footer form footer{margin:-30px}.modal.sticky-footer footer{position:sticky;bottom:-30px;background:var(--config-color-background-fade-super);height:50px;z-index:1;padding:30px;box-shadow:0 0 1px rgba(0,0,0,.15)}.modal.sticky-footer footer form{display:inline-block}[data-views-current="0"] .scroll-to,[data-views-current="1"] .scroll-to{opacity:0!important}.scroll-to-bottom .scroll-to,.scroll-to-top .scroll-to{opacity:1}.scroll-to{opacity:0;display:block;width:40px;height:40px;line-height:40px;border-radius:50%;position:fixed;transform:translateZ(0);margin:30px;padding:0;bottom:0;font-size:18px;z-index:100000;transition:opacity .15s ease-in-out;right:0}.phases{list-style:none;margin:0;padding:0;position:relative}.phases li{display:none}.phases li li{display:block}.phases li.selected{display:block}.phases .number{display:none}.phases h2,.phases h3,.phases h4,.phases h5,.phases h6{margin:0 0 30px 0;text-align:inherit}.container{position:relative}.container .tabs{height:55px;line-height:55px;list-style:none;padding:0;margin-bottom:50px!important;margin-top:-55px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.container .tabs:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.container .tabs .selected{font-weight:400;color:var(--config-color-focus);position:relative;opacity:1}.container .tabs .selected:after{content:"";display:block;height:2px;background:var(--config-color-focus);width:calc(100% + 6px);margin:0 -3px;position:absolute;bottom:0;border-radius:2px}.container .tabs .number{display:none}.container .tabs li{float:left;margin-right:50px;color:var(--config-color-focus);opacity:.9;cursor:pointer}.container .tabs li:focus{outline:0}@media only screen and (max-width:550px){.container .tabs li{margin-right:25px}}.container .icon{display:none}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.container .tabs{width:auto;overflow-x:scroll;overflow-y:hidden;white-space:nowrap}.container .tabs li{display:inline-block;float:none}}.ide{background-color:var(--config-prism-background);overflow:hidden;position:relative;z-index:1;box-shadow:0 2px 4px 0 rgba(50,50,93,.3);border-radius:10px;margin-bottom:30px}.ide *{font-family:'Source Code Pro',monospace}.ide[data-lang]::after{content:attr(data-lang-label);display:inline-block;background:#fff;color:#000;position:absolute;top:15px;padding:5px 10px;border-radius:15px;font-size:10px;right:10px;opacity:.95}.ide[data-lang=bash]::after{background:var(--config-language-bash);color:var(--config-language-bash-contrast)}.ide[data-lang=javascript]::after{background:var(--config-language-javascript);color:var(--config-language-javascript-contrast)}.ide[data-lang=web]::after{background:var(--config-language-web);color:var(--config-language-web-contrast)}.ide[data-lang=html]::after{background:var(--config-language-html);color:var(--config-language-html-contrast)}.ide[data-lang=php]::after{background:var(--config-language-php);color:var(--config-language-php-contrast)}.ide[data-lang=nodejs]::after{background:var(--config-language-nodejs);color:var(--config-language-nodejs-contrast)}.ide[data-lang=ruby]::after{background:var(--config-language-ruby);color:var(--config-language-ruby-contrast)}.ide[data-lang=python]::after{background:var(--config-language-python);color:var(--config-language-python-contrast)}.ide[data-lang=go]::after{background:var(--config-language-go);color:var(--config-language-go-contrast)}.ide[data-lang=dart]::after{background:var(--config-language-dart);color:var(--config-language-dart-contrast)}.ide[data-lang=flutter]::after{background:var(--config-language-flutter);color:var(--config-language-flutter-contrast)}.ide[data-lang=yaml]::after{background:var(--config-language-yaml);color:var(--config-language-yaml-contrast)}.ide .tag{color:inherit!important;background:0 0!important;padding:inherit!important;font-size:inherit!important;line-height:14px}.ide .copy{cursor:pointer;content:attr(data-lang);display:inline-block;background:#fff;color:#000;position:absolute;transform:translateX(-50%);bottom:-20px;padding:5px 10px;border-radius:15px;font-size:10px;font-style:normal;left:50%;opacity:0;transition:bottom .3s,opacity .3s;line-height:normal;font-family:Poppins,sans-serif}.ide .copy::before{padding-right:5px}.ide:hover .copy{transition:bottom .3s,opacity .3s;opacity:.9;bottom:16px}.ide pre{-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;color:#e6ebf1;font-weight:400;line-height:20px;font-size:13px;margin:0;padding:20px;padding-left:60px}.ide.light{box-shadow:0 2px 4px 0 rgba(50,50,93,.1);background-color:#fff}.ide.light pre{color:#414770}.ide.light .token.cdata,.ide.light .token.comment,.ide.light .token.doctype,.ide.light .token.prolog{color:#91a2b0}.ide.light .token.attr-name,.ide.light .token.builtin,.ide.light .token.char,.ide.light .token.inserted,.ide.light .token.selector,.ide.light .token.string{color:#149570}.ide.light .token.punctuation{color:#414770}.ide.light .language-css .token.string,.ide.light .style .token.string,.ide.light .token.entity,.ide.light .token.operator,.ide.light .token.url,.ide.light .token.variable{color:#414770}.ide.light .line-numbers .line-numbers-rows{background:#f2feef}.ide.light .line-numbers-rows>span:before{color:#5dc79e}.ide.light .token.keyword{color:#6772e4;font-weight:500}code[class*=language-],pre[class*=language-]{text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4}pre[class*=language-]{overflow:auto}:not(pre)>code[class*=language-]{padding:.1em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#6b7c93}.token.punctuation{color:#f8f8f2}.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#f79a59}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#3ecf8e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#45b2e8}.token.keyword{color:#7795f8}.token.important,.token.regex{color:#fd971f}.token.italic{font-style:italic}.token.entity{cursor:help}pre[class*=language-].line-numbers{position:relative;padding-left:60px;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{background:var(--config-prism-numbers);position:absolute;pointer-events:none;top:-20px;bottom:-21px;padding:20px 0;font-size:100%;left:-60px;width:40px;letter-spacing:-1px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{padding-right:5px;pointer-events:none;display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#636365;display:block;padding-right:.8em;text-align:right}html{padding:0;margin:0;direction:ltr}body{margin:0;background:var(--config-console-background) no-repeat fixed;min-width:300px}ul{padding:0;margin:0}ul li{margin:0;list-style:none} \ No newline at end of file diff --git a/public/dist/styles/default-rtl.css b/public/dist/styles/default-rtl.css index 594940608c..f4cbf15b79 100644 --- a/public/dist/styles/default-rtl.css +++ b/public/dist/styles/default-rtl.css @@ -1 +1 @@ -.pull-start{float:right}.pull-end{float:left}img[src=""]{visibility:hidden;display:inline-block}:root{--config-width:910px;--config-width-xxl:1000px;--config-width-xl:910px;--config-width-large:700px;--config-width-medium:550px;--config-width-small:320px;--config-color-link:#1e849e;--config-color-background:#eceff1;--config-color-background-dark:#dfe2e4;--config-color-background-fade:#ffffff;--config-color-background-fade-super:#fdfdfd;--config-color-background-focus:#f5f5f5;--config-color-background-input:#ffffff;--config-color-placeholder:#868686;--config-color-tooltip-text:#dce8f5;--config-color-tooltip-background:#333333;--config-color-focus:#f02e65;--config-color-focus-fade:#fef8fa;--config-color-focus-hover:#ff729b;--config-color-focus-glow:#fce5ec;--config-color-focus-dark:#c52653;--config-color-normal:#40404c;--config-color-dark:#313131;--config-color-fade:#8f8f8f;--config-color-fade-light:#e2e2e2;--config-color-fade-super:#f1f3f5;--config-color-danger:#f53d3d;--config-color-success:#1bbf61;--config-color-warning:#ffed4d;--config-color-info:#386fd2;--config-border-color:#f5f5f5;--config-border-radius:10px;--config-prism-background:#373738;--config-prism-numbers:#39393c;--config-note-background:#f1fbff;--config-note-border:#5bceff;--config-warning-background:#fdf7d9;--config-warning-border:#f8e380;--config-social-twitter:#1da1f2;--config-social-github:#000000;--config-social-discord:#7189dc;--config-social-facebook:#4070b4;--config-language-bash:#2b2626;--config-language-bash-contrast:#fff;--config-language-javascript:#fff054;--config-language-javascript-contrast:#333232;--config-language-web:#fff054;--config-language-web-contrast:#333232;--config-language-html:#ff895b;--config-language-html-contrast:#ffffff;--config-language-yaml:#ca3333;--config-language-yaml-contrast:#ffffff;--config-language-php:#6182bb;--config-language-php-contrast:#ffffff;--config-language-nodejs:#8cc500;--config-language-nodejs-contrast:#ffffff;--config-language-ruby:#fc3f48;--config-language-ruby-contrast:#ffffff;--config-language-python:#3873a2;--config-language-python-contrast:#ffffff;--config-language-go:#00add8;--config-language-go-contrast:#ffffff;--config-language-dart:#035698;--config-language-dart-contrast:#ffffff;--config-language-flutter:#035698;--config-language-flutter-contrast:#ffffff;--config-modal-note-background:#f5fbff;--config-modal-note-border:#eaf2f7;--config-modal-note-color:#3b5d73;--config-switch-background:#e2e2e2;--config-console-background:#eceff1;--config-console-nav-start:#143650;--config-console-nav-end:#302839;--config-console-nav-border:#2a253a;--config-console-nav-switch-background:#ececec;--config-console-nav-switch-color:#868686;--config-console-nav-switch-arrow:url("data:image/svg+xml;utf8,")}:root .theme-dark{--config-color-background:#061F2F;--config-color-background-dark:#262d50;--config-color-background-fade:#1c223a;--config-color-background-fade-super:#1a1f35;--config-color-background-focus:#1a1f35;--config-color-background-input:#dce8f5;--config-color-tooltip-text:#061F2F;--config-color-tooltip-background:#dce8f5;--config-color-link:#4caedb;--config-color-placeholder:#9ea1af;--config-color-focus:#c7d8eb;--config-color-focus-fade:#1e233e;--config-color-focus-hover:#d3deea;--config-color-focus-glow:#d3deea;--config-color-focus-dark:#657586;--config-color-normal:#c7d8eb;--config-color-dark:#c7d8eb;--config-color-fade:#bec3e0;--config-color-fade-light:#181818;--config-color-fade-super:#262D50;--config-color-danger:#d84a4a;--config-color-success:#34b86d;--config-color-warning:#e0d56d;--config-color-info:#386fd2;--config-border-color:#262D50;--config-prism-background:#1F253F;--config-prism-numbers:#1F253F;--config-note-background:#171e33;--config-note-border:#262D50;--config-warning-background:#1F253F;--config-warning-border:#262D50;--config-social-twitter:var(--config-color-normal);--config-social-github:var(--config-color-normal);--config-social-discord:var(--config-color-normal);--config-social-facebook:var(--config-color-normal);--config-language-bash:var(--config-color-normal);--config-language-bash-contrast:var(--config-color-background);--config-language-javascript:var(--config-color-normal);--config-language-javascript-contrast:var(--config-color-background);--config-language-web:var(--config-color-normal);--config-language-web-contrast:var(--config-color-background);--config-language-yaml:var(--config-color-normal);--config-language-yaml-contrast:var(--config-color-background);--config-language-html:var(--config-color-normal);--config-language-html-contrast:var(--config-color-background);--config-language-php:var(--config-color-normal);--config-language-php-contrast:var(--config-color-background);--config-language-nodejs:var(--config-color-normal);--config-language-nodejs-contrast:var(--config-color-background);--config-language-ruby:var(--config-color-normal);--config-language-ruby-contrast:var(--config-color-background);--config-language-python:var(--config-color-normal);--config-language-python-contrast:var(--config-color-background);--config-language-go:var(--config-color-normal);--config-language-go-contrast:var(--config-color-background);--config-language-dart:var(--config-color-normal);--config-language-dart-contrast:var(--config-color-background);--config-language-flutter:var(--config-color-normal);--config-language-flutter-contrast:var(--config-color-background);--config-modal-note-background:#15192b;--config-modal-note-border:#161b31;--config-modal-note-color:var(--config-color-normal);--config-switch-background:var(--config-color-normal);--config-console-background:#20263f;--config-console-nav-start:#1c2139;--config-console-nav-end:#151929;--config-console-nav-border:#171b30;--config-console-nav-switch-background:var(--config-color-focus);--config-console-nav-switch-color:var(--config-color-background);--config-console-nav-switch-arrow:url("data:image/svg+xml;utf8,")}.theme-light .force-light{display:block!important}.theme-dark .force-dark{display:block!important}.force-dark{display:none!important}.force-light{display:none!important}@font-face{font-family:Poppins;font-style:normal;font-weight:100;src:url(/fonts/poppins-v9-latin-100.eot);src:local('Poppins Thin'),local('Poppins-Thin'),url(/fonts/poppins-v9-latin-100.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-100.woff2) format('woff2'),url(/fonts/poppins-v9-latin-100.woff) format('woff'),url(/fonts/poppins-v9-latin-100.ttf) format('truetype'),url(/fonts/poppins-v9-latin-100.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:300;src:url(/fonts/poppins-v9-latin-300.eot);src:local('Poppins Light'),local('Poppins-Light'),url(/fonts/poppins-v9-latin-300.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-300.woff2) format('woff2'),url(/fonts/poppins-v9-latin-300.woff) format('woff'),url(/fonts/poppins-v9-latin-300.ttf) format('truetype'),url(/fonts/poppins-v9-latin-300.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:400;src:url(/fonts/poppins-v9-latin-regular.eot);src:local('Poppins Regular'),local('Poppins-Regular'),url(/fonts/poppins-v9-latin-regular.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-regular.woff2) format('woff2'),url(/fonts/poppins-v9-latin-regular.woff) format('woff'),url(/fonts/poppins-v9-latin-regular.ttf) format('truetype'),url(/fonts/poppins-v9-latin-regular.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:500;src:url(/fonts/poppins-v9-latin-500.eot);src:local('Poppins Medium'),local('Poppins-Medium'),url(/fonts/poppins-v9-latin-500.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-500.woff2) format('woff2'),url(/fonts/poppins-v9-latin-500.woff) format('woff'),url(/fonts/poppins-v9-latin-500.ttf) format('truetype'),url(/fonts/poppins-v9-latin-500.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:600;src:url(/fonts/poppins-v9-latin-600.eot);src:local('Poppins SemiBold'),local('Poppins-SemiBold'),url(/fonts/poppins-v9-latin-600.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-600.woff2) format('woff2'),url(/fonts/poppins-v9-latin-600.woff) format('woff'),url(/fonts/poppins-v9-latin-600.ttf) format('truetype'),url(/fonts/poppins-v9-latin-600.svg#Poppins) format('svg')}@font-face{font-family:'Source Code Pro';font-style:normal;font-weight:400;src:url(/fonts/source-code-pro-v11-latin-regular.eot);src:local('Source Code Pro Regular'),local('SourceCodePro-Regular'),url(/fonts/source-code-pro-v11-latin-regular.eot?#iefix) format('embedded-opentype'),url(/fonts/source-code-pro-v11-latin-regular.woff2) format('woff2'),url(/fonts/source-code-pro-v11-latin-regular.woff) format('woff'),url(/fonts/source-code-pro-v11-latin-regular.ttf) format('truetype'),url(/fonts/source-code-pro-v11-latin-regular.svg#SourceCodePro) format('svg')}.padding{padding:30px}.padding-top{padding-top:30px!important}.padding-top-large{padding-top:50px!important}.padding-top-xl{padding-top:80px!important}.padding-bottom{padding-bottom:30px!important}.padding-bottom-large{padding-bottom:50px!important}.padding-bottom-xl{padding-bottom:80px!important}.margin-end{margin-left:20px!important}.margin-start{margin-right:20px!important}.margin-end-small{margin-left:10px!important}.margin-start-small{margin-right:10px!important}.margin-end-large{margin-left:50px!important}.margin-start-large{margin-right:50px!important}.margin-end-no{margin-left:0!important}.margin-start-no{margin-right:0!important}.margin-end-negative{margin-left:-30px!important}.margin-start-negative{margin-right:-30px!important}.margin-end-negative-small{margin-left:-15px!important}.margin-start-negative-small{margin-right:-15px!important}.margin-end-negative-tiny{margin-left:-5px!important}.margin-start-negative-tiny{margin-right:-5px!important}.margin-top{margin-top:30px!important}.margin-bottom{margin-bottom:30px!important}.margin-top-no{margin-top:0!important}.margin-bottom-no{margin-bottom:0!important}.margin-top-xxl{margin-top:140px!important}.margin-top-xl{margin-top:80px!important}.margin-top-large{margin-top:50px!important}.margin-top-small{margin-top:15px!important}.margin-top-tiny{margin-top:5px!important}.margin-top-negative{margin-top:-30px!important}.margin-top-negative-tiny{margin-top:-5px!important}.margin-top-negative-small{margin-top:-15px!important}.margin-top-negative-large{margin-top:-50px!important}.margin-top-negative-xl{margin-top:-80px!important}.margin-top-negative-xxl{margin-top:-100px!important}.margin-bottom-xxl{margin-bottom:140px!important}.margin-bottom-xl{margin-bottom:80px!important}.margin-bottom-large{margin-bottom:50px!important}.margin-bottom-small{margin-bottom:15px!important}.margin-bottom-tiny{margin-bottom:5px!important}.margin-bottom-negative{margin-bottom:-30px!important}.margin-bottom-negative-tiny{margin-bottom:-5px!important}.margin-bottom-negative-small{margin-bottom:-15px!important}.margin-bottom-negative-large{margin-bottom:-50px!important}.margin-bottom-negative-xl{margin-bottom:-80px!important}.margin-bottom-negative-xl{margin-bottom:-100px!important}.force-left,.ide{direction:ltr;text-align:left}.force-right{direction:rtl;text-align:right}.pull-left{float:left}.pull-right{float:right}.ratio-wide{height:0;overflow:hidden;padding-top:56%;position:relative;width:100%}.ratio-wide>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-square{height:0;overflow:hidden;padding-top:56%;position:relative;width:100%}.ratio-square>*{position:absolute;top:0;left:0;width:100%;height:100%}.clear:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.phones-only{display:none}@media only screen and (max-width:550px){.phones-only{display:inherit!important}}.tablets-only{display:none}@media only screen and (min-width:551px) and (max-width:1198px){.tablets-only{display:inherit!important}}.desktops-only{display:none}@media only screen and (min-width:1199px){.desktops-only{display:inherit!important}}.phones-only-inline{display:none}@media only screen and (max-width:550px){.phones-only-inline{display:inline-block!important}}.tablets-only-inline{display:none}@media only screen and (min-width:551px) and (max-width:1198px){.tablets-only-inline{display:inline-block!important}}.desktops-only-inline{display:none}@media only screen and (min-width:1199px){.desktops-only-inline{display:inline-block!important}}*{font-family:Poppins,sans-serif;-webkit-font-smoothing:antialiased;font-weight:300}h1,h2,h3,h4,h5,h6{margin:0}h4,h5,h6{font-weight:400}.link,a{color:var(--config-color-link);text-decoration:none;border-left:2px solid transparent;border-right:2px solid transparent;transition:.2s;cursor:pointer}.link.disabled,a.disabled{opacity:.5}.link.tag:hover,a.tag:hover{opacity:.9}.link.danger,a.danger{color:var(--config-color-danger)}.link.link-animation-enabled,a.link-animation-enabled{display:inline-block}.link.link-animation-enabled:hover,a.link-animation-enabled:hover{transform:translateY(-2px)}.link-return-animation--start>i{display:inline-block;transition:.2s}.link-return-animation--start:hover>i{transform:translateX(2px)}.link-return-animation--end>i{display:inline-block;transition:.2s}.link-return-animation--end:hover>i{transform:translateX(-2px)}b,strong{font-weight:500}p{margin:0 0 20px 0;line-height:26px}small{font-size:16px;color:var(--config-color-fade)}.text-size-small{font-size:13px}.text-size-xs{font-size:10px}.text-size-normal{font-size:16px}.text-height-large{height:30px;line-height:30px}.text-height-small{line-height:13px}.text-one-liner{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.text-bold{font-weight:400!important}.text-bold-large{font-weight:500!important}.text-bold-xl{font-weight:600!important}.text-danger{color:var(--config-color-danger)!important}.text-success{color:var(--config-color-success)!important}.text-upper{text-transform:uppercase}.text-warning{color:var(--config-color-warning)}.text-focus{color:var(--config-color-focus)}.text-fade{color:var(--config-color-fade)}.text-green{color:var(--config-color-success)}.text-red{color:var(--config-color-danger)}.text-info{color:var(--config-color-info)}.text-yellow{color:#ffe28b}.text-disclaimer{font-size:11px;color:var(--config-color-fade)}.text-fade-extra{color:var(--config-color-fade);opacity:.5}.text-line-high-large{line-height:30px}.text-line-high-xl{line-height:40px}.text-sign{margin:5px 0;font-size:25px;width:25px;height:25px;line-height:25px;display:inline-block}.text-align-center{text-align:center}.text-align-start{text-align:right}.text-align-end{text-align:left}.text-align-left{text-align:left}.text-align-right{text-align:right}.text-dir-ltr{direction:ltr;display:inline-block}.text-dir-rtl{direction:rtl;display:inline-block}.icon-dot-3:before{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-o-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}i[class*=' icon-']:before,i[class^=icon-]:before{display:inline;line-height:unset}table{width:calc(100% + 60px);border-collapse:collapse;margin:-30px;border-radius:10px;overflow:hidden;position:relative;table-layout:fixed}table.y-scroll{overflow-y:auto}table thead{box-shadow:0 0 2px rgba(0,0,0,.25);border-bottom:solid 1px var(--config-color-fade-super);font-size:14px}table.small{font-size:14px}table.open-end tbody tr:last-child{border-bottom:none;font-weight:700;background:#f7fbf7}table.full tbody td,table.full tbody th{vertical-align:top;white-space:normal;overflow:auto;line-height:24px;padding-top:20px;padding-bottom:20px;height:auto}table .avatar{width:30px;height:30px}table tr{border-bottom:solid 1px var(--config-color-fade-super)}table tr:last-child{border-bottom:none}table tr:nth-child(even){background:var(--config-color-background-fade-super)}table tr.selected{background:var(--config-note-background)}table tr.selected td,table tr.selected td span{font-weight:500}table th{text-align:right;font-weight:400}table th i{color:var(--config-color-fade);font-size:10px;display:inline-block;vertical-align:top;line-height:16px;padding:0 3px}table td,table th{height:65px;padding:0 15px;line-height:50px}table td:first-child,table th:first-child{padding-right:30px}table td:last-child,table th:last-child{padding-left:30px}table td,table th{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){table.vertical{border-top:solid 1px var(--config-color-fade-super);display:block;overflow:hidden;padding-top:12px}table.vertical .hide{display:none}table.vertical tbody,table.vertical td,table.vertical th,table.vertical thead,table.vertical tr{width:100%;display:block}table.vertical th,table.vertical tr{padding-top:12px;padding-bottom:12px}table.vertical th:first-child,table.vertical tr:first-child{padding-top:0}table.vertical td,table.vertical th{padding:5px 20px!important;text-overflow:ellipsis;white-space:normal;height:40px;line-height:40px;width:calc(100% - 40px)}table.vertical td:first-child,table.vertical td:last-child,table.vertical th:first-child,table.vertical th:last-child{padding:0 10px}table.vertical td:last-child,table.vertical th:last-child{padding-bottom:0}table.vertical td p,table.vertical th p{display:inline-block;width:calc(100% - 40px)}table.vertical td:not([data-title=""]):before{content:attr(data-title);margin-right:4px;font-weight:400}table.vertical thead{display:none}}.zone{max-width:var(--config-width-xl);margin:0 auto 40px auto}.zone.xxxl{max-width:calc(1400px - 100px)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.zone.xxxl{max-width:100%}}.zone.xxl{max-width:var(--config-width-xxl)}.zone.xl{max-width:var(--config-width-xl)}.zone.large{max-width:var(--config-width-large)}.zone.medium{max-width:var(--config-width-medium)}.zone.small{max-width:var(--config-width-small)}.row{position:relative;margin:0 -50px;padding-right:50px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.row{margin:0 -30px;padding-right:30px}}.row.force-ltr>.col{float:left}.row.force-rtl>.col{float:right}.row.force-reverse>.col{float:left}.row.wide{margin:0 -100px;padding-right:100px}.row.wide>.span-1{width:calc(8.33333333% * 1 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-2{width:calc(8.33333333% * 2 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-3{width:calc(8.33333333% * 3 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-4{width:calc(8.33333333% * 4 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-5{width:calc(8.33333333% * 5 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-6{width:calc(8.33333333% * 6 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-7{width:calc(8.33333333% * 7 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-8{width:calc(8.33333333% * 8 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-9{width:calc(8.33333333% * 9 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-10{width:calc(8.33333333% * 10 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-11{width:calc(8.33333333% * 11 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-12{width:calc(8.33333333% * 12 - 100px);box-sizing:content-box;padding-left:100px}.row.thin{margin:0 -20px;padding-right:20px}.row.thin>.span-1{width:calc(8.33333333% * 1 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-2{width:calc(8.33333333% * 2 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-3{width:calc(8.33333333% * 3 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-4{width:calc(8.33333333% * 4 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-5{width:calc(8.33333333% * 5 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-6{width:calc(8.33333333% * 6 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-7{width:calc(8.33333333% * 7 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-8{width:calc(8.33333333% * 8 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-9{width:calc(8.33333333% * 9 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-10{width:calc(8.33333333% * 10 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-11{width:calc(8.33333333% * 11 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-12{width:calc(8.33333333% * 12 - 20px);box-sizing:content-box;padding-left:20px}.row.modalize{margin:0 -30px;padding-right:30px}.row.modalize>.span-1{width:calc(8.33333333% * 1 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-2{width:calc(8.33333333% * 2 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-3{width:calc(8.33333333% * 3 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-4{width:calc(8.33333333% * 4 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-5{width:calc(8.33333333% * 5 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-6{width:calc(8.33333333% * 6 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-7{width:calc(8.33333333% * 7 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-8{width:calc(8.33333333% * 8 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-9{width:calc(8.33333333% * 9 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-10{width:calc(8.33333333% * 10 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-11{width:calc(8.33333333% * 11 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-12{width:calc(8.33333333% * 12 - 30px);box-sizing:content-box;padding-left:30px}.row:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.row .col{float:right;box-sizing:border-box}.row .col.sticky-top{position:sticky;top:90px}.row .col.sticky-bottom{position:sticky;bottom:0}.row .span-1{width:calc(8.33333333% * 1 - 40px);box-sizing:content-box;padding-left:40px}.row .span-2{width:calc(8.33333333% * 2 - 40px);box-sizing:content-box;padding-left:40px}.row .span-3{width:calc(8.33333333% * 3 - 40px);box-sizing:content-box;padding-left:40px}.row .span-4{width:calc(8.33333333% * 4 - 40px);box-sizing:content-box;padding-left:40px}.row .span-5{width:calc(8.33333333% * 5 - 40px);box-sizing:content-box;padding-left:40px}.row .span-6{width:calc(8.33333333% * 6 - 40px);box-sizing:content-box;padding-left:40px}.row .span-7{width:calc(8.33333333% * 7 - 40px);box-sizing:content-box;padding-left:40px}.row .span-8{width:calc(8.33333333% * 8 - 40px);box-sizing:content-box;padding-left:40px}.row .span-9{width:calc(8.33333333% * 9 - 40px);box-sizing:content-box;padding-left:40px}.row .span-10{width:calc(8.33333333% * 10 - 40px);box-sizing:content-box;padding-left:40px}.row .span-11{width:calc(8.33333333% * 11 - 40px);box-sizing:content-box;padding-left:40px}.row .span-12{width:calc(8.33333333% * 12 - 40px);box-sizing:content-box;padding-left:40px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.row.responsive{width:100%;padding:0;margin:0}.row.responsive>.span-1,.row.responsive>.span-10,.row.responsive>.span-11,.row.responsive>.span-12,.row.responsive>.span-2,.row.responsive>.span-3,.row.responsive>.span-4,.row.responsive>.span-5,.row.responsive>.span-6,.row.responsive>.span-7,.row.responsive>.span-8,.row.responsive>.span-9{width:calc(8.33333333% * 12 - 0px)!important;box-sizing:content-box!important;padding-left:0!important;width:100%!important}}.tiles{position:relative}.tiles:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.tiles>*{margin-left:50px!important;float:right;width:calc(33.3333% - 33.3333px)}.tiles>* .photo-title{width:calc(100% + 30px);height:15px;margin:-15px -15px 10px -15px;border-radius:10px 10px 0 0;background:var(--config-color-fade-super);border-bottom:solid 1px var(--config-color-fade-super)}.tiles>:nth-child(3n){margin-left:0!important}@media only screen and (min-width:551px) and (max-width:1198px){.tiles>li{width:calc(50% - 25px)}.tiles>li:nth-child(3n){margin-left:50px!important}.tiles>li:nth-child(2n){margin-left:0!important}}@media only screen and (max-width:550px){.tiles>li{width:100%;margin-left:0!important}}@font-face{font-family:fontello;src:url(data:application/octet-stream;base64,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) format('woff'),url(data:application/octet-stream;base64,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6DAAAAAxAADoMQAA6DEAAAAyAADoMgAA6DIAAAAzAADoMwAA6DMAAAA0AADoNAAA6DQAAAA1AADoNQAA6DUAAAA2AADoNgAA6DYAAAA3AADoNwAA6DcAAAA4AADoOAAA6DgAAAA5AADoOQAA6DkAAAA6AADoOgAA6DoAAAA7AADoOwAA6DsAAAA8AADoPAAA6DwAAAA9AADoPQAA6D0AAAA+AADoPgAA6D4AAAA/AADoPwAA6D8AAABAAADoQAAA6EAAAABBAADoQQAA6EEAAABCAADoQgAA6EIAAABDAADoQwAA6EMAAABEAADoRAAA6EQAAABFAADoRQAA6EUAAABGAADoRgAA6EYAAABHAADoRwAA6EcAAABIAADoSAAA6EgAAABJAADoSQAA6EkAAABKAADoSgAA6EoAAABLAADoSwAA6EsAAABMAADoTAAA6EwAAABNAADwjgAA8I4AAABOAADwmwAA8JsAAABPAADwsAAA8LAAAABQAADwxQAA8MUAAABRAADwygAA8MoAAABSAADwywAA8MsAAABTAADwzQAA8M0AAABUAADw3AAA8NwAAABVAADw4QAA8OEAAABWAADxGAAA8RgAAABXAADxHAAA8RwAAABYAADxIQAA8SEAAABZAADxMgAA8TIAAABaAADxNwAA8TcAAABbAADxOAAA8TgAAABcAADxcQAA8XEAAABdAADxegAA8XoAAABeAADxkgAA8ZIAAABfAADxkwAA8ZMAAABgAADxnAAA8ZwAAABhAADxoAAA8aAAAABiAADxrQAA8a0AAABjAADxwAAA8cAAAABkAADxzQAA8c0AAABlAADx3AAA8dwAAABmAADx5QAA8eUAAABnAADx/gAA8f4AAABoAADyMQAA8jEAAABpAADyOgAA8joAAABqAADylgAA8pYAAABrAADyxgAA8sYAAABsAAAAAgAA/7ECygMMABUAHgAlQCIABQEFbwMBAQQBbwAEAgRvAAIAAm8AAABmExcRERcyBgUaKyUUBiMhIiY1ND4DFxYyNzIeAwMUBiIuATYeAQLKRjH+JDFGChgqPi1JykoqQiYcCI98tHoEgqyERTxYWDwwVFY8KAFISCY+VFYBwFh+frCAAnwAAAL//v/OA+oC7gAOAB4AZEuwDVBYQCMAAwQEA2MFAQACAQIAAW0AAQFuAAQCAgRU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OgAvAC8AZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AAAAAAgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtAQIBAwEEAQUBBgEHAQgBCQEKAQsBDAENAQ4BDwEQAREBEgETARQBFQEWARcBGAEZARoBGwEcAR0BHgEfASABIQEiASMBJAElASYBJwEoASkBKgErASwBLQEuAS8BMAExATIBMwE0ATUBNgE3ATgBOQE6ATsBPAE9AT4BPwFAAUEBQgFDAUQBRQFGAUcBSAFJAUoBSwFMAU0BTgFPAVABUQFSAVMBVAFVAVYBVwFYAVkBWgFbAVwBXQFeAV8BYAFhAWIBYwFkAWUBZgFnAWgBaQFqAWsBbAFtAW4ABHVzZXIGZm9sZGVyBGxpc3QFbG9naW4DY29nB3R3aXR0ZXILYXJ0aWNsZS1hbHQGY2FuY2VsBGhvbWUIZG93bi1kaXIIZmFjZWJvb2sIYXN0ZXJpc2sGdXBsb2FkCXN0b3B3YXRjaAZleHBvcnQFaGVhcnQEcGx1cwZ1cC1kaXIEbWVudQlsZWZ0LW9wZW4KcmlnaHQtb3BlbgVpbmJveAZ3cmVuY2gHY29tbWVudA1zdGFja292ZXJmbG93CHF1ZXN0aW9uCm9rLWNpcmNsZWQHd2FybmluZwRtYWlsBGxpbmsHa2V5LWludgV0cmFzaAhkb3dubG9hZAdnbGFzc2VzBnFyY29kZQdzaHVmZmxlA2V5ZQRsb2NrBnNlYXJjaARiZWxsBXVzZXJzCGxvY2F0aW9uCWJyaWVmY2FzZQlpbnN0YWdyYW0FY2xvY2sFcGhvbmUIY2FsZW5kYXIFcHJpbnQEZWRpdARib2xkBml0YWxpYwZyb2NrZXQId2hhdHNhcHAFZG90LTMMaW5mby1jaXJjbGVkCHZpZGVvY2FtC3F1b3RlLXJpZ2h0B3BpY3R1cmUHcGFsZXR0ZQRsYW1wCWJvb2stb3BlbgJvawhjaGF0LWFsdAdhcmNoaXZlBHBsYXkFcGF1c2UJZG93bi1vcGVuB3VwLW9wZW4FbWludXMIZXhjaGFuZ2UHbmV0d29yawdkaXNjb3JkCG1vb24taW52B3N1bi1pbnYOY2FuY2VsLWNpcmNsZWQJbGlnaHRuaW5nA2RldghsaW5rLWV4dA5naXRodWItY2lyY2xlZAZmaWx0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IyAgPLAEI0IjOLErARQrsARDLrArKy2wRCywABUgR7AAI0KyAAEBFRQTLrAxKi2wRSywABUgR7AAI0KyAAEBFRQTLrAxKi2wRiyxAAEUE7AyKi2wRyywNCotsEgssAAWRSMgLiBGiiNhOLErARQrLbBJLLAII0KwSCstsEossgAAQSstsEsssgABQSstsEwssgEAQSstsE0ssgEBQSstsE4ssgAAQistsE8ssgABQistsFAssgEAQistsFEssgEBQistsFIssgAAPistsFMssgABPistsFQssgEAPistsFUssgEBPistsFYssgAAQCstsFcssgABQCstsFgssgEAQCstsFkssgEBQCstsFossgAAQystsFsssgABQystsFwssgEAQystsF0ssgEBQystsF4ssgAAPystsF8ssgABPystsGAssgEAPystsGEssgEBPystsGIssDcrLrErARQrLbBjLLA3K7A7Ky2wZCywNyuwPCstsGUssAAWsDcrsD0rLbBmLLA4Ky6xKwEUKy2wZyywOCuwOystsGgssDgrsDwrLbBpLLA4K7A9Ky2waiywOSsusSsBFCstsGsssDkrsDsrLbBsLLA5K7A8Ky2wbSywOSuwPSstsG4ssDorLrErARQrLbBvLLA6K7A7Ky2wcCywOiuwPCstsHEssDorsD0rLbByLLMJBAIDRVghGyMhWUIrsAhlsAMkUHiwARUwLQBLuADIUlixAQGOWbABuQgACABjcLEABUKyAAEAKrEABUKzCgIBCCqxAAVCsw4AAQgqsQAGQroCwAABAAkqsQAHQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMMAgEMKrgB/4WwBI2xAgBEAAA=) format('truetype')}[class*=" icon-"]:before,[class^=icon-]:before{font-family:fontello;font-style:normal;font-weight:400;speak:none;display:inline-block;text-decoration:inherit;width:1em;margin-right:.2em;text-align:center;font-variant:normal;text-transform:none;line-height:1em;margin-left:.2em}.icon-user:before{content:'\e800'}.icon-folder:before{content:'\e801'}.icon-list:before{content:'\e802'}.icon-login:before{content:'\e803'}.icon-cog:before{content:'\e804'}.icon-twitter:before{content:'\e805'}.icon-article-alt:before{content:'\e806'}.icon-cancel:before{content:'\e807'}.icon-home:before{content:'\e808'}.icon-down-dir:before{content:'\e809'}.icon-facebook:before{content:'\e80a'}.icon-asterisk:before{content:'\e80b'}.icon-upload:before{content:'\e80c'}.icon-stopwatch:before{content:'\e80d'}.icon-export:before{content:'\e80e'}.icon-heart:before{content:'\e80f'}.icon-plus:before{content:'\e810'}.icon-up-dir:before{content:'\e811'}.icon-menu:before{content:'\e812'}.icon-left-open:before{content:'\e813'}.icon-right-open:before{content:'\e814'}.icon-inbox:before{content:'\e815'}.icon-wrench:before{content:'\e816'}.icon-comment:before{content:'\e817'}.icon-stackoverflow:before{content:'\e818'}.icon-question:before{content:'\e819'}.icon-ok-circled:before{content:'\e81a'}.icon-warning:before{content:'\e81b'}.icon-mail:before{content:'\e81c'}.icon-link:before{content:'\e81d'}.icon-key-inv:before{content:'\e81e'}.icon-trash:before{content:'\e81f'}.icon-download:before{content:'\e820'}.icon-glasses:before{content:'\e821'}.icon-qrcode:before{content:'\e822'}.icon-shuffle:before{content:'\e823'}.icon-eye:before{content:'\e824'}.icon-lock:before{content:'\e825'}.icon-search:before{content:'\e826'}.icon-bell:before{content:'\e827'}.icon-users:before{content:'\e828'}.icon-location:before{content:'\e829'}.icon-briefcase:before{content:'\e82a'}.icon-instagram:before{content:'\e82b'}.icon-clock:before{content:'\e82c'}.icon-phone:before{content:'\e82d'}.icon-calendar:before{content:'\e82e'}.icon-print:before{content:'\e82f'}.icon-edit:before{content:'\e830'}.icon-bold:before{content:'\e831'}.icon-italic:before{content:'\e832'}.icon-rocket:before{content:'\e833'}.icon-whatsapp:before{content:'\e834'}.icon-dot-3:before{content:'\e835'}.icon-info-circled:before{content:'\e836'}.icon-videocam:before{content:'\e837'}.icon-quote-right:before{content:'\e838'}.icon-picture:before{content:'\e839'}.icon-palette:before{content:'\e83a'}.icon-lamp:before{content:'\e83b'}.icon-book-open:before{content:'\e83c'}.icon-ok:before{content:'\e83d'}.icon-chat-alt:before{content:'\e83e'}.icon-archive:before{content:'\e83f'}.icon-play:before{content:'\e840'}.icon-pause:before{content:'\e841'}.icon-down-open:before{content:'\e842'}.icon-up-open:before{content:'\e843'}.icon-minus:before{content:'\e844'}.icon-exchange:before{content:'\e845'}.icon-network:before{content:'\e846'}.icon-discord:before{content:'\e847'}.icon-moon-inv:before{content:'\e848'}.icon-sun-inv:before{content:'\e849'}.icon-cancel-circled:before{content:'\e84a'}.icon-lightning:before{content:'\e84b'}.icon-dev:before{content:'\e84c'}.icon-link-ext:before{content:'\f08e'}.icon-github-circled:before{content:'\f09b'}.icon-filter:before{content:'\f0b0'}.icon-docs:before{content:'\f0c5'}.icon-list-bullet:before{content:'\f0ca'}.icon-list-numbered:before{content:'\f0cb'}.icon-underline:before{content:'\f0cd'}.icon-sort:before{content:'\f0dc'}.icon-linkedin:before{content:'\f0e1'}.icon-smile:before{content:'\f118'}.icon-keyboard:before{content:'\f11c'}.icon-code:before{content:'\f121'}.icon-shield:before{content:'\f132'}.icon-angle-circled-left:before{content:'\f137'}.icon-angle-circled-right:before{content:'\f138'}.icon-bitbucket:before{content:'\f171'}.icon-windows:before{content:'\f17a'}.icon-dot-circled:before{content:'\f192'}.icon-wheelchair:before{content:'\f193'}.icon-bank:before{content:'\f19c'}.icon-google:before{content:'\f1a0'}.icon-building-filled:before{content:'\f1ad'}.icon-database:before{content:'\f1c0'}.icon-lifebuoy:before{content:'\f1cd'}.icon-header:before{content:'\f1dc'}.icon-binoculars:before{content:'\f1e5'}.icon-chart-area:before{content:'\f1fe'}.icon-pinterest:before{content:'\f231'}.icon-medium:before{content:'\f23a'}.icon-gitlab:before{content:'\f296'}.icon-telegram:before{content:'\f2c6'}.datalist-polyfill{list-style:none;display:none;background:#fff;box-shadow:0 2px 2px #999;position:absolute;left:0;top:0;margin:0;padding:0;max-height:300px;overflow-y:auto}.datalist-polyfill:empty{display:none!important}.datalist-polyfill>li{padding:3px;font:13px "Lucida Grande",Sans-Serif}.datalist-polyfill__active{background:#3875d7;color:#fff}date-input-polyfill{z-index:1000!important;max-width:320px!important;width:320px!important}date-input-polyfill .monthSelect-wrapper,date-input-polyfill .yearSelect-wrapper{height:50px;line-height:50px;padding:0;width:40%!important;margin-bottom:10px!important}date-input-polyfill .monthSelect-wrapper select,date-input-polyfill .yearSelect-wrapper select{padding:0 12px;height:50px;line-height:50px;box-sizing:border-box}date-input-polyfill .yearSelect-wrapper{width:35%!important}date-input-polyfill table{width:100%!important;max-width:100%!important;padding:0 12px 12px 12px!important;box-sizing:border-box;margin:0}date-input-polyfill table td:first-child,date-input-polyfill table td:last-child,date-input-polyfill table th:first-child,date-input-polyfill table th:last-child{width:32px!important;padding:4px!important}date-input-polyfill select{margin-bottom:10px}date-input-polyfill button{width:25%!important;height:50px!important;line-height:50px!important;margin-bottom:10px!important;background:inherit;position:relative;color:inherit;padding:inherit;box-sizing:inherit;border-radius:inherit;font-size:inherit;box-shadow:none;border:none;border-bottom:none!important}::placeholder{color:var(--config-color-placeholder);text-align:right}::-webkit-input-placeholder{text-align:right}input:-moz-placeholder{text-align:right}input,textarea{background:var(--config-color-background-input)}input[type=file],input[type=file]::-webkit-file-upload-button{cursor:pointer}.button,button{display:inline-block;background:var(--config-color-focus);border-radius:26px;border:none;color:var(--config-color-background-fade);height:52px;line-height:52px;padding:0 25px;cursor:pointer;font-size:16px;box-sizing:border-box;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.button:focus,.button:hover,button:focus,button:hover{background:var(--config-color-focus-hover)}.button.fly,button.fly{position:fixed;z-index:2;bottom:30px;left:30px}@media only screen and (max-width:550px){.button.fly,button.fly{left:15px}}.button.fill,button.fill{display:block;width:100%;text-align:center;padding:0 10px!important}.button.fill-aligned,button.fill-aligned{display:block;width:100%;text-align:right;padding:0 20px!important}.button.icon,button.icon{padding-left:30px!important}.button.icon-reduce,button.icon-reduce{padding-right:15px!important}.button.reverse,button.reverse{background:0 0;height:50px;line-height:48px;padding:0 23px;color:var(--config-color-focus);border:solid 2px var(--config-color-focus)}.button.reverse:focus,.button.reverse:hover,button.reverse:focus,button.reverse:hover{color:var(--config-color-focus-hover);border-color:var(--config-color-focus-hover)}.button.small,button.small{padding:0 15px;height:40px;line-height:36px;font-size:13px}.button.round,button.round{width:52px;padding:0}.button.round.small,button.round.small{font-size:12px;width:30px;height:30px;line-height:30px}.button.white,button.white{background:#fff;color:var(--config-color-focus)}.button.white.reverse,button.white.reverse{color:#fff;background:0 0;border:solid 2px #fff}.button.trans,button.trans{background:0 0!important}.button.trans.reverse,button.trans.reverse{background:0 0!important}.button.success,button.success{background:var(--config-color-success)}.button.success.reverse,button.success.reverse{color:var(--config-color-success);background:#fff;border:solid 2px var(--config-color-success)}.button.danger,button.danger{background:var(--config-color-danger);color:#fff}.button.danger.reverse,button.danger.reverse{color:var(--config-color-danger);background:var(--config-color-background-fade);border:solid 2px var(--config-color-danger)}.button.dark,button.dark{background:var(--config-color-dark);color:var(--config-color-background-fade)}.button.dark.reverse,button.dark.reverse{color:var(--config-color-dark);background:var(--config-color-background-fade);border:solid 2px var(--config-color-dark)}.button .disabled,.button.disabled,.button:disabled,button .disabled,button.disabled,button:disabled{color:var(--config-color-normal);background:var(--config-color-background-dark);opacity:.6;cursor:default}.button.link,button.link{background:0 0;border-radius:0;color:var(--config-color-link);height:auto;line-height:normal;padding:0;padding-left:0!important}.button.link:focus,button.link:focus{box-shadow:inherit}.button.strip,button.strip{background:0 0;height:auto;line-height:16px;color:inherit;padding:0 5px}.button.facebook,button.facebook{color:#fff!important;background:#4070b4!important}.button.twitter,button.twitter{color:#fff!important;background:#56c2ea!important}.button.linkedin,button.linkedin{color:#fff!important;background:#0076b5!important}.button.github,button.github{color:#fff!important;background:#7e7c7c!important}.button:focus,button:focus{outline:0}label{margin-bottom:15px;display:block;line-height:normal}.input,input[type=date],input[type=datetime-local],input[type=email],input[type=file],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=url],select,textarea{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px}.input[type=file],input[type=date][type=file],input[type=datetime-local][type=file],input[type=email][type=file],input[type=file][type=file],input[type=number][type=file],input[type=password][type=file],input[type=search][type=file],input[type=tel][type=file],input[type=text][type=file],input[type=url][type=file],select[type=file],textarea[type=file]{line-height:0;padding:15px;height:auto}.input:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=email]:focus,input[type=file]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus,select:focus,textarea:focus{outline:0;border-color:#b3d7fd}.input:disabled,input[type=date]:disabled,input[type=datetime-local]:disabled,input[type=email]:disabled,input[type=file]:disabled,input[type=number]:disabled,input[type=password]:disabled,input[type=search]:disabled,input[type=tel]:disabled,input[type=text]:disabled,input[type=url]:disabled,select:disabled,textarea:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.input.strip,input[type=date].strip,input[type=datetime-local].strip,input[type=email].strip,input[type=file].strip,input[type=number].strip,input[type=password].strip,input[type=search].strip,input[type=tel].strip,input[type=text].strip,input[type=url].strip,select.strip,textarea.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:left 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.input.strip:focus,input[type=date].strip:focus,input[type=datetime-local].strip:focus,input[type=email].strip:focus,input[type=file].strip:focus,input[type=number].strip:focus,input[type=password].strip:focus,input[type=search].strip:focus,input[type=tel].strip:focus,input[type=text].strip:focus,input[type=url].strip:focus,select.strip:focus,textarea.strip:focus{border-color:#b3d7fd}.input:-webkit-autofill::first-line,input[type=date]:-webkit-autofill::first-line,input[type=datetime-local]:-webkit-autofill::first-line,input[type=email]:-webkit-autofill::first-line,input[type=file]:-webkit-autofill::first-line,input[type=number]:-webkit-autofill::first-line,input[type=password]:-webkit-autofill::first-line,input[type=search]:-webkit-autofill::first-line,input[type=tel]:-webkit-autofill::first-line,input[type=text]:-webkit-autofill::first-line,input[type=url]:-webkit-autofill::first-line,select:-webkit-autofill::first-line,textarea:-webkit-autofill::first-line{font-weight:300;font-size:16px}input[type=email],input[type=url]{direction:ltr}input[type=email]::placeholder,input[type=url]::placeholder{text-align:left;direction:ltr}select{background:0 0;-webkit-appearance:none;background-image:var(--config-console-nav-switch-arrow);background-position:left 15px top 50%;background-repeat:no-repeat;background-color:var(--config-color-background-input);width:calc(100% - 62px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-left:45px}select:-webkit-autofill{background-image:url("data:image/svg+xml;utf8,")!important;background-position:100% 50%!important;background-repeat:no-repeat!important}input[type=search],input[type=search].strip{background:0 0;-webkit-appearance:none;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAdZJREFUWIXt1s2LjWEYBvDfnDMzFpNIamZIFrMiJYMyFmKhZKfOwoiFr2LFn2BByG6WVrKwMcjWxgoLIlKIUk6RrzAjZWZ8LO731FlwvB+PUbjq6X0X7/VeV/d9P9fz8IdRL8Hpw3x8w0xaOz9GNxq4gJeZcGs1cRab0fU7xLfgMSYzoT3YgNXYhIO4iM+4iTWphGs4jikcFSXvhEGczr4/UFW8C2N4jXUFudvwCYeqGNgnSr6yJH8rpkWLCqMfE9hdUryFE3iC3qLEk7ij+kT34Q32FiHV8Qr7K4q3cArXihCGxd5elMjARnzBvE4f1dreV+AtnicycC/7/7K8BhaIvqXCO3zFwrwGZtCT0EAtW9N5DTSxWGR/CizNns/yEgbFEK5NZGCnaEPHE7e9Ai9wA6OJDIzistgJubFdxHB/RfFVYgCHixJruI5x5dNwDm6J47sUhkTvjpUw0Y1zeOrXR3hHjOA9zmBuTs4Arog4/yhuUZWwHPdFMh7280BZgiP4ILJ/UuymqRQmejPxphiquzgvKnMJDzOxB9glZqiRiecykbfHdawX98EhcdxO4BGu4nYm2EJDzEKPSMIdYrBnFYUq8d/EP2di1gey3cS4ErflvxffASbhcakIINaMAAAAAElFTkSuQmCC);background-color:var(--config-color-background-input);background-position:right 15px top 50%;background-repeat:no-repeat;background-size:20px 20px;width:calc(100% - 60px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:45px}select[multiple]{min-height:75px;padding:5px 10px!important;padding-left:50px!important}select[multiple] option{padding:10px 4px;border-bottom:solid 1px #f1f1f1}select[multiple] option:last-child{border-bottom:none}textarea{min-height:75px;resize:vertical;line-height:32px;padding:5px 15px}textarea.tall{min-height:180px}fieldset{border:none;margin:0;padding:0}.counter{font-size:13px;text-align:left;color:var(--config-color-fade);margin-top:-20px;margin-bottom:20px}.file-preview{background:var(--config-color-background-input) url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAIElEQVQoU2NkYGAwZsAEZ9GFGIeIQix+wfQgyDODXSEAcUwGCrDSHgkAAAAASUVORK5CYII=)!important;border:solid 1px #e2e2e2;box-shadow:inset 0 0 3px #a0a0a0;border-radius:8px;width:calc(100% - 2px);max-height:180px;visibility:visible!important}.video-preview{padding-top:56%;position:relative;border-radius:10px;background:#e7e7e7;overflow:hidden;margin:0}.video-preview iframe{position:absolute;top:0;width:100%;height:100%;border:none}.map-preview{padding-top:50%;position:relative;margin-bottom:10px;border-radius:10px;background:#e7e7e7;overflow:hidden;box-shadow:0 0 30px rgba(218,218,218,.5)}.map-preview iframe{position:absolute;top:0;width:100%;height:100%;border:none}.tooltip{position:relative}.tooltip.large:hover:after{white-space:normal;width:280px}.tooltip.small:hover:after{white-space:normal;width:180px}.tooltip:hover:after{white-space:nowrap;background:var(--config-color-tooltip-background);border-radius:5px;bottom:26px;color:var(--config-color-tooltip-text);content:attr(data-tooltip);padding:5px 15px;position:absolute;font-size:13px;line-height:20px;z-index:98;right:20%;margin-right:-30px;word-break:break-word}.tooltip:hover:before{border:solid;border-color:var(--config-color-tooltip-background) transparent;border-width:6px 6px 0 6px;bottom:20px;content:"";position:absolute;z-index:99;right:5px}.tooltip.down:hover:after{top:26px;bottom:inherit}.tooltip.down:hover:before{top:20px;border-width:0 6px 6px 6px;bottom:inherit}.tag{display:inline-block;background:var(--config-color-fade-light);color:var(--config-color-fade);border-radius:12px;line-height:24px;padding:0 8px;font-size:12px;box-shadow:none!important;border:none;height:auto;width:auto;white-space:nowrap;text-overflow:ellipsis}.tag:hover{border:none}.tag.green{background:var(--config-color-success);color:#fff}.tag.red{background:var(--config-color-danger);color:#fff}.tag.yellow{background:#ffe28b;color:#494949}.tag.focus{background:var(--config-color-focus);color:#fff}.tag.dark{background:var(--config-color-dark);color:#e7e7e7}.tag.blue{background:var(--config-color-info);color:#fff}.tag.link{background:var(--config-color-link);color:#fff}input[type=checkbox],input[type=radio]{width:26px;height:16px;position:relative;-webkit-appearance:none;border-radius:0;border:none;background:0 0;vertical-align:middle;margin:0}input[type=checkbox]:after,input[type=radio]:after{content:"";display:block;width:20px;height:20px;background:var(--config-color-background-fade);top:-5px;border-radius:50%;position:absolute;border:solid 3px var(--config-color-focus);vertical-align:middle}input[type=checkbox]:checked:after,input[type=radio]:checked:after{text-align:center;font-family:fontello;content:'\e83d';font-size:16px;line-height:20px;color:var(--config-color-background-fade);background:var(--config-color-focus)}input[type=checkbox][type=radio]:checked:after,input[type=radio][type=radio]:checked:after{content:'';display:block;width:10px;height:10px;border-radius:50%;background:var(--config-color-background-fade);border:solid 8px var(--config-color-focus)}input[type=checkbox]:focus,input[type=radio]:focus{outline:0}input[type=checkbox]:focus:after,input[type=checkbox]:hover:after,input[type=radio]:focus:after,input[type=radio]:hover:after{outline:0;border-color:#000}input[type=checkbox]:checked:focus:after,input[type=checkbox]:checked:hover:after,input[type=radio]:checked:focus:after,input[type=radio]:checked:hover:after{border-color:var(--config-color-focus)}.input-copy{position:relative}.input-copy input,.input-copy textarea{padding-left:65px;width:calc(100% - 82px);resize:none}.input-copy .copy{position:absolute;top:0;left:0;border-right:solid 1px var(--config-color-fade-light);height:calc(100% - 2px);width:50px;line-height:50px;text-align:center;background:var(--config-color-background-focus);margin:1px;border-radius:0 10px 10px 0}.paging{color:var(--config-color-fade);padding:5px 15px;font-size:12px}.paging form{display:inline-block}.paging button:disabled{color:var(--config-color-background-fade);opacity:.6}.blue-snap iframe{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px;float:none!important;height:40px!important;width:calc(100% - 32px)!important;border:solid 1px #e2e2e2!important;background:0 0!important;position:static!important}.blue-snap iframe[type=file]{line-height:0;padding:15px;height:auto}.blue-snap iframe:focus{outline:0;border-color:#b3d7fd}.blue-snap iframe:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.blue-snap iframe.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:left 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.blue-snap iframe.strip:focus{border-color:#b3d7fd}.blue-snap iframe:-webkit-autofill::first-line{font-weight:300;font-size:16px}.blue-snap .error{font-size:12px;margin-top:-25px;color:var(--config-color-danger);height:40px;padding-right:2px}.pell{height:auto;padding-bottom:0;margin-bottom:0;padding-top:0;background:var(--config-color-background-input);line-height:normal!important;position:relative}.pell.hide{padding:0!important;height:1px;min-height:1px;max-height:1px;border:none;box-shadow:none;margin-bottom:20px;opacity:0}.pell [contenteditable=true]:empty:before{content:attr(placeholder);display:block;color:var(--config-color-placeholder)}.pell .pell-actionbar{border-bottom:solid 1px var(--config-color-fade-light);margin:0 -15px 15px -15px;padding:10px 15px;position:sticky;top:70px;background:var(--config-color-background-input);border-radius:10px 10px 0 0}.pell .pell-content{min-height:100px;display:block;padding:10px;margin:-10px;cursor:text}.pell .pell-content:focus{outline:0}.pell button{background:inherit;color:inherit;margin:0;padding:0;padding-left:15px;height:40px;line-height:40px;box-shadow:none;cursor:pointer;font-size:13px;border-radius:0}.pell button.pell-button-selected,.pell button:focus,.pell button:hover{color:var(--config-color-link)}.pell h1,.pell h2,.pell h3,.pell h4,.pell h5,.pell h6{text-align:inherit;margin-bottom:30px}.pell b,.pell strong{font-weight:700}.pell ol,.pell ul{margin:0 0 20px 0}.pell ol li,.pell ul li{display:list-item!important;list-style:inherit;list-style-position:inside!important;margin:0 20px 2px 20px}.pell ol li p,.pell ul li p{margin:0;display:inline}.pell ol li{list-style:decimal}.pell ol li::before{content:'';display:none}label.switch{line-height:42px}.switch,input[type=checkbox].button.switch,input[type=checkbox].switch{width:52px;height:32px;line-height:32px;border-radius:21px;background:var(--config-color-fade);display:inline-block;margin:0;padding:5px;padding-right:5px;padding-left:30px}.switch.on,.switch:checked,input[type=checkbox].button.switch.on,input[type=checkbox].button.switch:checked,input[type=checkbox].switch.on,input[type=checkbox].switch:checked{background-color:var(--config-color-success);padding-right:25px;padding-left:5px}.switch.on:focus,.switch.on:hover,.switch:checked:focus,.switch:checked:hover,input[type=checkbox].button.switch.on:focus,input[type=checkbox].button.switch.on:hover,input[type=checkbox].button.switch:checked:focus,input[type=checkbox].button.switch:checked:hover,input[type=checkbox].switch.on:focus,input[type=checkbox].switch.on:hover,input[type=checkbox].switch:checked:focus,input[type=checkbox].switch:checked:hover{background:var(--config-color-success)}.switch:focus,.switch:hover,input[type=checkbox].button.switch:focus,input[type=checkbox].button.switch:hover,input[type=checkbox].switch:focus,input[type=checkbox].switch:hover{background:var(--config-color-fade)}.switch:focus:after,.switch:hover:after,input[type=checkbox].button.switch:focus:after,input[type=checkbox].button.switch:hover:after,input[type=checkbox].switch:focus:after,input[type=checkbox].switch:hover:after{background:#fff}.switch:after,input[type=checkbox].button.switch:after,input[type=checkbox].switch:after{content:"";display:block;width:22px;height:22px;background:#fff;border-radius:50%;border:none;position:static;top:0}.password-meter{margin:-41px 10px 30px 10px;height:2px;background:0 0;max-width:100%;z-index:2;position:relative}.password-meter.weak{background:var(--config-color-danger)}.password-meter.medium{background:var(--config-color-success)}.password-meter.strong{background:var(--config-color-success)}.color-input:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.color-input .color-preview{width:53px;height:53px;float:right;margin-left:10px;background:#000;border-radius:10px;box-shadow:inset 0 0 3px #a0a0a0;position:relative}.color-input .color-preview input{opacity:0;position:absolute;top:0;bottom:0;left:0;right:0;width:100%;height:100%;cursor:pointer}.color-input input{text-transform:uppercase;float:right;width:calc(100% - 95px)}.grecaptcha-badge{box-shadow:none!important;border-radius:10px!important;overflow:hidden!important;background:#4d92df!important;bottom:25px}.grecaptcha-badge:hover{width:256px!important}.back{font-size:15px;line-height:24px;height:24px;margin-right:-15px;margin-top:-25px;margin-bottom:20px}.back span{font-weight:inherit!important}@media only screen and (max-width:550px){.back{margin-right:-5px}}hr{height:1px;background:var(--config-border-color)!important;border:none}hr.fade{opacity:.7}.upload{position:relative}.upload:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.upload input{position:absolute;top:0;right:0;opacity:0;cursor:pointer}.upload.single .preview{height:0;position:relative;padding-top:100%;width:100%;margin-bottom:15px!important}.upload.single .preview li{position:absolute;top:0;width:calc(100% - 20px);height:calc(100% - 20px);margin-left:0!important;margin-bottom:0!important}.upload .button{float:right;margin-left:10px!important}.upload .button.disabled,.upload .button.disabled:hover{background:0 0;color:inherit;border-color:inherit}.upload .count{float:right;line-height:52px}.upload .progress{background:var(--config-color-success);height:6px;border-radius:3px;margin-bottom:15px!important}.upload .preview:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.upload .preview li{float:right;margin-left:20px!important;margin-bottom:15px!important;background:var(--config-color-background-fade-super);width:150px;height:150px;line-height:148px;text-align:center;border-radius:20px;overflow:hidden;position:relative;cursor:pointer;border:solid 1px var(--config-color-background-dark)}.upload .preview li:hover:before{background:var(--config-color-focus)}.upload .preview li:before{content:'\e807';font-family:fontello;font-size:12px;position:absolute;width:20px;height:20px;display:block;top:8px;left:8px;text-align:center;line-height:20px;vertical-align:middle;border-radius:50%;background:#484848;color:#fff;z-index:1}.upload .preview li img{vertical-align:middle;max-height:150px;max-width:150px;-webkit-filter:drop-shadow(0 0 6px rgba(0, 0, 0, .3));filter:drop-shadow(0 0 1px rgba(0, 0, 0, .3))}.upload.wide .preview li{height:0;width:100%;position:relative;padding-top:30.547%;background:#e7e7e7;border-radius:10px;overflow:hidden;border:solid 1px #f9f9f9;margin:0}.upload.wide .preview li img{border-radius:10px;position:absolute;top:0;width:100%;display:block;opacity:1;max-width:inherit;max-height:inherit}ol{list-style:none;counter-reset:x-counter;padding:0}ol li{counter-increment:x-counter;line-height:30px;margin-bottom:30px;margin-right:45px}ol li::before{display:inline-block;content:counter(x-counter);color:var(--config-color-background-fade);background:var(--config-color-focus);border:solid 2px var(--config-color-focus);margin-left:15px;margin-right:-45px;width:26px;height:26px;border-radius:50%;text-align:center;line-height:26px}.required{color:var(--config-color-danger);font-size:8px;position:relative;top:-8px}.drop-list{position:relative;outline:0}.drop-list.open ul{display:block}.drop-list ul{position:relative;background:var(--config-color-background-fade);border-radius:10px;box-shadow:0 0 3px rgba(0,0,0,.05);padding:30px;display:block;border-bottom:none;box-shadow:0 0 6px rgba(0,0,0,.1);display:none;position:absolute;bottom:calc(100% + 10px);z-index:2;padding:0;right:-10px;max-width:280px;min-width:240px}.drop-list ul.padding-small{padding:15px}.drop-list ul.y-scroll{overflow-y:auto}.drop-list ul.danger{background:var(--config-color-danger);color:#fff}.drop-list ul.danger .box{color:var(--config-color-normal);background:var(--config-color-background-fade)}.drop-list ul.danger>.button,.drop-list ul.danger>button{background:#fff;color:var(--config-color-danger)}.drop-list ul.note{background:var(--config-note-background)}.drop-list ul.focus{background:var(--config-color-focus);color:var(--config-color-background-fade)}.drop-list ul.focus .button,.drop-list ul.focus button{background:var(--config-color-background-fade);color:var(--config-color-focus)}.drop-list ul.line{background:0 0;border:solid 1px var(--config-color-background-dark);box-shadow:none}.drop-list ul.warning{background:var(--config-color-success);color:#2d2d2d}.drop-list ul.warning .button,.drop-list ul.warning button{background:rgba(45,45,45,.8);color:var(--config-color-success)}.drop-list ul .tabs{border-bottom:solid 1px var(--config-border-color);margin:0 -30px;padding:0 30px!important}.drop-list ul>footer{margin:0 -30px -30px -30px;padding:15px 30px;background:var(--config-color-background-fade);border:solid 1px var(--config-border-color);border-radius:0 0 10px 10px}.drop-list ul hr{height:1px;background:var(--config-console-background);border:none;margin:30px -30px}.drop-list ul .label{position:absolute;top:10px;z-index:2;left:10px}.drop-list ul.fade-bottom{position:relative;overflow:hidden}.drop-list ul.fade-bottom:after{content:"";position:absolute;display:block;bottom:15px;width:100%;background:#000;background:linear-gradient(180deg,rgba(0,0,0,0) 0,var(--config-color-background-fade) 80%);height:100px;margin:0 -15px}.drop-list ul .header{position:static;height:40px;padding:20px 30px 20px 30px;margin-bottom:30px;margin:-30px -30px 20px -30px;background:var(--config-color-background-fade);border-bottom:solid 1px #efefef}.drop-list ul ul.numbers>li{position:relative;margin-right:30px;margin-left:50px}.drop-list ul ul.numbers>li hr{margin-right:-60px;margin-left:-80px}.drop-list ul ul.numbers>li .settings{position:absolute;top:3px;left:-50px}.drop-list ul ul.numbers>li::after{display:block;width:25px;height:25px;line-height:25px;font-size:13px;font-weight:500;border-radius:50%;background:var(--config-color-focus);color:var(--config-color-background);counter-increment:section;content:counter(section);text-align:center;position:absolute;top:3px;right:-45px}.drop-list ul .scroll{margin:0 -30px;overflow-y:scroll}.drop-list ul .scroll table{width:100%;margin:0}.drop-list ul ul.sortable{counter-reset:section}.drop-list ul ul.sortable>li [data-move-down].round,.drop-list ul ul.sortable>li [data-move-up].round,.drop-list ul ul.sortable>li [data-remove].round{background:var(--config-color-focus);color:var(--config-color-background-fade);width:25px;height:25px;line-height:25px;display:inline-block;text-align:center;padding:0;margin-left:5px}.drop-list ul ul.sortable>li [data-move-down].round:disabled,.drop-list ul ul.sortable>li [data-move-up].round:disabled,.drop-list ul ul.sortable>li [data-remove].round:disabled{display:none}.drop-list ul ul.sortable>li:first-child [data-move-up]{display:none}.drop-list ul ul.sortable>li:first-child [data-move-up]:disabled{display:inline-block;background:var(--config-color-background)}.drop-list ul ul.sortable>li:last-child [data-move-down]{display:none}.drop-list ul ul.sortable>li:last-child [data-move-down]:disabled{display:inline-block;background:var(--config-color-background)}.drop-list ul .toggle{position:relative;border-top:1px solid var(--config-console-background);border-bottom:1px solid var(--config-console-background);margin:0 -30px;padding:30px 30px 0 30px;height:65px;overflow:hidden}.drop-list ul .toggle.list{border-bottom:none}.drop-list ul .toggle.sorts button.ls-ui-open{width:calc(100% - 100px)}.drop-list ul .toggle button.ls-ui-open{left:0;position:absolute;top:0;width:100%;height:95px;background:0 0;opacity:.5;border-radius:0}.drop-list ul .toggle .icon-minus,.drop-list ul .toggle .icon-up-open{display:none}.drop-list ul .toggle .content{display:none}.drop-list ul .toggle.open{height:auto}.drop-list ul .toggle.open .icon-minus,.drop-list ul .toggle.open .icon-up-open{display:block}.drop-list ul .toggle.open .icon-down-open,.drop-list ul .toggle.open .icon-plus{display:none}.drop-list ul .toggle.open .content{display:block}.drop-list ul .list li{border-bottom:solid 2px var(--config-border-color);margin:0 -30px 30px -30px;padding:0 30px 30px 30px}.drop-list ul .list li:last-child{padding-bottom:0;margin-bottom:0;border-bottom:none}@media only screen and (max-width:550px){.drop-list ul .list li .actions{float:none}}.drop-list ul.new{text-align:center}.drop-list ul.new i{font-size:80px;line-height:80px;font-family:Poppins,sans-serif;font-style:normal;font-weight:300}.drop-list ul.new b{margin-top:20px;display:block}.drop-list ul .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.drop-list ul .info hr{background:var(--config-modal-note-border)!important}.drop-list ul .table-wrap{margin:0 -30px;overflow-y:scroll}.drop-list ul .table-wrap table{margin:0}.drop-list ul:before{border:solid;border-color:var(--config-color-background-fade) transparent;border-width:8px 8px 0 8px;bottom:-8px;content:"";position:absolute;z-index:99;right:30px}.drop-list ul.arrow-end:before{left:30px;right:unset}.drop-list ul li{border-bottom:solid 1px var(--config-color-fade-super);margin:0;padding:0}.drop-list ul li:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.drop-list ul li:first-child{border-radius:10px 10px 0 0}.drop-list ul li:last-child{border-radius:0 0 10px 10px}.drop-list ul li:hover{background:var(--config-color-fade-super)}.drop-list ul li:first-child:hover,.drop-list ul li:last-child:hover{border-color:transparent}.drop-list ul li .link,.drop-list ul li a,.drop-list ul li button.link{display:block;vertical-align:middle;height:auto;line-height:30px;display:inline-block;padding:10px 15px!important;color:inherit;font-size:14px;border:none;cursor:pointer;width:calc(100% - 30px);text-align:right;box-sizing:content-box}.drop-list ul li.disabled .link:hover,.drop-list ul li.disabled a:hover{background:0 0}.drop-list ul li .avatar{width:30px;height:30px;margin-left:10px;float:right}.drop-list ul li:last-child{border-bottom:none}.drop-list.bottom ul{bottom:auto;margin-top:-2px}.drop-list.bottom ul:before{bottom:auto;top:-8px;border-width:0 8px 8px 8px}.drop-list.end ul{left:-10px;right:auto}.disabled{opacity:.2;cursor:default}.disabled .button,.disabled .link,.disabled a,.disabled button{cursor:default!important}.disabled .button:hover,.disabled .link:hover,.disabled a:hover,.disabled button:hover{background:0 0}.tags{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px;background:var(--config-color-background-input);min-height:42px;height:auto;cursor:text}.tags[type=file]{line-height:0;padding:15px;height:auto}.tags:focus{outline:0;border-color:#b3d7fd}.tags:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.tags.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:left 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.tags.strip:focus{border-color:#b3d7fd}.tags:-webkit-autofill::first-line{font-weight:300;font-size:16px}.tags .add{display:inline-block!important;border:none;padding:0;width:auto;margin:0;max-width:100%;min-width:200px}.tags ul.tags-list{display:inline;white-space:pre-line}.tags ul.tags-list li{display:inline-block!important;margin-left:10px;font-size:16px;padding:5px 10px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.tags ul.tags-list li::before{float:left;content:'\e807';font-family:fontello;font-style:normal;display:inline-block;text-align:center;line-height:16px;width:16px;height:16px;font-size:12px;background:#000;color:#fff;border-radius:50%;margin-top:4px;margin-bottom:4px;margin-right:6px;margin-left:0}.switch-theme{background:var(--config-switch-background);border-radius:19px;height:26px;width:44px;margin:9px 0}.switch-theme button{padding:3px;display:block;background:0 0;height:26px;width:100%}.switch-theme i{background:var(--config-color-background-fade);border-radius:50%;height:18px;width:18px;line-height:18px;font-size:12px;padding:0;margin:0;color:var(--config-color-fade)}.switch-theme i.force-light{float:left}.switch-theme i.force-dark{float:right}.console{width:100%;padding:0;overscroll-behavior:none}.console body{position:relative;width:calc(100% - 320px);padding-top:70px;padding-bottom:0;padding-left:50px;padding-right:270px;margin:0;color:var(--config-color-normal);background:var(--config-console-background)}.console body .project-only{display:none!important}.console body.show-nav .project-only{display:inline-block!important}.console body.hide-nav{padding-right:50px;width:calc(100% - 100px)}.console body.hide-nav header{width:calc(100% - 50px)}.console body.hide-nav header .logo{display:inline-block}.console body.hide-nav .console-back{display:block}.console body.hide-nav .console-index{display:none}.console body.hide-nav .account{display:none}.console body.index .console-back{display:none}.console body.index .console-index{display:block}.console body.index .account{display:block}.console body .console-index{display:block}.console body .console-back{display:none}.console main{min-height:480px}.console header{position:fixed;top:0;width:calc(100% - 280px);height:40px;line-height:40px;padding:15px 30px;background:var(--config-color-background-fade);box-shadow:0 0 2px rgba(0,0,0,.1);margin:0 -50px;z-index:2;font-size:14px}.console header .logo{display:none;border:none}.console header .logo:hover{border:none;opacity:.8}.console header .logo img{height:26px;margin:7px 0}.console header .setup-new{width:40px;height:40px;line-height:40px}.console header .list{width:240px}.console header .list select{height:40px;line-height:40px;padding-top:0;padding-bottom:0;border:none;border-radius:26px;background-color:var(--config-console-nav-switch-background);color:var(--config-console-nav-switch-color)}.console header .account{margin-right:25px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.console header .switch-theme{margin:2px 0}.console header .avatar{height:40px;width:40px}.console header .account-button{background:0 0;position:absolute;width:100%;height:40px;border-radius:0;z-index:1}.console header .notifications{position:relative;font-size:20px}.console header .notifications a{color:#1b3445}.console header .notifications:after{position:absolute;content:"";display:block;background:var(--config-color-danger);width:8px;height:8px;border-radius:50%;top:3px;left:3px}.console header nav{background:#1b3445;background:linear-gradient(var(--config-console-nav-start),var(--config-console-nav-end));color:#788c99;position:fixed;height:100%;width:220px;top:0;right:0}.console header nav .logo{height:39px;padding:15px 20px;display:block}.console header nav .logo img{display:inline-block;margin-top:7px;margin-bottom:14px}.console header nav .logo svg g{fill:var(--config-color-focus)}.console header nav .icon{display:block;border:none;margin:18px 10px 50px 10px}.console header nav .icon img{display:block}.console header nav .icon:hover{border-bottom:none}.console header nav .icon:hover svg g{fill:var(--config-color-focus)}.console header nav .container{overflow:auto;height:calc(100% - 133px);width:100%}.console header nav .project-box{padding:20px;text-align:center;display:block;border:none;line-height:100px;height:100px}.console header nav .project-box img{max-height:80px;max-width:80%;display:inline-block;vertical-align:middle}.console header nav .project{display:block;padding:85px 25px 20px 25px;color:#788c99;position:relative;border:none;height:20px}.console header nav .project:hover{border-bottom:none}.console header nav .project .name{height:20px;line-height:20px;margin:0;padding:0;display:inline-block;max-width:100%}.console header nav .project .arrow{display:block;position:absolute;left:5px;top:10px;width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #788c99;transform:rotate(225deg)}.console header nav .project img{position:absolute;bottom:40px;display:block;margin-bottom:10px;max-height:35px;max-width:40%}.console header nav .subtitle{padding:0 30px;display:block;font-size:12px;font-weight:300}.console header nav .links{margin-bottom:15px!important}.console header nav .links.top{border:none;padding-bottom:0;margin-bottom:5px!important}.console header nav .links.bottom{position:absolute;bottom:0;left:0;right:0;padding-bottom:0;border:none;margin-bottom:0!important;box-shadow:0 0 10px rgba(0,0,0,.1)}.console header nav .links.bottom a{border-top:solid 1px var(--config-console-nav-border);border-bottom:none}.console header nav .links .sub{display:inline-block;border:none;width:25px;height:25px;line-height:25px;border-radius:50%;padding:0;background:var(--config-color-focus);color:#fff;text-align:center;font-size:12px;margin:18px}.console header nav .links .sub i{width:auto;margin:0}.console header nav .links .sub:hover{border:none}.console header nav .links a{padding:8px 20px;border:none;display:block;color:#87a5b9;font-weight:400;border-right:solid 5px transparent;font-size:13px}.console header nav .links a i{margin-left:8px;width:22px;display:inline-block}.console header nav .links a.selected,.console header nav .links a:hover{color:#e4e4e4}.console header nav:after{content:'';display:block;position:absolute;background:#302839;height:100px;width:100%;bottom:-100px}.console>footer{width:calc(100% + 100px);margin:0 -50px;box-sizing:border-box;background:0 0;padding-left:30px;padding-right:30px}.console>footer ul{float:none;text-align:center}.console>footer ul li{float:none;display:inline-block}.console .projects{position:relative}.console .projects:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.console .projects li{float:right;margin-left:50px;margin-bottom:50px;width:270px}.console .projects li:nth-child(3n){margin-left:0}.console .dashboard{padding:20px;min-height:95px;overflow:hidden;position:relative;z-index:1}.console .dashboard hr{margin:20px -20px;height:2px;background:var(--config-console-background)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console .dashboard hr{height:3px}}.console .dashboard footer{margin:-20px;padding:20px;background:#fcfeff;border:none;color:var(--config-color-link)}.console .dashboard .col{position:relative}.console .dashboard .col:last-child:after{display:none}.console .dashboard .col:after{content:"";display:block;width:2px;background:var(--config-console-background);height:calc(100% + 110px);position:absolute;top:-20px;bottom:-20px;left:24px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console .dashboard .col:after{width:calc(100% + 40px);height:3px;position:static;margin:20px -20px}}.console .dashboard .value{color:var(--config-color-focus);vertical-align:bottom;line-height:45px}.console .dashboard .value .sum{font-size:45px;line-height:45px;font-weight:700;vertical-align:bottom}.console .dashboard .unit{font-weight:500;line-height:20px;vertical-align:bottom;font-size:16px;display:inline-block;margin-bottom:5px;margin-right:5px;color:var(--config-color-focus)}.console .dashboard .metric{color:var(--config-color-focus);font-weight:400;font-size:13px;line-height:16px}.console .dashboard .range{color:var(--config-color-fade);font-weight:400;font-size:14px;line-height:16px}.console .dashboard a{display:block;font-weight:400;font-size:14px;line-height:16px;padding:0;border:none}.console .dashboard .chart-metric{width:19%}@media only screen and (min-width:551px) and (max-width:1198px),only screen and (max-width:550px){.console .dashboard .chart-metric{width:100%}}.console .dashboard .chart{width:80%;position:relative;height:0;padding-top:20px;padding-bottom:26%;margin-left:-2px;overflow:hidden;background-color:var(--config-color-background-fade);background-image:linear-gradient(transparent 1px,transparent 1px),linear-gradient(90deg,transparent 1px,transparent 1px),linear-gradient(var(--config-border-color) 1px,transparent 1px),linear-gradient(90deg,var(--config-border-color) 1px,transparent 1px);background-size:100px 100px,100px 100px,20px 20px,20px 20px;background-position:-2px -2px,-2px -2px,-1px -1px,-1px -1px;background-repeat:round;border:solid 1px var(--config-border-color);border-right:solid 1px transparent;border-bottom:solid 1px transparent}@media only screen and (min-width:551px) and (max-width:1198px),only screen and (max-width:550px){.console .dashboard .chart{width:100%;padding-bottom:32%;float:none;margin-bottom:20px}}.console .dashboard .chart canvas{position:absolute;bottom:0;display:block;height:100%;width:100%}.console .community a{padding:0 10px;display:inline-block}.console .link-list li{margin-bottom:15px}.console .link-list i{display:inline-block;width:30px;height:30px;line-height:30px;text-align:center;background:var(--config-color-fade);color:var(--config-color-fade-super);border-radius:50%;margin-left:15px}.console .link-list i.fade{background:0 0;color:var(--config-color-fade)}.console .provider{width:50px;height:50px;background:#f5f5f5;color:#868686;line-height:50px;text-align:center;font-size:25px;border-radius:50%}.console .provider.facebook{color:#fff;background:#3b5998}.console .provider.twitter{color:#fff;background:#55beff}.console .provider.telegram{color:#fff;background:#3ba9e1}.console .provider.github{color:#fff;background:#24292e}.console .provider.whatsapp{color:#fff;background:#25d366}.console .provider.linkedin{color:#fff;background:#1074af}.console .provider.microsoft{color:#fff;background:#137ad4}.console .provider.google{color:#fff;background:#4489f1}.console .provider.bitbucket{color:#fff;background:#2a88fb}.console .provider.gitlab{color:#faa238;background:#30353e}.console .provider.instagram{color:#fff;background:radial-gradient(circle at 30% 107%,#fdf497 0,#fdf497 5%,#fd5949 45%,#d6249f 60%,#285aeb 90%)}.console .premium{z-index:3;margin-top:320px}.console .premium .message{height:190px;overflow:hidden;position:absolute;top:-280px}.console .premium:after{content:'';position:absolute;top:0;left:-20px;right:-20px;bottom:-20px;background:var(--config-color-background);opacity:.7;z-index:300}.console .app-section{height:90px}.console .confirm{background:var(--config-color-link);color:#fff;border-radius:25px;padding:12px;line-height:28px;text-align:center}.console .confirm .action{font-weight:500;cursor:pointer}.console .platforms{overflow:hidden}.console .platforms .box{overflow:hidden}.console .platforms .box img{width:50px;margin:0 auto;margin-bottom:20px}.console .platforms .box .cover{margin:-30px -30px 30px -30px;padding:30px}.console .platforms .box .cover.android{background:#a4ca24}.console .platforms .box .cover.android h1{color:#fff;font-size:18px;margin-top:20px}.console .platforms .col{text-align:center;line-height:30px}.console .platforms a{display:block;margin:-20px;padding:20px}.console .platforms a:hover{background:#fbfeff}.console .platforms img{display:block;margin:0 30px;width:calc(100% - 60px);border-radius:50%;margin-bottom:20px}.console .document-nav{display:none;position:sticky;top:90px}@media only screen and (min-width:1380px){.console .document-nav{display:block}}.console .document-nav ul{position:absolute;width:200px;right:-260px}.console .document-nav ul li{margin-bottom:20px}.console .document-nav ul li .selected{font-weight:500}.console .scroll-to{display:none}@media only screen and (min-width:1199px){.console .logo .top{display:none!important}}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console>header{width:calc(100% - 30px)!important;margin:0 -30px;padding:15px}.console>header nav{width:100%;height:70px;overflow:hidden}.console>header nav.close{background:0 0}.console>header nav.close .logo .nav{display:none!important}.console>header nav.open{height:100%}.console>header nav.open .logo .top{display:none!important}.console>header nav.open .bottom{display:block!important}.console>header nav.open button{color:#87a5b9}.console>header nav button{margin:9px;background:0 0;color:var(--config-color-normal)}.console>header nav button:focus,.console>header nav button:hover{background:0 0}.console>header nav .logo{display:block!important;position:absolute;top:0;left:50%;margin:auto;transform:translateX(-50%)}.console>header nav .bottom{display:none!important}.console>footer{width:auto;margin:50px -30px 0 -30px!important;padding:0 30px 30px 30px}.console body{height:"calc(100% - 70px)"!important;width:calc(100% - 60px)!important;padding:70px 30px 0 30px!important}.console .cover{padding:25px 30px;margin:0 -30px}}@media only screen and (max-width:550px){.console body{height:"calc(100% - 70px)"!important;width:calc(100% - 40px)!important;padding:70px 20px 0 20px!important}.console .cover{padding:20px 20px;margin:0 -20px}.console>header{margin:0 -20px}.console>header .list{width:175px;font-size:14px}.console>footer{margin:50px -20px 0 -20px!important;padding:0 20px 20px 20px}}.dev-feature{display:none}.prod-feature{display:none}.development .dev-feature{display:block;opacity:.6!important;outline:solid #ff0 3px;outline-offset:3px}.development .dev-feature.dev-inline{display:inline-block}.development .prod-feature{display:none}.production .dev-feature{display:none}.production .prod-feature{display:block}.search{opacity:1!important}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.search button{margin-top:20px}}html.home body{padding:0 50px;color:var(--config-color-normal)}html.home .logo a{display:block}html.home .logo a:hover{opacity:.8}html.home .logo img{max-height:35px;width:198px;margin:45px auto 25px auto}html.home footer{background:0 0;text-align:center}html.home main{min-height:400px}.alerts ul{width:100%;visibility:hidden;position:fixed;padding:0;right:0;left:0;color:var(--config-color-normal);z-index:1002;margin:0 auto;bottom:15px;max-width:560px}.alerts ul li{margin:10px 0 0 0;padding:0}.alerts ul li div.message{position:relative;padding:12px 35px;margin:0 auto;list-style:none;background:var(--config-color-background-dark);text-align:center;font-size:14px;border-radius:10px;line-height:16px;min-height:16px;box-shadow:0 0 10px rgba(0,0,0,.05);opacity:.95}.alerts ul li div.message a,.alerts ul li div.message span{font-weight:600}.alerts ul li div.message i{cursor:pointer;position:absolute;font-size:14px;line-height:22px;top:8px;right:8px}.alerts ul li div.message.error{color:#fff;background:var(--config-color-danger)}.alerts ul li div.message.success{color:#fff;background:var(--config-color-success)}.alerts ul li div.message.warning{color:#fff;background:var(--config-color-success)}.alerts ul li div.message.open{display:block}.alerts ul li div.message.close{display:none}.alerts .cookie-alert{background:var(--config-color-focus-fade)!important;color:var(--config-color-focus)}.alerts .cookie-alert a{color:var(--config-color-focus);font-weight:400;border-bottom:dotted 1px var(--config-color-focus)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.alerts ul{top:auto;bottom:0;max-width:100%}.alerts ul li{margin:5px 0 0 0}.alerts ul li div.message{border-radius:0}}article{overflow-wrap:break-word;word-wrap:break-word}article h1{font-size:36px}article h2{font-size:24px}article h3{font-size:20px}article h4{font-size:20px}article h5{font-size:18px}article h6{font-size:16px}article h1,article h2,article h3,article h4,article h5,article h6{margin-top:30px!important;margin-bottom:30px!important}article p{line-height:32px;font-size:16px}article .update{display:block;margin-top:50px!important}article table{width:100%;margin:0;margin-bottom:30px!important;border-radius:0;border-bottom:solid 1px var(--config-border-color)}article table thead td{font-weight:500;padding:5px 15px}article table td,article table th{padding:15px;height:auto}article table td:first-child,article table th:first-child{padding-right:10px}article table td:last-child,article table th:last-child{padding-left:10px}article table td p,article table th p{font-size:inherit;line-height:inherit}article table td p:last-child,article table th p:last-child{margin:0}.avatar-container{position:relative}.avatar-container .corner{position:absolute;bottom:-3px;left:-3px}.avatar{width:60px;height:60px;border-radius:50%;background:var(--config-color-background-focus);display:inline-block;overflow:hidden;box-shadow:0 0 6px rgba(0,0,0,.09);position:relative;z-index:1;opacity:1!important}.avatar:before{content:"";position:absolute;width:100%;height:100%;z-index:0;background:var(--config-color-background-focus)}.avatar.inline{display:inline-block;vertical-align:middle}.avatar.trans{background:0 0}.avatar .no-shadow{box-shadow:none}.avatar.xs{width:30px;height:30px}.avatar.xxs{width:20px;height:20px}.avatar.small{width:50px;height:50px}.avatar.big{width:100px;height:100px}.avatar.huge{width:150px;height:150px}.box{position:relative;background:var(--config-color-background-fade);border-radius:10px;box-shadow:0 0 3px rgba(0,0,0,.05);padding:30px;display:block;border-bottom:none}.box.padding-small{padding:15px}.box.y-scroll{overflow-y:auto}.box.danger{background:var(--config-color-danger);color:#fff}.box.danger .box{color:var(--config-color-normal);background:var(--config-color-background-fade)}.box.danger>.button,.box.danger>button{background:#fff;color:var(--config-color-danger)}.box.note{background:var(--config-note-background)}.box.focus{background:var(--config-color-focus);color:var(--config-color-background-fade)}.box.focus .button,.box.focus button{background:var(--config-color-background-fade);color:var(--config-color-focus)}.box.line{background:0 0;border:solid 1px var(--config-color-background-dark);box-shadow:none}.box.warning{background:var(--config-color-success);color:#2d2d2d}.box.warning .button,.box.warning button{background:rgba(45,45,45,.8);color:var(--config-color-success)}.box .tabs{border-bottom:solid 1px var(--config-border-color);margin:0 -30px;padding:0 30px!important}.box>footer{margin:0 -30px -30px -30px;padding:15px 30px;background:var(--config-color-background-fade);border:solid 1px var(--config-border-color);border-radius:0 0 10px 10px}.box hr{height:1px;background:var(--config-console-background);border:none;margin:30px -30px}.box .label{position:absolute;top:10px;z-index:2;left:10px}.box.fade-bottom{position:relative;overflow:hidden}.box.fade-bottom:after{content:"";position:absolute;display:block;bottom:15px;width:100%;background:#000;background:linear-gradient(180deg,rgba(0,0,0,0) 0,var(--config-color-background-fade) 80%);height:100px;margin:0 -15px}.box .header{position:static;height:40px;padding:20px 30px 20px 30px;margin-bottom:30px;margin:-30px -30px 20px -30px;background:var(--config-color-background-fade);border-bottom:solid 1px #efefef}.box ul.numbers>li{position:relative;margin-right:30px;margin-left:50px}.box ul.numbers>li hr{margin-right:-60px;margin-left:-80px}.box ul.numbers>li .settings{position:absolute;top:3px;left:-50px}.box ul.numbers>li::after{display:block;width:25px;height:25px;line-height:25px;font-size:13px;font-weight:500;border-radius:50%;background:var(--config-color-focus);color:var(--config-color-background);counter-increment:section;content:counter(section);text-align:center;position:absolute;top:3px;right:-45px}.box .scroll{margin:0 -30px;overflow-y:scroll}.box .scroll table{width:100%;margin:0}.box ul.sortable{counter-reset:section}.box ul.sortable>li [data-move-down].round,.box ul.sortable>li [data-move-up].round,.box ul.sortable>li [data-remove].round{background:var(--config-color-focus);color:var(--config-color-background-fade);width:25px;height:25px;line-height:25px;display:inline-block;text-align:center;padding:0;margin-left:5px}.box ul.sortable>li [data-move-down].round:disabled,.box ul.sortable>li [data-move-up].round:disabled,.box ul.sortable>li [data-remove].round:disabled{display:none}.box ul.sortable>li:first-child [data-move-up]{display:none}.box ul.sortable>li:first-child [data-move-up]:disabled{display:inline-block;background:var(--config-color-background)}.box ul.sortable>li:last-child [data-move-down]{display:none}.box ul.sortable>li:last-child [data-move-down]:disabled{display:inline-block;background:var(--config-color-background)}.box .toggle{position:relative;border-top:1px solid var(--config-console-background);border-bottom:1px solid var(--config-console-background);margin:0 -30px;padding:30px 30px 0 30px;height:65px;overflow:hidden}.box .toggle.list{border-bottom:none}.box .toggle.sorts button.ls-ui-open{width:calc(100% - 100px)}.box .toggle button.ls-ui-open{left:0;position:absolute;top:0;width:100%;height:95px;background:0 0;opacity:.5;border-radius:0}.box .toggle .icon-minus,.box .toggle .icon-up-open{display:none}.box .toggle .content{display:none}.box .toggle.open{height:auto}.box .toggle.open .icon-minus,.box .toggle.open .icon-up-open{display:block}.box .toggle.open .icon-down-open,.box .toggle.open .icon-plus{display:none}.box .toggle.open .content{display:block}.box .list li{border-bottom:solid 2px var(--config-border-color);margin:0 -30px 30px -30px;padding:0 30px 30px 30px}.box .list li:last-child{padding-bottom:0;margin-bottom:0;border-bottom:none}@media only screen and (max-width:550px){.box .list li .actions{float:none}}.box.new{text-align:center}.box.new i{font-size:80px;line-height:80px;font-family:Poppins,sans-serif;font-style:normal;font-weight:300}.box.new b{margin-top:20px;display:block}.box .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.box .info hr{background:var(--config-modal-note-border)!important}.box .table-wrap{margin:0 -30px;overflow-y:scroll}.box .table-wrap table{margin:0}a.box{border-right:none;border-left:none}a.box:hover{box-shadow:0 0 1px rgba(0,0,0,.2);opacity:.7}.box-asidex{padding-left:25px!important;padding-right:70px;left:0;background:#f9f9f9;border-radius:0 10px 10px 0;height:calc(100% - 30px);position:absolute;padding-top:30px}.box-asidex:after{content:"";display:block;position:absolute;height:100%;width:51px;background:#fff;top:0;bottom:0;right:-6px}.cover{background:var(--config-color-focus-fade);padding:30px 50px;margin:0 -50px;position:relative}.cover .title,.cover h1,.cover h2,.cover h3,.cover h4{color:var(--config-color-focus);font-weight:600;margin-bottom:50px!important;font-size:28px;line-height:42px}.cover .title span,.cover h1 span,.cover h2 span,.cover h3 span,.cover h4 span{font-weight:600}.cover i:before{margin:0!important}.cover p{color:var(--config-color-fade)}.cover .button{color:#fff}.cover .link,.cover a{color:var(--config-color-focus);border-left:none;border-right:none;cursor:pointer}.cover .link:hover,.cover a:hover{border-bottom-color:var(--config-color-focus)}.console .database .row .col{height:452px}.console .database .row .col:after{width:2px;left:20px}.console .database hr{margin:0 -20px;background:var(--config-color-background);height:1px}.console .database h3{font-size:13px;line-height:20px;height:20px;background-color:var(--config-color-fade-super);margin:-20px -20px 0 -20px;padding:10px 20px;border-bottom:solid 1px var(--config-color-background);font-weight:600}.console .database .empty{height:162px;font-size:12px;text-align:center;margin:50px 0}.console .database .empty h4{font-size:13px;font-weight:600;line-height:120px}.console .database .search{background-color:var(--config-color-fade-super);margin:0 -20px 0 -20px;padding:10px 15px}.console .database .search input{height:40px;background-color:#fff;border-radius:25px;padding-top:0;padding-bottom:0}.console .database .code{height:411px;background:var(--config-color-fade-super);margin:0 -20px -20px -20px;padding:20px;width:calc(100% - 10px)}.console .database .code .ide{overflow:scroll;height:451px;margin:-20px;box-shadow:none;border-radius:0}.console .database .paging{background:var(--config-color-fade-super);margin:0 -20px -20px -20px;padding:20px}.console .database .button{margin:0 -20px;padding:0 20px!important;text-align:inherit;color:var(--config-color-focus);width:100%;font-size:15px;line-height:55px;box-sizing:content-box}.console .database .button i{margin-left:8px}.console .database .button:hover{border:none;background:var(--config-color-focus-fade)}.console .database .items{margin:0 -20px;height:262px;overflow-x:hidden;overflow-y:scroll}.console .database .items form{opacity:0;position:relative}.console .database .items form button{position:absolute;top:0;bottom:0;right:0;left:0;width:100%;height:45px;border-radius:0;cursor:pointer}.console .database .items li{padding:0;margin:0 0;line-height:45px;font-size:15px;padding-right:50px;padding-left:30px;position:relative}.console .database .items li i{position:absolute;display:none;left:10px}.console .database .items li .name{display:inline-block;width:100%;height:28px}.console .database .items li.selected,.console .database .items li:hover{background:#f5f5f5}.console .database .items li.selected i,.console .database .items li:hover i{display:block}.console .database .items li:last-child{border-bottom:none}body>footer{color:var(--config-color-fade);line-height:40px;margin:0 -50px;padding:12px 50px;font-size:13px;width:100%;background:#f1f1f1;position:relative;margin-top:80px!important}body>footer:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}body>footer .logo img{height:22px;padding-top:12px}body>footer a{color:var(--config-color-fade);font-size:13px}body>footer a:hover{border-bottom-color:var(--config-color-fade)}body>footer ul:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}body>footer ul li{font-size:13px;float:right;margin-left:20px!important}body>footer .copyright{padding-right:2px}[data-ls-if]{display:none}[data-service]{opacity:0}.load-service-start{opacity:0}.load-service-end{opacity:1;transition:opacity .5s ease-out;-moz-transition:opacity .5s ease-out;-webkit-transition:opacity .5s ease-out;-o-transition:opacity .5s ease-out}.load-screen{z-index:100000;position:fixed;height:100%;width:100%;background-color:var(--config-color-background-focus);top:0;right:0}.load-screen.loaded{transition:opacity 1s ease-in-out,top 1s .7s;opacity:0;top:-100%}.load-screen .animation{position:absolute;top:45%;left:50%;transform:translate(-50%,-50%) translateZ(1px);width:140px;height:140px}.load-screen .animation div{box-sizing:border-box;display:block;position:absolute;width:124px;height:124px;margin:10px;border:10px solid var(--config-color-focus);border-radius:50%;animation:animation 1.2s cubic-bezier(.5,0,.5,1) infinite;border-color:var(--config-color-focus) transparent transparent transparent}.load-screen .animation div:nth-child(1){animation-delay:-.45s}.load-screen .animation div:nth-child(2){animation-delay:-.3s}.load-screen .animation div:nth-child(3){animation-delay:-.15s}@keyframes animation{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.load-screen img{position:absolute;height:20px;bottom:60px;left:50%;transform:translate(-50%,-50%)}.modal-open .modal-bg,.modal-open body .modal-bg{position:fixed;content:'';display:block;width:100%;height:100%;left:0;right:0;top:0;bottom:0;background:#0c0c0c;opacity:.5;z-index:4}.modal{overflow:auto;display:none;position:fixed;transform:translate3d(0,0,0);width:100%;max-height:90%;max-width:640px;background:var(--config-color-background-fade);z-index:1000;box-shadow:0 0 4px rgba(0,0,0,.25);padding:30px;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:10px;box-sizing:border-box;text-align:right;white-space:initial;line-height:normal}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.modal{width:calc(100% - 20px)}}.modal.full{max-width:none;max-height:none;height:100%;border-radius:0;padding:80px 120px}.modal.full h1{font-weight:700}.modal.padding-small{padding:15px}.modal.height-tiny>form{height:100px}.modal.height-small>form{height:220px}.modal.width-small{max-width:400px}.modal.width-medium{max-width:500px}.modal.width-large{max-width:800px}.modal.open{display:block}.modalbutton.close{display:none}.modal.fill{height:95%;max-height:95%;max-width:75%}.modal h1,.modal h2{margin-bottom:25px;margin-top:0;font-size:20px;text-align:right}.modal h1,.modal h2,.modal h3,.modal h4,.modal h5,.modal h6{color:inherit!important;line-height:35px}.modal .main,.modal>form{position:relative;border-top:solid 1px var(--config-border-color);padding:30px 30px 0 30px;margin:0 -30px}.modal .main.strip,.modal>form.strip{border:none;padding:0;margin:0}.modal .separator{margin:20px -30px}.modal .bullets{padding-right:40px}.modal .bullets li{margin-bottom:30px!important}.modal .bullets li:before{position:absolute}.modal .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.modal .ide.strech{box-shadow:none;border-radius:0;margin:0 -30px}.modal button.close{width:30px;height:30px;line-height:30px;padding:0;margin:0;background:var(--config-color-normal);color:var(--config-color-background-fade);border-radius:50%}.modal .paging form{padding:0;margin:0;border-top:none}.modal.sticky-footer form footer{margin:-30px}.modal.sticky-footer footer{position:sticky;bottom:-30px;background:var(--config-color-background-fade-super);height:50px;z-index:1;padding:30px;box-shadow:0 0 1px rgba(0,0,0,.15)}.modal.sticky-footer footer form{display:inline-block}[data-views-current="0"] .scroll-to,[data-views-current="1"] .scroll-to{opacity:0!important}.scroll-to-bottom .scroll-to,.scroll-to-top .scroll-to{opacity:1}.scroll-to{opacity:0;display:block;width:40px;height:40px;line-height:40px;border-radius:50%;position:fixed;transform:translateZ(0);margin:30px;padding:0;bottom:0;font-size:18px;z-index:100000;transition:opacity .15s ease-in-out;left:0}.phases{list-style:none;margin:0;padding:0;position:relative}.phases li{display:none}.phases li li{display:block}.phases li.selected{display:block}.phases .number{display:none}.phases h2,.phases h3,.phases h4,.phases h5,.phases h6{margin:0 0 30px 0;text-align:inherit}.container{position:relative}.container .tabs{height:55px;line-height:55px;list-style:none;padding:0;margin-bottom:50px!important;margin-top:-55px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.container .tabs:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.container .tabs .selected{font-weight:400;color:var(--config-color-focus);position:relative;opacity:1}.container .tabs .selected:after{content:"";display:block;height:2px;background:var(--config-color-focus);width:calc(100% + 6px);margin:0 -3px;position:absolute;bottom:0;border-radius:2px}.container .tabs .number{display:none}.container .tabs li{float:right;margin-left:50px;color:var(--config-color-focus);opacity:.9;cursor:pointer}.container .tabs li:focus{outline:0}@media only screen and (max-width:550px){.container .tabs li{margin-left:25px}}.container .icon{display:none}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.container .tabs{width:auto;overflow-x:scroll;overflow-y:hidden;white-space:nowrap}.container .tabs li{display:inline-block;float:none}}.ide{background-color:var(--config-prism-background);overflow:hidden;position:relative;z-index:1;box-shadow:0 2px 4px 0 rgba(50,50,93,.3);border-radius:10px;margin-bottom:30px}.ide *{font-family:'Source Code Pro',monospace}.ide[data-lang]::after{content:attr(data-lang-label);display:inline-block;background:#fff;color:#000;position:absolute;top:15px;padding:5px 10px;border-radius:15px;font-size:10px;left:10px;opacity:.95}.ide[data-lang=bash]::after{background:var(--config-language-bash);color:var(--config-language-bash-contrast)}.ide[data-lang=javascript]::after{background:var(--config-language-javascript);color:var(--config-language-javascript-contrast)}.ide[data-lang=web]::after{background:var(--config-language-web);color:var(--config-language-web-contrast)}.ide[data-lang=html]::after{background:var(--config-language-html);color:var(--config-language-html-contrast)}.ide[data-lang=php]::after{background:var(--config-language-php);color:var(--config-language-php-contrast)}.ide[data-lang=nodejs]::after{background:var(--config-language-nodejs);color:var(--config-language-nodejs-contrast)}.ide[data-lang=ruby]::after{background:var(--config-language-ruby);color:var(--config-language-ruby-contrast)}.ide[data-lang=python]::after{background:var(--config-language-python);color:var(--config-language-python-contrast)}.ide[data-lang=go]::after{background:var(--config-language-go);color:var(--config-language-go-contrast)}.ide[data-lang=dart]::after{background:var(--config-language-dart);color:var(--config-language-dart-contrast)}.ide[data-lang=flutter]::after{background:var(--config-language-flutter);color:var(--config-language-flutter-contrast)}.ide[data-lang=yaml]::after{background:var(--config-language-yaml);color:var(--config-language-yaml-contrast)}.ide .tag{color:inherit!important;background:0 0!important;padding:inherit!important;font-size:inherit!important;line-height:14px}.ide .copy{cursor:pointer;content:attr(data-lang);display:inline-block;background:#fff;color:#000;position:absolute;transform:translateX(-50%);bottom:-20px;padding:5px 10px;border-radius:15px;font-size:10px;font-style:normal;right:50%;opacity:0;transition:bottom .3s,opacity .3s;line-height:normal;font-family:Poppins,sans-serif}.ide .copy::before{padding-left:5px}.ide:hover .copy{transition:bottom .3s,opacity .3s;opacity:.9;bottom:16px}.ide pre{-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;color:#e6ebf1;font-weight:400;line-height:20px;font-size:13px;margin:0;padding:20px;padding-left:60px}.ide.light{box-shadow:0 2px 4px 0 rgba(50,50,93,.1);background-color:#fff}.ide.light pre{color:#414770}.ide.light .token.cdata,.ide.light .token.comment,.ide.light .token.doctype,.ide.light .token.prolog{color:#91a2b0}.ide.light .token.attr-name,.ide.light .token.builtin,.ide.light .token.char,.ide.light .token.inserted,.ide.light .token.selector,.ide.light .token.string{color:#149570}.ide.light .token.punctuation{color:#414770}.ide.light .language-css .token.string,.ide.light .style .token.string,.ide.light .token.entity,.ide.light .token.operator,.ide.light .token.url,.ide.light .token.variable{color:#414770}.ide.light .line-numbers .line-numbers-rows{background:#f2feef}.ide.light .line-numbers-rows>span:before{color:#5dc79e}.ide.light .token.keyword{color:#6772e4;font-weight:500}code[class*=language-],pre[class*=language-]{text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4}pre[class*=language-]{overflow:auto}:not(pre)>code[class*=language-]{padding:.1em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#6b7c93}.token.punctuation{color:#f8f8f2}.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#f79a59}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#3ecf8e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#45b2e8}.token.keyword{color:#7795f8}.token.important,.token.regex{color:#fd971f}.token.italic{font-style:italic}.token.entity{cursor:help}pre[class*=language-].line-numbers{position:relative;padding-left:60px;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{background:var(--config-prism-numbers);position:absolute;pointer-events:none;top:-20px;bottom:-21px;padding:20px 0;font-size:100%;left:-60px;width:40px;letter-spacing:-1px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{padding-left:5px;pointer-events:none;display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#636365;display:block;padding-right:.8em;text-align:right}html{padding:0;margin:0;direction:rtl}body{margin:0;background:var(--config-console-background) no-repeat fixed;min-width:300px}ul{padding:0;margin:0}ul li{margin:0;list-style:none}.icon-left-open:before{content:'\e814'!important}.icon-right-open:before{content:'\e813'!important}.icon-link-ext:before{-moz-transform:scaleX(-1);-o-transform:scaleX(-1);-webkit-transform:scaleX(-1);transform:scaleX(-1)}.icon-article-alt:before{-moz-transform:scaleX(-1);-o-transform:scaleX(-1);-webkit-transform:scaleX(-1);transform:scaleX(-1)}.copy{border-radius:10px 0 0 10px!important} \ No newline at end of file +.pull-start{float:right}.pull-end{float:left}img[src=""]{visibility:hidden;display:inline-block}:root{--config-width:910px;--config-width-xxl:1000px;--config-width-xl:910px;--config-width-large:700px;--config-width-medium:550px;--config-width-small:320px;--config-color-link:#1e849e;--config-color-background:#eceff1;--config-color-background-dark:#dfe2e4;--config-color-background-fade:#ffffff;--config-color-background-fade-super:#fdfdfd;--config-color-background-focus:#f5f5f5;--config-color-background-input:#ffffff;--config-color-placeholder:#868686;--config-color-tooltip-text:#dce8f5;--config-color-tooltip-background:#333333;--config-color-focus:#f02e65;--config-color-focus-fade:#fef8fa;--config-color-focus-hover:#ff729b;--config-color-focus-glow:#fce5ec;--config-color-focus-dark:#c52653;--config-color-normal:#40404c;--config-color-dark:#313131;--config-color-fade:#8f8f8f;--config-color-fade-light:#e2e2e2;--config-color-fade-super:#f1f3f5;--config-color-danger:#f53d3d;--config-color-success:#1bbf61;--config-color-warning:#ffed4d;--config-color-info:#386fd2;--config-border-color:#f5f5f5;--config-border-fade:#e0e3e4;--config-border-radius:10px;--config-prism-background:#373738;--config-prism-numbers:#39393c;--config-note-background:#f1fbff;--config-note-border:#5bceff;--config-warning-background:#fdf7d9;--config-warning-border:#f8e380;--config-social-twitter:#1da1f2;--config-social-github:#000000;--config-social-discord:#7189dc;--config-social-facebook:#4070b4;--config-language-bash:#2b2626;--config-language-bash-contrast:#fff;--config-language-javascript:#fff054;--config-language-javascript-contrast:#333232;--config-language-web:#fff054;--config-language-web-contrast:#333232;--config-language-html:#ff895b;--config-language-html-contrast:#ffffff;--config-language-yaml:#ca3333;--config-language-yaml-contrast:#ffffff;--config-language-php:#6182bb;--config-language-php-contrast:#ffffff;--config-language-nodejs:#8cc500;--config-language-nodejs-contrast:#ffffff;--config-language-ruby:#fc3f48;--config-language-ruby-contrast:#ffffff;--config-language-python:#3873a2;--config-language-python-contrast:#ffffff;--config-language-go:#00add8;--config-language-go-contrast:#ffffff;--config-language-dart:#035698;--config-language-dart-contrast:#ffffff;--config-language-flutter:#035698;--config-language-flutter-contrast:#ffffff;--config-modal-note-background:#f5fbff;--config-modal-note-border:#eaf2f7;--config-modal-note-color:#3b5d73;--config-switch-background:#e2e2e2;--config-console-background:#eceff1;--config-console-nav-start:#143650;--config-console-nav-end:#302839;--config-console-nav-border:#2a253a;--config-console-nav-switch-background:#ececec;--config-console-nav-switch-color:#868686;--config-console-nav-switch-arrow:url("data:image/svg+xml;utf8,")}:root .theme-dark{--config-color-background:#061F2F;--config-color-background-dark:#262d50;--config-color-background-fade:#1c223a;--config-color-background-fade-super:#1a1f35;--config-color-background-focus:#1a1f35;--config-color-background-input:#dce8f5;--config-color-tooltip-text:#061F2F;--config-color-tooltip-background:#dce8f5;--config-color-link:#4caedb;--config-color-placeholder:#9ea1af;--config-color-focus:#c7d8eb;--config-color-focus-fade:#1e233e;--config-color-focus-hover:#d3deea;--config-color-focus-glow:#d3deea;--config-color-focus-dark:#657586;--config-color-normal:#c7d8eb;--config-color-dark:#c7d8eb;--config-color-fade:#bec3e0;--config-color-fade-light:#181818;--config-color-fade-super:#262D50;--config-color-danger:#d84a4a;--config-color-success:#34b86d;--config-color-warning:#e0d56d;--config-color-info:#386fd2;--config-border-color:#262D50;--config-border-fade:#19203a;--config-prism-background:#1F253F;--config-prism-numbers:#1F253F;--config-note-background:#171e33;--config-note-border:#262D50;--config-warning-background:#1F253F;--config-warning-border:#262D50;--config-social-twitter:var(--config-color-normal);--config-social-github:var(--config-color-normal);--config-social-discord:var(--config-color-normal);--config-social-facebook:var(--config-color-normal);--config-language-bash:var(--config-color-normal);--config-language-bash-contrast:var(--config-color-background);--config-language-javascript:var(--config-color-normal);--config-language-javascript-contrast:var(--config-color-background);--config-language-web:var(--config-color-normal);--config-language-web-contrast:var(--config-color-background);--config-language-yaml:var(--config-color-normal);--config-language-yaml-contrast:var(--config-color-background);--config-language-html:var(--config-color-normal);--config-language-html-contrast:var(--config-color-background);--config-language-php:var(--config-color-normal);--config-language-php-contrast:var(--config-color-background);--config-language-nodejs:var(--config-color-normal);--config-language-nodejs-contrast:var(--config-color-background);--config-language-ruby:var(--config-color-normal);--config-language-ruby-contrast:var(--config-color-background);--config-language-python:var(--config-color-normal);--config-language-python-contrast:var(--config-color-background);--config-language-go:var(--config-color-normal);--config-language-go-contrast:var(--config-color-background);--config-language-dart:var(--config-color-normal);--config-language-dart-contrast:var(--config-color-background);--config-language-flutter:var(--config-color-normal);--config-language-flutter-contrast:var(--config-color-background);--config-modal-note-background:#15192b;--config-modal-note-border:#161b31;--config-modal-note-color:var(--config-color-normal);--config-switch-background:var(--config-color-normal);--config-console-background:#20263f;--config-console-nav-start:#1c2139;--config-console-nav-end:#151929;--config-console-nav-border:#171b30;--config-console-nav-switch-background:var(--config-color-focus);--config-console-nav-switch-color:var(--config-color-background);--config-console-nav-switch-arrow:url("data:image/svg+xml;utf8,")}.theme-light .force-light{display:block!important}.theme-dark .force-dark{display:block!important}.force-dark{display:none!important}.force-light{display:none!important}@font-face{font-family:Poppins;font-style:normal;font-weight:100;src:url(/fonts/poppins-v9-latin-100.eot);src:local('Poppins Thin'),local('Poppins-Thin'),url(/fonts/poppins-v9-latin-100.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-100.woff2) format('woff2'),url(/fonts/poppins-v9-latin-100.woff) format('woff'),url(/fonts/poppins-v9-latin-100.ttf) format('truetype'),url(/fonts/poppins-v9-latin-100.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:300;src:url(/fonts/poppins-v9-latin-300.eot);src:local('Poppins Light'),local('Poppins-Light'),url(/fonts/poppins-v9-latin-300.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-300.woff2) format('woff2'),url(/fonts/poppins-v9-latin-300.woff) format('woff'),url(/fonts/poppins-v9-latin-300.ttf) format('truetype'),url(/fonts/poppins-v9-latin-300.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:400;src:url(/fonts/poppins-v9-latin-regular.eot);src:local('Poppins Regular'),local('Poppins-Regular'),url(/fonts/poppins-v9-latin-regular.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-regular.woff2) format('woff2'),url(/fonts/poppins-v9-latin-regular.woff) format('woff'),url(/fonts/poppins-v9-latin-regular.ttf) format('truetype'),url(/fonts/poppins-v9-latin-regular.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:500;src:url(/fonts/poppins-v9-latin-500.eot);src:local('Poppins Medium'),local('Poppins-Medium'),url(/fonts/poppins-v9-latin-500.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-500.woff2) format('woff2'),url(/fonts/poppins-v9-latin-500.woff) format('woff'),url(/fonts/poppins-v9-latin-500.ttf) format('truetype'),url(/fonts/poppins-v9-latin-500.svg#Poppins) format('svg')}@font-face{font-family:Poppins;font-style:normal;font-weight:600;src:url(/fonts/poppins-v9-latin-600.eot);src:local('Poppins SemiBold'),local('Poppins-SemiBold'),url(/fonts/poppins-v9-latin-600.eot?#iefix) format('embedded-opentype'),url(/fonts/poppins-v9-latin-600.woff2) format('woff2'),url(/fonts/poppins-v9-latin-600.woff) format('woff'),url(/fonts/poppins-v9-latin-600.ttf) format('truetype'),url(/fonts/poppins-v9-latin-600.svg#Poppins) format('svg')}@font-face{font-family:'Source Code Pro';font-style:normal;font-weight:400;src:url(/fonts/source-code-pro-v11-latin-regular.eot);src:local('Source Code Pro Regular'),local('SourceCodePro-Regular'),url(/fonts/source-code-pro-v11-latin-regular.eot?#iefix) format('embedded-opentype'),url(/fonts/source-code-pro-v11-latin-regular.woff2) format('woff2'),url(/fonts/source-code-pro-v11-latin-regular.woff) format('woff'),url(/fonts/source-code-pro-v11-latin-regular.ttf) format('truetype'),url(/fonts/source-code-pro-v11-latin-regular.svg#SourceCodePro) format('svg')}.padding{padding:30px}.padding-top{padding-top:30px!important}.padding-top-large{padding-top:50px!important}.padding-top-xl{padding-top:80px!important}.padding-bottom{padding-bottom:30px!important}.padding-bottom-large{padding-bottom:50px!important}.padding-bottom-xl{padding-bottom:80px!important}.margin-end{margin-left:20px!important}.margin-start{margin-right:20px!important}.margin-end-small{margin-left:10px!important}.margin-start-small{margin-right:10px!important}.margin-end-large{margin-left:50px!important}.margin-start-large{margin-right:50px!important}.margin-end-no{margin-left:0!important}.margin-start-no{margin-right:0!important}.margin-end-negative{margin-left:-30px!important}.margin-start-negative{margin-right:-30px!important}.margin-end-negative-small{margin-left:-15px!important}.margin-start-negative-small{margin-right:-15px!important}.margin-end-negative-tiny{margin-left:-5px!important}.margin-start-negative-tiny{margin-right:-5px!important}.margin-top{margin-top:30px!important}.margin-bottom{margin-bottom:30px!important}.margin-top-no{margin-top:0!important}.margin-bottom-no{margin-bottom:0!important}.margin-top-xxl{margin-top:140px!important}.margin-top-xl{margin-top:80px!important}.margin-top-large{margin-top:50px!important}.margin-top-small{margin-top:15px!important}.margin-top-tiny{margin-top:5px!important}.margin-top-negative{margin-top:-30px!important}.margin-top-negative-tiny{margin-top:-5px!important}.margin-top-negative-small{margin-top:-15px!important}.margin-top-negative-large{margin-top:-50px!important}.margin-top-negative-xl{margin-top:-80px!important}.margin-top-negative-xxl{margin-top:-100px!important}.margin-bottom-xxl{margin-bottom:140px!important}.margin-bottom-xl{margin-bottom:80px!important}.margin-bottom-large{margin-bottom:50px!important}.margin-bottom-small{margin-bottom:15px!important}.margin-bottom-tiny{margin-bottom:5px!important}.margin-bottom-negative{margin-bottom:-30px!important}.margin-bottom-negative-tiny{margin-bottom:-5px!important}.margin-bottom-negative-small{margin-bottom:-15px!important}.margin-bottom-negative-large{margin-bottom:-50px!important}.margin-bottom-negative-xl{margin-bottom:-80px!important}.margin-bottom-negative-xl{margin-bottom:-100px!important}.force-left,.ide{direction:ltr;text-align:left}.force-right{direction:rtl;text-align:right}.pull-left{float:left}.pull-right{float:right}.ratio-wide{height:0;overflow:hidden;padding-top:56%;position:relative;width:100%}.ratio-wide>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-square{height:0;overflow:hidden;padding-top:56%;position:relative;width:100%}.ratio-square>*{position:absolute;top:0;left:0;width:100%;height:100%}.clear:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.phones-only{display:none}@media only screen and (max-width:550px){.phones-only{display:inherit!important}}.tablets-only{display:none}@media only screen and (min-width:551px) and (max-width:1198px){.tablets-only{display:inherit!important}}.desktops-only{display:none}@media only screen and (min-width:1199px){.desktops-only{display:inherit!important}}.phones-only-inline{display:none}@media only screen and (max-width:550px){.phones-only-inline{display:inline-block!important}}.tablets-only-inline{display:none}@media only screen and (min-width:551px) and (max-width:1198px){.tablets-only-inline{display:inline-block!important}}.desktops-only-inline{display:none}@media only screen and (min-width:1199px){.desktops-only-inline{display:inline-block!important}}*{font-family:Poppins,sans-serif;-webkit-font-smoothing:antialiased;font-weight:300}h1,h2,h3,h4,h5,h6{margin:0}h4,h5,h6{font-weight:400}.link,a{color:var(--config-color-link);text-decoration:none;border-left:2px solid transparent;border-right:2px solid transparent;transition:.2s;cursor:pointer}.link.disabled,a.disabled{opacity:.5}.link.tag:hover,a.tag:hover{opacity:.9}.link.danger,a.danger{color:var(--config-color-danger)}.link.link-animation-enabled,a.link-animation-enabled{display:inline-block}.link.link-animation-enabled:hover,a.link-animation-enabled:hover{transform:translateY(-2px)}.link-return-animation--start>i{display:inline-block;transition:.2s}.link-return-animation--start:hover>i{transform:translateX(2px)}.link-return-animation--end>i{display:inline-block;transition:.2s}.link-return-animation--end:hover>i{transform:translateX(-2px)}b,strong{font-weight:500}p{margin:0 0 20px 0;line-height:26px}small{font-size:16px;color:var(--config-color-fade)}.text-size-small{font-size:13px}.text-size-xs{font-size:10px}.text-size-normal{font-size:16px}.text-height-large{height:30px;line-height:30px}.text-height-small{line-height:13px}.text-one-liner{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.text-bold{font-weight:400!important}.text-bold-large{font-weight:500!important}.text-bold-xl{font-weight:600!important}.text-danger{color:var(--config-color-danger)!important}.text-success{color:var(--config-color-success)!important}.text-upper{text-transform:uppercase}.text-warning{color:var(--config-color-warning)}.text-focus{color:var(--config-color-focus)}.text-fade{color:var(--config-color-fade)}.text-green{color:var(--config-color-success)}.text-red{color:var(--config-color-danger)}.text-info{color:var(--config-color-info)}.text-yellow{color:#ffe28b}.text-disclaimer{font-size:11px;color:var(--config-color-fade)}.text-fade-extra{color:var(--config-color-fade);opacity:.5}.text-line-high-large{line-height:30px}.text-line-high-xl{line-height:40px}.text-sign{margin:5px 0;font-size:25px;width:25px;height:25px;line-height:25px;display:inline-block}.text-align-center{text-align:center}.text-align-start{text-align:right}.text-align-end{text-align:left}.text-align-left{text-align:left}.text-align-right{text-align:right}.text-dir-ltr{direction:ltr;display:inline-block}.text-dir-rtl{direction:rtl;display:inline-block}.icon-dot-3:before{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-o-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}i[class*=' icon-']:before,i[class^=icon-]:before{display:inline;line-height:unset}table{width:calc(100% + 60px);border-collapse:collapse;margin:-30px;border-radius:10px;overflow:hidden;position:relative;table-layout:fixed}table.y-scroll{overflow-y:auto}table thead{box-shadow:0 0 2px rgba(0,0,0,.25);border-bottom:solid 1px var(--config-color-fade-super);font-size:14px}table.small{font-size:14px}table.open-end tbody tr:last-child{border-bottom:none;font-weight:700;background:#f7fbf7}table.full tbody td,table.full tbody th{vertical-align:top;white-space:normal;overflow:auto;line-height:24px;padding-top:20px;padding-bottom:20px;height:auto}table .avatar{width:30px;height:30px}table tr{border-bottom:solid 1px var(--config-color-fade-super)}table tr:last-child{border-bottom:none}table tr:nth-child(even){background:var(--config-color-background-fade-super)}table tr.selected{background:var(--config-note-background)}table tr.selected td,table tr.selected td span{font-weight:500}table th{text-align:right;font-weight:400}table th i{color:var(--config-color-fade);font-size:10px;display:inline-block;vertical-align:top;line-height:16px;padding:0 3px}table td,table th{height:65px;padding:0 15px;line-height:50px}table td:first-child,table th:first-child{padding-right:30px}table td:last-child,table th:last-child{padding-left:30px}table td,table th{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){table.vertical{border-top:solid 1px var(--config-color-fade-super);display:block;overflow:hidden;padding-top:12px}table.vertical .hide{display:none}table.vertical tbody,table.vertical td,table.vertical th,table.vertical thead,table.vertical tr{width:100%;display:block}table.vertical th,table.vertical tr{padding-top:12px;padding-bottom:12px}table.vertical th:first-child,table.vertical tr:first-child{padding-top:0}table.vertical td,table.vertical th{padding:5px 20px!important;text-overflow:ellipsis;white-space:normal;height:40px;line-height:40px;width:calc(100% - 40px)}table.vertical td:first-child,table.vertical td:last-child,table.vertical th:first-child,table.vertical th:last-child{padding:0 10px}table.vertical td:last-child,table.vertical th:last-child{padding-bottom:0}table.vertical td p,table.vertical th p{display:inline-block;width:calc(100% - 40px)}table.vertical td:not([data-title=""]):before{content:attr(data-title);margin-right:4px;font-weight:400}table.vertical thead{display:none}}.zone{max-width:var(--config-width-xl);margin:0 auto 40px auto}.zone.xxxl{max-width:calc(1400px - 100px)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.zone.xxxl{max-width:100%}}.zone.xxl{max-width:var(--config-width-xxl)}.zone.xl{max-width:var(--config-width-xl)}.zone.large{max-width:var(--config-width-large)}.zone.medium{max-width:var(--config-width-medium)}.zone.small{max-width:var(--config-width-small)}.row{position:relative;margin:0 -50px;padding-right:50px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.row{margin:0 -30px;padding-right:30px}}.row.force-ltr>.col{float:left}.row.force-rtl>.col{float:right}.row.force-reverse>.col{float:left}.row.wide{margin:0 -100px;padding-right:100px}.row.wide>.span-1{width:calc(8.33333333% * 1 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-2{width:calc(8.33333333% * 2 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-3{width:calc(8.33333333% * 3 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-4{width:calc(8.33333333% * 4 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-5{width:calc(8.33333333% * 5 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-6{width:calc(8.33333333% * 6 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-7{width:calc(8.33333333% * 7 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-8{width:calc(8.33333333% * 8 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-9{width:calc(8.33333333% * 9 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-10{width:calc(8.33333333% * 10 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-11{width:calc(8.33333333% * 11 - 100px);box-sizing:content-box;padding-left:100px}.row.wide>.span-12{width:calc(8.33333333% * 12 - 100px);box-sizing:content-box;padding-left:100px}.row.thin{margin:0 -20px;padding-right:20px}.row.thin>.span-1{width:calc(8.33333333% * 1 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-2{width:calc(8.33333333% * 2 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-3{width:calc(8.33333333% * 3 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-4{width:calc(8.33333333% * 4 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-5{width:calc(8.33333333% * 5 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-6{width:calc(8.33333333% * 6 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-7{width:calc(8.33333333% * 7 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-8{width:calc(8.33333333% * 8 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-9{width:calc(8.33333333% * 9 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-10{width:calc(8.33333333% * 10 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-11{width:calc(8.33333333% * 11 - 20px);box-sizing:content-box;padding-left:20px}.row.thin>.span-12{width:calc(8.33333333% * 12 - 20px);box-sizing:content-box;padding-left:20px}.row.modalize{margin:0 -30px;padding-right:30px}.row.modalize>.span-1{width:calc(8.33333333% * 1 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-2{width:calc(8.33333333% * 2 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-3{width:calc(8.33333333% * 3 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-4{width:calc(8.33333333% * 4 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-5{width:calc(8.33333333% * 5 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-6{width:calc(8.33333333% * 6 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-7{width:calc(8.33333333% * 7 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-8{width:calc(8.33333333% * 8 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-9{width:calc(8.33333333% * 9 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-10{width:calc(8.33333333% * 10 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-11{width:calc(8.33333333% * 11 - 30px);box-sizing:content-box;padding-left:30px}.row.modalize>.span-12{width:calc(8.33333333% * 12 - 30px);box-sizing:content-box;padding-left:30px}.row:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.row .col{float:right;box-sizing:border-box}.row .col.sticky-top{position:sticky;top:90px}.row .col.sticky-bottom{position:sticky;bottom:0}.row .span-1{width:calc(8.33333333% * 1 - 40px);box-sizing:content-box;padding-left:40px}.row .span-2{width:calc(8.33333333% * 2 - 40px);box-sizing:content-box;padding-left:40px}.row .span-3{width:calc(8.33333333% * 3 - 40px);box-sizing:content-box;padding-left:40px}.row .span-4{width:calc(8.33333333% * 4 - 40px);box-sizing:content-box;padding-left:40px}.row .span-5{width:calc(8.33333333% * 5 - 40px);box-sizing:content-box;padding-left:40px}.row .span-6{width:calc(8.33333333% * 6 - 40px);box-sizing:content-box;padding-left:40px}.row .span-7{width:calc(8.33333333% * 7 - 40px);box-sizing:content-box;padding-left:40px}.row .span-8{width:calc(8.33333333% * 8 - 40px);box-sizing:content-box;padding-left:40px}.row .span-9{width:calc(8.33333333% * 9 - 40px);box-sizing:content-box;padding-left:40px}.row .span-10{width:calc(8.33333333% * 10 - 40px);box-sizing:content-box;padding-left:40px}.row .span-11{width:calc(8.33333333% * 11 - 40px);box-sizing:content-box;padding-left:40px}.row .span-12{width:calc(8.33333333% * 12 - 40px);box-sizing:content-box;padding-left:40px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.row.responsive{width:100%;padding:0;margin:0}.row.responsive>.span-1,.row.responsive>.span-10,.row.responsive>.span-11,.row.responsive>.span-12,.row.responsive>.span-2,.row.responsive>.span-3,.row.responsive>.span-4,.row.responsive>.span-5,.row.responsive>.span-6,.row.responsive>.span-7,.row.responsive>.span-8,.row.responsive>.span-9{width:calc(8.33333333% * 12 - 0px)!important;box-sizing:content-box!important;padding-left:0!important;width:100%!important}}.tiles{position:relative}.tiles:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.tiles>*{margin-left:50px!important;float:right;width:calc(33.3333% - 33.3333px)}.tiles>* .photo-title{width:calc(100% + 30px);height:15px;margin:-15px -15px 10px -15px;border-radius:10px 10px 0 0;background:var(--config-color-fade-super);border-bottom:solid 1px var(--config-color-fade-super)}.tiles>:nth-child(3n){margin-left:0!important}@media only screen and (min-width:551px) and (max-width:1198px){.tiles>li{width:calc(50% - 25px)}.tiles>li:nth-child(3n){margin-left:50px!important}.tiles>li:nth-child(2n){margin-left:0!important}}@media only screen and (max-width:550px){.tiles>li{width:100%;margin-left:0!important}}@font-face{font-family:fontello;src:url(data:application/octet-stream;base64,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) format('woff'),url(data:application/octet-stream;base64,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AADoMAAA6DAAAAAxAADoMQAA6DEAAAAyAADoMgAA6DIAAAAzAADoMwAA6DMAAAA0AADoNAAA6DQAAAA1AADoNQAA6DUAAAA2AADoNgAA6DYAAAA3AADoNwAA6DcAAAA4AADoOAAA6DgAAAA5AADoOQAA6DkAAAA6AADoOgAA6DoAAAA7AADoOwAA6DsAAAA8AADoPAAA6DwAAAA9AADoPQAA6D0AAAA+AADoPgAA6D4AAAA/AADoPwAA6D8AAABAAADoQAAA6EAAAABBAADoQQAA6EEAAABCAADoQgAA6EIAAABDAADoQwAA6EMAAABEAADoRAAA6EQAAABFAADoRQAA6EUAAABGAADoRgAA6EYAAABHAADoRwAA6EcAAABIAADoSAAA6EgAAABJAADoSQAA6EkAAABKAADoSgAA6EoAAABLAADoSwAA6EsAAABMAADoTAAA6EwAAABNAADoTQAA6E0AAABOAADoTgAA6E4AAABPAADoTwAA6E8AAABQAADwjgAA8I4AAABRAADwmwAA8JsAAABSAADwsAAA8LAAAABTAADwxQAA8MUAAABUAADwygAA8MoAAABVAADwywAA8MsAAABWAADwzQAA8M0AAABXAADw3AAA8NwAAABYAADw4QAA8OEAAABZAADxGAAA8RgAAABaAADxHAAA8RwAAABbAADxIQAA8SEAAABcAADxMgAA8TIAAABdAADxNwAA8TcAAABeAADxOAAA8TgAAABfAADxcQAA8XEAAABgAADxegAA8XoAAABhAADxkgAA8ZIAAABiAADxkwAA8ZMAAABjAADxnAAA8ZwAAABkAADxoAAA8aAAAABlAADxrQAA8a0AAABmAADxwAAA8cAAAABnAADxzQAA8c0AAABoAADx3AAA8dwAAABpAADx5QAA8eUAAABqAADx/gAA8f4AAABrAADyMQAA8jEAAABsAADyOgAA8joAAABtAADylgAA8pYAAABuAADyxgAA8sYAAABvAAIAAP+xAsoDDAAVAB4AJUAiAAUBBW8DAQEEAW8ABAIEbwACAAJvAAAAZhMXEREXMgYFGislFAYjISImNTQ+AxcWMjcyHgMDFAYiLgE2HgECykYx/iQxRgoYKj4tScpKKkImHAiPfLR6BIKshEU8WFg8MFRWPCgBSEgmPlRWAcBYfn6wgAJ8AAAC//7/zgPq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eSBzdmcydHRmIGZyb20gRm9udGVsbG8gcHJvamVjdC5odHRwOi8vZm9udGVsbG8uY29tAEMAbwBwAHkAcgBpAGcAaAB0ACAAKABDACkAIAAyADAAMgAwACAAYgB5ACAAbwByAGkAZwBpAG4AYQBsACAAYQB1AHQAaABvAHIAcwAgAEAAIABmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQBmAG8AbgB0AGUAbABsAG8AUgBlAGcAdQBsAGEAcgBmAG8AbgB0AGUAbABsAG8AZgBvAG4AdABlAGwAbABvAFYAZQByAHMAaQBvAG4AIAAxAC4AMABmAG8AbgB0AGUAbABsAG8ARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABzAHYAZwAyAHQAdABmACAAZgByAG8AbQAgAEYAbwBuAHQAZQBsAGwAbwAgAHAAcgBvAGoAZQBjAHQALgBoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAAAAAAIAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQEOAQ8BEAERARIBEwEUARUBFgEXARgBGQEaARsBHAEdAR4BHwEgASEBIgEjASQBJQEmAScBKAEpASoBKwEsAS0BLgEvATABMQEyATMBNAE1ATYBNwE4ATkBOgE7ATwBPQE+AT8BQAFBAUIBQwFEAUUBRgFHAUgBSQFKAUsBTAFNAU4BTwFQAVEBUgFTAVQBVQFWAVcBWAFZAVoBWwFcAV0BXgFfAWABYQFiAWMBZAFlAWYBZwFoAWkBagFrAWwBbQFuAW8BcAFxAAR1c2VyBmZvbGRlcgRsaXN0BWxvZ2luA2NvZwd0d2l0dGVyC2FydGljbGUtYWx0BmNhbmNlbARob21lCGRvd24tZGlyCGZhY2Vib29rCGFzdGVyaXNrBnVwbG9hZAlzdG9wd2F0Y2gGZXhwb3J0BWhlYXJ0BHBsdXMGdXAtZGlyBG1lbnUJbGVmdC1vcGVuCnJpZ2h0LW9wZW4FaW5ib3gGd3JlbmNoB2NvbW1lbnQNc3RhY2tvdmVyZmxvdwhxdWVzdGlvbgpvay1jaXJjbGVkB3dhcm5pbmcEbWFpbARsaW5rB2tleS1pbnYFdHJhc2gIZG93bmxvYWQHZ2xhc3NlcwZxcmNvZGUHc2h1ZmZsZQNleWUEbG9jawZzZWFyY2gEYmVsbAV1c2Vycwhsb2NhdGlvbglicmllZmNhc2UJaW5zdGFncmFtBWNsb2NrBXBob25lCGNhbGVuZGFyBXByaW50BGVkaXQEYm9sZAZpdGFsaWMGcm9ja2V0CHdoYXRzYXBwBWRvdC0zDGluZm8tY2lyY2xlZAh2aWRlb2NhbQtxdW90ZS1yaWdodAdwaWN0dXJlB3BhbGV0dGUEbGFtcAlib29rLW9wZW4Cb2sIY2hhdC1hbHQHYXJjaGl2ZQRwbGF5BXBhdXNlCWRvd24tb3Blbgd1cC1vcGVuBW1pbnVzCGV4Y2hhbmdlB25ldHdvcmsHZGlzY29yZAhtb29uLWludgdzdW4taW52DmNhbmNlbC1jaXJjbGVkCWxpZ2h0bmluZwNkZXYJcmlnaHQtZGlyCGxlZnQtZGlyBGZpcmUIbGluay1leHQOZ2l0aHViLWNpcmNsZWQGZmlsdGVyBGRvY3MLbGlzdC1idWxsZXQNbGlzdC1udW1iZXJlZAl1bmRlcmxpbmUEc29ydAhsaW5rZWRpbgVzbWlsZQhrZXlib2FyZARjb2RlBnNoaWVsZBJhbmdsZS1jaXJjbGVkLWxlZnQTYW5nbGUtY2lyY2xlZC1yaWdodAliaXRidWNrZXQHd2luZG93cwtkb3QtY2lyY2xlZAp3aGVlbGNoYWlyBGJhbmsGZ29vZ2xlD2J1aWxkaW5nLWZpbGxlZAhkYXRhYmFzZQhsaWZlYnVveQZoZWFkZXIKYmlub2N1bGFycwpjaGFydC1hcmVhCXBpbnRlcmVzdAZtZWRpdW0GZ2l0bGFiCHRlbGVncmFtAAAAAQAB//8ADwAAAAAAAAAAAAAAAAAAAAAAGAAYABgAGAOB/zoDgf86sAAsILAAVVhFWSAgS7gADlFLsAZTWliwNBuwKFlgZiCKVViwAiVhuQgACABjYyNiGyEhsABZsABDI0SyAAEAQ2BCLbABLLAgYGYtsAIsIGQgsMBQsAQmWrIoAQpDRWNFUltYISMhG4pYILBQUFghsEBZGyCwOFBYIbA4WVkgsQEKQ0VjRWFksChQWCGxAQpDRWNFILAwUFghsDBZGyCwwFBYIGYgiophILAKUFhgGyCwIFBYIbAKYBsgsDZQWCGwNmAbYFlZWRuwAStZ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) format('truetype')}[class*=" icon-"]:before,[class^=icon-]:before{font-family:fontello;font-style:normal;font-weight:400;speak:never;display:inline-block;text-decoration:inherit;width:1em;margin-right:.2em;text-align:center;font-variant:normal;text-transform:none;line-height:1em;margin-left:.2em}.icon-user:before{content:'\e800'}.icon-folder:before{content:'\e801'}.icon-list:before{content:'\e802'}.icon-login:before{content:'\e803'}.icon-cog:before{content:'\e804'}.icon-twitter:before{content:'\e805'}.icon-article-alt:before{content:'\e806'}.icon-cancel:before{content:'\e807'}.icon-home:before{content:'\e808'}.icon-down-dir:before{content:'\e809'}.icon-facebook:before{content:'\e80a'}.icon-asterisk:before{content:'\e80b'}.icon-upload:before{content:'\e80c'}.icon-stopwatch:before{content:'\e80d'}.icon-export:before{content:'\e80e'}.icon-heart:before{content:'\e80f'}.icon-plus:before{content:'\e810'}.icon-up-dir:before{content:'\e811'}.icon-menu:before{content:'\e812'}.icon-left-open:before{content:'\e813'}.icon-right-open:before{content:'\e814'}.icon-inbox:before{content:'\e815'}.icon-wrench:before{content:'\e816'}.icon-comment:before{content:'\e817'}.icon-stackoverflow:before{content:'\e818'}.icon-question:before{content:'\e819'}.icon-ok-circled:before{content:'\e81a'}.icon-warning:before{content:'\e81b'}.icon-mail:before{content:'\e81c'}.icon-link:before{content:'\e81d'}.icon-key-inv:before{content:'\e81e'}.icon-trash:before{content:'\e81f'}.icon-download:before{content:'\e820'}.icon-glasses:before{content:'\e821'}.icon-qrcode:before{content:'\e822'}.icon-shuffle:before{content:'\e823'}.icon-eye:before{content:'\e824'}.icon-lock:before{content:'\e825'}.icon-search:before{content:'\e826'}.icon-bell:before{content:'\e827'}.icon-users:before{content:'\e828'}.icon-location:before{content:'\e829'}.icon-briefcase:before{content:'\e82a'}.icon-instagram:before{content:'\e82b'}.icon-clock:before{content:'\e82c'}.icon-phone:before{content:'\e82d'}.icon-calendar:before{content:'\e82e'}.icon-print:before{content:'\e82f'}.icon-edit:before{content:'\e830'}.icon-bold:before{content:'\e831'}.icon-italic:before{content:'\e832'}.icon-rocket:before{content:'\e833'}.icon-whatsapp:before{content:'\e834'}.icon-dot-3:before{content:'\e835'}.icon-info-circled:before{content:'\e836'}.icon-videocam:before{content:'\e837'}.icon-quote-right:before{content:'\e838'}.icon-picture:before{content:'\e839'}.icon-palette:before{content:'\e83a'}.icon-lamp:before{content:'\e83b'}.icon-book-open:before{content:'\e83c'}.icon-ok:before{content:'\e83d'}.icon-chat-alt:before{content:'\e83e'}.icon-archive:before{content:'\e83f'}.icon-play:before{content:'\e840'}.icon-pause:before{content:'\e841'}.icon-down-open:before{content:'\e842'}.icon-up-open:before{content:'\e843'}.icon-minus:before{content:'\e844'}.icon-exchange:before{content:'\e845'}.icon-network:before{content:'\e846'}.icon-discord:before{content:'\e847'}.icon-moon-inv:before{content:'\e848'}.icon-sun-inv:before{content:'\e849'}.icon-cancel-circled:before{content:'\e84a'}.icon-lightning:before{content:'\e84b'}.icon-dev:before{content:'\e84c'}.icon-right-dir:before{content:'\e84d'}.icon-left-dir:before{content:'\e84e'}.icon-fire:before{content:'\e84f'}.icon-link-ext:before{content:'\f08e'}.icon-github-circled:before{content:'\f09b'}.icon-filter:before{content:'\f0b0'}.icon-docs:before{content:'\f0c5'}.icon-list-bullet:before{content:'\f0ca'}.icon-list-numbered:before{content:'\f0cb'}.icon-underline:before{content:'\f0cd'}.icon-sort:before{content:'\f0dc'}.icon-linkedin:before{content:'\f0e1'}.icon-smile:before{content:'\f118'}.icon-keyboard:before{content:'\f11c'}.icon-code:before{content:'\f121'}.icon-shield:before{content:'\f132'}.icon-angle-circled-left:before{content:'\f137'}.icon-angle-circled-right:before{content:'\f138'}.icon-bitbucket:before{content:'\f171'}.icon-windows:before{content:'\f17a'}.icon-dot-circled:before{content:'\f192'}.icon-wheelchair:before{content:'\f193'}.icon-bank:before{content:'\f19c'}.icon-google:before{content:'\f1a0'}.icon-building-filled:before{content:'\f1ad'}.icon-database:before{content:'\f1c0'}.icon-lifebuoy:before{content:'\f1cd'}.icon-header:before{content:'\f1dc'}.icon-binoculars:before{content:'\f1e5'}.icon-chart-area:before{content:'\f1fe'}.icon-pinterest:before{content:'\f231'}.icon-medium:before{content:'\f23a'}.icon-gitlab:before{content:'\f296'}.icon-telegram:before{content:'\f2c6'}.datalist-polyfill{list-style:none;display:none;background:#fff;box-shadow:0 2px 2px #999;position:absolute;left:0;top:0;margin:0;padding:0;max-height:300px;overflow-y:auto}.datalist-polyfill:empty{display:none!important}.datalist-polyfill>li{padding:3px;font:13px "Lucida Grande",Sans-Serif}.datalist-polyfill__active{background:#3875d7;color:#fff}date-input-polyfill{z-index:1000!important;max-width:320px!important;width:320px!important}date-input-polyfill .monthSelect-wrapper,date-input-polyfill .yearSelect-wrapper{height:50px;line-height:50px;padding:0;width:40%!important;margin-bottom:10px!important}date-input-polyfill .monthSelect-wrapper select,date-input-polyfill .yearSelect-wrapper select{padding:0 12px;height:50px;line-height:50px;box-sizing:border-box}date-input-polyfill .yearSelect-wrapper{width:35%!important}date-input-polyfill table{width:100%!important;max-width:100%!important;padding:0 12px 12px 12px!important;box-sizing:border-box;margin:0}date-input-polyfill table td:first-child,date-input-polyfill table td:last-child,date-input-polyfill table th:first-child,date-input-polyfill table th:last-child{width:32px!important;padding:4px!important}date-input-polyfill select{margin-bottom:10px}date-input-polyfill button{width:25%!important;height:50px!important;line-height:50px!important;margin-bottom:10px!important;background:inherit;position:relative;color:inherit;padding:inherit;box-sizing:inherit;border-radius:inherit;font-size:inherit;box-shadow:none;border:none;border-bottom:none!important}::placeholder{color:var(--config-color-placeholder);text-align:right}::-webkit-input-placeholder{text-align:right}input:-moz-placeholder{text-align:right}input,textarea{background:var(--config-color-background-input)}input[type=file],input[type=file]::-webkit-file-upload-button{cursor:pointer}.button,button{display:inline-block;background:var(--config-color-focus);border-radius:26px;border:none;color:var(--config-color-background-fade);height:52px;line-height:52px;padding:0 25px;cursor:pointer;font-size:16px;box-sizing:border-box;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.button:focus,.button:hover,button:focus,button:hover{background:var(--config-color-focus-hover)}.button.fly,button.fly{position:fixed;z-index:2;bottom:30px;left:30px}@media only screen and (max-width:550px){.button.fly,button.fly{left:15px}}.button.fill,button.fill{display:block;width:100%;text-align:center;padding:0 10px!important}.button.fill-aligned,button.fill-aligned{display:block;width:100%;text-align:right;padding:0 20px!important}.button.icon,button.icon{padding-left:30px!important}.button.icon-reduce,button.icon-reduce{padding-right:15px!important}.button.reverse,button.reverse{background:0 0;height:50px;line-height:48px;padding:0 23px;color:var(--config-color-focus);border:solid 2px var(--config-color-focus)}.button.reverse:focus,.button.reverse:hover,button.reverse:focus,button.reverse:hover{color:var(--config-color-focus-hover);border-color:var(--config-color-focus-hover)}.button.small,button.small{padding:0 15px;height:40px;line-height:36px;font-size:13px}.button.round,button.round{width:52px;padding:0}.button.round.small,button.round.small{font-size:12px;width:30px;height:30px;line-height:30px}.button.white,button.white{background:#fff;color:var(--config-color-focus)}.button.white.reverse,button.white.reverse{color:#fff;background:0 0;border:solid 2px #fff}.button.trans,button.trans{background:0 0!important}.button.trans.reverse,button.trans.reverse{background:0 0!important}.button.success,button.success{background:var(--config-color-success)}.button.success.reverse,button.success.reverse{color:var(--config-color-success);background:#fff;border:solid 2px var(--config-color-success)}.button.danger,button.danger{background:var(--config-color-danger);color:#fff}.button.danger.reverse,button.danger.reverse{color:var(--config-color-danger);background:var(--config-color-background-fade);border:solid 2px var(--config-color-danger)}.button.dark,button.dark{background:var(--config-color-dark);color:var(--config-color-background-fade)}.button.dark.reverse,button.dark.reverse{color:var(--config-color-dark);background:var(--config-color-background-fade);border:solid 2px var(--config-color-dark)}.button .disabled,.button.disabled,.button:disabled,button .disabled,button.disabled,button:disabled{color:var(--config-color-normal);background:var(--config-color-background-dark);opacity:.6;cursor:default}.button.link,button.link{background:0 0;border-radius:0;color:var(--config-color-link);height:auto;line-height:normal;padding:0;padding-left:0!important}.button.link:focus,button.link:focus{box-shadow:inherit}.button.strip,button.strip{background:0 0;height:auto;line-height:16px;color:inherit;padding:0 5px}.button.facebook,button.facebook{color:#fff!important;background:#4070b4!important}.button.twitter,button.twitter{color:#fff!important;background:#56c2ea!important}.button.linkedin,button.linkedin{color:#fff!important;background:#0076b5!important}.button.github,button.github{color:#fff!important;background:#7e7c7c!important}.button:focus,button:focus{outline:0}label{margin-bottom:15px;display:block;line-height:normal}.input,input[type=date],input[type=datetime-local],input[type=email],input[type=file],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=url],select,textarea{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px}.input[type=file],input[type=date][type=file],input[type=datetime-local][type=file],input[type=email][type=file],input[type=file][type=file],input[type=number][type=file],input[type=password][type=file],input[type=search][type=file],input[type=tel][type=file],input[type=text][type=file],input[type=url][type=file],select[type=file],textarea[type=file]{line-height:0;padding:15px;height:auto}.input:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=email]:focus,input[type=file]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus,select:focus,textarea:focus{outline:0;border-color:#b3d7fd}.input:disabled,input[type=date]:disabled,input[type=datetime-local]:disabled,input[type=email]:disabled,input[type=file]:disabled,input[type=number]:disabled,input[type=password]:disabled,input[type=search]:disabled,input[type=tel]:disabled,input[type=text]:disabled,input[type=url]:disabled,select:disabled,textarea:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.input.strip,input[type=date].strip,input[type=datetime-local].strip,input[type=email].strip,input[type=file].strip,input[type=number].strip,input[type=password].strip,input[type=search].strip,input[type=tel].strip,input[type=text].strip,input[type=url].strip,select.strip,textarea.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:left 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.input.strip:focus,input[type=date].strip:focus,input[type=datetime-local].strip:focus,input[type=email].strip:focus,input[type=file].strip:focus,input[type=number].strip:focus,input[type=password].strip:focus,input[type=search].strip:focus,input[type=tel].strip:focus,input[type=text].strip:focus,input[type=url].strip:focus,select.strip:focus,textarea.strip:focus{border-color:#b3d7fd}.input:-webkit-autofill::first-line,input[type=date]:-webkit-autofill::first-line,input[type=datetime-local]:-webkit-autofill::first-line,input[type=email]:-webkit-autofill::first-line,input[type=file]:-webkit-autofill::first-line,input[type=number]:-webkit-autofill::first-line,input[type=password]:-webkit-autofill::first-line,input[type=search]:-webkit-autofill::first-line,input[type=tel]:-webkit-autofill::first-line,input[type=text]:-webkit-autofill::first-line,input[type=url]:-webkit-autofill::first-line,select:-webkit-autofill::first-line,textarea:-webkit-autofill::first-line{font-weight:300;font-size:16px}input[type=email],input[type=url]{direction:ltr}input[type=email]::placeholder,input[type=url]::placeholder{text-align:left;direction:ltr}select{background:0 0;-webkit-appearance:none;background-image:var(--config-console-nav-switch-arrow);background-position:left 15px top 50%;background-repeat:no-repeat;background-color:var(--config-color-background-input);width:calc(100% - 62px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-left:45px}select:-webkit-autofill{background-image:url("data:image/svg+xml;utf8,")!important;background-position:100% 50%!important;background-repeat:no-repeat!important}input[type=search],input[type=search].strip{background:0 0;-webkit-appearance:none;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAdZJREFUWIXt1s2LjWEYBvDfnDMzFpNIamZIFrMiJYMyFmKhZKfOwoiFr2LFn2BByG6WVrKwMcjWxgoLIlKIUk6RrzAjZWZ8LO731FlwvB+PUbjq6X0X7/VeV/d9P9fz8IdRL8Hpw3x8w0xaOz9GNxq4gJeZcGs1cRab0fU7xLfgMSYzoT3YgNXYhIO4iM+4iTWphGs4jikcFSXvhEGczr4/UFW8C2N4jXUFudvwCYeqGNgnSr6yJH8rpkWLCqMfE9hdUryFE3iC3qLEk7ij+kT34Q32FiHV8Qr7K4q3cArXihCGxd5elMjARnzBvE4f1dreV+AtnicycC/7/7K8BhaIvqXCO3zFwrwGZtCT0EAtW9N5DTSxWGR/CizNns/yEgbFEK5NZGCnaEPHE7e9Ai9wA6OJDIzistgJubFdxHB/RfFVYgCHixJruI5x5dNwDm6J47sUhkTvjpUw0Y1zeOrXR3hHjOA9zmBuTs4Arog4/yhuUZWwHPdFMh7280BZgiP4ILJ/UuymqRQmejPxphiquzgvKnMJDzOxB9glZqiRiecykbfHdawX98EhcdxO4BGu4nYm2EJDzEKPSMIdYrBnFYUq8d/EP2di1gey3cS4ErflvxffASbhcakIINaMAAAAAElFTkSuQmCC);background-color:var(--config-color-background-input);background-position:right 15px top 50%;background-repeat:no-repeat;background-size:20px 20px;width:calc(100% - 60px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:45px}select[multiple]{min-height:75px;padding:5px 10px!important;padding-left:50px!important}select[multiple] option{padding:10px 4px;border-bottom:solid 1px #f1f1f1}select[multiple] option:last-child{border-bottom:none}textarea{min-height:75px;resize:vertical;line-height:32px;padding:5px 15px}textarea.tall{min-height:180px}fieldset{border:none;margin:0;padding:0}.counter{font-size:13px;text-align:left;color:var(--config-color-fade);margin-top:-20px;margin-bottom:20px}.file-preview{background:var(--config-color-background-input) url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAIElEQVQoU2NkYGAwZsAEZ9GFGIeIQix+wfQgyDODXSEAcUwGCrDSHgkAAAAASUVORK5CYII=)!important;border:solid 1px #e2e2e2;box-shadow:inset 0 0 3px #a0a0a0;border-radius:8px;width:calc(100% - 2px);max-height:180px;visibility:visible!important}.video-preview{padding-top:56%;position:relative;border-radius:10px;background:#e7e7e7;overflow:hidden;margin:0}.video-preview iframe{position:absolute;top:0;width:100%;height:100%;border:none}.map-preview{padding-top:50%;position:relative;margin-bottom:10px;border-radius:10px;background:#e7e7e7;overflow:hidden;box-shadow:0 0 30px rgba(218,218,218,.5)}.map-preview iframe{position:absolute;top:0;width:100%;height:100%;border:none}.tooltip{position:relative}.tooltip.large:hover:after{white-space:normal;width:280px}.tooltip.small:hover:after{white-space:normal;width:180px}.tooltip:hover:after{white-space:nowrap;background:var(--config-color-tooltip-background);border-radius:5px;bottom:26px;color:var(--config-color-tooltip-text);content:attr(data-tooltip);padding:5px 15px;position:absolute;font-size:13px;line-height:20px;z-index:98;right:20%;margin-right:-30px;word-break:break-word}.tooltip:hover:before{border:solid;border-color:var(--config-color-tooltip-background) transparent;border-width:6px 6px 0 6px;bottom:20px;content:"";position:absolute;z-index:99;right:5px}.tooltip.down:hover:after{top:26px;bottom:inherit}.tooltip.down:hover:before{top:20px;border-width:0 6px 6px 6px;bottom:inherit}.tag{display:inline-block;background:var(--config-color-fade-light);color:var(--config-color-fade);border-radius:12px;line-height:24px;padding:0 8px;font-size:12px;box-shadow:none!important;border:none;height:auto;width:auto;white-space:nowrap;text-overflow:ellipsis}.tag:hover{border:none}.tag.green{background:var(--config-color-success);color:#fff}.tag.red{background:var(--config-color-danger);color:#fff}.tag.yellow{background:#ffe28b;color:#494949}.tag.focus{background:var(--config-color-focus);color:#fff}.tag.dark{background:var(--config-color-dark);color:#e7e7e7}.tag.blue{background:var(--config-color-info);color:#fff}.tag.link{background:var(--config-color-link);color:#fff}input[type=checkbox],input[type=radio]{width:26px;height:16px;position:relative;-webkit-appearance:none;border-radius:0;border:none;background:0 0;vertical-align:middle;margin:0}input[type=checkbox]:after,input[type=radio]:after{content:"";display:block;width:20px;height:20px;background:var(--config-color-background-fade);top:-5px;border-radius:50%;position:absolute;border:solid 3px var(--config-color-focus);vertical-align:middle}input[type=checkbox]:checked:after,input[type=radio]:checked:after{text-align:center;font-family:fontello;content:'\e83d';font-size:16px;line-height:20px;color:var(--config-color-background-fade);background:var(--config-color-focus)}input[type=checkbox][type=radio]:checked:after,input[type=radio][type=radio]:checked:after{content:'';display:block;width:10px;height:10px;border-radius:50%;background:var(--config-color-background-fade);border:solid 8px var(--config-color-focus)}input[type=checkbox]:focus,input[type=radio]:focus{outline:0}input[type=checkbox]:focus:after,input[type=checkbox]:hover:after,input[type=radio]:focus:after,input[type=radio]:hover:after{outline:0;border-color:#000}input[type=checkbox]:checked:focus:after,input[type=checkbox]:checked:hover:after,input[type=radio]:checked:focus:after,input[type=radio]:checked:hover:after{border-color:var(--config-color-focus)}.input-copy{position:relative}.input-copy input,.input-copy textarea{padding-left:65px;width:calc(100% - 82px);resize:none}.input-copy .copy{position:absolute;top:0;left:0;border-right:solid 1px var(--config-color-fade-light);height:calc(100% - 2px);width:50px;line-height:50px;text-align:center;background:var(--config-color-background-focus);margin:1px;border-radius:0 10px 10px 0}.paging{color:var(--config-color-fade);padding:5px 15px;font-size:12px}.paging form{display:inline-block}.paging button:disabled{color:var(--config-color-background-fade);opacity:.6}.blue-snap iframe{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px;float:none!important;height:40px!important;width:calc(100% - 32px)!important;border:solid 1px #e2e2e2!important;background:0 0!important;position:static!important}.blue-snap iframe[type=file]{line-height:0;padding:15px;height:auto}.blue-snap iframe:focus{outline:0;border-color:#b3d7fd}.blue-snap iframe:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.blue-snap iframe.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:left 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.blue-snap iframe.strip:focus{border-color:#b3d7fd}.blue-snap iframe:-webkit-autofill::first-line{font-weight:300;font-size:16px}.blue-snap .error{font-size:12px;margin-top:-25px;color:var(--config-color-danger);height:40px;padding-right:2px}.pell{height:auto;padding-bottom:0;margin-bottom:0;padding-top:0;background:var(--config-color-background-input);line-height:normal!important;position:relative}.pell.hide{padding:0!important;height:1px;min-height:1px;max-height:1px;border:none;box-shadow:none;margin-bottom:20px;opacity:0}.pell [contenteditable=true]:empty:before{content:attr(placeholder);display:block;color:var(--config-color-placeholder)}.pell .pell-actionbar{border-bottom:solid 1px var(--config-color-fade-light);margin:0 -15px 15px -15px;padding:10px 15px;position:sticky;top:70px;background:var(--config-color-background-input);border-radius:10px 10px 0 0}.pell .pell-content{min-height:100px;display:block;padding:10px;margin:-10px;cursor:text}.pell .pell-content:focus{outline:0}.pell button{background:inherit;color:inherit;margin:0;padding:0;padding-left:15px;height:40px;line-height:40px;box-shadow:none;cursor:pointer;font-size:13px;border-radius:0}.pell button.pell-button-selected,.pell button:focus,.pell button:hover{color:var(--config-color-link)}.pell h1,.pell h2,.pell h3,.pell h4,.pell h5,.pell h6{text-align:inherit;margin-bottom:30px}.pell b,.pell strong{font-weight:700}.pell ol,.pell ul{margin:0 0 20px 0}.pell ol li,.pell ul li{display:list-item!important;list-style:inherit;list-style-position:inside!important;margin:0 20px 2px 20px}.pell ol li p,.pell ul li p{margin:0;display:inline}.pell ol li{list-style:decimal}.pell ol li::before{content:'';display:none}label.switch{line-height:42px}.switch,input[type=checkbox].button.switch,input[type=checkbox].switch{width:52px;height:32px;line-height:32px;border-radius:21px;background:var(--config-color-fade);display:inline-block;margin:0;padding:5px;padding-right:5px;padding-left:30px}.switch.on,.switch:checked,input[type=checkbox].button.switch.on,input[type=checkbox].button.switch:checked,input[type=checkbox].switch.on,input[type=checkbox].switch:checked{background-color:var(--config-color-success);padding-right:25px;padding-left:5px}.switch.on:focus,.switch.on:hover,.switch:checked:focus,.switch:checked:hover,input[type=checkbox].button.switch.on:focus,input[type=checkbox].button.switch.on:hover,input[type=checkbox].button.switch:checked:focus,input[type=checkbox].button.switch:checked:hover,input[type=checkbox].switch.on:focus,input[type=checkbox].switch.on:hover,input[type=checkbox].switch:checked:focus,input[type=checkbox].switch:checked:hover{background:var(--config-color-success)}.switch:focus,.switch:hover,input[type=checkbox].button.switch:focus,input[type=checkbox].button.switch:hover,input[type=checkbox].switch:focus,input[type=checkbox].switch:hover{background:var(--config-color-fade)}.switch:focus:after,.switch:hover:after,input[type=checkbox].button.switch:focus:after,input[type=checkbox].button.switch:hover:after,input[type=checkbox].switch:focus:after,input[type=checkbox].switch:hover:after{background:#fff}.switch:after,input[type=checkbox].button.switch:after,input[type=checkbox].switch:after{content:"";display:block;width:22px;height:22px;background:#fff;border-radius:50%;border:none;position:static;top:0}.password-meter{margin:-41px 10px 30px 10px;height:2px;background:0 0;max-width:100%;z-index:2;position:relative}.password-meter.weak{background:var(--config-color-danger)}.password-meter.medium{background:var(--config-color-success)}.password-meter.strong{background:var(--config-color-success)}.color-input:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.color-input .color-preview{width:53px;height:53px;float:right;margin-left:10px;background:#000;border-radius:10px;box-shadow:inset 0 0 3px #a0a0a0;position:relative}.color-input .color-preview input{opacity:0;position:absolute;top:0;bottom:0;left:0;right:0;width:100%;height:100%;cursor:pointer}.color-input input{text-transform:uppercase;float:right;width:calc(100% - 95px)}.grecaptcha-badge{box-shadow:none!important;border-radius:10px!important;overflow:hidden!important;background:#4d92df!important;bottom:25px}.grecaptcha-badge:hover{width:256px!important}.back{font-size:15px;line-height:24px;height:24px;margin-right:-15px;margin-top:-25px;margin-bottom:20px}.back span{font-weight:inherit!important}@media only screen and (max-width:550px){.back{margin-right:-5px}}hr{height:1px;background:var(--config-border-color)!important;border:none}hr.fade{opacity:.7}.upload{position:relative}.upload:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.upload input{position:absolute;top:0;right:0;opacity:0;cursor:pointer}.upload.single .preview{height:0;position:relative;padding-top:100%;width:100%;margin-bottom:15px!important}.upload.single .preview li{position:absolute;top:0;width:calc(100% - 20px);height:calc(100% - 20px);margin-left:0!important;margin-bottom:0!important}.upload .button{float:right;margin-left:10px!important}.upload .button.disabled,.upload .button.disabled:hover{background:0 0;color:inherit;border-color:inherit}.upload .count{float:right;line-height:52px}.upload .progress{background:var(--config-color-success);height:6px;border-radius:3px;margin-bottom:15px!important}.upload .preview:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.upload .preview li{float:right;margin-left:20px!important;margin-bottom:15px!important;background:var(--config-color-background-fade-super);width:150px;height:150px;line-height:148px;text-align:center;border-radius:20px;overflow:hidden;position:relative;cursor:pointer;border:solid 1px var(--config-color-background-dark)}.upload .preview li:hover:before{background:var(--config-color-focus)}.upload .preview li:before{content:'\e807';font-family:fontello;font-size:12px;position:absolute;width:20px;height:20px;display:block;top:8px;left:8px;text-align:center;line-height:20px;vertical-align:middle;border-radius:50%;background:#484848;color:#fff;z-index:1}.upload .preview li img{vertical-align:middle;max-height:150px;max-width:150px;-webkit-filter:drop-shadow(0 0 6px rgba(0, 0, 0, .3));filter:drop-shadow(0 0 1px rgba(0, 0, 0, .3))}.upload.wide .preview li{height:0;width:100%;position:relative;padding-top:30.547%;background:#e7e7e7;border-radius:10px;overflow:hidden;border:solid 1px #f9f9f9;margin:0}.upload.wide .preview li img{border-radius:10px;position:absolute;top:0;width:100%;display:block;opacity:1;max-width:inherit;max-height:inherit}ol{list-style:none;counter-reset:x-counter;padding:0}ol li{counter-increment:x-counter;line-height:30px;margin-bottom:30px;margin-right:45px}ol li::before{display:inline-block;content:counter(x-counter);color:var(--config-color-background-fade);background:var(--config-color-focus);border:solid 2px var(--config-color-focus);margin-left:15px;margin-right:-45px;width:26px;height:26px;border-radius:50%;text-align:center;line-height:26px}.required{color:var(--config-color-danger);font-size:8px;position:relative;top:-8px}.drop-list{position:relative;outline:0}.drop-list.open ul{display:block}.drop-list ul{position:relative;background:var(--config-color-background-fade);border-radius:10px;box-shadow:0 0 3px rgba(0,0,0,.05);padding:30px;display:block;border-bottom:none;box-shadow:0 0 6px rgba(0,0,0,.1);display:none;position:absolute;bottom:calc(100% + 10px);z-index:2;padding:0;right:-10px;max-width:280px;min-width:240px}.drop-list ul.padding-small{padding:15px}.drop-list ul.y-scroll{overflow-y:auto}.drop-list ul.danger{background:var(--config-color-danger);color:#fff}.drop-list ul.danger .box{color:var(--config-color-normal);background:var(--config-color-background-fade)}.drop-list ul.danger>.button,.drop-list ul.danger>button{background:#fff;color:var(--config-color-danger)}.drop-list ul.note{background:var(--config-note-background)}.drop-list ul.focus{background:var(--config-color-focus);color:var(--config-color-background-fade)}.drop-list ul.focus .button,.drop-list ul.focus button{background:var(--config-color-background-fade);color:var(--config-color-focus)}.drop-list ul.line{background:0 0;border:solid 1px var(--config-color-background-dark);box-shadow:none}.drop-list ul.warning{background:var(--config-color-success);color:#2d2d2d}.drop-list ul.warning .button,.drop-list ul.warning button{background:rgba(45,45,45,.8);color:var(--config-color-success)}.drop-list ul .tabs{border-bottom:solid 1px var(--config-border-color);margin:0 -30px;padding:0 30px!important}.drop-list ul>footer{margin:0 -30px -30px -30px;padding:15px 30px;background:var(--config-color-background-fade);border:solid 1px var(--config-border-color);border-radius:0 0 10px 10px}.drop-list ul hr{height:1px;background:var(--config-console-background);border:none;margin:30px -30px}.drop-list ul .label{position:absolute;top:10px;z-index:2;left:10px}.drop-list ul.fade-bottom{position:relative;overflow:hidden}.drop-list ul.fade-bottom:after{content:"";position:absolute;display:block;bottom:15px;width:100%;background:#000;background:linear-gradient(180deg,rgba(0,0,0,0) 0,var(--config-color-background-fade) 80%);height:100px;margin:0 -15px}.drop-list ul .header{position:static;height:40px;padding:20px 30px 20px 30px;margin-bottom:30px;margin:-30px -30px 20px -30px;background:var(--config-color-background-fade);border-bottom:solid 1px #efefef}.drop-list ul ul.numbers>li{position:relative;margin-right:30px;margin-left:50px}.drop-list ul ul.numbers>li hr{margin-right:-60px;margin-left:-80px}.drop-list ul ul.numbers>li .settings{position:absolute;top:3px;left:-50px}.drop-list ul ul.numbers>li::after{display:block;width:25px;height:25px;line-height:25px;font-size:13px;font-weight:500;border-radius:50%;background:var(--config-color-focus);color:var(--config-color-background);counter-increment:section;content:counter(section);text-align:center;position:absolute;top:3px;right:-45px}.drop-list ul .scroll{margin:0 -30px;overflow-y:scroll}.drop-list ul .scroll table{width:100%;margin:0}.drop-list ul ul.sortable{counter-reset:section}.drop-list ul ul.sortable>li [data-move-down].round,.drop-list ul ul.sortable>li [data-move-up].round,.drop-list ul ul.sortable>li [data-remove].round{background:var(--config-color-focus);color:var(--config-color-background-fade);width:25px;height:25px;line-height:25px;display:inline-block;text-align:center;padding:0;margin-left:5px}.drop-list ul ul.sortable>li [data-move-down].round:disabled,.drop-list ul ul.sortable>li [data-move-up].round:disabled,.drop-list ul ul.sortable>li [data-remove].round:disabled{display:none}.drop-list ul ul.sortable>li:first-child [data-move-up]{display:none}.drop-list ul ul.sortable>li:first-child [data-move-up]:disabled{display:inline-block;background:var(--config-color-background)}.drop-list ul ul.sortable>li:last-child [data-move-down]{display:none}.drop-list ul ul.sortable>li:last-child [data-move-down]:disabled{display:inline-block;background:var(--config-color-background)}.drop-list ul .toggle{position:relative;border-top:1px solid var(--config-console-background);border-bottom:1px solid var(--config-console-background);margin:0 -30px;padding:30px 30px 0 30px;height:65px;overflow:hidden}.drop-list ul .toggle.list{border-bottom:none}.drop-list ul .toggle.sorts button.ls-ui-open{width:calc(100% - 100px)}.drop-list ul .toggle button.ls-ui-open{left:0;position:absolute;top:0;width:100%;height:95px;background:0 0;opacity:.5;border-radius:0}.drop-list ul .toggle .icon-minus,.drop-list ul .toggle .icon-up-open{display:none}.drop-list ul .toggle .content{display:none}.drop-list ul .toggle.open{height:auto}.drop-list ul .toggle.open .icon-minus,.drop-list ul .toggle.open .icon-up-open{display:block}.drop-list ul .toggle.open .icon-down-open,.drop-list ul .toggle.open .icon-plus{display:none}.drop-list ul .toggle.open .content{display:block}.drop-list ul .list li{border-bottom:solid 2px var(--config-border-color);margin:0 -30px 30px -30px;padding:0 30px 30px 30px}.drop-list ul .list li:last-child{padding-bottom:0;margin-bottom:0;border-bottom:none}@media only screen and (max-width:550px){.drop-list ul .list li .actions{float:none}}.drop-list ul .list li .avatar{display:block}.drop-list ul .list li .avatar.inline{display:inline-block}.drop-list ul.new{text-align:center}.drop-list ul.new i{font-size:80px;line-height:80px;font-family:Poppins,sans-serif;font-style:normal;font-weight:300}.drop-list ul.new b{margin-top:20px;display:block}.drop-list ul .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.drop-list ul .info hr{background:var(--config-modal-note-border)!important}.drop-list ul .table-wrap{margin:0 -30px;overflow-y:scroll}.drop-list ul .table-wrap table{margin:0}.drop-list ul:before{border:solid;border-color:var(--config-color-background-fade) transparent;border-width:8px 8px 0 8px;bottom:-8px;content:"";position:absolute;z-index:99;right:30px}.drop-list ul.arrow-end:before{left:30px;right:unset}.drop-list ul li{border-bottom:solid 1px var(--config-color-fade-super);margin:0;padding:0}.drop-list ul li:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.drop-list ul li:first-child{border-radius:10px 10px 0 0}.drop-list ul li:last-child{border-radius:0 0 10px 10px}.drop-list ul li:hover{background:var(--config-color-fade-super)}.drop-list ul li:first-child:hover,.drop-list ul li:last-child:hover{border-color:transparent}.drop-list ul li .link,.drop-list ul li a,.drop-list ul li button.link{display:block;vertical-align:middle;height:auto;line-height:30px;display:inline-block;padding:10px 15px!important;color:inherit;font-size:14px;border:none;cursor:pointer;width:calc(100% - 30px);text-align:right;box-sizing:content-box}.drop-list ul li.disabled .link:hover,.drop-list ul li.disabled a:hover{background:0 0}.drop-list ul li .avatar{width:30px;height:30px;margin-left:10px;float:right}.drop-list ul li:last-child{border-bottom:none}.drop-list.bottom ul{bottom:auto;margin-top:-2px}.drop-list.bottom ul:before{bottom:auto;top:-8px;border-width:0 8px 8px 8px}.drop-list.end ul{left:-10px;right:auto}.disabled{opacity:.2;cursor:default}.disabled .button,.disabled .link,.disabled a,.disabled button{cursor:default!important}.disabled .button:hover,.disabled .link:hover,.disabled a:hover,.disabled button:hover{background:0 0}.tags{-webkit-appearance:none;-moz-appearance:none;-webkit-transform:translateZ(0);box-sizing:content-box;color:#313131;height:40px;line-height:40px;border:solid 1px var(--config-color-fade-light);border-radius:10px;padding:5px 15px;font-size:16px;display:block;width:calc(100% - 32px);margin-bottom:30px;background:var(--config-color-background-input);min-height:42px;height:auto;cursor:text}.tags[type=file]{line-height:0;padding:15px;height:auto}.tags:focus{outline:0;border-color:#b3d7fd}.tags:disabled{color:var(--config-color-normal);background:var(--config-color-fade-super);opacity:1!important}.tags.strip{border:none;border-radius:0;padding:5px 0;width:100%;background-color:transparent;background-position:left 2px top 50%;border-bottom:solid 1px var(--config-color-fade-light);color:var(--config-color-placeholder)}.tags.strip:focus{border-color:#b3d7fd}.tags:-webkit-autofill::first-line{font-weight:300;font-size:16px}.tags .add{display:inline-block!important;border:none;padding:0;width:auto;margin:0;max-width:100%;min-width:200px}.tags ul.tags-list{display:inline;white-space:pre-line}.tags ul.tags-list li{display:inline-block!important;margin-left:10px;font-size:16px;padding:5px 10px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.tags ul.tags-list li::before{float:left;content:'\e807';font-family:fontello;font-style:normal;display:inline-block;text-align:center;line-height:16px;width:16px;height:16px;font-size:12px;background:#000;color:#fff;border-radius:50%;margin-top:4px;margin-bottom:4px;margin-right:6px;margin-left:0}.switch-theme{background:var(--config-switch-background);border-radius:19px;height:26px;width:44px;margin:9px 0}.switch-theme button{padding:3px;display:block;background:0 0;height:26px;width:100%}.switch-theme i{background:var(--config-color-background-fade);border-radius:50%;height:18px;width:18px;line-height:18px;font-size:12px;padding:0;margin:0;color:var(--config-color-fade)}.switch-theme i.force-light{float:left}.switch-theme i.force-dark{float:right}.dot{width:20px;height:20px;background:var(--config-color-fade);border-radius:50%;display:inline-block;vertical-align:middle;margin:0!important;padding:0!important}.dot.danger{background:var(--config-color-danger)!important}.dot.success{background:var(--config-color-success)!important}.dot.warning{background:var(--config-color-warning)!important}.dot.info{background:var(--config-color-info)!important}.console{width:100%;padding:0;overscroll-behavior:none}.console body{position:relative;width:calc(100% - 320px);padding-top:70px;padding-bottom:0;padding-left:50px;padding-right:270px;margin:0;color:var(--config-color-normal);background:var(--config-console-background)}.console body .project-only{display:none!important}.console body.show-nav .project-only{display:inline-block!important}.console body.hide-nav{padding-right:50px;width:calc(100% - 100px)}.console body.hide-nav header{width:calc(100% - 50px)}.console body.hide-nav header .logo{display:inline-block}.console body.hide-nav .console-back{display:block}.console body.hide-nav .console-index{display:none}.console body.hide-nav .account{display:none}.console body.index .console-back{display:none}.console body.index .console-index{display:block}.console body.index .account{display:block}.console body .console-index{display:block}.console body .console-back{display:none}.console main{min-height:480px}.console header{position:fixed;top:0;width:calc(100% - 280px);height:40px;line-height:40px;padding:15px 30px;background:var(--config-color-background-fade);box-shadow:0 0 2px rgba(0,0,0,.1);margin:0 -50px;z-index:2;font-size:14px}.console header .logo{display:none;border:none}.console header .logo:hover{border:none;opacity:.8}.console header .logo img{height:26px;margin:7px 0}.console header .setup-new{width:40px;height:40px;line-height:40px}.console header .list{width:240px}.console header .list select{height:40px;line-height:40px;padding-top:0;padding-bottom:0;border:none;border-radius:26px;background-color:var(--config-console-nav-switch-background);color:var(--config-console-nav-switch-color)}.console header .account{margin-right:25px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.console header .switch-theme{margin:2px 0}.console header .avatar{height:40px;width:40px}.console header .account-button{background:0 0;position:absolute;width:100%;height:40px;border-radius:0;z-index:1}.console header .notifications{position:relative;font-size:20px}.console header .notifications a{color:#1b3445}.console header .notifications:after{position:absolute;content:"";display:block;background:var(--config-color-danger);width:8px;height:8px;border-radius:50%;top:3px;left:3px}.console header nav{background:#1b3445;background:linear-gradient(var(--config-console-nav-start),var(--config-console-nav-end));color:#788c99;position:fixed;height:100%;width:220px;top:0;right:0}.console header nav .logo{height:39px;padding:15px 20px;display:block}.console header nav .logo img{display:inline-block;margin-top:7px;margin-bottom:14px}.console header nav .logo svg g{fill:var(--config-color-focus)}.console header nav .icon{display:block;border:none;margin:18px 10px 50px 10px}.console header nav .icon img{display:block}.console header nav .icon:hover{border-bottom:none}.console header nav .icon:hover svg g{fill:var(--config-color-focus)}.console header nav .container{overflow:auto;height:calc(100% - 133px);width:100%}.console header nav .project-box{padding:20px;text-align:center;display:block;border:none;line-height:100px;height:100px}.console header nav .project-box img{max-height:80px;max-width:80%;display:inline-block;vertical-align:middle}.console header nav .project{display:block;padding:85px 25px 20px 25px;color:#788c99;position:relative;border:none;height:20px}.console header nav .project:hover{border-bottom:none}.console header nav .project .name{height:20px;line-height:20px;margin:0;padding:0;display:inline-block;max-width:100%}.console header nav .project .arrow{display:block;position:absolute;left:5px;top:10px;width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #788c99;transform:rotate(225deg)}.console header nav .project img{position:absolute;bottom:40px;display:block;margin-bottom:10px;max-height:35px;max-width:40%}.console header nav .subtitle{padding:0 30px;display:block;font-size:12px;font-weight:300}.console header nav .links{margin-bottom:15px!important}.console header nav .links.top{border:none;padding-bottom:0;margin-bottom:5px!important}.console header nav .links.bottom{position:absolute;bottom:0;left:0;right:0;padding-bottom:0;border:none;margin-bottom:0!important;box-shadow:0 0 10px rgba(0,0,0,.1)}.console header nav .links.bottom a{border-top:solid 1px var(--config-console-nav-border);border-bottom:none}.console header nav .links .sub{display:inline-block;border:none;width:25px;height:25px;line-height:25px;border-radius:50%;padding:0;background:var(--config-color-focus);color:#fff;text-align:center;font-size:12px;margin:18px}.console header nav .links .sub i{width:auto;margin:0}.console header nav .links .sub:hover{border:none}.console header nav .links a{padding:8px 20px;border:none;display:block;color:#87a5b9;font-weight:400;border-right:solid 5px transparent;font-size:13px}.console header nav .links a i{margin-left:8px;width:22px;display:inline-block}.console header nav .links a.selected,.console header nav .links a:hover{color:#e4e4e4}.console header nav:after{content:'';display:block;position:absolute;background:#302839;height:100px;width:100%;bottom:-100px}.console>footer{width:calc(100% + 100px);margin:0 -50px;box-sizing:border-box;background:0 0;padding-left:30px;padding-right:30px}.console>footer ul{float:none;text-align:center}.console>footer ul li{float:none;display:inline-block}.console .projects{position:relative}.console .projects:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.console .projects li{float:right;margin-left:50px;margin-bottom:50px;width:270px}.console .projects li:nth-child(3n){margin-left:0}.console .dashboard{padding:20px;min-height:95px;overflow:hidden;position:relative;z-index:1}.console .dashboard .chart{width:80%}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console .dashboard .chart{width:100%}}.console .dashboard hr{margin:20px -20px;height:2px;background:var(--config-console-background)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console .dashboard hr{height:3px}}.console .dashboard footer{margin:-20px;padding:20px;background:#fcfeff;border:none;color:var(--config-color-link)}.console .dashboard .col{position:relative}.console .dashboard .col:last-child:after{display:none}.console .dashboard .col:after{content:"";display:block;width:2px;background:var(--config-console-background);height:calc(100% + 110px);position:absolute;top:-20px;bottom:-20px;left:24px}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console .dashboard .col:after{width:calc(100% + 40px);height:3px;position:static;margin:20px -20px}}.console .dashboard .value{color:var(--config-color-focus);vertical-align:bottom;line-height:45px}.console .dashboard .value .sum{font-size:45px;line-height:45px;font-weight:700;vertical-align:bottom}.console .dashboard .unit{font-weight:500;line-height:20px;vertical-align:bottom;font-size:16px;display:inline-block;margin-bottom:5px;margin-right:5px;color:var(--config-color-focus)}.console .dashboard .metric{color:var(--config-color-focus);font-weight:400;font-size:13px;line-height:16px}.console .dashboard .range{color:var(--config-color-fade);font-weight:400;font-size:14px;line-height:16px}.console .dashboard a{display:block;font-weight:400;font-size:14px;line-height:16px;padding:0;border:none}.console .chart-metric{width:19%}@media only screen and (min-width:551px) and (max-width:1198px),only screen and (max-width:550px){.console .chart-metric{width:100%}}.console .chart{width:100%;position:relative;height:0;padding-top:20px;padding-bottom:26%;margin-left:-2px;overflow:hidden;background-color:var(--config-color-background-fade);background-image:linear-gradient(transparent 1px,transparent 1px),linear-gradient(90deg,transparent 1px,transparent 1px),linear-gradient(var(--config-border-color) 1px,transparent 1px),linear-gradient(90deg,var(--config-border-color) 1px,transparent 1px);background-size:100px 100px,100px 100px,20px 20px,20px 20px;background-position:-2px -2px,-2px -2px,-1px -1px,-1px -1px;background-repeat:round;border:solid 1px var(--config-border-color);border-right:solid 1px transparent;border-bottom:solid 1px transparent}@media only screen and (min-width:551px) and (max-width:1198px),only screen and (max-width:550px){.console .chart{width:100%;padding-bottom:32%;float:none;margin-bottom:20px}}.console .chart canvas{position:absolute;bottom:0;display:block;height:100%;width:100%}.console .chart-notes{font-size:12px}.console .chart-notes li{line-height:20px;display:inline-block;margin-left:15px}.console .chart-notes li::before{display:inline-block;content:'';width:14px;height:14px;background:var(--config-color-normal);border-radius:50%;margin-left:8px;vertical-align:middle}.console .chart-notes li:nth-child(1){color:#29b5d9}.console .chart-notes li:nth-child(1)::before{background:#29b5d9}.console .chart-notes li:nth-child(2){color:#4eb55b}.console .chart-notes li:nth-child(2)::before{background:#4eb55b}.console .chart-notes li:nth-child(3){color:#ec9323}.console .chart-notes li:nth-child(3)::before{background:#ec9323}.console .community a{padding:0 10px;display:inline-block}.console .link-list li{margin-bottom:15px}.console .link-list i{display:inline-block;width:30px;height:30px;line-height:30px;text-align:center;background:var(--config-color-fade);color:var(--config-color-fade-super);border-radius:50%;margin-left:15px}.console .link-list i.fade{background:0 0;color:var(--config-color-fade)}.console .provider{width:50px;height:50px;background:#f5f5f5;color:#868686;line-height:50px;text-align:center;font-size:25px;border-radius:50%}.console .provider.facebook{color:#fff;background:#3b5998}.console .provider.twitter{color:#fff;background:#55beff}.console .provider.telegram{color:#fff;background:#3ba9e1}.console .provider.github{color:#fff;background:#24292e}.console .provider.whatsapp{color:#fff;background:#25d366}.console .provider.linkedin{color:#fff;background:#1074af}.console .provider.microsoft{color:#fff;background:#137ad4}.console .provider.google{color:#fff;background:#4489f1}.console .provider.bitbucket{color:#fff;background:#2a88fb}.console .provider.gitlab{color:#faa238;background:#30353e}.console .provider.instagram{color:#fff;background:radial-gradient(circle at 30% 107%,#fdf497 0,#fdf497 5%,#fd5949 45%,#d6249f 60%,#285aeb 90%)}.console .premium{z-index:3;margin-top:320px}.console .premium .message{height:190px;overflow:hidden;position:absolute;top:-280px}.console .premium:after{content:'';position:absolute;top:0;left:-20px;right:-20px;bottom:-20px;background:var(--config-color-background);opacity:.7;z-index:300}.console .app-section{height:90px}.console .confirm{background:var(--config-color-link);color:#fff;border-radius:25px;padding:12px;line-height:28px;text-align:center}.console .confirm .action{font-weight:500;cursor:pointer}.console .platforms{overflow:hidden}.console .platforms .box{overflow:hidden}.console .platforms .box img{width:50px;margin:0 auto;margin-bottom:20px}.console .platforms .box .cover{margin:-30px -30px 30px -30px;padding:30px}.console .platforms .box .cover.android{background:#a4ca24}.console .platforms .box .cover.android h1{color:#fff;font-size:18px;margin-top:20px}.console .platforms .col{text-align:center;line-height:30px}.console .platforms a{display:block;margin:-20px;padding:20px}.console .platforms a:hover{background:#fbfeff}.console .platforms img{display:block;margin:0 30px;width:calc(100% - 60px);border-radius:50%;margin-bottom:20px}.console .document-nav{display:none;position:sticky;top:90px}@media only screen and (min-width:1380px){.console .document-nav{display:block}}.console .document-nav ul{position:absolute;width:200px;right:-260px}.console .document-nav ul li{margin-bottom:20px}.console .document-nav ul li .selected{font-weight:500}.console .scroll-to{display:none}@media only screen and (min-width:1199px){.console .logo .top{display:none!important}}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.console>header{width:calc(100% - 30px)!important;margin:0 -30px;padding:15px}.console>header nav{width:100%;height:70px;overflow:hidden}.console>header nav.close{background:0 0}.console>header nav.close .logo .nav{display:none!important}.console>header nav.open{height:100%}.console>header nav.open .logo .top{display:none!important}.console>header nav.open .bottom{display:block!important}.console>header nav.open button{color:#87a5b9}.console>header nav button{margin:9px;background:0 0;color:var(--config-color-normal)}.console>header nav button:focus,.console>header nav button:hover{background:0 0}.console>header nav .logo{display:block!important;position:absolute;top:0;left:50%;margin:auto;transform:translateX(-50%)}.console>header nav .bottom{display:none!important}.console>footer{width:auto;margin:50px -30px 0 -30px!important;padding:0 30px 30px 30px}.console body{height:"calc(100% - 70px)"!important;width:calc(100% - 60px)!important;padding:70px 30px 0 30px!important}.console .cover{padding:25px 30px;margin:0 -30px}}@media only screen and (max-width:550px){.console body{height:"calc(100% - 70px)"!important;width:calc(100% - 40px)!important;padding:70px 20px 0 20px!important}.console .cover{padding:20px 20px;margin:0 -20px}.console>header{margin:0 -20px}.console>header .list{width:175px;font-size:14px}.console>footer{margin:50px -20px 0 -20px!important;padding:0 20px 20px 20px}}.dev-feature{display:none}.prod-feature{display:none}.development .dev-feature{display:block;opacity:.6!important;outline:solid #ff0 3px;outline-offset:3px}.development .dev-feature.dev-inline{display:inline-block}.development .prod-feature{display:none}.production .dev-feature{display:none}.production .prod-feature{display:block}.search{opacity:1!important}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.search button{margin-top:20px}}html.home body{padding:0 50px;color:var(--config-color-normal)}html.home .logo a{display:block}html.home .logo a:hover{opacity:.8}html.home .logo img{max-height:35px;width:198px;margin:45px auto 25px auto}html.home footer{background:0 0;text-align:center}html.home main{min-height:400px}.alerts ul{width:100%;visibility:hidden;position:fixed;padding:0;right:0;left:0;color:var(--config-color-normal);z-index:1002;margin:0 auto;bottom:15px;max-width:560px}.alerts ul li{margin:10px 0 0 0;padding:0}.alerts ul li div.message{position:relative;padding:12px 35px;margin:0 auto;list-style:none;background:var(--config-color-background-dark);text-align:center;font-size:14px;border-radius:10px;line-height:16px;min-height:16px;box-shadow:0 0 10px rgba(0,0,0,.05);opacity:.95}.alerts ul li div.message a,.alerts ul li div.message span{font-weight:600}.alerts ul li div.message i{cursor:pointer;position:absolute;font-size:14px;line-height:22px;top:8px;right:8px}.alerts ul li div.message.error{color:#fff;background:var(--config-color-danger)}.alerts ul li div.message.error a{color:#fff;border-bottom:dotted 1px #fff}.alerts ul li div.message.success{color:#fff;background:var(--config-color-success)}.alerts ul li div.message.success a{color:#fff;border-bottom:dotted 1px #fff}.alerts ul li div.message.warning{color:#fff;background:var(--config-color-success)}.alerts ul li div.message.warning a{color:#fff;border-bottom:dotted 1px #fff}.alerts ul li div.message.open{display:block}.alerts ul li div.message.close{display:none}.alerts .cookie-alert{background:var(--config-color-focus-fade)!important;color:var(--config-color-focus)}.alerts .cookie-alert a{color:var(--config-color-focus);font-weight:400;border-bottom:dotted 1px var(--config-color-focus)}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.alerts ul{top:auto;bottom:0;max-width:100%}.alerts ul li{margin:5px 0 0 0}.alerts ul li div.message{border-radius:0}}article{overflow-wrap:break-word;word-wrap:break-word}article h1{font-size:36px}article h2{font-size:24px}article h3{font-size:20px}article h4{font-size:20px}article h5{font-size:18px}article h6{font-size:16px}article h1,article h2,article h3,article h4,article h5,article h6{margin-top:30px!important;margin-bottom:30px!important}article p{line-height:32px;font-size:16px}article .update{display:block;margin-top:50px!important}article table{width:100%;margin:0;margin-bottom:30px!important;border-radius:0;border-bottom:solid 1px var(--config-border-color)}article table thead td{font-weight:500;padding:5px 15px}article table td,article table th{padding:15px;height:auto}article table td:first-child,article table th:first-child{padding-right:10px}article table td:last-child,article table th:last-child{padding-left:10px}article table td p,article table th p{font-size:inherit;line-height:inherit}article table td p:last-child,article table th p:last-child{margin:0}.avatar-container{position:relative}.avatar-container .corner{position:absolute;bottom:-3px;left:-3px}.avatar{width:60px;height:60px;border-radius:50%;background:var(--config-color-background-focus);display:inline-block;overflow:hidden;box-shadow:0 0 6px rgba(0,0,0,.09);position:relative;z-index:1;opacity:1!important}.avatar:before{content:"";position:absolute;width:100%;height:100%;z-index:0;background:var(--config-color-background-focus)}.avatar.inline{display:inline-block;vertical-align:middle}.avatar.trans{background:0 0}.avatar .no-shadow{box-shadow:none}.avatar.xs{width:30px;height:30px}.avatar.xxs{width:20px;height:20px}.avatar.small{width:50px;height:50px}.avatar.big{width:100px;height:100px}.avatar.huge{width:150px;height:150px}.box{position:relative;background:var(--config-color-background-fade);border-radius:10px;box-shadow:0 0 3px rgba(0,0,0,.05);padding:30px;display:block;border-bottom:none}.box.padding-small{padding:15px}.box.y-scroll{overflow-y:auto}.box.danger{background:var(--config-color-danger);color:#fff}.box.danger .box{color:var(--config-color-normal);background:var(--config-color-background-fade)}.box.danger>.button,.box.danger>button{background:#fff;color:var(--config-color-danger)}.box.note{background:var(--config-note-background)}.box.focus{background:var(--config-color-focus);color:var(--config-color-background-fade)}.box.focus .button,.box.focus button{background:var(--config-color-background-fade);color:var(--config-color-focus)}.box.line{background:0 0;border:solid 1px var(--config-color-background-dark);box-shadow:none}.box.warning{background:var(--config-color-success);color:#2d2d2d}.box.warning .button,.box.warning button{background:rgba(45,45,45,.8);color:var(--config-color-success)}.box .tabs{border-bottom:solid 1px var(--config-border-color);margin:0 -30px;padding:0 30px!important}.box>footer{margin:0 -30px -30px -30px;padding:15px 30px;background:var(--config-color-background-fade);border:solid 1px var(--config-border-color);border-radius:0 0 10px 10px}.box hr{height:1px;background:var(--config-console-background);border:none;margin:30px -30px}.box .label{position:absolute;top:10px;z-index:2;left:10px}.box.fade-bottom{position:relative;overflow:hidden}.box.fade-bottom:after{content:"";position:absolute;display:block;bottom:15px;width:100%;background:#000;background:linear-gradient(180deg,rgba(0,0,0,0) 0,var(--config-color-background-fade) 80%);height:100px;margin:0 -15px}.box .header{position:static;height:40px;padding:20px 30px 20px 30px;margin-bottom:30px;margin:-30px -30px 20px -30px;background:var(--config-color-background-fade);border-bottom:solid 1px #efefef}.box ul.numbers>li{position:relative;margin-right:30px;margin-left:50px}.box ul.numbers>li hr{margin-right:-60px;margin-left:-80px}.box ul.numbers>li .settings{position:absolute;top:3px;left:-50px}.box ul.numbers>li::after{display:block;width:25px;height:25px;line-height:25px;font-size:13px;font-weight:500;border-radius:50%;background:var(--config-color-focus);color:var(--config-color-background);counter-increment:section;content:counter(section);text-align:center;position:absolute;top:3px;right:-45px}.box .scroll{margin:0 -30px;overflow-y:scroll}.box .scroll table{width:100%;margin:0}.box ul.sortable{counter-reset:section}.box ul.sortable>li [data-move-down].round,.box ul.sortable>li [data-move-up].round,.box ul.sortable>li [data-remove].round{background:var(--config-color-focus);color:var(--config-color-background-fade);width:25px;height:25px;line-height:25px;display:inline-block;text-align:center;padding:0;margin-left:5px}.box ul.sortable>li [data-move-down].round:disabled,.box ul.sortable>li [data-move-up].round:disabled,.box ul.sortable>li [data-remove].round:disabled{display:none}.box ul.sortable>li:first-child [data-move-up]{display:none}.box ul.sortable>li:first-child [data-move-up]:disabled{display:inline-block;background:var(--config-color-background)}.box ul.sortable>li:last-child [data-move-down]{display:none}.box ul.sortable>li:last-child [data-move-down]:disabled{display:inline-block;background:var(--config-color-background)}.box .toggle{position:relative;border-top:1px solid var(--config-console-background);border-bottom:1px solid var(--config-console-background);margin:0 -30px;padding:30px 30px 0 30px;height:65px;overflow:hidden}.box .toggle.list{border-bottom:none}.box .toggle.sorts button.ls-ui-open{width:calc(100% - 100px)}.box .toggle button.ls-ui-open{left:0;position:absolute;top:0;width:100%;height:95px;background:0 0;opacity:.5;border-radius:0}.box .toggle .icon-minus,.box .toggle .icon-up-open{display:none}.box .toggle .content{display:none}.box .toggle.open{height:auto}.box .toggle.open .icon-minus,.box .toggle.open .icon-up-open{display:block}.box .toggle.open .icon-down-open,.box .toggle.open .icon-plus{display:none}.box .toggle.open .content{display:block}.box .list li{border-bottom:solid 2px var(--config-border-color);margin:0 -30px 30px -30px;padding:0 30px 30px 30px}.box .list li:last-child{padding-bottom:0;margin-bottom:0;border-bottom:none}@media only screen and (max-width:550px){.box .list li .actions{float:none}}.box .list li .avatar{display:block}.box .list li .avatar.inline{display:inline-block}.box.new{text-align:center}.box.new i{font-size:80px;line-height:80px;font-family:Poppins,sans-serif;font-style:normal;font-weight:300}.box.new b{margin-top:20px;display:block}.box .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.box .info hr{background:var(--config-modal-note-border)!important}.box .table-wrap{margin:0 -30px;overflow-y:scroll}.box .table-wrap table{margin:0}a.box{border-right:none;border-left:none}a.box:hover{box-shadow:0 0 1px rgba(0,0,0,.2);opacity:.7}.box-asidex{padding-left:25px!important;padding-right:70px;left:0;background:#f9f9f9;border-radius:0 10px 10px 0;height:calc(100% - 30px);position:absolute;padding-top:30px}.box-asidex:after{content:"";display:block;position:absolute;height:100%;width:51px;background:#fff;top:0;bottom:0;right:-6px}.cover{background:var(--config-color-focus-fade);padding:30px 50px;margin:0 -50px;position:relative;border-bottom:solid 1px var(--config-border-fade)}.cover .title,.cover h1,.cover h2,.cover h3,.cover h4{color:var(--config-color-focus);font-weight:600;margin-bottom:50px!important;font-size:28px;line-height:42px}.cover .title span,.cover h1 span,.cover h2 span,.cover h3 span,.cover h4 span{font-weight:600}.cover i:before{margin:0!important}.cover p{color:var(--config-color-fade)}.cover .button{color:#fff}.cover .link,.cover a{color:var(--config-color-focus);border-left:none;border-right:none;cursor:pointer}.cover .link:hover,.cover a:hover{border-bottom-color:var(--config-color-focus)}.console .database .row .col{height:452px}.console .database .row .col:after{width:2px;left:20px}.console .database hr{margin:0 -20px;background:var(--config-color-background);height:1px}.console .database h3{font-size:13px;line-height:20px;height:20px;background-color:var(--config-color-fade-super);margin:-20px -20px 0 -20px;padding:10px 20px;border-bottom:solid 1px var(--config-color-background);font-weight:600}.console .database .empty{height:162px;font-size:12px;text-align:center;margin:50px 0}.console .database .empty h4{font-size:13px;font-weight:600;line-height:120px}.console .database .search{background-color:var(--config-color-fade-super);margin:0 -20px 0 -20px;padding:10px 15px}.console .database .search input{height:40px;background-color:#fff;border-radius:25px;padding-top:0;padding-bottom:0}.console .database .code{height:411px;background:var(--config-color-fade-super);margin:0 -20px -20px -20px;padding:20px;width:calc(100% - 10px)}.console .database .code .ide{overflow:scroll;height:451px;margin:-20px;box-shadow:none;border-radius:0}.console .database .paging{background:var(--config-color-fade-super);margin:0 -20px -20px -20px;padding:20px}.console .database .button{margin:0 -20px;padding:0 20px!important;text-align:inherit;color:var(--config-color-focus);width:100%;font-size:15px;line-height:55px;box-sizing:content-box}.console .database .button i{margin-left:8px}.console .database .button:hover{border:none;background:var(--config-color-focus-fade)}.console .database .items{margin:0 -20px;height:262px;overflow-x:hidden;overflow-y:scroll}.console .database .items form{opacity:0;position:relative}.console .database .items form button{position:absolute;top:0;bottom:0;right:0;left:0;width:100%;height:45px;border-radius:0;cursor:pointer}.console .database .items li{padding:0;margin:0 0;line-height:45px;font-size:15px;padding-right:50px;padding-left:30px;position:relative}.console .database .items li i{position:absolute;display:none;left:10px}.console .database .items li .name{display:inline-block;width:100%;height:28px}.console .database .items li.selected,.console .database .items li:hover{background:#f5f5f5}.console .database .items li.selected i,.console .database .items li:hover i{display:block}.console .database .items li:last-child{border-bottom:none}body>footer{color:var(--config-color-fade);line-height:40px;margin:0 -50px;padding:12px 50px;font-size:13px;width:100%;background:#f1f1f1;position:relative;margin-top:80px!important}body>footer:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}body>footer .logo img{height:22px;padding-top:12px}body>footer a{color:var(--config-color-fade);font-size:13px}body>footer a:hover{border-bottom-color:var(--config-color-fade)}body>footer ul:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}body>footer ul li{font-size:13px;float:right;margin-left:20px!important}body>footer .copyright{padding-right:2px}[data-ls-if]{display:none}[data-service]{opacity:0}.load-service-start{opacity:0}.load-service-end{opacity:1;transition:opacity .5s ease-out;-moz-transition:opacity .5s ease-out;-webkit-transition:opacity .5s ease-out;-o-transition:opacity .5s ease-out}.load-screen{z-index:100000;position:fixed;height:100%;width:100%;background-color:var(--config-color-background-focus);top:0;right:0}.load-screen.loaded{transition:opacity 1s ease-in-out,top 1s .7s;opacity:0;top:-100%}.load-screen .animation{position:absolute;top:45%;left:50%;transform:translate(-50%,-50%) translateZ(1px);width:140px;height:140px}.load-screen .animation div{box-sizing:border-box;display:block;position:absolute;width:124px;height:124px;margin:10px;border:10px solid var(--config-color-focus);border-radius:50%;animation:animation 1.2s cubic-bezier(.5,0,.5,1) infinite;border-color:var(--config-color-focus) transparent transparent transparent}.load-screen .animation div:nth-child(1){animation-delay:-.45s}.load-screen .animation div:nth-child(2){animation-delay:-.3s}.load-screen .animation div:nth-child(3){animation-delay:-.15s}@keyframes animation{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.load-screen img{position:absolute;height:20px;bottom:60px;left:50%;transform:translate(-50%,-50%)}.modal-open .modal-bg,.modal-open body .modal-bg{position:fixed;content:'';display:block;width:100%;height:100%;left:0;right:0;top:0;bottom:0;background:#0c0c0c;opacity:.5;z-index:4}.modal{overflow:auto;display:none;position:fixed;transform:translate3d(0,0,0);width:100%;max-height:90%;max-width:640px;background:var(--config-color-background-fade);z-index:1000;box-shadow:0 0 4px rgba(0,0,0,.25);padding:30px;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:10px;box-sizing:border-box;text-align:right;white-space:initial;line-height:normal}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.modal{width:calc(100% - 20px)}}.modal.full{max-width:none;max-height:none;height:100%;border-radius:0;padding:80px 120px}.modal.full h1{font-weight:700}.modal.padding-small{padding:15px}.modal.height-tiny>form{height:100px}.modal.height-small>form{height:220px}.modal.width-small{max-width:400px}.modal.width-medium{max-width:500px}.modal.width-large{max-width:800px}.modal.open{display:block}.modalbutton.close{display:none}.modal.fill{height:95%;max-height:95%;max-width:75%}.modal h1,.modal h2{margin-bottom:25px;margin-top:0;font-size:20px;text-align:right}.modal h1,.modal h2,.modal h3,.modal h4,.modal h5,.modal h6{color:inherit!important;line-height:35px}.modal .main,.modal>form{position:relative;border-top:solid 1px var(--config-border-color);padding:30px 30px 0 30px;margin:0 -30px}.modal .main.strip,.modal>form.strip{border:none;padding:0;margin:0}.modal .separator{margin:20px -30px}.modal .bullets{padding-right:40px}.modal .bullets li{margin-bottom:30px!important}.modal .bullets li:before{position:absolute}.modal .info{margin:0 -30px;padding:20px 30px;background:var(--config-modal-note-background);color:var(--config-modal-note-color);border-top:solid 1px var(--config-modal-note-border);border-bottom:solid 1px var(--config-modal-note-border)}.modal .ide.strech{box-shadow:none;border-radius:0;margin:0 -30px}.modal button.close{width:30px;height:30px;line-height:30px;padding:0;margin:0;background:var(--config-color-normal);color:var(--config-color-background-fade);border-radius:50%}.modal .paging form{padding:0;margin:0;border-top:none}.modal.sticky-footer form footer{margin:-30px}.modal.sticky-footer footer{position:sticky;bottom:-30px;background:var(--config-color-background-fade-super);height:50px;z-index:1;padding:30px;box-shadow:0 0 1px rgba(0,0,0,.15)}.modal.sticky-footer footer form{display:inline-block}[data-views-current="0"] .scroll-to,[data-views-current="1"] .scroll-to{opacity:0!important}.scroll-to-bottom .scroll-to,.scroll-to-top .scroll-to{opacity:1}.scroll-to{opacity:0;display:block;width:40px;height:40px;line-height:40px;border-radius:50%;position:fixed;transform:translateZ(0);margin:30px;padding:0;bottom:0;font-size:18px;z-index:100000;transition:opacity .15s ease-in-out;left:0}.phases{list-style:none;margin:0;padding:0;position:relative}.phases li{display:none}.phases li li{display:block}.phases li.selected{display:block}.phases .number{display:none}.phases h2,.phases h3,.phases h4,.phases h5,.phases h6{margin:0 0 30px 0;text-align:inherit}.container{position:relative}.container .tabs{height:55px;line-height:55px;list-style:none;padding:0;margin-bottom:50px!important;margin-top:-55px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.container .tabs:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.container .tabs .selected{font-weight:400;color:var(--config-color-focus);position:relative;opacity:1}.container .tabs .selected:after{content:"";display:block;height:2px;background:var(--config-color-focus);width:calc(100% + 6px);margin:0 -3px;position:absolute;bottom:0;border-radius:2px}.container .tabs .number{display:none}.container .tabs li{float:right;margin-left:50px;color:var(--config-color-focus);opacity:.9;cursor:pointer}.container .tabs li:focus{outline:0}@media only screen and (max-width:550px){.container .tabs li{margin-left:25px}}.container .icon{display:none}@media only screen and (max-width:550px),only screen and (min-width:551px) and (max-width:1198px){.container .tabs{width:auto;overflow-x:scroll;overflow-y:hidden;white-space:nowrap}.container .tabs li{display:inline-block;float:none}}.ide{background-color:var(--config-prism-background);overflow:hidden;position:relative;z-index:1;box-shadow:0 2px 4px 0 rgba(50,50,93,.3);border-radius:10px;margin-bottom:30px}.ide *{font-family:'Source Code Pro',monospace}.ide[data-lang]::after{content:attr(data-lang-label);display:inline-block;background:#fff;color:#000;position:absolute;top:15px;padding:5px 10px;border-radius:15px;font-size:10px;right:10px;opacity:.95}.ide[data-lang=bash]::after{background:var(--config-language-bash);color:var(--config-language-bash-contrast)}.ide[data-lang=javascript]::after{background:var(--config-language-javascript);color:var(--config-language-javascript-contrast)}.ide[data-lang=web]::after{background:var(--config-language-web);color:var(--config-language-web-contrast)}.ide[data-lang=html]::after{background:var(--config-language-html);color:var(--config-language-html-contrast)}.ide[data-lang=php]::after{background:var(--config-language-php);color:var(--config-language-php-contrast)}.ide[data-lang=nodejs]::after{background:var(--config-language-nodejs);color:var(--config-language-nodejs-contrast)}.ide[data-lang=ruby]::after{background:var(--config-language-ruby);color:var(--config-language-ruby-contrast)}.ide[data-lang=python]::after{background:var(--config-language-python);color:var(--config-language-python-contrast)}.ide[data-lang=go]::after{background:var(--config-language-go);color:var(--config-language-go-contrast)}.ide[data-lang=dart]::after{background:var(--config-language-dart);color:var(--config-language-dart-contrast)}.ide[data-lang=flutter]::after{background:var(--config-language-flutter);color:var(--config-language-flutter-contrast)}.ide[data-lang=yaml]::after{background:var(--config-language-yaml);color:var(--config-language-yaml-contrast)}.ide .tag{color:inherit!important;background:0 0!important;padding:inherit!important;font-size:inherit!important;line-height:14px}.ide .copy{cursor:pointer;content:attr(data-lang);display:inline-block;background:#fff;color:#000;position:absolute;transform:translateX(-50%);bottom:-20px;padding:5px 10px;border-radius:15px;font-size:10px;font-style:normal;right:50%;opacity:0;transition:bottom .3s,opacity .3s;line-height:normal;font-family:Poppins,sans-serif}.ide .copy::before{padding-left:5px}.ide:hover .copy{transition:bottom .3s,opacity .3s;opacity:.9;bottom:16px}.ide pre{-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;color:#e6ebf1;font-weight:400;line-height:20px;font-size:13px;margin:0;padding:20px;padding-left:60px}.ide.light{box-shadow:0 2px 4px 0 rgba(50,50,93,.1);background-color:#fff}.ide.light pre{color:#414770}.ide.light .token.cdata,.ide.light .token.comment,.ide.light .token.doctype,.ide.light .token.prolog{color:#91a2b0}.ide.light .token.attr-name,.ide.light .token.builtin,.ide.light .token.char,.ide.light .token.inserted,.ide.light .token.selector,.ide.light .token.string{color:#149570}.ide.light .token.punctuation{color:#414770}.ide.light .language-css .token.string,.ide.light .style .token.string,.ide.light .token.entity,.ide.light .token.operator,.ide.light .token.url,.ide.light .token.variable{color:#414770}.ide.light .line-numbers .line-numbers-rows{background:#f2feef}.ide.light .line-numbers-rows>span:before{color:#5dc79e}.ide.light .token.keyword{color:#6772e4;font-weight:500}code[class*=language-],pre[class*=language-]{text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4}pre[class*=language-]{overflow:auto}:not(pre)>code[class*=language-]{padding:.1em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#6b7c93}.token.punctuation{color:#f8f8f2}.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#f79a59}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#3ecf8e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#45b2e8}.token.keyword{color:#7795f8}.token.important,.token.regex{color:#fd971f}.token.italic{font-style:italic}.token.entity{cursor:help}pre[class*=language-].line-numbers{position:relative;padding-left:60px;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{background:var(--config-prism-numbers);position:absolute;pointer-events:none;top:-20px;bottom:-21px;padding:20px 0;font-size:100%;left:-60px;width:40px;letter-spacing:-1px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{padding-left:5px;pointer-events:none;display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#636365;display:block;padding-right:.8em;text-align:right}html{padding:0;margin:0;direction:rtl}body{margin:0;background:var(--config-console-background) no-repeat fixed;min-width:300px}ul{padding:0;margin:0}ul li{margin:0;list-style:none}.icon-left-open:before{content:'\e814'!important}.icon-right-open:before{content:'\e813'!important}.icon-right-dir:before{content:'\e84e'!important}.icon-left-dir:before{content:'\e84d'!important}.icon-link-ext:before{-moz-transform:scaleX(-1);-o-transform:scaleX(-1);-webkit-transform:scaleX(-1);transform:scaleX(-1)}.icon-article-alt:before{-moz-transform:scaleX(-1);-o-transform:scaleX(-1);-webkit-transform:scaleX(-1);transform:scaleX(-1)}.copy{border-radius:10px 0 0 10px!important} \ No newline at end of file diff --git a/public/images/environments/deno.png b/public/images/environments/deno.png new file mode 100644 index 0000000000..5ff1b53cef Binary files /dev/null and b/public/images/environments/deno.png differ diff --git a/public/images/environments/node.png b/public/images/environments/node.png new file mode 100644 index 0000000000..9004cb15fe Binary files /dev/null and b/public/images/environments/node.png differ diff --git a/public/images/environments/php.png b/public/images/environments/php.png new file mode 100644 index 0000000000..4f7e6067c4 Binary files /dev/null and b/public/images/environments/php.png differ diff --git a/public/images/environments/python.png b/public/images/environments/python.png new file mode 100644 index 0000000000..a705fee274 Binary files /dev/null and b/public/images/environments/python.png differ diff --git a/public/images/environments/ruby.png b/public/images/environments/ruby.png new file mode 100644 index 0000000000..7a9187fed4 Binary files /dev/null and b/public/images/environments/ruby.png differ diff --git a/public/index.php b/public/index.php index c87b14a26e..01f9dbb117 100644 --- a/public/index.php +++ b/public/index.php @@ -8,16 +8,20 @@ * ― Rick Cook, The Wizardry Compiled */ +use Appwrite\Utopia\Response; +use Utopia\App; +use Utopia\Request; + error_reporting(0); ini_set('display_errors', 0); -// ini_set('display_errors', 1); -// ini_set('display_startup_errors', 1); -// error_reporting(E_ALL); +ini_set('display_errors', 1); +ini_set('display_startup_errors', 1); +error_reporting(E_ALL); +//trigger_error('hide errors in prod', E_USER_NOTICE); -$path = (isset($_GET['q'])) ? explode('/', $_GET['q']) : []; +include __DIR__ . '/../app/controllers/general.php'; -array_shift($path); -$version = array_shift($path); +$app = new App('Asia/Tel_Aviv'); -include __DIR__ . '/../app/app.php'; +$app->run(new Request(), new Response()); diff --git a/public/scripts/app.js b/public/scripts/app.js index b9f9ce64b7..b175f3c088 100644 --- a/public/scripts/app.js +++ b/public/scripts/app.js @@ -27,61 +27,6 @@ window.ls.container } } }) - .add({ - selector: "data-forms-headers", - controller: function(element) { - let key = document.createElement("input"); - let value = document.createElement("input"); - let wrap = document.createElement("div"); - let cell1 = document.createElement("div"); - let cell2 = document.createElement("div"); - - key.type = "text"; - key.className = "margin-bottom-no"; - key.placeholder = "Key"; - value.type = "text"; - value.className = "margin-bottom-no"; - value.placeholder = "Value"; - - wrap.className = "row thin margin-bottom-small"; - cell1.className = "col span-6"; - cell2.className = "col span-6"; - - element.parentNode.insertBefore(wrap, element); - cell1.appendChild(key); - cell2.appendChild(value); - wrap.appendChild(cell1); - wrap.appendChild(cell2); - - key.addEventListener("input", function() { - syncA(); - }); - - value.addEventListener("input", function() { - syncA(); - }); - - element.addEventListener("change", function() { - syncB(); - }); - - let syncA = function() { - element.value = - key.value.toLowerCase() + ":" + value.value.toLowerCase(); - }; - - let syncB = function() { - let split = element.value.toLowerCase().split(":"); - key.value = split[0] || ""; - value.value = split[1] || ""; - - key.value = key.value.trim(); - value.value = value.value.trim(); - }; - - syncB(); - } - }) .add({ selector: "data-prism", controller: function(window, document, element, alerts) { @@ -115,82 +60,4 @@ window.ls.container element.parentNode.parentNode.appendChild(copy); } }) - .add({ - selector: "data-ls-ui-chart", - controller: function(element, container, date, document) { - let child = document.createElement("canvas"); - - child.width = 500; - child.height = 175; - - let stats = container.get("usage"); - - if (!stats || !stats["requests"] || !stats["requests"]["data"]) { - return; - } - - let config = { - type: "line", - data: { - labels: [], - datasets: [ - { - label: "Requests", - backgroundColor: "rgba(230, 248, 253, 0.3)", - borderColor: "#29b5d9", - borderWidth: 2, - data: [0, 0, 0, 0, 0, 0, 0], - fill: true - } - ] - }, - options: { - responsive: true, - title: { - display: false, - text: "Stats" - }, - legend: { - display: false - }, - tooltips: { - mode: "index", - intersect: false, - caretPadding: 0 - }, - hover: { - mode: "nearest", - intersect: true - }, - scales: { - xAxes: [ - { - display: false - } - ], - yAxes: [ - { - display: false - } - ] - } - } - }; - - for (let i = 0; i < stats["requests"]["data"].length; i++) { - config.data.datasets[0].data[i] = stats["requests"]["data"][i].value; - config.data.labels[i] = date.format( - "d F Y", - stats["requests"]["data"][i].date - ); - } - - element.innerHTML = ""; - - element.appendChild(child); - - container.set("chart", new Chart(child.getContext("2d"), config), true); - - element.dataset["canvas"] = true; - } - }); +; diff --git a/public/scripts/dependencies/appwrite.js b/public/scripts/dependencies/appwrite.js index 27eb23b742..b246db4cc5 100644 --- a/public/scripts/dependencies/appwrite.js +++ b/public/scripts/dependencies/appwrite.js @@ -825,16 +825,17 @@ * Use this endpoint to send a verification message to your user email address * to confirm they are the valid owners of that address. Both the **userId** * and **secret** arguments will be passed as query parameters to the URL you - * have provider to be attached to the verification email. The provided URL - * should redirect the user back for your app and allow you to complete the + * have provided to be attached to the verification email. The provided URL + * should redirect the user back to your app and allow you to complete the * verification process by verifying both the **userId** and **secret** * parameters. Learn more about how to [complete the verification * process](/docs/client/account#updateAccountVerification). * * Please note that in order to avoid a [Redirect - * Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) + * Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), * the only valid redirect URLs are the ones from domains you have set when * adding your platforms in the console interface. + * * * @param {string} url * @throws {Error} @@ -1267,11 +1268,11 @@ * @param {string} text * @param {number} size * @param {number} margin - * @param {number} download + * @param {boolean} download * @throws {Error} * @return {string} */ - getQR: function(text, size = 400, margin = 1, download = 0) { + getQR: function(text, size = 400, margin = 1, download = false) { if(text === undefined) { throw new Error('Missing required parameter: "text"'); } @@ -1536,18 +1537,16 @@ * * @param {string} collectionId * @param {string[]} filters - * @param {number} offset * @param {number} limit + * @param {number} offset * @param {string} orderField * @param {string} orderType * @param {string} orderCast * @param {string} search - * @param {number} first - * @param {number} last * @throws {Error} * @return {Promise} */ - listDocuments: function(collectionId, filters = [], offset = 0, limit = 50, orderField = '$id', orderType = 'ASC', orderCast = 'string', search = '', first = 0, last = 0) { + listDocuments: function(collectionId, filters = [], limit = 25, offset = 0, orderField = '$id', orderType = 'ASC', orderCast = 'string', search = '') { if(collectionId === undefined) { throw new Error('Missing required parameter: "collectionId"'); } @@ -1560,14 +1559,14 @@ payload['filters'] = filters; } - if(offset) { - payload['offset'] = offset; - } - if(limit) { payload['limit'] = limit; } + if(offset) { + payload['offset'] = offset; + } + if(orderField) { payload['orderField'] = orderField; } @@ -1584,14 +1583,6 @@ payload['search'] = search; } - if(first) { - payload['first'] = first; - } - - if(last) { - payload['last'] = last; - } - return http .get(path, { 'content-type': 'application/json', @@ -1781,22 +1772,119 @@ .delete(path, { 'content-type': 'application/json', }, payload); - }, + } + }; + + let functions = { /** - * Get Collection Logs + * List Functions * * - * @param {string} collectionId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType * @throws {Error} * @return {Promise} */ - getCollectionLogs: function(collectionId) { - if(collectionId === undefined) { - throw new Error('Missing required parameter: "collectionId"'); + list: function(search = '', limit = 25, offset = 0, orderType = 'ASC') { + let path = '/functions'; + + let payload = {}; + + if(search) { + payload['search'] = search; + } + + if(limit) { + payload['limit'] = limit; + } + + if(offset) { + payload['offset'] = offset; + } + + if(orderType) { + payload['orderType'] = orderType; + } + + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Create Function + * + * + * @param {string} name + * @param {string} env + * @param {object} vars + * @param {string[]} events + * @param {string} schedule + * @param {number} timeout + * @throws {Error} + * @return {Promise} + */ + create: function(name, env, vars = [], events = [], schedule = '', timeout = 15) { + if(name === undefined) { + throw new Error('Missing required parameter: "name"'); } - let path = '/database/collections/{collectionId}/logs'.replace(new RegExp('{collectionId}', 'g'), collectionId); + if(env === undefined) { + throw new Error('Missing required parameter: "env"'); + } + + let path = '/functions'; + + let payload = {}; + + if(name) { + payload['name'] = name; + } + + if(env) { + payload['env'] = env; + } + + if(vars) { + payload['vars'] = vars; + } + + if(events) { + payload['events'] = events; + } + + if(schedule) { + payload['schedule'] = schedule; + } + + if(timeout) { + payload['timeout'] = timeout; + } + + return http + .post(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Get Function + * + * + * @param {string} functionId + * @throws {Error} + * @return {Promise} + */ + get: function(functionId) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); let payload = {}; @@ -1804,6 +1892,352 @@ .get(path, { 'content-type': 'application/json', }, payload); + }, + + /** + * Update Function + * + * + * @param {string} functionId + * @param {string} name + * @param {object} vars + * @param {string[]} events + * @param {string} schedule + * @param {number} timeout + * @throws {Error} + * @return {Promise} + */ + update: function(functionId, name, vars = [], events = [], schedule = '', timeout = 15) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(name === undefined) { + throw new Error('Missing required parameter: "name"'); + } + + let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(name) { + payload['name'] = name; + } + + if(vars) { + payload['vars'] = vars; + } + + if(events) { + payload['events'] = events; + } + + if(schedule) { + payload['schedule'] = schedule; + } + + if(timeout) { + payload['timeout'] = timeout; + } + + return http + .put(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Delete Function + * + * + * @param {string} functionId + * @throws {Error} + * @return {Promise} + */ + delete: function(functionId) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + return http + .delete(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * List Executions + * + * + * @param {string} functionId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {Error} + * @return {Promise} + */ + listExecutions: function(functionId, search = '', limit = 25, offset = 0, orderType = 'ASC') { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + let path = '/functions/{functionId}/executions'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(search) { + payload['search'] = search; + } + + if(limit) { + payload['limit'] = limit; + } + + if(offset) { + payload['offset'] = offset; + } + + if(orderType) { + payload['orderType'] = orderType; + } + + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Create Execution + * + * + * @param {string} functionId + * @param {number} async + * @throws {Error} + * @return {Promise} + */ + createExecution: function(functionId, async = 1) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + let path = '/functions/{functionId}/executions'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(async) { + payload['async'] = async; + } + + return http + .post(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Get Execution + * + * + * @param {string} functionId + * @param {string} executionId + * @throws {Error} + * @return {Promise} + */ + getExecution: function(functionId, executionId) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(executionId === undefined) { + throw new Error('Missing required parameter: "executionId"'); + } + + let path = '/functions/{functionId}/executions/{executionId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{executionId}', 'g'), executionId); + + let payload = {}; + + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Update Function Tag + * + * + * @param {string} functionId + * @param {string} tag + * @throws {Error} + * @return {Promise} + */ + updateTag: function(functionId, tag) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(tag === undefined) { + throw new Error('Missing required parameter: "tag"'); + } + + let path = '/functions/{functionId}/tag'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(tag) { + payload['tag'] = tag; + } + + return http + .patch(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * List Tags + * + * + * @param {string} functionId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {Error} + * @return {Promise} + */ + listTags: function(functionId, search = '', limit = 25, offset = 0, orderType = 'ASC') { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + let path = '/functions/{functionId}/tags'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(search) { + payload['search'] = search; + } + + if(limit) { + payload['limit'] = limit; + } + + if(offset) { + payload['offset'] = offset; + } + + if(orderType) { + payload['orderType'] = orderType; + } + + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Create Tag + * + * + * @param {string} functionId + * @param {string} command + * @param {File} code + * @throws {Error} + * @return {Promise} + */ + createTag: function(functionId, command, code) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(command === undefined) { + throw new Error('Missing required parameter: "command"'); + } + + if(code === undefined) { + throw new Error('Missing required parameter: "code"'); + } + + let path = '/functions/{functionId}/tags'.replace(new RegExp('{functionId}', 'g'), functionId); + + let payload = {}; + + if(command) { + payload['command'] = command; + } + + if(code) { + payload['code'] = code; + } + + return http + .post(path, { + 'content-type': 'multipart/form-data', + }, payload); + }, + + /** + * Get Tag + * + * + * @param {string} functionId + * @param {string} tagId + * @throws {Error} + * @return {Promise} + */ + getTag: function(functionId, tagId) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(tagId === undefined) { + throw new Error('Missing required parameter: "tagId"'); + } + + let path = '/functions/{functionId}/tags/{tagId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{tagId}', 'g'), tagId); + + let payload = {}; + + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * Delete Tag + * + * + * @param {string} functionId + * @param {string} tagId + * @throws {Error} + * @return {Promise} + */ + deleteTag: function(functionId, tagId) { + if(functionId === undefined) { + throw new Error('Missing required parameter: "functionId"'); + } + + if(tagId === undefined) { + throw new Error('Missing required parameter: "tagId"'); + } + + let path = '/functions/{functionId}/tags/{tagId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{tagId}', 'g'), tagId); + + let payload = {}; + + return http + .delete(path, { + 'content-type': 'application/json', + }, payload); } }; @@ -2204,14 +2638,34 @@ * List Projects * * + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType * @throws {Error} * @return {Promise} */ - list: function() { + list: function(search = '', limit = 25, offset = 0, orderType = 'ASC') { let path = '/projects'; let payload = {}; + if(search) { + payload['search'] = search; + } + + if(limit) { + payload['limit'] = limit; + } + + if(offset) { + payload['offset'] = offset; + } + + if(orderType) { + payload['orderType'] = orderType; + } + return http .get(path, { 'content-type': 'application/json', @@ -2996,7 +3450,7 @@ * @param {string} name * @param {string} status * @param {string} schedule - * @param {number} security + * @param {boolean} security * @param {string} httpMethod * @param {string} httpUrl * @param {string[]} httpHeaders @@ -3117,7 +3571,7 @@ * @param {string} name * @param {string} status * @param {string} schedule - * @param {number} security + * @param {boolean} security * @param {string} httpMethod * @param {string} httpUrl * @param {string[]} httpHeaders @@ -3292,7 +3746,7 @@ * @param {string} name * @param {string[]} events * @param {string} url - * @param {number} security + * @param {boolean} security * @param {string} httpUser * @param {string} httpPass * @throws {Error} @@ -3390,7 +3844,7 @@ * @param {string} name * @param {string[]} events * @param {string} url - * @param {number} security + * @param {boolean} security * @param {string} httpUser * @param {string} httpPass * @throws {Error} @@ -4498,6 +4952,7 @@ account: account, avatars: avatars, database: database, + functions: functions, health: health, locale: locale, projects: projects, diff --git a/public/scripts/filters.js b/public/scripts/filters.js index d1a6f54256..9f5308d557 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -31,12 +31,49 @@ window.ls.filter .add("date", function($value, date) { return date.format("Y-m-d", $value); }) - .add("date-time", function($value, date) { + .add("dateTime", function($value, date) { return date.format("Y-m-d H:i", $value); }) - .add("date-text", function($value, date) { + .add("dateText", function($value, date) { return date.format("d M Y", $value); }) + .add("timeSince", function($value) { + $value = $value * 1000; + + let seconds = Math.floor((Date.now() - $value) / 1000); + let unit = "second"; + let direction = "ago"; + + if (seconds < 0) { + seconds = -seconds; + direction = "from now"; + } + + let value = seconds; + + if (seconds >= 31536000) { + value = Math.floor(seconds / 31536000); + unit = "year"; + } + else if (seconds >= 86400) { + value = Math.floor(seconds / 86400); + unit = "day"; + } + else if (seconds >= 3600) { + value = Math.floor(seconds / 3600); + unit = "hour"; + } + else if (seconds >= 60) { + value = Math.floor(seconds / 60); + unit = "minute"; + } + + if (value != 1) { + unit = unit + "s"; + } + + return value + " " + unit + " " + direction; + }) .add("ms2hum", function($value) { let temp = $value; const years = Math.floor(temp / 31536000), @@ -58,6 +95,26 @@ window.ls.filter return "< 1s"; }) + .add("seconds2hum", function($value) { + + var seconds = ($value).toFixed(3); + + var minutes = ($value / (60)).toFixed(1); + + var hours = ($value / (60 * 60)).toFixed(1); + + var days = ($value / (60 * 60 * 24)).toFixed(1); + + if (seconds < 60) { + return seconds + "s"; + } else if (minutes < 60) { + return minutes + "m"; + } else if (hours < 24) { + return hours + "h"; + } else { + return days + "d" + } + }) .add("markdown", function($value, markdown) { return markdown.render($value); }) @@ -161,6 +218,27 @@ window.ls.filter return $value.join(", ").replace(/,\s([^,]+)$/, ' and $1'); }) + .add("envName", function($value, env) { + if(env && env.ENVIRONMENTS && env.ENVIRONMENTS[$value]) { + return env.ENVIRONMENTS[$value].name; + } + + return ''; + }) + .add("envLogo", function($value, env) { + if(env && env.ENVIRONMENTS && env.ENVIRONMENTS[$value]) { + return env.ENVIRONMENTS[$value].logo; + } + + return ''; + }) + .add("envVersion", function($value, env) { + if(env && env.ENVIRONMENTS && env.ENVIRONMENTS[$value]) { + return env.ENVIRONMENTS[$value].version; + } + + return ''; + }) ; function abbreviate(number, maxPlaces, forcePlaces, forceLetter) { diff --git a/public/scripts/routes.js b/public/scripts/routes.js index cd9f1a377d..0e48df0092 100644 --- a/public/scripts/routes.js +++ b/public/scripts/routes.js @@ -1,171 +1,192 @@ window.ls.router .add("/auth/signin", { - template: "/auth/signin?version=" + APP_ENV.VERSION, + template: "/auth/signin?version=" + APP_ENV.CACHEBUSTER, scope: "home" }) .add("/auth/signup", { - template: "/auth/signup?version=" + APP_ENV.VERSION, + template: "/auth/signup?version=" + APP_ENV.CACHEBUSTER, scope: "home" }) .add("/auth/recovery", { - template: "/auth/recovery?version=" + APP_ENV.VERSION, + template: "/auth/recovery?version=" + APP_ENV.CACHEBUSTER, scope: "home" }) .add("/auth/recovery/reset", { - template: "/auth/recovery/reset?version=" + APP_ENV.VERSION, + template: "/auth/recovery/reset?version=" + APP_ENV.CACHEBUSTER, scope: "home" }) .add("/auth/confirm", { - template: "/auth/confirm?version=" + APP_ENV.VERSION, + template: "/auth/confirm?version=" + APP_ENV.CACHEBUSTER, scope: "home" }) .add("/auth/join", { - template: "/auth/join?version=" + APP_ENV.VERSION, + template: "/auth/join?version=" + APP_ENV.CACHEBUSTER, scope: "home" }) .add("/console", { - template: "/console?version=" + APP_ENV.VERSION, + template: "/console?version=" + APP_ENV.CACHEBUSTER, scope: "console" }) .add("/console/account", { - template: "/console/account?version=" + APP_ENV.VERSION, + template: "/console/account?version=" + APP_ENV.CACHEBUSTER, scope: "console" }) .add("/console/account/:tab", { - template: "/console/account?version=" + APP_ENV.VERSION, + template: "/console/account?version=" + APP_ENV.CACHEBUSTER, scope: "console" }) .add("/console/home", { - template: "/console/home?version=" + APP_ENV.VERSION, + template: "/console/home?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/home/:tab", { - template: "/console/home?version=" + APP_ENV.VERSION, + template: "/console/home?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/platforms/:platform", { template: function(window) { - return window.location.pathname + "?version=" + APP_ENV.VERSION; + return window.location.pathname + "?version=" + APP_ENV.CACHEBUSTER; }, scope: "console", project: true }) .add("/console/notifications", { - template: "/console/notifications?version=" + APP_ENV.VERSION, + template: "/console/notifications?version=" + APP_ENV.CACHEBUSTER, scope: "console" }) .add("/console/settings", { - template: "/console/settings?version=" + APP_ENV.VERSION, + template: "/console/settings?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/settings/:tab", { - template: "/console/settings?version=" + APP_ENV.VERSION, + template: "/console/settings?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/webhooks", { - template: "/console/webhooks?version=" + APP_ENV.VERSION, + template: "/console/webhooks?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/webhooks/:tab", { - template: "/console/webhooks?version=" + APP_ENV.VERSION, + template: "/console/webhooks?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/keys", { - template: "/console/keys?version=" + APP_ENV.VERSION, + template: "/console/keys?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/keys/:tab", { - template: "/console/keys?version=" + APP_ENV.VERSION, + template: "/console/keys?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/tasks", { - template: "/console/tasks?version=" + APP_ENV.VERSION, + template: "/console/tasks?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/tasks/:tab", { - template: "/console/tasks?version=" + APP_ENV.VERSION, + template: "/console/tasks?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/database", { - template: "/console/database?version=" + APP_ENV.VERSION, + template: "/console/database?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/database/collection", { template: function(window) { - return window.location.pathname + window.location.search + '&version=' + APP_ENV.VERSION; + return window.location.pathname + window.location.search + '&version=' + APP_ENV.CACHEBUSTER; }, scope: "console", project: true }) .add("/console/database/collection/:tab", { template: function(window) { - return window.location.pathname + window.location.search + '&version=' + APP_ENV.VERSION; + return window.location.pathname + window.location.search + '&version=' + APP_ENV.CACHEBUSTER; }, scope: "console", project: true }) .add("/console/database/document", { template: function(window) { - return window.location.pathname + window.location.search + '&version=' + APP_ENV.VERSION; + return window.location.pathname + window.location.search + '&version=' + APP_ENV.CACHEBUSTER; }, scope: "console", project: true }) .add("/console/database/document/:tab", { template: function(window) { - return window.location.pathname + window.location.search + '&version=' + APP_ENV.VERSION; + return window.location.pathname + window.location.search + '&version=' + APP_ENV.CACHEBUSTER; }, scope: "console", project: true }) .add("/console/storage", { - template: "/console/storage?version=" + APP_ENV.VERSION, + template: "/console/storage?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/storage/:tab", { - template: "/console/storage?version=" + APP_ENV.VERSION, + template: "/console/storage?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/users", { - template: "/console/users?version=" + APP_ENV.VERSION, + template: "/console/users?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/users/user", { - template: "/console/users/user?version=" + APP_ENV.VERSION, + template: "/console/users/user?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/users/user/:tab", { - template: "/console/users/user?version=" + APP_ENV.VERSION, + template: "/console/users/user?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/users/teams/team", { - template: "/console/users/teams/team?version=" + APP_ENV.VERSION, + template: "/console/users/teams/team?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/users/teams/team/:tab", { - template: "/console/users/teams/team?version=" + APP_ENV.VERSION, + template: "/console/users/teams/team?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true }) .add("/console/users/:tab", { - template: "/console/users?version=" + APP_ENV.VERSION, + template: "/console/users?version=" + APP_ENV.CACHEBUSTER, scope: "console", project: true - }); + }) + .add("/console/functions", { + template: "/console/functions?version=" + APP_ENV.CACHEBUSTER, + scope: "console", + project: true + }) + .add("/console/functions/function", { + template: "/console/functions/function?version=" + APP_ENV.CACHEBUSTER, + scope: "console", + project: true + }) + .add("/console/functions/function/:tab", { + template: "/console/functions/function?version=" + APP_ENV.CACHEBUSTER, + scope: "console", + project: true + }) + .add("/console/functions/:tab", { + template: "/console/functions?version=" + APP_ENV.CACHEBUSTER, + scope: "console", + project: true + }) +; diff --git a/public/scripts/services/date.js b/public/scripts/services/date.js index 7ef5985bed..e395a83514 100644 --- a/public/scripts/services/date.js +++ b/public/scripts/services/date.js @@ -320,7 +320,7 @@ var _date = function (format, timestamp) { jsdate = (timestamp === undefined ? new Date() // Not provided : (timestamp instanceof Date) ? new Date(timestamp) // JS Date() - : new Date(timestamp * 1000) // UNIX timestamp (auto-convert to int) + : new Date(timestamp * 1000) // Unix timestamp (auto-convert to int) ) return format.replace(formatChr, formatChrCb) } diff --git a/public/scripts/views/forms/chart.js b/public/scripts/views/forms/chart.js new file mode 100644 index 0000000000..f6740dc0b0 --- /dev/null +++ b/public/scripts/views/forms/chart.js @@ -0,0 +1,90 @@ +(function(window) { + "use strict"; + + window.ls.container.get("view").add({ + selector: "data-forms-chart", + controller: function(element, container, date, document) { + let child = document.createElement("canvas"); + let sources = element.getAttribute('data-forms-chart'); + let width = element.getAttribute('data-width') || 500; + let height = element.getAttribute('data-height') || 175; + let colors = ['#29b5d9' /* blue */, '#4eb55b' /* green */, '#fba233', /* orange */,]; + + child.width = width; + child.height = height; + + let config = { + type: "line", + data: { + labels: [], + datasets: [] + }, + options: { + responsive: true, + title: { + display: false, + text: "Stats" + }, + legend: { + display: false + }, + tooltips: { + mode: "index", + intersect: false, + caretPadding: 0 + }, + hover: { + mode: "nearest", + intersect: true + }, + scales: { + xAxes: [ + { + display: false + } + ], + yAxes: [ + { + display: false + } + ] + } + } + }; + + sources = sources.split(','); + + for (let i = 0; i < sources.length; i++) { + let label = sources[i].substring(0, sources[i].indexOf('=')); + let path = sources[i].substring(sources[i].indexOf('=') + 1); + let data = container.path(path); + + config.data.labels[i] = label; + config.data.datasets[i] = {}; + config.data.datasets[i].label = label; + config.data.datasets[i].borderColor = colors[i]; + config.data.datasets[i].backgroundColor = colors[i] + '36'; + config.data.datasets[i].borderWidth = 2; + config.data.datasets[i].data = [0, 0, 0, 0, 0, 0, 0]; + config.data.datasets[i].fill = true; + + if(!data) { + return; + } + + for (let x = 0; x < data.length; x++) { + config.data.datasets[i].data[x] = data[x].value; + config.data.labels[x] = date.format("d F Y", data[x].date); + } + } + + element.innerHTML = ""; + + element.appendChild(child); + + container.set("chart", new Chart(child.getContext("2d"), config), true); + + element.dataset["canvas"] = true; + } + }); +})(window); diff --git a/public/scripts/views/forms/headers.js b/public/scripts/views/forms/headers.js new file mode 100644 index 0000000000..6818f97351 --- /dev/null +++ b/public/scripts/views/forms/headers.js @@ -0,0 +1,59 @@ +(function(window) { + //"use strict"; + + window.ls.container.get("view").add({ + selector: "data-forms-headers", + controller: function(element) { + let key = document.createElement("input"); + let value = document.createElement("input"); + let wrap = document.createElement("div"); + let cell1 = document.createElement("div"); + let cell2 = document.createElement("div"); + + key.type = "text"; + key.className = "margin-bottom-no"; + key.placeholder = "Key"; + value.type = "text"; + value.className = "margin-bottom-no"; + value.placeholder = "Value"; + + wrap.className = "row thin margin-bottom-small"; + cell1.className = "col span-6"; + cell2.className = "col span-6"; + + element.parentNode.insertBefore(wrap, element); + cell1.appendChild(key); + cell2.appendChild(value); + wrap.appendChild(cell1); + wrap.appendChild(cell2); + + key.addEventListener("input", function() { + syncA(); + }); + + value.addEventListener("input", function() { + syncA(); + }); + + element.addEventListener("change", function() { + syncB(); + }); + + let syncA = function() { + element.value = + key.value.toLowerCase() + ":" + value.value.toLowerCase(); + }; + + let syncB = function() { + let split = element.value.toLowerCase().split(":"); + key.value = split[0] || ""; + value.value = split[1] || ""; + + key.value = key.value.trim(); + value.value = value.value.trim(); + }; + + syncB(); + } + }); + })(window); \ No newline at end of file diff --git a/public/scripts/views/forms/key-value.js b/public/scripts/views/forms/key-value.js new file mode 100644 index 0000000000..6a118d483f --- /dev/null +++ b/public/scripts/views/forms/key-value.js @@ -0,0 +1,55 @@ +(function(window) { + //"use strict"; + + window.ls.container.get("view").add({ + selector: "data-forms-key-value", + controller: function(element) { + let key = document.createElement("input"); + let value = document.createElement("input"); + let wrap = document.createElement("div"); + let cell1 = document.createElement("div"); + let cell2 = document.createElement("div"); + + key.type = "text"; + key.className = "margin-bottom-no"; + key.placeholder = "Key"; + value.type = "text"; + value.className = "margin-bottom-no"; + value.placeholder = "Value"; + + wrap.className = "row thin margin-bottom-small"; + cell1.className = "col span-6"; + cell2.className = "col span-6"; + + element.parentNode.insertBefore(wrap, element); + cell1.appendChild(key); + cell2.appendChild(value); + wrap.appendChild(cell1); + wrap.appendChild(cell2); + + key.addEventListener("input", function() { + syncA(); + }); + + value.addEventListener("input", function() { + syncA(); + }); + + element.addEventListener("change", function() { + syncB(); + }); + + let syncA = function() { + element.name = key.value; + element.value = value.value; + }; + + let syncB = function() { + key.value = element.name || ""; + value.value = element.value || ""; + }; + + syncB(); + } + }); + })(window); \ No newline at end of file diff --git a/public/scripts/views/general/cookies.js b/public/scripts/views/general/cookies.js index 40d8908645..52f974d8ce 100644 --- a/public/scripts/views/general/cookies.js +++ b/public/scripts/views/general/cookies.js @@ -10,6 +10,7 @@ text: text, class: "cookie-alert", link: env.HOME + "/policy/cookies", + label: 'Learn More', callback: function() { cookie.set("cookie-alert", "true", 365 * 10); // 10 years } diff --git a/public/scripts/views/general/version.js b/public/scripts/views/general/version.js new file mode 100644 index 0000000000..63f1eb524e --- /dev/null +++ b/public/scripts/views/general/version.js @@ -0,0 +1,47 @@ +(function(window) { + window.ls.container.get("view").add({ + selector: "data-version", + controller: function(alerts, env, cookie) { + let cookieName = "version-update-" + env.VERSION.replace(/\./g, "_"); + + if (!cookie.get(cookieName)) { + var xhr = new XMLHttpRequest(); + + xhr.open('GET', '/console/version', true); + + xhr.onload = function () { + if (this.readyState == 4 && this.status == 200) { + let data = JSON.parse(this.responseText); + let text = 'Appwrite version ' + data.version + ' is avaliable, check the'; + + if(isNewerVersion(env.VERSION, data.version)) { + alerts.add({ + text: text, + class: "success", + link: "https://github.com/appwrite/appwrite/releases", + label: 'release notes', + callback: function() { + cookie.set(cookieName, "true", 365 * 10); // 10 years + } + }, 0); + } + } + }; + + xhr.send(null); + + function isNewerVersion (oldVer, newVer) { + const oldParts = oldVer.split('.') + const newParts = newVer.split('.') + for (var i = 0; i < newParts.length; i++) { + const a = parseInt(newParts[i]) || 0 + const b = parseInt(oldParts[i]) || 0 + if (a > b) return true + if (a < b) return false + } + return false + } + } + } + }); + })(window); \ No newline at end of file diff --git a/public/styles/comps/alerts.less b/public/styles/comps/alerts.less index 8637117c2b..525c8e323b 100644 --- a/public/styles/comps/alerts.less +++ b/public/styles/comps/alerts.less @@ -46,16 +46,31 @@ &.error { color: #ffffff; background: var(--config-color-danger); + + a { + color: #ffffff; + border-bottom: dotted 1px #ffffff; + } } &.success { color: #ffffff; background: var(--config-color-success); + + a { + color: #ffffff; + border-bottom: dotted 1px #ffffff; + } } &.warning { color: #ffffff; background: var(--config-color-success); + + a { + color: #ffffff; + border-bottom: dotted 1px #ffffff; + } } &.open { diff --git a/public/styles/comps/box.less b/public/styles/comps/box.less index ed53d98432..7d7a617b38 100644 --- a/public/styles/comps/box.less +++ b/public/styles/comps/box.less @@ -281,6 +281,14 @@ float: none; } } + + .avatar { + display: block; + + &.inline { + display: inline-block; + } + } } } diff --git a/public/styles/comps/cover.less b/public/styles/comps/cover.less index 9dc18c29b4..697691ec37 100644 --- a/public/styles/comps/cover.less +++ b/public/styles/comps/cover.less @@ -3,6 +3,7 @@ padding: 30px 50px; margin: 0 -50px; position: relative; + border-bottom: solid 1px var(--config-border-fade); h1, h2, diff --git a/public/styles/default-rtl.less b/public/styles/default-rtl.less index 45f641ba96..c6f8e8252b 100644 --- a/public/styles/default-rtl.less +++ b/public/styles/default-rtl.less @@ -64,6 +64,9 @@ // Overwrite button directions .icon-left-open:before { content: '\e814'!important; } /* '' */ .icon-right-open:before { content: '\e813'!important; } /* '' */ +.icon-right-dir:before { content: '\e84e'!important; } /* '' */ +.icon-left-dir:before { content: '\e84d'!important; } /* '' */ + .icon-link-ext:before { -moz-transform: scaleX(-1); /* Gecko */ -o-transform: scaleX(-1); /* Opera */ -webkit-transform: scaleX(-1); /* Webkit */ diff --git a/public/styles/dependencies/prism.less b/public/styles/dependencies/prism.less index 3343544848..53048d3398 100644 --- a/public/styles/dependencies/prism.less +++ b/public/styles/dependencies/prism.less @@ -31,7 +31,7 @@ https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clik padding: 5px 10px; border-radius: 15px; font-size: 10px; - .func-end(10px); + right: 10px; // should be right for both ltr and rtl opacity: 0.95; } diff --git a/public/styles/fontello/config.json b/public/styles/fontello/config.json index 28f93b5d3d..50d80368c3 100644 --- a/public/styles/fontello/config.json +++ b/public/styles/fontello/config.json @@ -677,6 +677,24 @@ "search": [ "dev" ] + }, + { + "uid": "fb1c799ffe5bf8fb7f8bcb647c8fe9e6", + "css": "right-dir", + "code": 59469, + "src": "fontawesome" + }, + { + "uid": "9dc654095085167524602c9acc0c5570", + "css": "left-dir", + "code": 59470, + "src": "fontawesome" + }, + { + "uid": "dc73bee1d58539abe8c96b958bf4c931", + "css": "fire", + "code": 59471, + "src": "elusive" } ] } \ No newline at end of file diff --git a/public/styles/forms.less b/public/styles/forms.less index d7656082fd..25d336753f 100644 --- a/public/styles/forms.less +++ b/public/styles/forms.less @@ -1301,3 +1301,30 @@ ol { } } } + +.dot { + width: 20px; + height: 20px; + background: var(--config-color-fade); + border-radius: 50%; + display: inline-block; + vertical-align: middle; + margin: 0!important; + padding: 0!important; + + &.danger { + background: var(--config-color-danger)!important; + } + + &.success { + background: var(--config-color-success)!important; + } + + &.warning { + background: var(--config-color-warning)!important; + } + + &.info { + background: var(--config-color-info)!important; + } +} \ No newline at end of file diff --git a/public/styles/icons.less b/public/styles/icons.less index 78cf1e8122..3a0cfce67b 100644 --- a/public/styles/icons.less +++ b/public/styles/icons.less @@ -1,7 +1,7 @@ @font-face { font-family: 'fontello'; - src: url('data:application/octet-stream;base64,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') format('woff'), - url('data:application/octet-stream;base64,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6DAAAAAxAADoMQAA6DEAAAAyAADoMgAA6DIAAAAzAADoMwAA6DMAAAA0AADoNAAA6DQAAAA1AADoNQAA6DUAAAA2AADoNgAA6DYAAAA3AADoNwAA6DcAAAA4AADoOAAA6DgAAAA5AADoOQAA6DkAAAA6AADoOgAA6DoAAAA7AADoOwAA6DsAAAA8AADoPAAA6DwAAAA9AADoPQAA6D0AAAA+AADoPgAA6D4AAAA/AADoPwAA6D8AAABAAADoQAAA6EAAAABBAADoQQAA6EEAAABCAADoQgAA6EIAAABDAADoQwAA6EMAAABEAADoRAAA6EQAAABFAADoRQAA6EUAAABGAADoRgAA6EYAAABHAADoRwAA6EcAAABIAADoSAAA6EgAAABJAADoSQAA6EkAAABKAADoSgAA6EoAAABLAADoSwAA6EsAAABMAADoTAAA6EwAAABNAADwjgAA8I4AAABOAADwmwAA8JsAAABPAADwsAAA8LAAAABQAADwxQAA8MUAAABRAADwygAA8MoAAABSAADwywAA8MsAAABTAADwzQAA8M0AAABUAADw3AAA8NwAAABVAADw4QAA8OEAAABWAADxGAAA8RgAAABXAADxHAAA8RwAAABYAADxIQAA8SEAAABZAADxMgAA8TIAAABaAADxNwAA8TcAAABbAADxOAAA8TgAAABcAADxcQAA8XEAAABdAADxegAA8XoAAABeAADxkgAA8ZIAAABfAADxkwAA8ZMAAABgAADxnAAA8ZwAAABhAADxoAAA8aAAAABiAADxrQAA8a0AAABjAADxwAAA8cAAAABkAADxzQAA8c0AAABlAADx3AAA8dwAAABmAADx5QAA8eUAAABnAADx/gAA8f4AAABoAADyMQAA8jEAAABpAADyOgAA8joAAABqAADylgAA8pYAAABrAADyxgAA8sYAAABsAAAAAgAA/7ECygMMABUAHgAlQCIABQEFbwMBAQQBbwAEAgRvAAIAAm8AAABmExcRERcyBgUaKyUUBiMhIiY1ND4DFxYyNzIeAwMUBiIuATYeAQLKRjH+JDFGChgqPi1JykoqQiYcCI98tHoEgqyERTxYWDwwVFY8KAFISCY+VFYBwFh+frCAAnwAAAL//v/OA+oC7gAOAB4AZEuwDVBYQCMAAwQEA2MFAQACAQIAAW0AAQFuAAQCAgRU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OgAvAC8AZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AAAAAAgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtAQIBAwEEAQUBBgEHAQgBCQEKAQsBDAENAQ4BDwEQAREBEgETARQBFQEWARcBGAEZARoBGwEcAR0BHgEfASABIQEiASMBJAElASYBJwEoASkBKgErASwBLQEuAS8BMAExATIBMwE0ATUBNgE3ATgBOQE6ATsBPAE9AT4BPwFAAUEBQgFDAUQBRQFGAUcBSAFJAUoBSwFMAU0BTgFPAVABUQFSAVMBVAFVAVYBVwFYAVkBWgFbAVwBXQFeAV8BYAFhAWIBYwFkAWUBZgFnAWgBaQFqAWsBbAFtAW4ABHVzZXIGZm9sZGVyBGxpc3QFbG9naW4DY29nB3R3aXR0ZXILYXJ0aWNsZS1hbHQGY2FuY2VsBGhvbWUIZG93bi1kaXIIZmFjZWJvb2sIYXN0ZXJpc2sGdXBsb2FkCXN0b3B3YXRjaAZleHBvcnQFaGVhcnQEcGx1cwZ1cC1kaXIEbWVudQlsZWZ0LW9wZW4KcmlnaHQtb3BlbgVpbmJveAZ3cmVuY2gHY29tbWVudA1zdGFja292ZXJmbG93CHF1ZXN0aW9uCm9rLWNpcmNsZWQHd2FybmluZwRtYWlsBGxpbmsHa2V5LWludgV0cmFzaAhkb3dubG9hZAdnbGFzc2VzBnFyY29kZQdzaHVmZmxlA2V5ZQRsb2NrBnNlYXJjaARiZWxsBXVzZXJzCGxvY2F0aW9uCWJyaWVmY2FzZQlpbnN0YWdyYW0FY2xvY2sFcGhvbmUIY2FsZW5kYXIFcHJpbnQEZWRpdARib2xkBml0YWxpYwZyb2NrZXQId2hhdHNhcHAFZG90LTMMaW5mby1jaXJjbGVkCHZpZGVvY2FtC3F1b3RlLXJpZ2h0B3BpY3R1cmUHcGFsZXR0ZQRsYW1wCWJvb2stb3BlbgJvawhjaGF0LWFsdAdhcmNoaXZlBHBsYXkFcGF1c2UJZG93bi1vcGVuB3VwLW9wZW4FbWludXMIZXhjaGFuZ2UHbmV0d29yawdkaXNjb3JkCG1vb24taW52B3N1bi1pbnYOY2FuY2VsLWNpcmNsZWQJbGlnaHRuaW5nA2RldghsaW5rLWV4dA5naXRodWItY2lyY2xlZAZmaWx0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IyAgPLAEI0IjOLErARQrsARDLrArKy2wRCywABUgR7AAI0KyAAEBFRQTLrAxKi2wRSywABUgR7AAI0KyAAEBFRQTLrAxKi2wRiyxAAEUE7AyKi2wRyywNCotsEgssAAWRSMgLiBGiiNhOLErARQrLbBJLLAII0KwSCstsEossgAAQSstsEsssgABQSstsEwssgEAQSstsE0ssgEBQSstsE4ssgAAQistsE8ssgABQistsFAssgEAQistsFEssgEBQistsFIssgAAPistsFMssgABPistsFQssgEAPistsFUssgEBPistsFYssgAAQCstsFcssgABQCstsFgssgEAQCstsFkssgEBQCstsFossgAAQystsFsssgABQystsFwssgEAQystsF0ssgEBQystsF4ssgAAPystsF8ssgABPystsGAssgEAPystsGEssgEBPystsGIssDcrLrErARQrLbBjLLA3K7A7Ky2wZCywNyuwPCstsGUssAAWsDcrsD0rLbBmLLA4Ky6xKwEUKy2wZyywOCuwOystsGgssDgrsDwrLbBpLLA4K7A9Ky2waiywOSsusSsBFCstsGsssDkrsDsrLbBsLLA5K7A8Ky2wbSywOSuwPSstsG4ssDorLrErARQrLbBvLLA6K7A7Ky2wcCywOiuwPCstsHEssDorsD0rLbByLLMJBAIDRVghGyMhWUIrsAhlsAMkUHiwARUwLQBLuADIUlixAQGOWbABuQgACABjcLEABUKyAAEAKrEABUKzCgIBCCqxAAVCsw4AAQgqsQAGQroCwAABAAkqsQAHQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMMAgEMKrgB/4WwBI2xAgBEAAA=') format('truetype'); + src: url('data:application/octet-stream;base64,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') format('woff'), + url('data:application/octet-stream;base64,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AADoMAAA6DAAAAAxAADoMQAA6DEAAAAyAADoMgAA6DIAAAAzAADoMwAA6DMAAAA0AADoNAAA6DQAAAA1AADoNQAA6DUAAAA2AADoNgAA6DYAAAA3AADoNwAA6DcAAAA4AADoOAAA6DgAAAA5AADoOQAA6DkAAAA6AADoOgAA6DoAAAA7AADoOwAA6DsAAAA8AADoPAAA6DwAAAA9AADoPQAA6D0AAAA+AADoPgAA6D4AAAA/AADoPwAA6D8AAABAAADoQAAA6EAAAABBAADoQQAA6EEAAABCAADoQgAA6EIAAABDAADoQwAA6EMAAABEAADoRAAA6EQAAABFAADoRQAA6EUAAABGAADoRgAA6EYAAABHAADoRwAA6EcAAABIAADoSAAA6EgAAABJAADoSQAA6EkAAABKAADoSgAA6EoAAABLAADoSwAA6EsAAABMAADoTAAA6EwAAABNAADoTQAA6E0AAABOAADoTgAA6E4AAABPAADoTwAA6E8AAABQAADwjgAA8I4AAABRAADwmwAA8JsAAABSAADwsAAA8LAAAABTAADwxQAA8MUAAABUAADwygAA8MoAAABVAADwywAA8MsAAABWAADwzQAA8M0AAABXAADw3AAA8NwAAABYAADw4QAA8OEAAABZAADxGAAA8RgAAABaAADxHAAA8RwAAABbAADxIQAA8SEAAABcAADxMgAA8TIAAABdAADxNwAA8TcAAABeAADxOAAA8TgAAABfAADxcQAA8XEAAABgAADxegAA8XoAAABhAADxkgAA8ZIAAABiAADxkwAA8ZMAAABjAADxnAAA8ZwAAABkAADxoAAA8aAAAABlAADxrQAA8a0AAABmAADxwAAA8cAAAABnAADxzQAA8c0AAABoAADx3AAA8dwAAABpAADx5QAA8eUAAABqAADx/gAA8f4AAABrAADyMQAA8jEAAABsAADyOgAA8joAAABtAADylgAA8pYAAABuAADyxgAA8sYAAABvAAIAAP+xAsoDDAAVAB4AJUAiAAUBBW8DAQEEAW8ABAIEbwACAAJvAAAAZhMXEREXMgYFGislFAYjISImNTQ+AxcWMjcyHgMDFAYiLgE2HgECykYx/iQxRgoYKj4tScpKKkImHAiPfLR6BIKshEU8WFg8MFRWPCgBSEgmPlRWAcBYfn6wgAJ8AAAC//7/zgPq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eSBzdmcydHRmIGZyb20gRm9udGVsbG8gcHJvamVjdC5odHRwOi8vZm9udGVsbG8uY29tAEMAbwBwAHkAcgBpAGcAaAB0ACAAKABDACkAIAAyADAAMgAwACAAYgB5ACAAbwByAGkAZwBpAG4AYQBsACAAYQB1AHQAaABvAHIAcwAgAEAAIABmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQBmAG8AbgB0AGUAbABsAG8AUgBlAGcAdQBsAGEAcgBmAG8AbgB0AGUAbABsAG8AZgBvAG4AdABlAGwAbABvAFYAZQByAHMAaQBvAG4AIAAxAC4AMABmAG8AbgB0AGUAbABsAG8ARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABzAHYAZwAyAHQAdABmACAAZgByAG8AbQAgAEYAbwBuAHQAZQBsAGwAbwAgAHAAcgBvAGoAZQBjAHQALgBoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAAAAAAIAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQEOAQ8BEAERARIBEwEUARUBFgEXARgBGQEaARsBHAEdAR4BHwEgASEBIgEjASQBJQEmAScBKAEpASoBKwEsAS0BLgEvATABMQEyATMBNAE1ATYBNwE4ATkBOgE7ATwBPQE+AT8BQAFBAUIBQwFEAUUBRgFHAUgBSQFKAUsBTAFNAU4BTwFQAVEBUgFTAVQBVQFWAVcBWAFZAVoBWwFcAV0BXgFfAWABYQFiAWMBZAFlAWYBZwFoAWkBagFrAWwBbQFuAW8BcAFxAAR1c2VyBmZvbGRlcgRsaXN0BWxvZ2luA2NvZwd0d2l0dGVyC2FydGljbGUtYWx0BmNhbmNlbARob21lCGRvd24tZGlyCGZhY2Vib29rCGFzdGVyaXNrBnVwbG9hZAlzdG9wd2F0Y2gGZXhwb3J0BWhlYXJ0BHBsdXMGdXAtZGlyBG1lbnUJbGVmdC1vcGVuCnJpZ2h0LW9wZW4FaW5ib3gGd3JlbmNoB2NvbW1lbnQNc3RhY2tvdmVyZmxvdwhxdWVzdGlvbgpvay1jaXJjbGVkB3dhcm5pbmcEbWFpbARsaW5rB2tleS1pbnYFdHJhc2gIZG93bmxvYWQHZ2xhc3NlcwZxcmNvZGUHc2h1ZmZsZQNleWUEbG9jawZzZWFyY2gEYmVsbAV1c2Vycwhsb2NhdGlvbglicmllZmNhc2UJaW5zdGFncmFtBWNsb2NrBXBob25lCGNhbGVuZGFyBXByaW50BGVkaXQEYm9sZAZpdGFsaWMGcm9ja2V0CHdoYXRzYXBwBWRvdC0zDGluZm8tY2lyY2xlZAh2aWRlb2NhbQtxdW90ZS1yaWdodAdwaWN0dXJlB3BhbGV0dGUEbGFtcAlib29rLW9wZW4Cb2sIY2hhdC1hbHQHYXJjaGl2ZQRwbGF5BXBhdXNlCWRvd24tb3Blbgd1cC1vcGVuBW1pbnVzCGV4Y2hhbmdlB25ldHdvcmsHZGlzY29yZAhtb29uLWludgdzdW4taW52DmNhbmNlbC1jaXJjbGVkCWxpZ2h0bmluZwNkZXYJcmlnaHQtZGlyCGxlZnQtZGlyBGZpcmUIbGluay1leHQOZ2l0aHViLWNpcmNsZWQGZmlsdGVyBGRvY3MLbGlzdC1idWxsZXQNbGlzdC1udW1iZXJlZAl1bmRlcmxpbmUEc29ydAhsaW5rZWRpbgVzbWlsZQhrZXlib2FyZARjb2RlBnNoaWVsZBJhbmdsZS1jaXJjbGVkLWxlZnQTYW5nbGUtY2lyY2xlZC1yaWdodAliaXRidWNrZXQHd2luZG93cwtkb3QtY2lyY2xlZAp3aGVlbGNoYWlyBGJhbmsGZ29vZ2xlD2J1aWxkaW5nLWZpbGxlZAhkYXRhYmFzZQhsaWZlYnVveQZoZWFkZXIKYmlub2N1bGFycwpjaGFydC1hcmVhCXBpbnRlcmVzdAZtZWRpdW0GZ2l0bGFiCHRlbGVncmFtAAAAAQAB//8ADwAAAAAAAAAAAAAAAAAAAAAAGAAYABgAGAOB/zoDgf86sAAsILAAVVhFWSAgS7gADlFLsAZTWliwNBuwKFlgZiCKVViwAiVhuQgACABjYyNiGyEhsABZsABDI0SyAAEAQ2BCLbABLLAgYGYtsAIsIGQgsMBQsAQmWrIoAQpDRWNFUltYISMhG4pYILBQUFghsEBZGyCwOFBYIbA4WVkgsQEKQ0VjRWFksChQWCGxAQpDRWNFILAwUFghsDBZGyCwwFBYIGYgiophILAKUFhgGyCwIFBYIbAKYBsgsDZQWCGwNmAbYFlZWRuwAStZ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') format('truetype'); } /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ @@ -9,7 +9,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?46945125#fontello') format('svg'); + src: url('../font/fontello.svg?23079814#fontello') format('svg'); } } */ @@ -18,7 +18,7 @@ font-family: "fontello"; font-style: normal; font-weight: normal; - speak: none; + speak: never; display: inline-block; text-decoration: inherit; @@ -30,7 +30,7 @@ /* For safety - reset parent styles, that can break glyph codes*/ font-variant: normal; text-transform: none; - + /* fix buttons height, for twitter bootstrap */ line-height: 1em; @@ -121,6 +121,9 @@ .icon-cancel-circled:before { content: '\e84a'; } /* '' */ .icon-lightning:before { content: '\e84b'; } /* '' */ .icon-dev:before { content: '\e84c'; } /* '' */ +.icon-right-dir:before { content: '\e84d'; } /* '' */ +.icon-left-dir:before { content: '\e84e'; } /* '' */ +.icon-fire:before { content: '\e84f'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */ .icon-github-circled:before { content: '\f09b'; } /* '' */ .icon-filter:before { content: '\f0b0'; } /* '' */ diff --git a/public/styles/scopes/console.less b/public/styles/scopes/console.less index cf7bebdb57..b19e3eb2ee 100644 --- a/public/styles/scopes/console.less +++ b/public/styles/scopes/console.less @@ -413,6 +413,14 @@ position: relative; z-index: 1; + .chart { + width: 80%; + + @media @phones, @tablets { + width: 100%; + } + } + hr { margin: 20px -20px; height: 2px; @@ -506,49 +514,92 @@ padding: 0; border: none; } + } - .chart-metric { - width: 19%; + .chart-metric { + width: 19%; - @media @tablets, @phones { - width: 100%; - } + @media @tablets, @phones { + width: 100%; } + } - .chart { - width: 80%; - position: relative; - height: 0; - padding-top: 20px; - padding-bottom: 26%; - .func-margin-end(-2px); - overflow: hidden; - background-color: var(--config-color-background-fade); - background-image: - linear-gradient(transparent 1px, transparent 1px), - linear-gradient(90deg, transparent 1px, transparent 1px), - linear-gradient(var(--config-border-color) 1px, transparent 1px), - linear-gradient(90deg, var(--config-border-color) 1px, transparent 1px); - background-size: 100px 100px, 100px 100px, 20px 20px, 20px 20px; - background-position: -2px -2px, -2px -2px, -1px -1px, -1px -1px; - background-repeat: round; - border: solid 1px var(--config-border-color); - border-right: solid 1px transparent; - border-bottom: solid 1px transparent; + .chart { + width: 100%; + position: relative; + height: 0; + padding-top: 20px; + padding-bottom: 26%; + .func-margin-end(-2px); + overflow: hidden; + background-color: var(--config-color-background-fade); + background-image: + linear-gradient(transparent 1px, transparent 1px), + linear-gradient(90deg, transparent 1px, transparent 1px), + linear-gradient(var(--config-border-color) 1px, transparent 1px), + linear-gradient(90deg, var(--config-border-color) 1px, transparent 1px); + background-size: 100px 100px, 100px 100px, 20px 20px, 20px 20px; + background-position: -2px -2px, -2px -2px, -1px -1px, -1px -1px; + background-repeat: round; + border: solid 1px var(--config-border-color); + border-right: solid 1px transparent; + border-bottom: solid 1px transparent; + @media @tablets, @phones { + width: 100%; + padding-bottom: 32%; + float: none; + margin-bottom: 20px; + } + canvas { + position: absolute; + bottom: 0; + display: block; + height: 100%; + width: 100%; + } + } - @media @tablets, @phones { - width: 100%; - padding-bottom: 32%; - float: none; - margin-bottom: 20px; + .chart-notes { + font-size: 12px; + + li { + line-height: 20px; + display: inline-block; + .func-margin-end(15px); + + &::before { + display: inline-block; + content: ''; + width: 14px; + height: 14px; + background: var(--config-color-normal); + border-radius: 50%; + .func-margin-end(8px); + vertical-align: middle; } - canvas { - position: absolute; - bottom: 0; - display: block; - height: 100%; - width: 100%; + &:nth-child(1) { + color: #29b5d9; + + &::before { + background: #29b5d9; + } + } + + &:nth-child(2) { + color: #4eb55b; + + &::before { + background: #4eb55b; + } + } + + &:nth-child(3) { + color: #ec9323; + + &::before { + background: #ec9323; + } } } } @@ -928,7 +979,6 @@ } .search { - opacity: 1!important; @media @phones, @tablets { @@ -936,4 +986,4 @@ margin-top: 20px; } } -} +} \ No newline at end of file diff --git a/public/styles/themes.less b/public/styles/themes.less index 0194b2c440..1d46ee7fe7 100644 --- a/public/styles/themes.less +++ b/public/styles/themes.less @@ -30,6 +30,7 @@ --config-color-warning: #ffed4d; --config-color-info: #386fd2; --config-border-color: #f5f5f5; + --config-border-fade: #e0e3e4; --config-border-radius: 10px; --config-prism-background: #373738; --config-prism-numbers: #39393c; @@ -105,6 +106,7 @@ --config-color-warning: #e0d56d; --config-color-info: #386fd2; --config-border-color: #262D50; + --config-border-fade: #19203a; --config-prism-background: #1F253F; --config-prism-numbers: #1F253F; --config-note-background: #171e33; diff --git a/src/Appwrite/Database/Adapter.php b/src/Appwrite/Database/Adapter.php index 3526256b22..4dc7152571 100644 --- a/src/Appwrite/Database/Adapter.php +++ b/src/Appwrite/Database/Adapter.php @@ -68,7 +68,7 @@ abstract class Adapter * * @return array */ - abstract public function createDocument(array $data, array $unique); + abstract public function createDocument(array $data = [], array $unique = []); /** * Update Document. @@ -77,7 +77,7 @@ abstract class Adapter * * @return array */ - abstract public function updateDocument(array $data); + abstract public function updateDocument(array $data = []); /** * Delete Node. @@ -136,7 +136,7 @@ abstract class Adapter /** * Last Modified. * - * Return unix timestamp of last time a node queried in corrent session has been changed + * Return Unix timestamp of last time a node queried in corrent session has been changed * * @return int */ diff --git a/src/Appwrite/Database/Adapter/MySQL.php b/src/Appwrite/Database/Adapter/MySQL.php index 3d966d165b..dffa39b7bf 100644 --- a/src/Appwrite/Database/Adapter/MySQL.php +++ b/src/Appwrite/Database/Adapter/MySQL.php @@ -28,20 +28,6 @@ class MySQL extends Adapter */ protected $register; - /** - * Saved nodes. - * - * @var array - */ - protected $nodes = []; - - /** - * Count documents get usage. - * - * @var int - */ - protected $count = 0; - /** * Last modified. * @@ -79,8 +65,6 @@ class MySQL extends Adapter */ public function getDocument($id) { - ++$this->count; - // Get fields abstraction $st = $this->getPDO()->prepare('SELECT * FROM `'.$this->getNamespace().'.database.documents` a WHERE a.uid = :uid AND a.status = 0 @@ -673,7 +657,6 @@ class MySQL extends Adapter ->setDebug('joins', \substr_count($query, 'JOIN')) ->setDebug('count', \count($results['data'])) ->setDebug('sum', (int) $count['sum']) - ->setDebug('documents', $this->count) ; return $results['data']; @@ -694,6 +677,11 @@ class MySQL extends Adapter $where = []; $join = []; + $options = array_merge([ + 'attribute' => '', + 'filters' => [], + ], $options); + // Filters foreach ($options['filters'] as $i => $filter) { $filter = $this->parseFilter($filter); @@ -736,9 +724,10 @@ class MySQL extends Adapter $where = \implode("\n", $where); $join = \implode("\n", $join); + $attribute = $this->getPDO()->quote($options['attribute'], PDO::PARAM_STR); $func = 'JOIN `'.$this->getNamespace().".database.properties` b_func ON a.uid IS NOT NULL AND a.uid = b_func.documentUid - AND (b_func.key = 'sizeOriginal')"; + AND (b_func.key = {$attribute})"; $roles = []; foreach (Authorization::getRoles() as $role) { @@ -749,8 +738,8 @@ class MySQL extends Adapter $roles = ['1=1']; } - $query = 'SELECT SUM(b_func.value) as result - FROM `'.$this->getNamespace().".database.documents` a {$where}{$join}{$func} + $query = "SELECT SUM(b_func.value) as result + FROM `".$this->getNamespace().".database.documents` a {$where}{$join}{$func} WHERE status = 0 AND (".\implode('||', $roles).')'; @@ -769,7 +758,7 @@ class MySQL extends Adapter ->setDebug('joins', \substr_count($query, 'JOIN')) ; - return (int) (isset($result['result'])) ? $result['result'] : 0; + return (isset($result['result'])) ? (int)$result['result'] : 0; } /** @@ -794,7 +783,7 @@ class MySQL extends Adapter /** * Last Modified. * - * Return unix timestamp of last time a node queried in corrent session has been changed + * Return Unix timestamp of last time a node queried in corrent session has been changed * * @return int */ @@ -926,7 +915,7 @@ class MySQL extends Adapter * * @throws Exception */ - protected function getPDO():PDO + protected function getPDO() { return $this->register->get('db'); } diff --git a/src/Appwrite/Database/Adapter/Redis.php b/src/Appwrite/Database/Adapter/Redis.php index cb35378a9c..7fe6ee0689 100644 --- a/src/Appwrite/Database/Adapter/Redis.php +++ b/src/Appwrite/Database/Adapter/Redis.php @@ -237,7 +237,7 @@ class Redis extends Adapter /** * Last Modified. * - * Return unix timestamp of last time a node queried in current session has been changed + * Return Unix timestamp of last time a node queried in current session has been changed * * @return int */ diff --git a/src/Appwrite/Database/Database.php b/src/Appwrite/Database/Database.php index f461f6acdf..4c62307b07 100644 --- a/src/Appwrite/Database/Database.php +++ b/src/Appwrite/Database/Database.php @@ -36,6 +36,27 @@ class Database // Storage const SYSTEM_COLLECTION_FILES = 'files'; + // Functions + const SYSTEM_COLLECTION_FUNCTIONS = 'functions'; + const SYSTEM_COLLECTION_TAGS = 'tags'; + const SYSTEM_COLLECTION_EXECUTIONS = 'executions'; + + // Var Types + const SYSTEM_VAR_TYPE_TEXT = 'text'; + const SYSTEM_VAR_TYPE_NUMERIC = 'numeric'; + const SYSTEM_VAR_TYPE_BOOLEAN = 'boolean'; + const SYSTEM_VAR_TYPE_DOCUMENT = 'document'; + const SYSTEM_VAR_TYPE_WILDCARD = 'wildcard'; + const SYSTEM_VAR_TYPE_EMAIL = 'email'; + const SYSTEM_VAR_TYPE_IP = 'ip'; + const SYSTEM_VAR_TYPE_URL = 'url'; + const SYSTEM_VAR_TYPE_KEY = 'key'; + + /** + * @var array + */ + static protected $filters = []; + /** * @var array */ @@ -119,7 +140,7 @@ class Database /** * @param array $options * - * @return Document[]|Document + * @return Document[] */ public function getCollection(array $options) { @@ -171,7 +192,7 @@ class Database * * @return Document */ - public function getDocument($id, $mock = true) + public function getDocument($id, $mock = true, $decode = true) { if (\is_null($id)) { return new Document([]); @@ -184,6 +205,8 @@ class Database return new Document([]); } + $document = ($decode) ? $this->decode($document) : $document; + return $document; } @@ -206,12 +229,18 @@ class Database } $validator = new Structure($this); + + $document = $this->encode($document); if (!$validator->isValid($document)) { throw new StructureException($validator->getDescription()); // var_dump($validator->getDescription()); return false; } + + $document = new Document($this->adapter->createDocument($document->getArrayCopy(), $unique)); + + $document = $this->decode($document); - return new Document($this->adapter->createDocument($data, $unique)); + return $document; } /** @@ -245,13 +274,19 @@ class Database throw new AuthorizationException($validator->getDescription()); // var_dump($validator->getDescription()); return false; } + $new = $this->encode($new); + $validator = new Structure($this); if (!$validator->isValid($new)) { // Make sure updated structure still apply collection rules (if any) throw new StructureException($validator->getDescription()); // var_dump($validator->getDescription()); return false; } - return new Document($this->adapter->updateDocument($data)); + $new = new Document($this->adapter->updateDocument($new->getArrayCopy())); + + $new = $this->decode($new); + + return $new; } /** @@ -389,10 +424,111 @@ class Database return $this->mocks; } + /** + * Add Attribute Filter + * + * @param string $name + * @param callable $encode + * @param callable $decode + * + * return $this + */ + static public function addFilter(string $name, callable $encode, callable $decode) + { + self::$filters[$name] = [ + 'encode' => $encode, + 'decode' => $decode, + ]; + } + + public function encode(Document $document):Document + { + $collection = $this->getDocument($document->getCollection(), true , false); + $rules = $collection->getAttribute('rules', []); + + foreach ($rules as $key => $rule) { + $key = $rule->getAttribute('key', null); + $filters = $rule->getAttribute('filter', null); + $value = $document->getAttribute($key, null); + + if(($value !== null) && is_array($filters)) { + foreach ($filters as $filter) { + $value = $this->encodeAttribute($filter, $value); + $document->setAttribute($key, $value); + } + } + } + + return $document; + } + + public function decode(Document $document):Document + { + $collection = $this->getDocument($document->getCollection(), true , false); + $rules = $collection->getAttribute('rules', []); + + foreach ($rules as $key => $rule) { + $key = $rule->getAttribute('key', null); + $filters = $rule->getAttribute('filter', null); + $value = $document->getAttribute($key, null); + + if(($value !== null) && is_array($filters)) { + foreach (array_reverse($filters) as $filter) { + $value = $this->decodeAttribute($filter, $value); + $document->setAttribute($key, $value); + } + } + } + + return $document; + } + + /** + * Encode Attribute + * + * @param string $name + * @param mixed $value + */ + static protected function encodeAttribute(string $name, $value) + { + if(!isset(self::$filters[$name])) { + throw new Exception('Filter not found'); + } + + try { + $value = self::$filters[$name]['encode']($value); + } catch (\Throwable $th) { + $value = null; + } + + return $value; + } + + /** + * Decode Attribute + * + * @param string $name + * @param mixed $value + */ + static protected function decodeAttribute(string $name, $value) + { + if(!isset(self::$filters[$name])) { + throw new Exception('Filter not found'); + } + + try { + $value = self::$filters[$name]['decode']($value); + } catch (\Throwable $th) { + $value = null; + } + + return $value; + } + /** * Get Last Modified. * - * Return unix timestamp of last time a node queried in current session has been changed + * Return Unix timestamp of last time a node queried in current session has been changed * * @return int */ diff --git a/src/Appwrite/Database/Document.php b/src/Appwrite/Database/Document.php index bd085c2fa5..2c0ad4131d 100644 --- a/src/Appwrite/Database/Document.php +++ b/src/Appwrite/Database/Document.php @@ -25,11 +25,11 @@ class Document extends ArrayObject { foreach ($input as $key => &$value) { if (\is_array($value)) { - if (isset($value['$id']) || isset($value['$collection'])) { + if ((isset($value['$id']) || isset($value['$collection'])) && (!$value instanceof self)) { $input[$key] = new self($value); } else { foreach ($value as $childKey => $child) { - if (isset($child['$id']) || isset($child['$collection'])) { + if ((isset($child['$id']) || isset($child['$collection'])) && (!$child instanceof self)) { $value[$childKey] = new self($child); } } @@ -193,6 +193,18 @@ class Document extends ArrayObject return empty($this->getId()); } + /** + * Checks if a document key is set. + * + * @param $key + * + * @return bool + */ + public function isSet($key) + { + return isset($this[$key]); + } + /** * Get Array Copy. * diff --git a/src/Appwrite/Database/Validator/Authorization.php b/src/Appwrite/Database/Validator/Authorization.php index 444f651be0..2938a3796b 100644 --- a/src/Appwrite/Database/Validator/Authorization.php +++ b/src/Appwrite/Database/Validator/Authorization.php @@ -10,7 +10,7 @@ class Authorization extends Validator /** * @var array */ - protected static $roles = ['*']; + static $roles = ['*']; /** * @var Document diff --git a/src/Appwrite/Database/Validator/UID.php b/src/Appwrite/Database/Validator/UID.php index 67a9b6c2fa..c6a3c10910 100644 --- a/src/Appwrite/Database/Validator/UID.php +++ b/src/Appwrite/Database/Validator/UID.php @@ -29,8 +29,8 @@ class UID extends Validator */ public function isValid($value) { - if (\is_numeric($value)) { - //return false; + if(mb_strlen($value) > 32) { + return false; } return true; diff --git a/src/Appwrite/Docker/Compose.php b/src/Appwrite/Docker/Compose.php new file mode 100644 index 0000000000..d186f94790 --- /dev/null +++ b/src/Appwrite/Docker/Compose.php @@ -0,0 +1,73 @@ +compose = yaml_parse($data); + + $this->compose['services'] = (isset($this->compose['services']) && is_array($this->compose['services'])) + ? $this->compose['services'] : []; + + foreach ($this->compose['services'] as $key => &$service) { + $service = new Service($service); + } + } + + /** + * @return array + */ + public function getVersion(): string + { + return (isset($this->compose['version'])) ? $this->compose['version'] : ''; + } + + /** + * @return Service[] + */ + public function getServices(): array + { + return $this->compose['services']; + } + + /** + * @return Service + */ + public function getService(string $name): Service + { + if(!isset($this->compose['services'][$name])) { + throw new Exception('Service not found'); + } + + return $this->compose['services'][$name]; + } + + /** + * @return array + */ + public function getNetworks(): array + { + return (isset($this->compose['networks'])) ? array_keys($this->compose['networks']) : []; + } + + /** + * @return array + */ + public function getVolumes(): array + { + return (isset($this->compose['volumes'])) ? array_keys($this->compose['volumes']) : []; + } +} diff --git a/src/Appwrite/Docker/Compose/Service.php b/src/Appwrite/Docker/Compose/Service.php new file mode 100644 index 0000000000..dcb4ce7375 --- /dev/null +++ b/src/Appwrite/Docker/Compose/Service.php @@ -0,0 +1,75 @@ +service = $service; + + $ports = (isset($this->service['ports']) && is_array($this->service['ports'])) ? $this->service['ports'] : []; + $this->service['ports'] = []; + + array_walk($ports, function(&$value, &$key) { + $split = explode(':', $value); + $this->service['ports'][ + (isset($split[0])) ? $split[0] : '' + ] = (isset($split[1])) ? $split[1] : ''; + }); + + $this->service['environment'] = (isset($this->service['environment']) && is_array($this->service['environment'])) ? $this->service['environment'] : []; + $this->service['environment'] = new Env(implode("\n", $this->service['environment'])); + } + + /** + * @return string + */ + public function getContainerName(): string + { + return (isset($this->service['container_name'])) ? $this->service['container_name'] : ''; + } + + /** + * @return string + */ + public function getImage(): string + { + return (isset($this->service['image'])) ? $this->service['image'] : ''; + } + + /** + * @return string + */ + public function getImageVersion(): string + { + $image = $this->getImage(); + return substr($image, strpos($image, ':')+1); + } + + /** + * @return Env + */ + public function getEnvironment(): Env + { + return $this->service['environment']; + } + + /** + * @return array + */ + public function getPorts(): array + { + return $this->service['ports']; + } +} diff --git a/src/Appwrite/Docker/Env.php b/src/Appwrite/Docker/Env.php new file mode 100644 index 0000000000..c1f25d5e4c --- /dev/null +++ b/src/Appwrite/Docker/Env.php @@ -0,0 +1,79 @@ +vars[$key] = $value; + } + } + } + + /** + * @param string $key + * @param mixed $value + * + * @return $this + */ + public function setVar(string $key, $value): self + { + $this->vars[$key] = $value; + + return $this; + } + + /** + * @param string $key + * + * @return mixed|null + */ + public function getVar(string $key): string + { + return (isset($this->vars[$key])) ? $this->vars[$key] : ''; + } + + /** + * Get All Vars + * + * @return array + */ + public function list(): array + { + return $this->vars; + } + + /** + * @return string + */ + public function export(): string + { + $output = ''; + + foreach ($this->vars as $key => $value) { + $output .= $key.'='.$value."\n"; + } + + return $output; + } +} diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index 45b4405b9e..f89b2e594f 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -27,7 +27,7 @@ class Event * @param string $queue * @param string $class */ - public function __construct($queue, $class) + public function __construct(string $queue, string $class) { $this->queue = $queue; $this->class = $class; @@ -39,7 +39,7 @@ class Event * * @return $this */ - public function setParam($key, $value) + public function setParam(string $key, $value): self { $this->params[$key] = $value; @@ -51,7 +51,7 @@ class Event * * @return mixed|null */ - public function getParam($key) + public function getParam(string $key) { return (isset($this->params[$key])) ? $this->params[$key] : null; } @@ -59,8 +59,17 @@ class Event /** * Execute Event. */ - public function trigger() + public function trigger(): void { Resque::enqueue($this->queue, $this->class, $this->params); + + $this->reset(); + } + + public function reset(): self + { + $this->params = []; + + return $this; } } diff --git a/src/Appwrite/Extend/PDO.php b/src/Appwrite/Extend/PDO.php new file mode 100644 index 0000000000..fce9165b4e --- /dev/null +++ b/src/Appwrite/Extend/PDO.php @@ -0,0 +1,72 @@ +dsn = $dsn; + $this->username = $username; + $this->passwd = $passwd; + $this->options = $options; + + $this->pdo = new PDONative($dsn, $username, $passwd, $options); + } + + public function setAttribute($attribute, $value) + { + return $this->pdo->setAttribute($attribute, $value); + } + + public function prepare($statement, $driver_options = NULL) + { + return new PDOStatement($this, $this->pdo->prepare($statement, [])); + } + + public function quote($string, $parameter_type = PDONative::PARAM_STR) + { + return $this->pdo->quote($string, $parameter_type); + } + + public function reconnect() + { + $this->pdo = new PDONative($this->dsn, $this->username, $this->passwd, $this->options); + + // Connection settings + $this->pdo->setAttribute(PDONative::ATTR_DEFAULT_FETCH_MODE, PDONative::FETCH_ASSOC); // Return arrays + $this->pdo->setAttribute(PDONative::ATTR_ERRMODE, PDONative::ERRMODE_EXCEPTION); // Handle all errors with exceptions + + return $this->pdo; + } +} \ No newline at end of file diff --git a/src/Appwrite/Extend/PDOStatement.php b/src/Appwrite/Extend/PDOStatement.php new file mode 100644 index 0000000000..a752f9f27f --- /dev/null +++ b/src/Appwrite/Extend/PDOStatement.php @@ -0,0 +1,107 @@ +pdo = &$pdo; + $this->PDOStatement = $PDOStatement; + } + + public function bindValue($parameter, $value, $data_type = PDONative::PARAM_STR) + { + $this->values[$parameter] = ['value' => $value, 'data_type' => $data_type]; + + $result = $this->PDOStatement->bindValue($parameter, $value, $data_type); + + return $result; + } + + public function bindParam($parameter, &$variable, $data_type = PDONative::PARAM_STR, $length = null, $driver_options = null) + { + $this->params[$parameter] = ['value' => &$variable, 'data_type' => $data_type, 'length' => $length, 'driver_options' => $driver_options]; + + $result = $this->PDOStatement->bindParam($parameter, $variable, $data_type, $length, $driver_options); + + return $result; + } + + public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null) + { + $this->columns[$column] = ['param' => &$param, 'type' => $type, 'maxlen' => $maxlen, 'driverdata' => $driverdata]; + + $result = $this->PDOStatement->bindColumn($column, $param, $type, $maxlen, $driverdata); + + return $result; + } + + public function execute($input_parameters = null) + { + try { + $result = $this->PDOStatement->execute($input_parameters); + } catch (\Throwable $th) { + $this->pdo = $this->pdo->reconnect(); + $this->PDOStatement = $this->pdo->prepare($this->PDOStatement->queryString, []); + + foreach($this->values as $key => $set) { + $this->PDOStatement->bindValue($key, $set['value'], $set['data_type']); + } + + foreach($this->params as $key => $set) { + $this->PDOStatement->bindParam($key, $set['variable'], $set['data_type'], $set['length'], $set['driver_options']); + } + + foreach($this->columns as $key => $set) { + $this->PDOStatement->bindColumn($key, $set['param'], $set['type'], $set['maxlen'], $set['driverdata']); + } + + $result = $this->PDOStatement->execute($input_parameters); + } + + return $result; + } + + public function fetch($fetch_style = PDONative::FETCH_ASSOC, $cursor_orientation = PDONative::FETCH_ORI_NEXT, $cursor_offset = 0) + { + $result = $this->PDOStatement->fetch($fetch_style, $cursor_orientation, $cursor_offset); + + return $result; + } + + public function fetchAll($how = null, $class_name = null, $ctor_args = null) + { + $result = $this->PDOStatement->fetchAll(); + + return $result; + } +} \ No newline at end of file diff --git a/src/Appwrite/Preloader/Preloader.php b/src/Appwrite/Preloader/Preloader.php new file mode 100644 index 0000000000..a03c0cb28e --- /dev/null +++ b/src/Appwrite/Preloader/Preloader.php @@ -0,0 +1,139 @@ +paths = $paths; + + $classMap = require __DIR__.'/../../../vendor/composer/autoload_classmap.php'; + + $this->paths = \array_merge( + $this->paths, + \array_values($classMap) + ); + } + + public function paths(string ...$paths): self + { + $this->paths = \array_merge( + $this->paths, + $paths + ); + + return $this; + } + + public function ignore(string ...$names): self + { + foreach($names as $name) { + if(is_readable($name)) { + $this->ignores[] = $name; + } + else { + echo "[Preloader] Failed to ignore path `{$name}`".PHP_EOL; + } + } + + return $this; + } + + public function load(): void + { + $this->included = get_included_files(); + + foreach ($this->paths as $path) { + $this->loadPath(\rtrim($path, '/')); + } + + $already = count($this->included); + + echo "[Preloader] Preloaded {$already} files.".PHP_EOL; + } + + private function loadPath(string $path): void + { + if (\is_dir($path)) { + $this->loadDir($path); + + return; + } + + $this->loadFile($path); + } + + private function loadDir(string $path): void + { + $handle = \opendir($path); + + while ($file = \readdir($handle)) { + if (\in_array($file, ['.', '..'])) { + continue; + } + + $this->loadPath("{$path}/{$file}"); + } + + \closedir($handle); + } + + private function loadFile(string $path): void + { + if ($this->shouldIgnore($path)) { + return; + } + + if(in_array(realpath($path), $this->included)) { + // echo "[Preloader] Skiped `{$path}`".PHP_EOL; + return; + } + + // echo "[Preloader] Preloaded `{$path}`".PHP_EOL; + + try { + // opcache_compile_file($path); + require $path; + } catch (\Throwable $th) { + echo "[Preloader] Failed to load `{$path}`: ".$th->getMessage().PHP_EOL; + return; + } + + $this->included = array_merge(get_included_files(), [realpath($path)]); + } + + private function shouldIgnore(?string $path): bool + { + if($path === null) { + return true; + } + + if(!\in_array(\pathinfo($path, PATHINFO_EXTENSION), ['php'])) { + return true; + } + + foreach ($this->ignores as $ignore) { + if (\strpos($path, $ignore) === 0) { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/src/Appwrite/Resize/Resize.php b/src/Appwrite/Resize/Resize.php index 525232e717..28d345d039 100644 --- a/src/Appwrite/Resize/Resize.php +++ b/src/Appwrite/Resize/Resize.php @@ -131,36 +131,38 @@ class Resize break; case 'webp': - //$this->image->setImageFormat('webp'); + try { + $this->image->setImageFormat('webp'); + } catch (\ImagickException $th) { + $signature = $this->image->getImageSignature(); + $temp = '/tmp/temp-'.$signature.'.'.\strtolower($this->image->getImageFormat()); + $output = '/tmp/output-'.$signature.'.webp'; - $signature = $this->image->getImageSignature(); - $temp = '/tmp/temp-'.$signature.'.'.\strtolower($this->image->getImageFormat()); - $output = '/tmp/output-'.$signature.'.webp'; + // save temp + $this->image->writeImages($temp, true); - // save temp - $this->image->writeImages($temp, true); + // convert temp + \exec("cwebp -quiet -metadata none -q $quality $temp -o $output"); - // convert temp - \exec("cwebp -quiet -metadata none -q $quality $temp -o $output"); + $data = \file_get_contents($output); - $data = \file_get_contents($output); + //load webp + if (empty($path)) { + return $data; + } else { + \file_put_contents($path, $data, LOCK_EX); + } - //load webp - if (empty($path)) { - return $data; - } else { - \file_put_contents($path, $data, LOCK_EX); + $this->image->clear(); + $this->image->destroy(); + + //delete webp + \unlink($output); + \unlink($temp); + + return; } - $this->image->clear(); - $this->image->destroy(); - - //delete webp - \unlink($output); - \unlink($temp); - - return; - break; case 'png': @@ -215,4 +217,4 @@ class Resize return $newHeight; } -} +} \ No newline at end of file diff --git a/src/Appwrite/Storage/Device/S3.php b/src/Appwrite/Storage/Device/S3.php index 5270c14d94..2b46c8d447 100644 --- a/src/Appwrite/Storage/Device/S3.php +++ b/src/Appwrite/Storage/Device/S3.php @@ -9,7 +9,7 @@ class S3 extends Device /** * @return string */ - public function getName() + public function getName():string { return 'S3 Storage'; } @@ -17,7 +17,7 @@ class S3 extends Device /** * @return string */ - public function getDescription() + public function getDescription():string { return 'S3 Bucket Storage drive for AWS or on premise solution'; } @@ -25,7 +25,7 @@ class S3 extends Device /** * @return string */ - public function getRoot() + public function getRoot():string { return ''; } @@ -35,14 +35,162 @@ class S3 extends Device * * @return string */ - public function getPath($filename) + public function getPath($filename):string { - $path = ''; - - for ($i = 0; $i < 4; ++$i) { - $path = ($i < \strlen($filename)) ? $path.DIRECTORY_SEPARATOR.$filename[$i] : $path.DIRECTORY_SEPARATOR.'x'; - } - - return $this->getRoot().$path.DIRECTORY_SEPARATOR.$filename; + return ''; } -} + + + /** + * Upload. + * + * Upload a file to desired destination in the selected disk. + * + * @param string $target + * @param string $filename + * + * @throws \Exception + * + * @return string|bool saved destination on success or false on failures + */ + public function upload($source, $path):bool + { + return false; + } + + /** + * Read file by given path. + * + * @param string $path + * + * @return string + */ + public function read(string $path):string + { + return ''; + } + + /** + * Write file by given path. + * + * @param string $path + * @param string $data + * + * @return bool + */ + public function write(string $path, string $data):bool + { + return false; + } + + /** + * Move file from given source to given path, Return true on success and false on failure. + * + * @see http://php.net/manual/en/function.filesize.php + * + * @param string $source + * @param string $target + * + * @return bool + */ + public function move(string $source, string $target):bool + { + return false; + } + + /** + * Delete file in given path, Return true on success and false on failure. + * + * @see http://php.net/manual/en/function.filesize.php + * + * @param string $path + * + * @return bool + */ + public function delete(string $path, bool $recursive = false):bool + { + return false; + } + + /** + * Returns given file path its size. + * + * @see http://php.net/manual/en/function.filesize.php + * + * @param $path + * + * @return int + */ + public function getFileSize(string $path):int + { + return 0; + } + + /** + * Returns given file path its mime type. + * + * @see http://php.net/manual/en/function.mime-content-type.php + * + * @param $path + * + * @return string + */ + public function getFileMimeType(string $path):string + { + return ''; + } + + /** + * Returns given file path its MD5 hash value. + * + * @see http://php.net/manual/en/function.md5-file.php + * + * @param $path + * + * @return string + */ + public function getFileHash(string $path):string + { + return ''; + } + + /** + * Get directory size in bytes. + * + * Return -1 on error + * + * Based on http://www.jonasjohn.de/snippets/php/dir-size.htm + * + * @param $path + * + * @return int + */ + public function getDirectorySize(string $path):int + { + return 0; + } + + /** + * Get Partition Free Space. + * + * disk_free_space — Returns available space on filesystem or disk partition + * + * @return float + */ + public function getPartitionFreeSpace():float + { + return 0.0; + } + + /** + * Get Partition Total Space. + * + * disk_total_space — Returns the total size of a filesystem or disk partition + * + * @return float + */ + public function getPartitionTotalSpace():float + { + return 0.0; + } +} \ No newline at end of file diff --git a/src/Appwrite/Storage/Storage.php b/src/Appwrite/Storage/Storage.php index 40c2889738..30718dc0ab 100644 --- a/src/Appwrite/Storage/Storage.php +++ b/src/Appwrite/Storage/Storage.php @@ -16,7 +16,7 @@ class Storage public static $devices = array(); /** - * Add Device. + * Set Device. * * Add device by name * @@ -25,12 +25,8 @@ class Storage * * @throws Exception */ - public static function addDevice($name, Device $device) + public static function setDevice($name, Device $device) { - if (\array_key_exists($name, self::$devices)) { - throw new Exception('The device "'.$name.'" is already listed'); - } - self::$devices[$name] = $device; } @@ -71,24 +67,45 @@ class Storage /** * Human readable data size format from bytes input. * - * As published on https://gist.github.com/liunian/9338301 (first comment) + * Based on: https://stackoverflow.com/a/38659168/2299554 * * @param int $bytes * @param int $decimals + * @param string $system * * @return string */ - public static function human($bytes, $decimals = 2) + public static function human(int $bytes, $decimals = 2, $system = 'metric') { - $units = array('B','kB','MB','GB','TB','PB','EB','ZB','YB'); - $step = 1024; - $i = 0; + $mod = ($system === 'binary') ? 1024 : 1000; - while (($bytes / $step) > 0.9) { - $bytes = $bytes / $step; - ++$i; - } + $units = array( + 'binary' => array( + 'B', + 'KiB', + 'MiB', + 'GiB', + 'TiB', + 'PiB', + 'EiB', + 'ZiB', + 'YiB', + ), + 'metric' => array( + 'B', + 'kB', + 'MB', + 'GB', + 'TB', + 'PB', + 'EB', + 'ZB', + 'YB', + ), + ); - return \round($bytes, $decimals).$units[$i]; + $factor = floor((strlen($bytes) - 1) / 3); + + return sprintf("%.{$decimals}f%s", $bytes / pow($mod, $factor), $units[$system][$factor]); } -} +} \ No newline at end of file diff --git a/src/Appwrite/Storage/Validator/FileSize.php b/src/Appwrite/Storage/Validator/FileSize.php index b4aab0c57c..1f66e3f021 100644 --- a/src/Appwrite/Storage/Validator/FileSize.php +++ b/src/Appwrite/Storage/Validator/FileSize.php @@ -12,6 +12,8 @@ class FileSize extends Validator protected $max; /** + * Max size in bytes + * * @param int $max */ public function __construct($max) diff --git a/src/Appwrite/Storage/Validator/FileType.php b/src/Appwrite/Storage/Validator/FileType.php index 164e59924b..7b13a98396 100644 --- a/src/Appwrite/Storage/Validator/FileType.php +++ b/src/Appwrite/Storage/Validator/FileType.php @@ -13,6 +13,7 @@ class FileType extends Validator const FILE_TYPE_JPEG = 'jpeg'; const FILE_TYPE_GIF = 'gif'; const FILE_TYPE_PNG = 'png'; + const FILE_TYPE_GZIP = 'gz'; /** * File Type Binaries. @@ -23,6 +24,7 @@ class FileType extends Validator self::FILE_TYPE_JPEG => "\xFF\xD8\xFF", self::FILE_TYPE_GIF => 'GIF', self::FILE_TYPE_PNG => "\x89\x50\x4e\x47\x0d\x0a", + self::FILE_TYPE_GZIP => "application/x-gzip", ); /** diff --git a/src/Appwrite/Swoole/Files.php b/src/Appwrite/Swoole/Files.php new file mode 100644 index 0000000000..aedfd22e01 --- /dev/null +++ b/src/Appwrite/Swoole/Files.php @@ -0,0 +1,170 @@ + 'text/css', + 'js' => 'text/javascript', + 'svg' => 'image/svg+xml', + ]; + + /** + * Add MimeType + * + * @var string $mimeType + */ + public static function addMimeType(string $mimeType) + { + self::$mimeTypes[$mimeType] = true; + } + + /** + * Remove MimeType + * + * @var string $mimeType + */ + public static function removeMimeType(string $mimeType) + { + if(isset(self::$mimeTypes[$mimeType])) { + unset(self::$mimeTypes[$mimeType]); + } + } + + /** + * Get MimeType List + * + * @return array + */ + public static function getMimeTypes(): array + { + return self::$mimeTypes; + } + + /** + * Get Files Loaded Count + * + * @return int + */ + public static function getCount(): int + { + return self::$count; + } + + /** + * Load + * + * @var string $path + */ + public static function load(string $directory, string $root = null) + { + if(!is_readable($directory)) { + throw new Exception('Failed to load directory: '.$directory); + } + + $directory = realpath($directory); + + $root = ($root) ? $root : $directory; + + $handle = opendir($directory); + + while ($path = readdir($handle)) { + $extension = pathinfo($path, PATHINFO_EXTENSION); + + if (in_array($path, ['.', '..'])) { + continue; + } + + if (in_array($extension, ['php', 'phtml'])) { + continue; + } + + if(substr($path, 0, 1) === '.') { + continue; + } + + if (is_dir($directory.'/'.$path)) { + self::load($directory.'/'.$path, $root); + continue; + } + + self::$count++; + + self::$loaded[substr($directory.'/'.$path , strlen($root))] = [ + 'contents' => file_get_contents($directory.'/'.$path), + 'mimeType' => (array_key_exists($extension, self::$extensions)) + ? self::$extensions[$extension] + : mime_content_type($directory.'/'.$path) + ]; + } + + closedir($handle); + + if($directory === $root) { + echo '[Static Files] Loadded '.self::$count.' files'.PHP_EOL; + } + } + + /** + * Is File Loaded + * + * @var string $uri + */ + public static function isFileLoaded(string $uri): bool + { + if(!array_key_exists($uri, self::$loaded)) { + return false; + } + + return true; + } + + /** + * Get File Contants + * + * @var string $uri + */ + public static function getFileContents(string $uri): string + { + if(!array_key_exists($uri, self::$loaded)) { + throw new Exception('File not found or not loaded: '.$uri); + } + + return self::$loaded[$uri]['contents']; + } + + /** + * Get File MimeType + * + * @var string $uri + */ + public static function getFileMimeType(string $uri): string + { + if(!array_key_exists($uri, self::$loaded)) { + throw new Exception('File not found or not loaded: '.$uri); + } + + return self::$loaded[$uri]['mimeType']; + } +} diff --git a/src/Appwrite/Swoole/Request.php b/src/Appwrite/Swoole/Request.php new file mode 100644 index 0000000000..55b599738c --- /dev/null +++ b/src/Appwrite/Swoole/Request.php @@ -0,0 +1,338 @@ +swoole = $request; + } + + /** + * Get Param + * + * Get param by current method name + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getParam(string $key, $default = null) + { + switch($this->getMethod()) { + case self::METHOD_GET: + return $this->getQuery($key, $default); + break; + case self::METHOD_POST: + case self::METHOD_PUT: + case self::METHOD_PATCH: + case self::METHOD_DELETE: + return $this->getPayload($key, $default); + break; + default: + return $this->getQuery($key, $default); + } + } + + /** + * Get Params + * + * Get all params of current method + * + * @return array + */ + public function getParams(): array + { + switch($this->getMethod()) { + case self::METHOD_GET: + return (!empty($this->swoole->get)) ? $this->swoole->get : []; + break; + case self::METHOD_POST: + case self::METHOD_PUT: + case self::METHOD_PATCH: + case self::METHOD_DELETE: + return $this->generateInput(); + break; + default: + return (!empty($this->swoole->get)) ? $this->swoole->get : []; + } + + return []; + } + + /** + * Get Query + * + * Method for querying HTTP GET request parameters. If $key is not found $default value will be returned. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getQuery(string $key, $default = null) + { + return (isset($this->swoole->get[$key])) ? $this->swoole->get[$key] : $default; + } + + /** + * Get payload + * + * Method for querying HTTP request payload parameters. If $key is not found $default value will be returned. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getPayload(string $key, $default = null) + { + $payload = $this->generateInput(); + + return (isset($payload[$key])) ? $payload[$key] : $default; + } + + /** + * Get server + * + * Method for querying server parameters. If $key is not found $default value will be returned. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getServer(string $key, $default = null) + { + return (isset($this->swoole->server) && isset($this->swoole->server[$key])) ? $this->swoole->server[$key] : $default; + } + + /** + * Get IP + * + * Returns users IP address. + * Support HTTP_X_FORWARDED_FOR header usually return + * from different proxy servers or PHP default REMOTE_ADDR + */ + public function getIP(): string + { + return $this->getHeader('x-forwarded-for', $this->getServer('remote_addr', '0.0.0.0')); + } + + /** + * Get Protocol + * + * Returns request protocol. + * Support HTTP_X_FORWARDED_PROTO header usually return + * from different proxy servers or PHP default REQUEST_SCHEME + * + * @return string + */ + public function getProtocol(): string + { + $protocol = $this->getHeader('x-forwarded-proto', $this->getServer('server_protocol', 'https')); + + if($protocol === 'HTTP/1.1') { + return 'http'; + } + + return $protocol; + } + + /** + * Get Port + * + * Returns request port. + * + * @return string + */ + public function getPort(): string + { + return $this->getHeader('x-forwarded-port', (string)\parse_url($this->getProtocol().'://'.$this->getHeader('x-forwarded-host', $this->getHeader('host')), PHP_URL_PORT)); + } + + /** + * Get Hostname + * + * Returns request hostname. + * + * @return string + */ + public function getHostname(): string + { + return \parse_url($this->getProtocol().'://'.$this->getHeader('x-forwarded-host', $this->getHeader('host')), PHP_URL_HOST); + } + + /** + * Get Method + * + * Return HTTP request method + * + * @return string + */ + public function getMethod(): string + { + return $this->getServer('request_method', 'UNKNOWN'); + } + + /** + * Get URI + * + * Return HTTP request URI + * + * @return string + */ + public function getURI(): string + { + return $this->getServer('request_uri', ''); + } + + /** + * Get Referer + * + * Return HTTP referer header + * + * @return string + */ + public function getReferer(string $default = ''): string + { + return $this->getHeader('referer', ''); + } + + /** + * Get Origin + * + * Return HTTP origin header + * + * @return string + */ + public function getOrigin(string $default = ''): string + { + return $this->getHeader('origin', $default); + } + + /** + * Get User Agent + * + * Return HTTP user agent header + * + * @return string + */ + public function getUserAgent(string $default = ''): string + { + return $this->getHeader('user-agent', $default); + } + + /** + * Get Accept + * + * Return HTTP accept header + * + * @return string + */ + public function getAccept(string $default = ''): string + { + return $this->getHeader('accept', $default); + } + + /** + * Get files + * + * Method for querying upload files data. If $key is not found empty array will be returned. + * + * @param string $key + * @return array + */ + public function getFiles($key): array + { + $key = strtolower($key); + return (isset($this->swoole->files[$key])) ? $this->swoole->files[$key] : []; + } + + /** + * Get cookie + * + * Method for querying HTTP cookie parameters. If $key is not found $default value will be returned. + * + * @param string $key + * @param string $default + * @return mixed + */ + public function getCookie(string $key, string $default = ''): string + { + $key = strtolower($key); + return (isset($this->swoole->cookie[$key])) ? $this->swoole->cookie[$key] : $default; + } + + /** + * Get header + * + * Method for querying HTTP header parameters. If $key is not found $default value will be returned. + * + * @param string $key + * @param string $default + * @return string + */ + public function getHeader(string $key, string $default = ''): string + { + return (isset($this->swoole->header[$key])) ? $this->swoole->header[$key] : $default; + } + + /** + * Generate input + * + * Generate PHP input stream and parse it as an array in order to handle different content type of requests + * + * @return array + */ + protected function generateInput(): array + { + if (null === $this->payload) { + $contentType = $this->getHeader('content-type'); + + // Get content-type without the charset + $length = strpos($contentType, ';'); + $length = (empty($length)) ? strlen($contentType) : $length; + $contentType = substr($contentType, 0, $length); + + switch ($contentType) { + case 'application/json': + $this->payload = json_decode($this->swoole->rawContent(), true); + break; + + default: + $this->payload = $this->swoole->post; + break; + } + + if(empty($this->payload)) { // Make sure we return same data type even if json payload is empty or failed + $this->payload = []; + } + } + + return $this->payload; + } + + /** + * Generate headers + * + * Parse request headers as an array for easy querying using the getHeader method + * + * @return array + */ + protected function generateHeaders(): array + { + return $this->swoole->header; + } +} \ No newline at end of file diff --git a/src/Appwrite/Swoole/Response.php b/src/Appwrite/Swoole/Response.php new file mode 100644 index 0000000000..88ab1fcd43 --- /dev/null +++ b/src/Appwrite/Swoole/Response.php @@ -0,0 +1,138 @@ + true, + 'text/css' => true, + 'text/javascript' => true, + 'application/javascript' => true, + 'text/html' => true, + 'text/html; charset=UTF-8' => true, + 'application/json' => true, + 'application/json; charset=UTF-8' => true, + 'image/svg+xml' => true, + 'application/xml+rss' => true, + ]; + + /** + * Response constructor. + */ + public function __construct(SwooleResponse $response) + { + $this->swoole = $response; + parent::__construct(\microtime(true)); + } + + /** + * Output response + * + * Generate HTTP response output including the response header (+cookies) and body and prints them. + * + * @param string $body + * @param int $exit exit code or don't exit if code is null + * + * @return self + */ + public function send(string $body = '', int $exit = null): void + { + if(!$this->disablePayload) { + $this->addHeader('X-Debug-Speed', microtime(true) - $this->startTime); + + $this + ->appendCookies() + ->appendHeaders() + ; + + $chunk = 2000000; // Max chunk of 2 mb + $length = strlen($body); + + $this->size = $this->size + strlen(implode("\n", $this->headers)) + $length; + + if(array_key_exists( + $this->contentType, + $this->compressed + ) && ($length <= $chunk)) { // Dont compress with GZIP / Brotli if header is not listed and size is bigger than 2mb + $this->swoole->end($body); + } + else { + for ($i=0; $i < ceil($length / $chunk); $i++) { + $this->swoole->write(substr($body, ($i * $chunk), min((($i * $chunk) + $chunk), $length))); + } + + $this->swoole->end(); + } + + $this->disablePayload(); + } + } + + /** + * Append headers + * + * Iterating over response headers to generate them using native PHP header function. + * This method is also responsible for generating the response and content type headers. + * + * @return self + */ + protected function appendHeaders(): self + { + // Send status code header + $this->swoole->status($this->statusCode); + + // Send content type header + $this + ->addHeader('Content-Type', $this->contentType) + ; + + // Set application headers + foreach ($this->headers as $key => $value) { + $this->swoole->header($key, $value); + } + + return $this; + } + + /** + * Append cookies + * + * Iterating over response cookies to generate them using native PHP cookie function. + * + * @return self + */ + protected function appendCookies(): self + { + foreach ($this->cookies as $cookie) { + $this->swoole->cookie( + $cookie['name'], + $cookie['value'], + $cookie['expire'], + $cookie['path'], + $cookie['domain'], + $cookie['secure'], + $cookie['httponly'], + $cookie['samesite'], + ); + } + + return $this; + } +} \ No newline at end of file diff --git a/src/Appwrite/Task/Validator/Cron.php b/src/Appwrite/Task/Validator/Cron.php index 2188b04ec4..68c7c4b0f6 100644 --- a/src/Appwrite/Task/Validator/Cron.php +++ b/src/Appwrite/Task/Validator/Cron.php @@ -30,6 +30,10 @@ class Cron extends Validator */ public function isValid($value) { + if(empty($value)) { + return true; + } + if (!CronExpression::isValidExpression($value)) { return false; } diff --git a/src/Appwrite/Template/Template.php b/src/Appwrite/Template/Template.php index 127afef15e..695667ca26 100644 --- a/src/Appwrite/Template/Template.php +++ b/src/Appwrite/Template/Template.php @@ -95,4 +95,34 @@ class Template extends View return \http_build_query($parsed); } + + /** + * From Camel Case + * + * @var string $input + * + * @return string + */ + public static function fromCamelCaseToSnake($input): string + { + \preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); + $ret = $matches[0]; + foreach ($ret as &$match) { + $match = $match == \strtoupper($match) ? \strtolower($match) : \lcfirst($match); + } + + return \implode('_', $ret); + } + + /** + * From Camel Case to Dash Case + * + * @var string $input + * + * @return string + */ + public static function fromCamelCaseToDash($input): string + { + return \str_replace([' ', '_'], '-', \strtolower(\preg_replace('/([a-zA-Z])(?=[A-Z])/', '$1-', $input))); + } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php new file mode 100644 index 0000000000..414168ce93 --- /dev/null +++ b/src/Appwrite/Utopia/Response.php @@ -0,0 +1,176 @@ +setModel(new Error()) + ->setModel(new ErrorDev()) + ->setModel(new User()) + ->setModel(new Session()) + ->setModel(new Locale()) + ->setModel(new Team()) + ->setModel(new TeamList()) + ->setModel(new Membership()) + ->setModel(new MembershipList()) + ; + + parent::__construct($time); + } + + /** + * HTTP content types + */ + const CONTENT_TYPE_YAML = 'application/x-yaml'; + + /** + * List of defined output objects + */ + protected $models = []; + + /** + * Set Model Object + * + * @return self + */ + public function setModel(Model $instance): self + { + $this->models[$instance->getType()] = $instance; + + return $this; + } + + /** + * Get Model Object + * + * @return Model + */ + public function getModel(string $key): Model + { + if(!isset($this->models[$key])) { + throw new Exception('Undefined model: '.$key); + } + + return $this->models[$key]; + } + + /** + * Validate response objects and outputs + * the response according to given format type + */ + public function dynamic(Document $document, string $model) + { + return $this->json($this->output($document, $model)); + } + + /** + * Generate valid response object from document data + */ + protected function output(Document $document, string $model): array + { + $data = $document; + $model = $this->getModel($model); + $output = []; + + foreach($model->getRules() as $key => $rule) { + if(!$document->isSet($key)) { + if(!is_null($rule['default'])) { + $document->setAttribute($key, $rule['default']); + } + else { + throw new Exception('Missing response key: '.$key); + } + } + + if($rule['array']) { + if(!is_array($data[$key])) { + throw new Exception($key.' must be an array of '.$rule['type'].' types'); + } + + foreach ($data[$key] as &$item) { + if(array_key_exists($rule['type'], $this->models) && $item instanceof Document) { + $item = $this->output($item, $rule['type']); + } + } + } + + $output[$key] = $data[$key]; + } + + return $output; + } + + /** + * YAML + * + * This helper is for sending YAML HTTP response. + * It sets relevant content type header ('application/x-yaml') and convert a PHP array ($data) to valid YAML using native yaml_parse + * + * @see https://en.wikipedia.org/wiki/YAML + * + * @param array $data + */ + public function yaml(array $data) + { + if(!extension_loaded('yaml')) { + throw new Exception('Missing yaml extension. Learn more at: https://www.php.net/manual/en/book.yaml.php'); + } + + $this + ->setContentType(Response::CONTENT_TYPE_YAML) + ->send(yaml_emit($data, YAML_UTF8_ENCODING)) + ; + } +} diff --git a/src/Appwrite/Utopia/Response/Model.php b/src/Appwrite/Utopia/Response/Model.php new file mode 100644 index 0000000000..b50224935b --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model.php @@ -0,0 +1,48 @@ +rules; + } + + /** + * Add a New Rule + */ + protected function addRule(string $key, array $options): self + { + $this->rules[$key] = array_merge([ + 'type' => '', + 'description' => '', + 'default' => null, + 'example' => '', + 'array' => false, + ], $options); + + return $this; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/BaseList.php b/src/Appwrite/Utopia/Response/Model/BaseList.php new file mode 100644 index 0000000000..24f0a76486 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/BaseList.php @@ -0,0 +1,40 @@ +addRule('sum', [ + 'type' => 'integer', + 'description' => 'Total sum of items in the list.', + 'example' => '5', + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Base List'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_BASE_LIST; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/Error.php b/src/Appwrite/Utopia/Response/Model/Error.php new file mode 100644 index 0000000000..95a4764d4a --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Error.php @@ -0,0 +1,50 @@ +addRule('message', [ + 'type' => 'string', + 'description' => 'Error message.', + 'example' => 'Not found', + ]) + ->addRule('code', [ + 'type' => 'string', + 'description' => 'Error code.', + 'example' => '404', + ]) + ->addRule('version', [ + 'type' => 'string', + 'description' => 'Server version number.', + 'example' => APP_VERSION_STABLE, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Error'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_ERROR; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/ErrorDev.php b/src/Appwrite/Utopia/Response/Model/ErrorDev.php new file mode 100644 index 0000000000..65ab38d2b5 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ErrorDev.php @@ -0,0 +1,43 @@ +addRule('file', [ + 'type' => 'string', + 'description' => 'File path.', + 'example' => '/usr/code/vendor/utopia-php/framework/src/App.php', + ]) + ->addRule('line', [ + 'type' => 'integer', + 'description' => 'Line number.', + 'example' => 209, + ]) + ->addRule('trace', [ + 'type' => 'string', + 'description' => 'Error trace.', + 'example' => [ + '' + ], + ]) + ; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_ERROR_DEV; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/File.php b/src/Appwrite/Utopia/Response/Model/File.php new file mode 100644 index 0000000000..875c6f4bd7 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/File.php @@ -0,0 +1,34 @@ +addRule('ip', [ + 'type' => 'string', + 'description' => 'User IP address.', + 'example' => '127.0.0.1', + ]) + ->addRule('countryCode', [ + 'type' => 'string', + 'description' => 'Country code in [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) two-character format', + 'example' => 'US', + ]) + ->addRule('country', [ + 'type' => 'string', + 'description' => 'Country name. This field support localization.', + 'example' => 'United States', + ]) + ->addRule('continentCode', [ + 'type' => 'string', + 'description' => 'Continent code. A two character continent code "AF" for Africa, "AN" for Antarctica, "AS" for Asia, "EU" for Europe, "NA" for North America, "OC" for Oceania, and "SA" for South America.', + 'example' => 'NA', + ]) + ->addRule('continent', [ + 'type' => 'string', + 'description' => 'Continent name. This field support localization.', + 'example' => 'North America', + ]) + ->addRule('eu', [ + 'type' => 'Boolean', + 'description' => 'True if country is part of the Europian Union.', + 'default' => false, + 'example' => false, + ]) + ->addRule('currency', [ + 'type' => 'string', + 'description' => 'ISO 4217 Email verification status.', + 'description' => 'Currency code in [ISO 4217-1](http://en.wikipedia.org/wiki/ISO_4217) three-character format', + 'example' => 'USD', + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Locale'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_LOCALE; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/Log.php b/src/Appwrite/Utopia/Response/Model/Log.php new file mode 100644 index 0000000000..93e7e88efd --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Log.php @@ -0,0 +1,33 @@ +addRule('$id', [ + 'type' => 'string', + 'description' => 'Membership ID.', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('userId', [ + 'type' => 'string', + 'description' => 'User ID.', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('teamId', [ + 'type' => 'string', + 'description' => 'Team ID.', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('name', [ + 'type' => 'string', + 'description' => 'User name.', + 'default' => '', + 'example' => 'VIP', + ]) + ->addRule('email', [ + 'type' => 'string', + 'description' => 'User email address.', + 'default' => '', + 'example' => 'john@appwrite.io', + ]) + ->addRule('invited', [ + 'type' => 'integer', + 'description' => 'Date, the user has been invited to join the team in Unix timestamp.', + 'example' => 1592981250, + ]) + ->addRule('joined', [ + 'type' => 'integer', + 'description' => 'Date, the user has accepted the invitation to join the team in Unix timestamp.', + 'example' => 1592981250, + ]) + ->addRule('confirm', [ + 'type' => 'boolean', + 'description' => 'User confirmation status, true if the user has joined the team or false otherwise.', + 'example' => false, + ]) + ->addRule('roles', [ + 'type' => 'string', + 'description' => 'User list of roles', + 'default' => [], + 'example' => [], + 'array' => true, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Membership'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_MEMBERSHIP; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/MembershipList.php b/src/Appwrite/Utopia/Response/Model/MembershipList.php new file mode 100644 index 0000000000..56937c8d33 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/MembershipList.php @@ -0,0 +1,41 @@ +addRule('memberships', [ + 'type' => Response::MODEL_MEMBERSHIP, + 'description' => 'List of memberships.', + 'example' => [], + 'array' => true, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Membership List'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_MEMBERSHIP_LIST; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/Session.php b/src/Appwrite/Utopia/Response/Model/Session.php new file mode 100644 index 0000000000..6146feb31d --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Session.php @@ -0,0 +1,50 @@ +addRule('$id', [ + 'type' => 'string', + 'description' => 'Session ID.', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('expire', [ + 'type' => 'string', + 'description' => 'Session expiration date in Unix timestamp.', + 'example' => 1592981250, + ]) + ->addRule('ip', [ + 'type' => 'string', + 'description' => 'IP session in use when the session was created.', + 'example' => '127.0.0.1', + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Session'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_SESSION; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/Team.php b/src/Appwrite/Utopia/Response/Model/Team.php new file mode 100644 index 0000000000..f86ff22e5d --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Team.php @@ -0,0 +1,56 @@ +addRule('$id', [ + 'type' => 'string', + 'description' => 'Team ID.', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('name', [ + 'type' => 'string', + 'description' => 'Team name.', + 'default' => '', + 'example' => 'VIP', + ]) + ->addRule('dateCreated', [ + 'type' => 'integer', + 'description' => 'Team creation date in Unix timestamp.', + 'example' => 1592981250, + ]) + ->addRule('sum', [ // TODO change key name? + 'type' => 'integer', + 'description' => 'Total sum of team members.', + 'example' => 7, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Team'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_TEAM; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/TeamList.php b/src/Appwrite/Utopia/Response/Model/TeamList.php new file mode 100644 index 0000000000..c96e4cd0d4 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/TeamList.php @@ -0,0 +1,41 @@ +addRule('teams', [ + 'type' => Response::MODEL_TEAM, + 'description' => 'List of teams.', + 'example' => [], + 'array' => true, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Team List'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_TEAM_LIST; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/Token.php b/src/Appwrite/Utopia/Response/Model/Token.php new file mode 100644 index 0000000000..6d189fbe37 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Token.php @@ -0,0 +1,33 @@ +addRule('$id', [ + 'type' => 'string', + 'description' => 'User ID.', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('name', [ + 'type' => 'string', + 'description' => 'User name.', + 'default' => '', + 'example' => 'John Doe', + ]) + ->addRule('registration', [ + 'type' => 'integer', + 'description' => 'User registration date in Unix timestamp.', + 'example' => 1592981250, + ]) + ->addRule('status', [ + 'type' => 'integer', + 'description' => 'User status. 0 for Unavtivated, 1 for active and 2 is blocked.', + 'default' => false, + 'example' => true, + ]) + ->addRule('email', [ + 'type' => 'string', + 'description' => 'User email address.', + 'default' => '', + 'example' => 'john@appwrite.io', + ]) + ->addRule('emailVerification', [ + 'type' => 'boolean', + 'description' => 'Email verification status.', + 'default' => false, + 'example' => true, + ]) + ->addRule('prefs', [ + 'type' => 'json', + 'description' => 'User preferences as a key-value object', + 'default' => new \stdClass, + 'example' => ['theme' => 'dark', 'timezone' => 'UTC'], + ]) + ->addRule('roles', [ + 'type' => 'string', + 'description' => 'User list of roles', + 'default' => [], + 'example' => [], + 'array' => true, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'User'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_USER; + } +} \ No newline at end of file diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php new file mode 100644 index 0000000000..e01b35952e --- /dev/null +++ b/tests/e2e/General/HTTPTest.php @@ -0,0 +1,50 @@ +client->call(Client::METHOD_OPTIONS, '/', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + ]), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Appwrite', $response['headers']['server']); + $this->assertEquals('GET, POST, PUT, PATCH, DELETE', $response['headers']['access-control-allow-methods']); + $this->assertEquals('Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-SDK-Version, Cache-Control, Expires, Pragma, X-Fallback-Cookies', $response['headers']['access-control-allow-headers']); + $this->assertEquals('X-Fallback-Cookies', $response['headers']['access-control-expose-headers']); + $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); + $this->assertEquals('true', $response['headers']['access-control-allow-credentials']); + $this->assertEmpty($response['body']); + } + + public function testError() + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/error', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + ]), []); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertEquals('Not Found', $response['body']['message']); + $this->assertEquals(404, $response['body']['code']); + $this->assertEquals('dev', $response['body']['version']); + } +} \ No newline at end of file diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index 2e03eeac4a..d4d7877846 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -73,6 +73,8 @@ trait ProjectCustom 'documents.write', 'files.read', 'files.write', + 'functions.read', + 'functions.write', 'locale.read', 'avatars.read', 'health.read', diff --git a/tests/e2e/Scopes/ProjectNone.php b/tests/e2e/Scopes/ProjectNone.php new file mode 100644 index 0000000000..8880562093 --- /dev/null +++ b/tests/e2e/Scopes/ProjectNone.php @@ -0,0 +1,11 @@ + $password, ]); - $session = $this->client->parseCookie($session['headers']['set-cookie'])['a_session_console']; + $session = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_console']; self::$root = [ '$id' => $root['body']['$id'], @@ -143,7 +143,7 @@ abstract class Scope extends TestCase 'password' => $password, ]); - $session = $this->client->parseCookie($session['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; + $session = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; self::$user[$this->getProject()['$id']] = [ '$id' => $user['body']['$id'], diff --git a/tests/e2e/Scopes/SideNone.php b/tests/e2e/Scopes/SideNone.php new file mode 100644 index 0000000000..5fe002923b --- /dev/null +++ b/tests/e2e/Scopes/SideNone.php @@ -0,0 +1,11 @@ +assertEquals($response['headers']['status-code'], 201); $sessionId = $response['body']['$id']; - $session = $this->client->parseCookie($response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; + $session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; /** * Test for FAILURE @@ -774,7 +774,7 @@ trait AccountBase ]); $sessionNewId = $response['body']['$id']; - $sessionNew = $this->client->parseCookie($response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; + $sessionNew = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; $this->assertEquals($response['headers']['status-code'], 201); @@ -840,7 +840,7 @@ trait AccountBase 'password' => $password, ]); - $sessionNew = $this->client->parseCookie($response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; + $sessionNew = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; $this->assertEquals($response['headers']['status-code'], 201); @@ -922,7 +922,7 @@ trait AccountBase 'password' => $password, ]); - $data['session'] = $this->client->parseCookie($response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; + $data['session'] = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; return $data; } diff --git a/tests/e2e/Services/Avatars/AvatarsBase.php b/tests/e2e/Services/Avatars/AvatarsBase.php index 206ea0c400..e27be04e18 100644 --- a/tests/e2e/Services/Avatars/AvatarsBase.php +++ b/tests/e2e/Services/Avatars/AvatarsBase.php @@ -16,7 +16,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/credit-cards/visa', [ @@ -27,7 +27,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/credit-cards/visa', [ @@ -39,7 +39,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); /** @@ -79,7 +79,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/browsers/ch', [ @@ -90,7 +90,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/browsers/ch', [ @@ -102,7 +102,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); /** @@ -142,7 +142,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/flags/us', [ @@ -153,7 +153,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/flags/us', [ @@ -165,7 +165,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); /** @@ -206,7 +206,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/image', [ @@ -218,7 +218,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/image', [ @@ -231,7 +231,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); /** @@ -276,7 +276,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); // $response = $this->client->call(Client::METHOD_GET, '/avatars/favicon', [ @@ -286,7 +286,7 @@ trait AvatarsBase // ]); // $this->assertEquals(200, $response['headers']['status-code']); - // $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + // $this->assertEquals('image/png', $response['headers']['content-type']); // $this->assertNotEmpty($response['body']); // $response = $this->client->call(Client::METHOD_GET, '/avatars/favicon', [ @@ -296,7 +296,7 @@ trait AvatarsBase // ]); // $this->assertEquals(200, $response['headers']['status-code']); - // $this->assertEquals('image/x-icon; charset=UTF-8', $response['headers']['content-type']); + // $this->assertEquals('image/x-icon', $response['headers']['content-type']); // $this->assertNotEmpty($response['body']); /** @@ -333,7 +333,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/qr', [ @@ -344,7 +344,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/qr', [ @@ -356,7 +356,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/qr', [ @@ -370,7 +370,7 @@ trait AvatarsBase $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('attachment; filename="qr.png"', $response['headers']['content-disposition']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); /** @@ -423,7 +423,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ @@ -434,7 +434,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ @@ -446,7 +446,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ @@ -460,7 +460,7 @@ trait AvatarsBase ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertEquals('image/png', $response['headers']['content-type']); $this->assertNotEmpty($response['body']); /** diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php new file mode 100644 index 0000000000..80ee47c72a --- /dev/null +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -0,0 +1,203 @@ +client->call(Client::METHOD_GET, '/teams/'.$id, array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders())); + + // $this->assertEquals(200, $response['headers']['status-code']); + // $this->assertNotEmpty($response['body']['$id']); + // $this->assertEquals('Arsenal', $response['body']['name']); + // $this->assertGreaterThan(-1, $response['body']['sum']); + // $this->assertIsInt($response['body']['sum']); + // $this->assertIsInt($response['body']['dateCreated']); + + // /** + // * Test for FAILURE + // */ + + // return []; + // } + + // /** + // * @depends testCreateTeam + // */ + // public function testListTeams($data):array + // { + // /** + // * Test for SUCCESS + // */ + // $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders())); + + // $this->assertEquals(200, $response['headers']['status-code']); + // $this->assertGreaterThan(0, $response['body']['sum']); + // $this->assertIsInt($response['body']['sum']); + // $this->assertCount(3, $response['body']['teams']); + + // $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'limit' => 2, + // ]); + + // $this->assertEquals(200, $response['headers']['status-code']); + // $this->assertGreaterThan(0, $response['body']['sum']); + // $this->assertIsInt($response['body']['sum']); + // $this->assertCount(2, $response['body']['teams']); + + // $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'offset' => 1, + // ]); + + // $this->assertEquals(200, $response['headers']['status-code']); + // $this->assertGreaterThan(0, $response['body']['sum']); + // $this->assertIsInt($response['body']['sum']); + // $this->assertCount(2, $response['body']['teams']); + + // $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'search' => 'Manchester', + // ]); + + // $this->assertEquals(200, $response['headers']['status-code']); + // $this->assertGreaterThan(0, $response['body']['sum']); + // $this->assertIsInt($response['body']['sum']); + // $this->assertCount(1, $response['body']['teams']); + // $this->assertEquals('Manchester United', $response['body']['teams'][0]['name']); + + // $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'search' => 'United', + // ]); + + // $this->assertEquals(200, $response['headers']['status-code']); + // $this->assertGreaterThan(0, $response['body']['sum']); + // $this->assertIsInt($response['body']['sum']); + // $this->assertCount(1, $response['body']['teams']); + // $this->assertEquals('Manchester United', $response['body']['teams'][0]['name']); + + // /** + // * Test for FAILURE + // */ + + // return []; + // } + + // public function testUpdateTeam():array + // { + // /** + // * Test for SUCCESS + // */ + // $response = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'name' => 'Demo' + // ]); + + // $this->assertEquals(201, $response['headers']['status-code']); + // $this->assertNotEmpty($response['body']['$id']); + // $this->assertEquals('Demo', $response['body']['name']); + // $this->assertGreaterThan(-1, $response['body']['sum']); + // $this->assertIsInt($response['body']['sum']); + // $this->assertIsInt($response['body']['dateCreated']); + + // $response = $this->client->call(Client::METHOD_PUT, '/teams/'.$response['body']['$id'], array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'name' => 'Demo New' + // ]); + + // $this->assertEquals(200, $response['headers']['status-code']); + // $this->assertNotEmpty($response['body']['$id']); + // $this->assertEquals('Demo New', $response['body']['name']); + // $this->assertGreaterThan(-1, $response['body']['sum']); + // $this->assertIsInt($response['body']['sum']); + // $this->assertIsInt($response['body']['dateCreated']); + + // /** + // * Test for FAILURE + // */ + // $response = $this->client->call(Client::METHOD_PUT, '/teams/'.$response['body']['$id'], array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // ]); + + // $this->assertEquals(400, $response['headers']['status-code']); + + // return []; + // } + + // public function testDeleteTeam():array + // { + // /** + // * Test for SUCCESS + // */ + // $response = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'name' => 'Demo' + // ]); + + // $teamUid = $response['body']['$id']; + + // $this->assertEquals(201, $response['headers']['status-code']); + // $this->assertNotEmpty($response['body']['$id']); + // $this->assertEquals('Demo', $response['body']['name']); + // $this->assertGreaterThan(-1, $response['body']['sum']); + // $this->assertIsInt($response['body']['sum']); + // $this->assertIsInt($response['body']['dateCreated']); + + // $response = $this->client->call(Client::METHOD_DELETE, '/teams/'.$teamUid, array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders())); + + // $this->assertEquals(204, $response['headers']['status-code']); + // $this->assertEmpty($response['body']); + + // /** + // * Test for FAILURE + // */ + // $response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamUid, array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders())); + + // $this->assertEquals(404, $response['headers']['status-code']); + + // return []; + // } +} \ No newline at end of file diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php new file mode 100644 index 0000000000..9f33156c43 --- /dev/null +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -0,0 +1,43 @@ +client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Test', + 'vars' => [ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3', + ], + 'events' => [ + 'account.create', + 'account.delete', + ], + 'schedule' => '* * * * *', + 'timeout' => 10, + ]); + + $this->assertEquals(401, $response1['headers']['status-code']); + + return []; + } +} \ No newline at end of file diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php new file mode 100644 index 0000000000..7f0441a88d --- /dev/null +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -0,0 +1,430 @@ +client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Test', + 'env' => 'node-14', + 'vars' => [ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3', + ], + 'events' => [ + 'account.create', + 'account.delete', + ], + 'schedule' => '* * * * *', + 'timeout' => 10, + ]); + + $functionId = (isset($response1['body']['$id'])) ? $response1['body']['$id'] : ''; + + $this->assertEquals(201, $response1['headers']['status-code']); + $this->assertNotEmpty($response1['body']['$id']); + $this->assertEquals('Test', $response1['body']['name']); + $this->assertEquals('node-14', $response1['body']['env']); + $this->assertIsInt($response1['body']['dateCreated']); + $this->assertIsInt($response1['body']['dateUpdated']); + $this->assertEquals('', $response1['body']['tag']); + $this->assertEquals([ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3', + ], $response1['body']['vars']); + $this->assertEquals([ + 'account.create', + 'account.delete', + ], $response1['body']['events']); + $this->assertEquals('* * * * *', $response1['body']['schedule']); + $this->assertEquals(10, $response1['body']['timeout']); + + /** + * Test for FAILURE + */ + + return [ + 'functionId' => $functionId, + ]; + } + + /** + * @depends testCreate + */ + public function testList(array $data):array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_GET, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($function['headers']['status-code'], 200); + $this->assertEquals($function['body']['sum'], 1); + $this->assertIsArray($function['body']['functions']); + $this->assertCount(1, $function['body']['functions']); + $this->assertEquals($function['body']['functions'][0]['name'], 'Test'); + + return $data; + } + + /** + * @depends testList + */ + public function testGet(array $data):array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($function['headers']['status-code'], 200); + $this->assertEquals($function['body']['name'], 'Test'); + + /** + * Test for FAILURE + */ + $function = $this->client->call(Client::METHOD_GET, '/functions/x', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($function['headers']['status-code'], 404); + + return $data; + } + + /** + * @depends testGet + */ + public function testUpdate($data):array + { + /** + * Test for SUCCESS + */ + $response1 = $this->client->call(Client::METHOD_PUT, '/functions/'.$data['functionId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Test1', + 'vars' => [ + 'key4' => 'value4', + 'key5' => 'value5', + 'key6' => 'value6', + ], + 'events' => [ + 'account.update.name', + 'account.update.email', + ], + 'schedule' => '* * * * 1', + 'timeout' => 5, + ]); + + $this->assertEquals(200, $response1['headers']['status-code']); + $this->assertNotEmpty($response1['body']['$id']); + $this->assertEquals('Test1', $response1['body']['name']); + $this->assertIsInt($response1['body']['dateCreated']); + $this->assertIsInt($response1['body']['dateUpdated']); + $this->assertEquals('', $response1['body']['tag']); + $this->assertEquals([ + 'key4' => 'value4', + 'key5' => 'value5', + 'key6' => 'value6', + ], $response1['body']['vars']); + $this->assertEquals([ + 'account.update.name', + 'account.update.email', + ], $response1['body']['events']); + $this->assertEquals('* * * * 1', $response1['body']['schedule']); + $this->assertEquals(5, $response1['body']['timeout']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testUpdate + */ + public function testCreateTag($data):array + { + /** + * Test for SUCCESS + */ + $tag = $this->client->call(Client::METHOD_POST, '/functions/'.$data['functionId'].'/tags', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'command' => 'php function.php', + 'code' => new CURLFile(realpath(__DIR__ . '/../../../resources/functions/php-fx.tar.gz'), 'application/x-gzip', 'php-fx.tar.gz'), + ]); + + $tagId = (isset($tag['body']['$id'])) ? $tag['body']['$id'] : ''; + + $this->assertEquals(201, $tag['headers']['status-code']); + $this->assertNotEmpty($tag['body']['$id']); + $this->assertIsInt($tag['body']['dateCreated']); + $this->assertEquals('php function.php', $tag['body']['command']); + $this->assertStringStartsWith('/storage/functions/app-', $tag['body']['codePath']); + $this->assertEquals(751, $tag['body']['codeSize']); + + /** + * Test for FAILURE + */ + + return array_merge($data, ['tagId' => $tagId]); + } + + /** + * @depends testCreateTag + */ + public function testUpdateTag($data):array + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$data['functionId'].'/tag', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'tag' => $data['tagId'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertIsInt($response['body']['dateCreated']); + $this->assertIsInt($response['body']['dateUpdated']); + $this->assertEquals($data['tagId'], $response['body']['tag']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testCreateTag + */ + public function testListTags(array $data):array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/tags', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($function['headers']['status-code'], 200); + $this->assertEquals($function['body']['sum'], 1); + $this->assertIsArray($function['body']['tags']); + $this->assertCount(1, $function['body']['tags']); + + return $data; + } + + /** + * @depends testCreateTag + */ + public function testGetTag(array $data):array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/tags/' . $data['tagId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals(751, $function['body']['codeSize']); + + /** + * Test for FAILURE + */ + $function = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/tags/x', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($function['headers']['status-code'], 404); + + return $data; + } + + + /** + * @depends testUpdateTag + */ + public function testCreateExecution($data):array + { + /** + * Test for SUCCESS + */ + $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$data['functionId'].'/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'async' => 1, + ]); + + $executionId = (isset($execution['body']['$id'])) ? $execution['body']['$id'] : ''; + + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertNotEmpty($execution['body']['$id']); + $this->assertNotEmpty($execution['body']['functionId']); + $this->assertIsInt($execution['body']['dateCreated']); + $this->assertEquals($data['functionId'], $execution['body']['functionId']); + $this->assertEquals('waiting', $execution['body']['status']); + $this->assertEquals(0, $execution['body']['exitCode']); + $this->assertEquals('', $execution['body']['stdout']); + $this->assertEquals('', $execution['body']['stderr']); + $this->assertEquals(0, $execution['body']['time']); + + /** + * Test for FAILURE + */ + + return array_merge($data, ['executionId' => $executionId]); + } + + + /** + * @depends testCreateExecution + */ + public function testListExecutions(array $data):array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($function['headers']['status-code'], 200); + $this->assertEquals($function['body']['sum'], 1); + $this->assertIsArray($function['body']['executions']); + $this->assertCount(1, $function['body']['executions']); + $this->assertEquals($function['body']['executions'][0]['$id'], $data['executionId']); + + return $data; + } + + /** + * @depends testListExecutions + */ + public function testGetExecution(array $data):array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/executions/' . $data['executionId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($function['headers']['status-code'], 200); + $this->assertEquals($function['body']['$id'], $data['executionId']); + + /** + * Test for FAILURE + */ + $function = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/executions/x', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($function['headers']['status-code'], 404); + + return $data; + } + + /** + * @depends testGetExecution + */ + public function testDeleteTag($data):array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_DELETE, '/functions/'.$data['functionId'].'/tags/' . $data['tagId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $function['headers']['status-code']); + $this->assertEmpty($function['body']); + + $function = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/tags/' . $data['tagId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $function['headers']['status-code']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testCreateTag + */ + public function testDelete($data):array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_DELETE, '/functions/'.$data['functionId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $function['headers']['status-code']); + $this->assertEmpty($function['body']); + + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $function['headers']['status-code']); + + /** + * Test for FAILURE + */ + + return $data; + } +} \ No newline at end of file diff --git a/tests/e2e/Services/Health/HealthCustomServerTest.php b/tests/e2e/Services/Health/HealthCustomServerTest.php index 572ad2906c..52b0437a74 100644 --- a/tests/e2e/Services/Health/HealthCustomServerTest.php +++ b/tests/e2e/Services/Health/HealthCustomServerTest.php @@ -173,7 +173,7 @@ class HealthCustomClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertIsInt($response['body']['size']); - $this->assertLessThan(120, $response['body']['size']); + $this->assertLessThan(160, $response['body']['size']); /** * Test for FAILURE diff --git a/tests/e2e/Services/Locale/LocaleBase.php b/tests/e2e/Services/Locale/LocaleBase.php index b276eef621..3f5d5e9486 100644 --- a/tests/e2e/Services/Locale/LocaleBase.php +++ b/tests/e2e/Services/Locale/LocaleBase.php @@ -217,9 +217,9 @@ trait LocaleBase /** * Test for SUCCESS */ - $languages = require('app/config/locales.php'); - $defaultCountries = require('app/config/locales/en.countries.php'); - $defaultContinents = require('app/config/locales/en.continents.php'); + $languages = require('app/config/locale/codes.php'); + $defaultCountries = require('app/config/locale/translations/en.countries.php'); + $defaultContinents = require('app/config/locale/translations/en.continents.php'); foreach ($languages as $lang) { $response = $this->client->call(Client::METHOD_GET, '/locale/countries', [ diff --git a/tests/e2e/Services/Projects/ProjectsBase.php b/tests/e2e/Services/Projects/ProjectsBase.php new file mode 100644 index 0000000000..dd802a5403 --- /dev/null +++ b/tests/e2e/Services/Projects/ProjectsBase.php @@ -0,0 +1,10 @@ +client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Project Test', + ]); + + $this->assertEquals(201, $team['headers']['status-code']); + $this->assertEquals('Project Test', $team['body']['name']); + $this->assertNotEmpty($team['body']['$id']); + + $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Project Test', + 'teamId' => $team['body']['$id'], + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals('Project Test', $response['body']['name']); + $this->assertEquals($team['body']['$id'], $response['body']['teamId']); + $this->assertArrayHasKey('platforms', $response['body']); + $this->assertArrayHasKey('webhooks', $response['body']); + $this->assertArrayHasKey('keys', $response['body']); + $this->assertArrayHasKey('tasks', $response['body']); + + $projectId = $response['body']['$id']; + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => '', + 'teamId' => $team['body']['$id'], + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Project Test', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return ['projectId' => $projectId]; + } + + /** + * @depends testCreateProject + */ + public function testListProject($data):array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals($id, $response['body']['projects'][0]['$id']); + $this->assertEquals('Project Test', $response['body']['projects'][0]['name']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testCreateProject + */ + public function testGetProject($data):array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals($id, $response['body']['$id']); + $this->assertEquals('Project Test', $response['body']['name']); + + /** + * Test for FAILURE + */ + + $response = $this->client->call(Client::METHOD_GET, '/projects/empty', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/projects/id-is-really-long-id-is-really-long-id-is-really-long-id-is-really-long', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(400, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProject + */ + public function testGetProjectUsage($data):array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/usage', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertArrayHasKey('collections', $response['body']); + $this->assertArrayHasKey('documents', $response['body']); + $this->assertArrayHasKey('network', $response['body']); + $this->assertArrayHasKey('requests', $response['body']); + $this->assertArrayHasKey('storage', $response['body']); + $this->assertArrayHasKey('tasks', $response['body']); + $this->assertArrayHasKey('users', $response['body']); + $this->assertIsArray($response['body']['collections']['data']); + $this->assertIsInt($response['body']['collections']['total']); + $this->assertIsArray($response['body']['documents']['data']); + $this->assertIsInt($response['body']['documents']['total']); + $this->assertIsArray($response['body']['network']['data']); + $this->assertIsInt($response['body']['network']['total']); + $this->assertIsArray($response['body']['requests']['data']); + $this->assertIsInt($response['body']['requests']['total']); + $this->assertIsInt($response['body']['storage']['total']); + $this->assertIsArray($response['body']['tasks']['data']); + $this->assertIsInt($response['body']['tasks']['total']); + $this->assertIsArray($response['body']['users']['data']); + $this->assertIsInt($response['body']['users']['total']); + + /** + * Test for FAILURE + */ + + $response = $this->client->call(Client::METHOD_GET, '/projects/empty', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/projects/id-is-really-long-id-is-really-long-id-is-really-long-id-is-really-long', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(400, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testGetProjectUsage + */ + public function testUpdateProject($data):array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/'.$id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Project Test 2', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals('Project Test 2', $response['body']['name']); + // $this->assertArrayHasKey('platforms', $response['body']); // TODO enable when response model is enabled + // $this->assertArrayHasKey('webhooks', $response['body']); // TODO enable when response model is enabled + // $this->assertArrayHasKey('keys', $response['body']); // TODO enable when response model is enabled + // $this->assertArrayHasKey('tasks', $response['body']); // TODO enable when response model is enabled + + $projectId = $response['body']['$id']; + + /** + * Test for FAILURE + */ + + $response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => '', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return ['projectId' => $projectId]; + } + + /** + * @depends testGetProjectUsage + */ + public function testUpdateProjectOAuth($data):array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $providers = require('app/config/providers.php'); + + /** + * Test for SUCCESS + */ + + foreach($providers as $key => $provider) { + $response = $this->client->call(Client::METHOD_PATCH, '/projects/'.$id.'/oauth2', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'provider' => $key, + 'appId' => 'AppId-'.ucfirst($key), + 'secret' => 'Secret-'.ucfirst($key), + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + } + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals($id, $response['body']['$id']); + + foreach($providers as $key => $provider) { + $this->assertEquals('AppId-'.ucfirst($key), $response['body']['usersOauth2'.ucfirst($key).'Appid']); + $this->assertEquals('Secret-'.ucfirst($key), $response['body']['usersOauth2'.ucfirst($key).'Secret']); + } + + /** + * Test for FAILURE + */ + + $response = $this->client->call(Client::METHOD_PATCH, '/projects/'.$id.'/oauth2', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'provider' => 'unknown', + 'appId' => 'AppId', + 'secret' => 'Secret', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProject + */ + public function testCreateProjectWebhook($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/webhooks', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Webhook Test', + 'events' => ['account.create', 'account.update.email'], + 'url' => 'https://appwrite.io', + 'security' => true, + 'httpUser' => 'username', + 'httpPass' => 'password', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertContains('account.create', $response['body']['events']); + $this->assertContains('account.update.email', $response['body']['events']); + $this->assertCount(2, $response['body']['events']); + $this->assertEquals('https://appwrite.io', $response['body']['url']); + $this->assertIsBool($response['body']['security']); + $this->assertEquals(true, $response['body']['security']); + $this->assertEquals('username', $response['body']['httpUser']); + + $data = array_merge($data, ['webhookId' => $response['body']['$id']]); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/webhooks', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Webhook Test', + 'events' => ['account.unknown', 'account.update.email'], + 'url' => 'https://appwrite.io', + 'security' => true, + 'httpUser' => 'username', + 'httpPass' => 'password', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectWebhook + */ + public function testListProjectWebhook($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/webhooks', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testCreateProjectWebhook + */ + public function testGetProjectWebhook($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $webhookId = (isset($data['webhookId'])) ? $data['webhookId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/webhooks/'.$webhookId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($webhookId, $response['body']['$id']); + $this->assertContains('account.create', $response['body']['events']); + $this->assertContains('account.update.email', $response['body']['events']); + $this->assertCount(2, $response['body']['events']); + $this->assertEquals('https://appwrite.io', $response['body']['url']); + $this->assertEquals('username', $response['body']['httpUser']); + $this->assertEquals('password', $response['body']['httpPass']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/webhooks/error', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectWebhook + */ + public function testUpdateProjectWebhook($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $webhookId = (isset($data['webhookId'])) ? $data['webhookId'] : ''; + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/webhooks/'.$webhookId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Webhook Test Update', + 'events' => ['account.delete', 'account.sessions.delete', 'storage.files.create'], + 'url' => 'https://appwrite.io/new', + 'security' => false, + 'httpUser' => '', + 'httpPass' => '', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($webhookId, $response['body']['$id']); + $this->assertEquals('Webhook Test Update', $response['body']['name']); + $this->assertContains('account.delete', $response['body']['events']); + $this->assertContains('account.sessions.delete', $response['body']['events']); + $this->assertContains('storage.files.create', $response['body']['events']); + $this->assertCount(3, $response['body']['events']); + $this->assertEquals('https://appwrite.io/new', $response['body']['url']); + $this->assertIsBool($response['body']['security']); + $this->assertEquals(false, $response['body']['security']); + $this->assertEquals('', $response['body']['httpUser']); + // $this->assertEquals('', $response['body']['httpPass']); // TODO add after encrypt refactor + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/webhooks/'.$webhookId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($webhookId, $response['body']['$id']); + $this->assertEquals('Webhook Test Update', $response['body']['name']); + $this->assertContains('account.delete', $response['body']['events']); + $this->assertContains('account.sessions.delete', $response['body']['events']); + $this->assertContains('storage.files.create', $response['body']['events']); + $this->assertCount(3, $response['body']['events']); + $this->assertEquals('https://appwrite.io/new', $response['body']['url']); + $this->assertIsBool($response['body']['security']); + $this->assertEquals(false, $response['body']['security']); + $this->assertEquals('', $response['body']['httpUser']); + // $this->assertEquals('', $response['body']['httpPass']); // TODO add after encrypt refactor + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/webhooks/'.$webhookId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Webhook Test Update', + 'events' => ['account.delete', 'account.sessions.delete', 'storage.files.create', 'unknown'], + 'url' => 'https://appwrite.io/new', + 'security' => false, + 'httpUser' => '', + 'httpPass' => '', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/webhooks/'.$webhookId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Webhook Test Update', + 'events' => ['account.delete', 'account.sessions.delete', 'storage.files.create'], + 'url' => 'appwrite.io/new', + 'security' => false, + 'httpUser' => '', + 'httpPass' => '', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectWebhook + */ + public function testDeleteProjectWebhook($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $webhookId = (isset($data['webhookId'])) ? $data['webhookId'] : ''; + + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/webhooks/'.$webhookId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $response['headers']['status-code']); + $this->assertEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/webhooks/'.$webhookId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/webhooks/error', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + return $data; + } + + // Keys + + /** + * @depends testCreateProject + */ + public function testCreateProjectKey($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/keys', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Key Test', + 'scopes' => ['teams.read', 'teams.write'], + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals('Key Test', $response['body']['name']); + $this->assertContains('teams.read', $response['body']['scopes']); + $this->assertContains('teams.write', $response['body']['scopes']); + $this->assertNotEmpty($response['body']['secret']); + + $data = array_merge($data, ['keyId' => $response['body']['$id']]); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/keys', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Key Test', + 'scopes' => ['unknown'], + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectKey + */ + public function testListProjectKey($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/keys', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testCreateProjectKey + */ + public function testGetProjectKey($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $keyId = (isset($data['keyId'])) ? $data['keyId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/keys/'.$keyId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($keyId, $response['body']['$id']); + $this->assertEquals('Key Test', $response['body']['name']); + $this->assertContains('teams.read', $response['body']['scopes']); + $this->assertContains('teams.write', $response['body']['scopes']); + $this->assertCount(2, $response['body']['scopes']); + $this->assertNotEmpty($response['body']['secret']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/keys/error', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectKey + */ + public function testUpdateProjectKey($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $keyId = (isset($data['keyId'])) ? $data['keyId'] : ''; + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/keys/'.$keyId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Key Test Update', + 'scopes' => ['users.read', 'users.write', 'collections.read'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($keyId, $response['body']['$id']); + $this->assertEquals('Key Test Update', $response['body']['name']); + $this->assertContains('users.read', $response['body']['scopes']); + $this->assertContains('users.write', $response['body']['scopes']); + $this->assertContains('collections.read', $response['body']['scopes']); + $this->assertCount(3, $response['body']['scopes']); + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/keys/'.$keyId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($keyId, $response['body']['$id']); + $this->assertEquals('Key Test Update', $response['body']['name']); + $this->assertContains('users.read', $response['body']['scopes']); + $this->assertContains('users.write', $response['body']['scopes']); + $this->assertContains('collections.read', $response['body']['scopes']); + $this->assertCount(3, $response['body']['scopes']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/keys/'.$keyId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Key Test Update', + 'scopes' => ['users.read', 'users.write', 'collections.read', 'unknown'], + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectKey + */ + public function testDeleteProjectKey($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $keyId = (isset($data['keyId'])) ? $data['keyId'] : ''; + + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/keys/'.$keyId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $response['headers']['status-code']); + $this->assertEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/keys/'.$keyId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/keys/error', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + return $data; + } + + // Tasks + + /** + * @depends testCreateProject + */ + public function testCreateProjectTask($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/tasks', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test', + 'status' => 'play', + 'schedule' => '* * * * *', + 'security' => true, + 'httpMethod' => 'GET', + 'httpUrl' => 'http://example.com', + 'httpHeaders' => ['demo:value'], + 'httpUser' => 'username', + 'httpPass' => 'password', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals('Task Test', $response['body']['name']); + $this->assertEquals('play', $response['body']['status']); + $this->assertEquals(true, $response['body']['security']); + $this->assertEquals('* * * * *', $response['body']['schedule']); + $this->assertEquals('GET', $response['body']['httpMethod']); + $this->assertEquals('http://example.com', $response['body']['httpUrl']); + $this->assertContains('demo:value', $response['body']['httpHeaders']); + $this->assertCount(1, $response['body']['httpHeaders']); + $this->assertEquals('username', $response['body']['httpUser']); + // $this->assertEquals('password', $response['body']['httpPass']); // TODO add after encrypt refactor + + $data = array_merge($data, ['taskId' => $response['body']['$id']]); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/tasks', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test', + 'status' => 'unknown', + 'schedule' => '* * * * *', + 'security' => true, + 'httpMethod' => 'GET', + 'httpUrl' => 'http://example.com', + 'httpHeaders' => ['demo:value'], + 'httpUser' => 'username', + 'httpPass' => 'password', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/tasks', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test', + 'status' => 'play', + 'schedule' => 'unknown', + 'security' => true, + 'httpMethod' => 'GET', + 'httpUrl' => 'http://example.com', + 'httpHeaders' => ['demo:value'], + 'httpUser' => 'username', + 'httpPass' => 'password', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/tasks', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test', + 'status' => 'play', + 'schedule' => '* * * * *', + 'security' => 'string', + 'httpMethod' => 'GET', + 'httpUrl' => 'http://example.com', + 'httpHeaders' => ['demo:value'], + 'httpUser' => 'username', + 'httpPass' => 'password', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/tasks', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test', + 'status' => 'play', + 'schedule' => '* * * * *', + 'security' => true, + 'httpMethod' => 'UNKNOWN', + 'httpUrl' => 'http://example.com', + 'httpHeaders' => ['demo:value'], + 'httpUser' => 'username', + 'httpPass' => 'password', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/tasks', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test', + 'status' => 'play', + 'schedule' => '* * * * *', + 'security' => true, + 'httpMethod' => 'GET', + 'httpUrl' => 'http://example.com', + 'httpHeaders' => 'string', + 'httpUser' => 'username', + 'httpPass' => 'password', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectTask + */ + public function testListProjectTask($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/tasks', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testCreateProjectTask + */ + public function testGetProjectTask($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $taskId = (isset($data['taskId'])) ? $data['taskId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/tasks/'.$taskId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($taskId, $response['body']['$id']); + $this->assertEquals('Task Test', $response['body']['name']); + $this->assertEquals('play', $response['body']['status']); + $this->assertEquals(true, $response['body']['security']); + $this->assertEquals('* * * * *', $response['body']['schedule']); + $this->assertEquals('GET', $response['body']['httpMethod']); + $this->assertEquals('http://example.com', $response['body']['httpUrl']); + $this->assertContains('demo:value', $response['body']['httpHeaders']); + $this->assertCount(1, $response['body']['httpHeaders']); + $this->assertEquals('username', $response['body']['httpUser']); + // $this->assertEquals('password', $response['body']['httpPass']); // TODO add after encrypt refactor + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/tasks/error', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectTask + */ + public function testUpdateProjectTask($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $taskId = (isset($data['taskId'])) ? $data['taskId'] : ''; + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/tasks/'.$taskId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test 2', + 'status' => 'pause', + 'schedule' => '*/5 * * * *', + 'security' => false, + 'httpMethod' => 'POST', + 'httpUrl' => 'http://example.com/demo', + 'httpHeaders' => ['demo1: value1', 'demo2:value2'], + 'httpUser' => 'username1', + 'httpPass' => 'password1', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($taskId, $response['body']['$id']); + $this->assertEquals('Task Test 2', $response['body']['name']); + $this->assertEquals('pause', $response['body']['status']); + $this->assertEquals(false, $response['body']['security']); + $this->assertEquals('*/5 * * * *', $response['body']['schedule']); + $this->assertEquals('POST', $response['body']['httpMethod']); + $this->assertEquals('http://example.com/demo', $response['body']['httpUrl']); + $this->assertContains('demo1: value1', $response['body']['httpHeaders']); + $this->assertContains('demo2:value2', $response['body']['httpHeaders']); + $this->assertCount(2, $response['body']['httpHeaders']); + $this->assertEquals('username1', $response['body']['httpUser']); + // $this->assertEquals('password1', $response['body']['httpPass']); // TODO add after encrypt refactor + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/tasks/'.$taskId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($taskId, $response['body']['$id']); + $this->assertEquals('Task Test 2', $response['body']['name']); + $this->assertEquals('pause', $response['body']['status']); + $this->assertEquals(false, $response['body']['security']); + $this->assertEquals('*/5 * * * *', $response['body']['schedule']); + $this->assertEquals('POST', $response['body']['httpMethod']); + $this->assertEquals('http://example.com/demo', $response['body']['httpUrl']); + $this->assertContains('demo1: value1', $response['body']['httpHeaders']); + $this->assertContains('demo2:value2', $response['body']['httpHeaders']); + $this->assertCount(2, $response['body']['httpHeaders']); + $this->assertEquals('username1', $response['body']['httpUser']); + // $this->assertEquals('password1', $response['body']['httpPass']); // TODO add after encrypt refactor + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/tasks/'.$taskId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test 2', + 'status' => 'pause1', + 'schedule' => '* * * * *', + 'security' => false, + 'httpMethod' => 'POST', + 'httpUrl' => 'http://example.com/demo', + 'httpHeaders' => ['demo1: value1', 'demo2:value2'], + 'httpUser' => 'username1', + 'httpPass' => 'password1', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/tasks/'.$taskId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test 2', + 'status' => 'pause', + 'schedule' => '* * * * *xxx', + 'security' => false, + 'httpMethod' => 'POST', + 'httpUrl' => 'http://example.com/demo', + 'httpHeaders' => ['demo1: value1', 'demo2:value2'], + 'httpUser' => 'username1', + 'httpPass' => 'password1', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/tasks/'.$taskId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test 2', + 'status' => 'pause', + 'schedule' => '* * * * *', + 'security' => 'string', + 'httpMethod' => 'POST', + 'httpUrl' => 'http://example.com/demo', + 'httpHeaders' => ['demo1: value1', 'demo2:value2'], + 'httpUser' => 'username1', + 'httpPass' => 'password1', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/tasks/'.$taskId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test 2', + 'status' => 'pause', + 'schedule' => '* * * * *', + 'security' => false, + 'httpMethod' => 'UNKNOWN', + 'httpUrl' => 'http://example.com/demo', + 'httpHeaders' => ['demo1: value1', 'demo2:value2'], + 'httpUser' => 'username1', + 'httpPass' => 'password1', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/tasks/'.$taskId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test 2', + 'status' => 'pause', + 'schedule' => '* * * * *', + 'security' => false, + 'httpMethod' => 'POST', + 'httpUrl' => 'example.com/demo', + 'httpHeaders' => ['demo1: value1', 'demo2:value2'], + 'httpUser' => 'username1', + 'httpPass' => 'password1', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/tasks/'.$taskId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Task Test 2', + 'status' => 'pause', + 'schedule' => '* * * * *', + 'security' => false, + 'httpMethod' => 'POST', + 'httpUrl' => 'http://example.com/demo', + 'httpHeaders' => 'string', + 'httpUser' => 'username1', + 'httpPass' => 'password1', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectTask + */ + public function testDeleteProjectTask($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $taskId = (isset($data['taskId'])) ? $data['taskId'] : ''; + + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/tasks/'.$taskId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $response['headers']['status-code']); + $this->assertEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/tasks/'.$taskId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/tasks/error', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + return $data; + } + + // Platforms + + /** + * @depends testCreateProject + */ + public function testCreateProjectPlatform($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/platforms', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'type' => 'web', + 'name' => 'Web App', + 'key' => '', + 'store' => '', + 'hostname' => 'localhost', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals('web', $response['body']['type']); + $this->assertEquals('Web App', $response['body']['name']); + $this->assertEquals('', $response['body']['key']); + $this->assertEquals('', $response['body']['store']); + $this->assertEquals('localhost', $response['body']['hostname']); + + $data = array_merge($data, ['platformWebId' => $response['body']['$id']]); + + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/platforms', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'type' => 'flutter-ios', + 'name' => 'Flutter App (iOS)', + 'key' => 'com.example.ios', + 'store' => '', + 'hostname' => '', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals('flutter-ios', $response['body']['type']); + $this->assertEquals('Flutter App (iOS)', $response['body']['name']); + $this->assertEquals('com.example.ios', $response['body']['key']); + $this->assertEquals('', $response['body']['store']); + $this->assertEquals('', $response['body']['hostname']); + + $data = array_merge($data, ['platformFultteriOSId' => $response['body']['$id']]); + + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/platforms', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'type' => 'flutter-android', + 'name' => 'Flutter App (Android)', + 'key' => 'com.example.android', + 'store' => '', + 'hostname' => '', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals('flutter-android', $response['body']['type']); + $this->assertEquals('Flutter App (Android)', $response['body']['name']); + $this->assertEquals('com.example.android', $response['body']['key']); + $this->assertEquals('', $response['body']['store']); + $this->assertEquals('', $response['body']['hostname']); + + $data = array_merge($data, ['platformFultterAndroidId' => $response['body']['$id']]); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/platforms', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'type' => 'unknown', + 'name' => 'Web App', + 'key' => '', + 'store' => '', + 'hostname' => 'localhost', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/platforms', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'type' => 'web', + // 'name' => 'Web App', + // 'key' => '', + // 'store' => '', + // 'hostname' => 'https://localhost', + // ]); + + // $this->assertEquals(400, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectPlatform + */ + public function testListProjectPlatform($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/platforms', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(3, $response['body']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testCreateProjectPlatform + */ + public function testGetProjectPlatform($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $platformWebId = (isset($data['platformWebId'])) ? $data['platformWebId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/platforms/'.$platformWebId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($platformWebId, $response['body']['$id']); + $this->assertEquals('web', $response['body']['type']); + $this->assertEquals('Web App', $response['body']['name']); + $this->assertEquals('', $response['body']['key']); + $this->assertEquals('', $response['body']['store']); + $this->assertEquals('localhost', $response['body']['hostname']); + + $platformFultteriOSId = (isset($data['platformFultteriOSId'])) ? $data['platformFultteriOSId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/platforms/'.$platformFultteriOSId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($platformFultteriOSId, $response['body']['$id']); + $this->assertEquals('flutter-ios', $response['body']['type']); + $this->assertEquals('Flutter App (iOS)', $response['body']['name']); + $this->assertEquals('com.example.ios', $response['body']['key']); + $this->assertEquals('', $response['body']['store']); + $this->assertEquals('', $response['body']['hostname']); + + $platformFultterAndroidId = (isset($data['platformFultterAndroidId'])) ? $data['platformFultterAndroidId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/platforms/'.$platformFultterAndroidId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($platformFultterAndroidId, $response['body']['$id']); + $this->assertEquals('flutter-android', $response['body']['type']); + $this->assertEquals('Flutter App (Android)', $response['body']['name']); + $this->assertEquals('com.example.android', $response['body']['key']); + $this->assertEquals('', $response['body']['store']); + $this->assertEquals('', $response['body']['hostname']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/platforms/error', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectPlatform + */ + public function testUpdateProjectPlatform($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $platformWebId = (isset($data['platformWebId'])) ? $data['platformWebId'] : ''; + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/platforms/'.$platformWebId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Web App 2', + 'key' => '', + 'store' => '', + 'hostname' => 'localhost-new', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($platformWebId, $response['body']['$id']); + $this->assertEquals('web', $response['body']['type']); + $this->assertEquals('Web App 2', $response['body']['name']); + $this->assertEquals('', $response['body']['key']); + $this->assertEquals('', $response['body']['store']); + $this->assertEquals('localhost-new', $response['body']['hostname']); + + $platformFultteriOSId = (isset($data['platformFultteriOSId'])) ? $data['platformFultteriOSId'] : ''; + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/platforms/'.$platformFultteriOSId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Flutter App (iOS) 2', + 'key' => 'com.example.ios2', + 'store' => '', + 'hostname' => '', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($platformFultteriOSId, $response['body']['$id']); + $this->assertEquals('flutter-ios', $response['body']['type']); + $this->assertEquals('Flutter App (iOS) 2', $response['body']['name']); + $this->assertEquals('com.example.ios2', $response['body']['key']); + $this->assertEquals('', $response['body']['store']); + $this->assertEquals('', $response['body']['hostname']); + + $platformFultterAndroidId = (isset($data['platformFultterAndroidId'])) ? $data['platformFultterAndroidId'] : ''; + + $response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/platforms/'.$platformFultterAndroidId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Flutter App (Android) 2', + 'key' => 'com.example.android2', + 'store' => '', + 'hostname' => '', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($platformFultterAndroidId, $response['body']['$id']); + $this->assertEquals('flutter-android', $response['body']['type']); + $this->assertEquals('Flutter App (Android) 2', $response['body']['name']); + $this->assertEquals('com.example.android2', $response['body']['key']); + $this->assertEquals('', $response['body']['store']); + $this->assertEquals('', $response['body']['hostname']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testCreateProjectPlatform + */ + public function testDeleteProjectPlatform($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $platformWebId = (isset($data['platformWebId'])) ? $data['platformWebId'] : ''; + + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/platforms/'.$platformWebId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $response['headers']['status-code']); + $this->assertEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/platforms/'.$platformWebId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + $platformFultteriOSId = (isset($data['platformFultteriOSId'])) ? $data['platformFultteriOSId'] : ''; + + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/platforms/'.$platformFultteriOSId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $response['headers']['status-code']); + $this->assertEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/platforms/'.$platformFultteriOSId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + $platformFultterAndroidId = (isset($data['platformFultterAndroidId'])) ? $data['platformFultterAndroidId'] : ''; + + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/platforms/'.$platformFultterAndroidId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $response['headers']['status-code']); + $this->assertEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/platforms/'.$platformFultterAndroidId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/tasks/error', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + return $data; + } + + // Domains + + /** + * @depends testCreateProject + */ + public function testCreateProjectDomain($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/domains', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => 'sub.example.com', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertIsInt($response['body']['updated']); + $this->assertEquals('sub.example.com', $response['body']['domain']); + $this->assertEquals('com', $response['body']['tld']); + $this->assertEquals('example.com', $response['body']['registerable']); + $this->assertEquals(false, $response['body']['verification']); + + $data = array_merge($data, ['domainId' => $response['body']['$id']]); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/platforms', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => '123', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectDomain + */ + public function testListProjectDomain($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/domains', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testCreateProjectDomain + */ + public function testGetProjectDomain($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $domainId = (isset($data['domainId'])) ? $data['domainId'] : ''; + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/domains/'.$domainId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($domainId, $response['body']['$id']); + $this->assertIsInt($response['body']['updated']); + $this->assertEquals('sub.example.com', $response['body']['domain']); + $this->assertEquals('com', $response['body']['tld']); + $this->assertEquals('example.com', $response['body']['registerable']); + $this->assertEquals(false, $response['body']['verification']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/domains/error', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + return $data; + } + + /** + * @depends testCreateProjectDomain + */ + public function testUpdateProjectDomain($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $domainId = (isset($data['domainId'])) ? $data['domainId'] : ''; + + $response = $this->client->call(Client::METHOD_PATCH, '/projects/'.$id.'/domains/'.$domainId.'/verification', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(401, $response['headers']['status-code']); + + /** + * Test for FAILURE + */ + + return $data; + } + + /** + * @depends testCreateProjectDomain + */ + public function testDeleteProjectDomain($data): array + { + $id = (isset($data['projectId'])) ? $data['projectId'] : ''; + $domainId = (isset($data['domainId'])) ? $data['domainId'] : ''; + + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/domains/'.$domainId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $response['headers']['status-code']); + $this->assertEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/domains/'.$domainId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_DELETE, '/projects/'.$id.'/domains/error', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(404, $response['headers']['status-code']); + + return $data; + } +} diff --git a/tests/e2e/Services/Projects/ProjectsCustomClientTest.php b/tests/e2e/Services/Projects/ProjectsCustomClientTest.php new file mode 100644 index 0000000000..e9e3049937 --- /dev/null +++ b/tests/e2e/Services/Projects/ProjectsCustomClientTest.php @@ -0,0 +1,19 @@ +assertEquals(true, true); + } +} \ No newline at end of file diff --git a/tests/e2e/Services/Projects/ProjectsCustomServerTest.php b/tests/e2e/Services/Projects/ProjectsCustomServerTest.php new file mode 100644 index 0000000000..e232848460 --- /dev/null +++ b/tests/e2e/Services/Projects/ProjectsCustomServerTest.php @@ -0,0 +1,19 @@ +assertEquals(true, true); + } +} \ No newline at end of file diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index cf0fd50bd3..0c7abc0f8d 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -78,7 +78,7 @@ trait StorageBase ], $this->getHeaders())); $this->assertEquals(200, $file2['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $file2['headers']['content-type']); + $this->assertEquals('image/png', $file2['headers']['content-type']); $this->assertNotEmpty($file2['body']); $file3 = $this->client->call(Client::METHOD_GET, '/storage/files/' . $data['fileId'] . '/download', array_merge([ @@ -88,7 +88,7 @@ trait StorageBase $this->assertEquals(200, $file3['headers']['status-code']); $this->assertEquals('attachment; filename="logo.png"', $file3['headers']['content-disposition']); - $this->assertEquals('image/png; charset=UTF-8', $file3['headers']['content-type']); + $this->assertEquals('image/png', $file3['headers']['content-type']); $this->assertNotEmpty($file3['body']); $file4 = $this->client->call(Client::METHOD_GET, '/storage/files/' . $data['fileId'] . '/view', array_merge([ @@ -97,7 +97,7 @@ trait StorageBase ], $this->getHeaders())); $this->assertEquals(200, $file4['headers']['status-code']); - $this->assertEquals('image/png; charset=UTF-8', $file4['headers']['content-type']); + $this->assertEquals('image/png', $file4['headers']['content-type']); $this->assertNotEmpty($file4['body']); /** @@ -186,6 +186,13 @@ trait StorageBase $this->assertEquals(204, $file['headers']['status-code']); $this->assertEmpty($file['body']); + + $file = $this->client->call(Client::METHOD_GET, '/storage/files/' . $data['fileId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $file['headers']['status-code']); /** * Test for FAILURE diff --git a/tests/e2e/Services/Teams/TeamsBase.php b/tests/e2e/Services/Teams/TeamsBase.php index 443144cfeb..73bdfe7199 100644 --- a/tests/e2e/Services/Teams/TeamsBase.php +++ b/tests/e2e/Services/Teams/TeamsBase.php @@ -115,7 +115,7 @@ trait TeamsBase $this->assertEquals(200, $response['headers']['status-code']); $this->assertGreaterThan(0, $response['body']['sum']); $this->assertIsInt($response['body']['sum']); - $this->assertCount(3, $response['body']['teams']); + $this->assertGreaterThan(2, count($response['body']['teams'])); $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ 'content-type' => 'application/json', @@ -139,7 +139,7 @@ trait TeamsBase $this->assertEquals(200, $response['headers']['status-code']); $this->assertGreaterThan(0, $response['body']['sum']); $this->assertIsInt($response['body']['sum']); - $this->assertCount(2, $response['body']['teams']); + $this->assertGreaterThan(2, $response['body']['teams']); $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/Teams/TeamsBaseServer.php b/tests/e2e/Services/Teams/TeamsBaseServer.php index 8de0f2ede8..1547a4e289 100644 --- a/tests/e2e/Services/Teams/TeamsBaseServer.php +++ b/tests/e2e/Services/Teams/TeamsBaseServer.php @@ -30,4 +30,93 @@ trait TeamsBaseServer return []; } + + + /** + * @depends testCreateTeam + */ + public function testCreateTeamMembership($data):array + { + $teamUid = (isset($data['teamUid'])) ? $data['teamUid'] : ''; + $teamName = (isset($data['teamName'])) ? $data['teamName'] : ''; + $email = uniqid().'friend@localhost.test'; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_POST, '/teams/'.$teamUid.'/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'email' => $email, + 'name' => 'Friend User', + 'roles' => ['admin', 'editor'], + 'url' => 'http://localhost:5000/join-us#title' + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertNotEmpty($response['body']['userId']); + $this->assertNotEmpty($response['body']['teamId']); + $this->assertCount(2, $response['body']['roles']); + $this->assertIsInt($response['body']['joined']); + $this->assertGreaterThan(0, $response['body']['joined']); + $this->assertEquals(true, $response['body']['confirm']); + + $userUid = $response['body']['userId']; + + // $response = $this->client->call(Client::METHOD_GET, '/users/'.$userUid, array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), []); + + // $this->assertEquals($userUid, $response['body']['$id']); + // $this->assertContains('team:'.$teamUid, $response['body']['roles']); + // $this->assertContains('team:'.$teamUid.'/admin', $response['body']['roles']); + // $this->assertContains('team:'.$teamUid.'/editor', $response['body']['roles']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/teams/'.$teamUid.'/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'email' => 'dasdkaskdjaskdjasjkd', + 'name' => 'Friend User', + 'roles' => ['admin', 'editor'], + 'url' => 'http://localhost:5000/join-us#title' + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/teams/'.$teamUid.'/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'email' => $email, + 'name' => 'Friend User', + 'roles' => 'bad string', + 'url' => 'http://localhost:5000/join-us#title' + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/teams/'.$teamUid.'/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'email' => $email, + 'name' => 'Friend User', + 'roles' => ['admin', 'editor'], + 'url' => 'http://example.com/join-us#title' // bad url + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + return [ + 'teamUid' => $teamUid, + 'userUid' => $userUid, + ]; + } } \ No newline at end of file diff --git a/tests/resources/docker/.env b/tests/resources/docker/.env new file mode 100644 index 0000000000..1c1359f0fe --- /dev/null +++ b/tests/resources/docker/.env @@ -0,0 +1,3 @@ +_APP_X=value1 +_APP_Y=value2 +_APP_Z = value3 \ No newline at end of file diff --git a/tests/resources/docker/docker-compose.yml b/tests/resources/docker/docker-compose.yml new file mode 100644 index 0000000000..4a31929470 --- /dev/null +++ b/tests/resources/docker/docker-compose.yml @@ -0,0 +1,409 @@ +version: '3' + +services: + traefik: + image: traefik:2.2 + container_name: appwrite-traefik + command: + - --log.level=DEBUG + - --api.insecure=true + - --providers.file.directory=/storage/config + - --providers.file.watch=true + - --providers.docker=true + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --accesslog=true + restart: unless-stopped + ports: + - 2080:80 + - 2443:443 + - 8080:8080 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - appwrite-config:/storage/config:ro + - appwrite-certificates:/storage/certificates:ro + depends_on: + - appwrite + networks: + - gateway + - appwrite + + appwrite: + container_name: appwrite + build: + context: . + args: + - TESTING=true + - VERSION=dev + restart: unless-stopped + ports: + - 9501:80 + networks: + - appwrite + labels: + - traefik.http.routers.appwrite.rule=PathPrefix(`/`) + - traefik.http.routers.appwrite-secure.rule=PathPrefix(`/`) + - traefik.http.routers.appwrite-secure.tls=true + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - appwrite-uploads:/storage/uploads:rw + - appwrite-cache:/storage/cache:rw + - appwrite-config:/storage/config:rw + - appwrite-certificates:/storage/certificates:rw + - appwrite-functions:/storage/functions:rw + - ./phpunit.xml:/usr/src/code/phpunit.xml + - ./tests:/usr/src/code/tests + - ./app:/usr/src/code/app + # - ./vendor:/usr/src/code/vendor + - ./docs:/usr/src/code/docs + - ./public:/usr/src/code/public + - ./src:/usr/src/code/src + - ./debug:/tmp + depends_on: + - mariadb + - redis + - clamav + - influxdb + environment: + - _APP_ENV + - _APP_OPTIONS_ABUSE + - _APP_OPTIONS_FORCE_HTTPS + - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN + - _APP_DOMAIN_TARGET + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_INFLUXDB_HOST + - _APP_INFLUXDB_PORT + - _APP_STORAGE_LIMIT + - _APP_FUNCTIONS_TIMEOUT + - _APP_FUNCTIONS_CONTAINERS + + appwrite-worker-usage: + entrypoint: worker-usage + container_name: appwrite-worker-usage + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - telegraf + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_STATSD_HOST + - _APP_STATSD_PORT + + appwrite-worker-audits: + entrypoint: worker-audits + container_name: appwrite-worker-audits + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-webhooks: + entrypoint: worker-webhooks + container_name: appwrite-worker-webhooks + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-tasks: + entrypoint: worker-tasks + container_name: appwrite-worker-tasks + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-deletes: + entrypoint: worker-deletes + container_name: appwrite-worker-deletes + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + volumes: + - appwrite-uploads:/storage/uploads:rw + - appwrite-cache:/storage/cache:rw + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-certificates: + entrypoint: worker-certificates + container_name: appwrite-worker-certificates + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + volumes: + - appwrite-config:/storage/config:rw + - appwrite-certificates:/storage/certificates:rw + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + + appwrite-worker-functions: + entrypoint: worker-functions + container_name: appwrite-worker-functions + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - mariadb + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /tmp:/tmp:rw + - appwrite-functions:/storage/functions:rw + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_FUNCTIONS_TIMEOUT + - _APP_FUNCTIONS_CONTAINERS + + appwrite-worker-mails: + entrypoint: worker-mails + container_name: appwrite-worker-mails + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + - maildev + # - smtp + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_SMTP_HOST + - _APP_SMTP_PORT + + appwrite-schedule: + entrypoint: schedule + container_name: appwrite-schedule + build: + context: . + restart: unless-stopped + networks: + - appwrite + depends_on: + - redis + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + + mariadb: + image: appwrite/mariadb:1.0.3 # fix issues when upgrading using: mysql_upgrade -u root -p + container_name: appwrite-mariadb + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-mariadb:/var/lib/mysql:rw + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD=rootsecretpassword + - MYSQL_DATABASE=appwrite + - MYSQL_USER=user + - MYSQL_PASSWORD=password + command: 'mysqld --innodb-flush-method=fsync' + + maildev: + image: djfarrelly/maildev + container_name: appwrite-maildev + restart: unless-stopped + ports: + - '1080:80' + networks: + - appwrite + + # smtp: + # image: appwrite/smtp:1.0.1 + # container_name: appwrite-smtp + # restart: unless-stopped + # networks: + # - appwrite + # environment: + # - MAILNAME=appwrite + # - RELAY_NETWORKS=:192.168.0.0/24:10.0.0.0/16 + + redis: + image: redis:5.0 + container_name: appwrite-redis + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-redis:/data:rw + + clamav: + image: appwrite/clamav:1.0.12 + container_name: appwrite-clamav + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-uploads:/storage/uploads + + influxdb: + image: influxdb:1.6 + container_name: appwrite-influxdb + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-influxdb:/var/lib/influxdb:rw + + telegraf: + image: appwrite/telegraf:1.0.0 + container_name: appwrite-telegraf + restart: unless-stopped + networks: + - appwrite + + # redis-commander: + # image: rediscommander/redis-commander:latest + # restart: unless-stopped + # networks: + # - appwrite + # environment: + # - REDIS_HOSTS=redis + # ports: + # - "8081:8081" + + # resque: + # image: registry.gitlab.com/appwrite/appwrite/resque-web:v1.0.2 + # restart: unless-stopped + # networks: + # - appwrite + # ports: + # - "5678:5678" + # environment: + # - RESQUE_WEB_HOST=redis + # - RESQUE_WEB_PORT=6379 + # - RESQUE_WEB_HTTP_BASIC_AUTH_USER=user + # - RESQUE_WEB_HTTP_BASIC_AUTH_PASSWORD=password + + # chronograf: + # image: chronograf:1.5 + # container_name: appwrite-chronograf + # restart: unless-stopped + # networks: + # - appwrite + # volumes: + # - appwrite-chronograf:/var/lib/chronograf + # ports: + # - "8888:8888" + # environment: + # - INFLUXDB_URL=http://influxdb:8086 + # - KAPACITOR_URL=http://kapacitor:9092 + # - AUTH_DURATION=48h + # - TOKEN_SECRET=duperduper5674829!jwt + # - GH_CLIENT_ID=d86f7145a41eacfc52cc + # - GH_CLIENT_SECRET=9e0081062367a2134e7f2ea95ba1a32d08b6c8ab + # - GH_ORGS=appwrite + + # webgrind: + # image: 'jokkedk/webgrind:latest' + # volumes: + # - './debug:/tmp' + # ports: + # - '3001:80' + +networks: + gateway: + appwrite: + +volumes: + appwrite-mariadb: + appwrite-redis: + appwrite-cache: + appwrite-uploads: + appwrite-certificates: + appwrite-functions: + appwrite-influxdb: + appwrite-chronograf: + appwrite-config: diff --git a/tests/resources/functions/php-fx.tar.gz b/tests/resources/functions/php-fx.tar.gz new file mode 100644 index 0000000000..f8e3cbd811 Binary files /dev/null and b/tests/resources/functions/php-fx.tar.gz differ diff --git a/tests/resources/functions/python-fx.tar.gz b/tests/resources/functions/python-fx.tar.gz new file mode 100644 index 0000000000..22edb784b1 Binary files /dev/null and b/tests/resources/functions/python-fx.tar.gz differ diff --git a/tests/resources/resize/100x100-q30.jpg b/tests/resources/resize/100x100-q30.jpg index 67cfd6b951..23933efc86 100644 Binary files a/tests/resources/resize/100x100-q30.jpg and b/tests/resources/resize/100x100-q30.jpg differ diff --git a/tests/resources/resize/100x100.gif b/tests/resources/resize/100x100.gif index 30ef716cd0..864dc7c024 100644 Binary files a/tests/resources/resize/100x100.gif and b/tests/resources/resize/100x100.gif differ diff --git a/tests/resources/resize/100x100.jpg b/tests/resources/resize/100x100.jpg index 24c694a77a..21a69c0e60 100644 Binary files a/tests/resources/resize/100x100.jpg and b/tests/resources/resize/100x100.jpg differ diff --git a/tests/resources/resize/100x100.png b/tests/resources/resize/100x100.png index b66a6a2870..14288e2019 100644 Binary files a/tests/resources/resize/100x100.png and b/tests/resources/resize/100x100.png differ diff --git a/tests/resources/resize/100x100.webp b/tests/resources/resize/100x100.webp index 1d29b20e90..c0e1cc922f 100644 Binary files a/tests/resources/resize/100x100.webp and b/tests/resources/resize/100x100.webp differ diff --git a/tests/resources/resize/100x400.jpg b/tests/resources/resize/100x400.jpg index 8c33ac005d..0515c98d52 100644 Binary files a/tests/resources/resize/100x400.jpg and b/tests/resources/resize/100x400.jpg differ diff --git a/tests/resources/resize/400x100.jpg b/tests/resources/resize/400x100.jpg index 447e2490fb..caa6cd18f5 100644 Binary files a/tests/resources/resize/400x100.jpg and b/tests/resources/resize/400x100.jpg differ diff --git a/tests/unit/Database/Validator/UIDTest.php b/tests/unit/Database/Validator/UIDTest.php new file mode 100644 index 0000000000..158223b489 --- /dev/null +++ b/tests/unit/Database/Validator/UIDTest.php @@ -0,0 +1,30 @@ +object = new UID(); + } + + public function tearDown() + { + } + + public function testValues() + { + $this->assertEquals($this->object->isValid('5f058a8925807'), true); + $this->assertEquals($this->object->isValid('5f058a89258075f058a89258075f058t'), true); + $this->assertEquals($this->object->isValid('5f058a89258075f058a89258075f058tx'), false); + } +} \ No newline at end of file diff --git a/tests/unit/Docker/ComposeTest.php b/tests/unit/Docker/ComposeTest.php new file mode 100644 index 0000000000..b7d7c40fab --- /dev/null +++ b/tests/unit/Docker/ComposeTest.php @@ -0,0 +1,59 @@ +object = new Compose($data); + } + + public function tearDown() + { + } + + public function testVersion() + { + $this->assertEquals('3', $this->object->getVersion()); + } + + public function testServices() + { + $this->assertCount(17, $this->object->getServices()); + $this->assertEquals('appwrite-telegraf', $this->object->getService('telegraf')->getContainerName()); + $this->assertEquals('appwrite', $this->object->getService('appwrite')->getContainerName()); + $this->assertEquals('', $this->object->getService('appwrite')->getImageVersion()); + $this->assertEquals('2.2', $this->object->getService('traefik')->getImageVersion()); + $this->assertEquals(['2080' => '80', '2443' => '443', '8080' => '8080'], $this->object->getService('traefik')->getPorts()); + } + + public function testNetworks() + { + $this->assertCount(2, $this->object->getNetworks()); + } + + public function testVolumes() + { + $this->assertCount(9, $this->object->getVolumes()); + $this->assertEquals('appwrite-mariadb', $this->object->getVolumes()[0]); + $this->assertEquals('appwrite-redis', $this->object->getVolumes()[1]); + $this->assertEquals('appwrite-cache', $this->object->getVolumes()[2]); + } +} \ No newline at end of file diff --git a/tests/unit/Docker/EnvTest.php b/tests/unit/Docker/EnvTest.php new file mode 100644 index 0000000000..ce4d4b3917 --- /dev/null +++ b/tests/unit/Docker/EnvTest.php @@ -0,0 +1,49 @@ +object = new Env($data); + } + + public function tearDown() + { + } + + public function testVars() + { + $this->object->setVar('_APP_TEST', 'value4'); + + $this->assertEquals('value1', $this->object->getVar('_APP_X')); + $this->assertEquals('value2', $this->object->getVar('_APP_Y')); + $this->assertEquals('value3', $this->object->getVar('_APP_Z')); + $this->assertEquals('value4', $this->object->getVar('_APP_TEST')); + } + + public function testExport() + { + $this->assertEquals("_APP_X=value1 +_APP_Y=value2 +_APP_Z=value3 +", $this->object->export()); + } +} \ No newline at end of file diff --git a/tests/unit/Event/EventTest.php b/tests/unit/Event/EventTest.php index 151dab1233..cbcd1bdce1 100644 --- a/tests/unit/Event/EventTest.php +++ b/tests/unit/Event/EventTest.php @@ -2,9 +2,9 @@ namespace Appwrite\Tests; -use Utopia\Request; use Appwrite\Event\Event; use PHPUnit\Framework\TestCase; +use Utopia\App; class EventTest extends TestCase { @@ -20,9 +20,8 @@ class EventTest extends TestCase public function setUp() { - $request = new Request(); - $redisHost = $request->getServer('_APP_REDIS_HOST', ''); - $redisPort = $request->getServer('_APP_REDIS_PORT', ''); + $redisHost = App::getEnv('_APP_REDIS_HOST', ''); + $redisPort = App::getEnv('_APP_REDIS_PORT', ''); \Resque::setBackend($redisHost.':'.$redisPort); $this->queue = 'v1-tests' . uniqid(); @@ -42,9 +41,26 @@ class EventTest extends TestCase $this->object->trigger(); - $this->assertEquals('value1', $this->object->getParam('key1')); - $this->assertEquals('value2', $this->object->getParam('key2')); + $this->assertEquals(null, $this->object->getParam('key1')); + $this->assertEquals(null, $this->object->getParam('key2')); $this->assertEquals(null, $this->object->getParam('key3')); $this->assertEquals(\Resque::size($this->queue), 1); } + + public function testReset() + { + $this->object + ->setParam('key1', 'value1') + ->setParam('key2', 'value2') + ; + + $this->assertEquals('value1', $this->object->getParam('key1')); + $this->assertEquals('value2', $this->object->getParam('key2')); + + $this->object->reset(); + + $this->assertEquals(null, $this->object->getParam('key1')); + $this->assertEquals(null, $this->object->getParam('key2')); + $this->assertEquals(null, $this->object->getParam('key3')); + } } \ No newline at end of file diff --git a/tests/unit/Resize/ResizeTest.php b/tests/unit/Resize/ResizeTest.php index b5926bfdc7..f75d7d1e0f 100644 --- a/tests/unit/Resize/ResizeTest.php +++ b/tests/unit/Resize/ResizeTest.php @@ -19,14 +19,18 @@ class ResizeTest extends TestCase { $resize = new Resize(\file_get_contents(__DIR__ . '/../../resources/disk-a/kitten-1.jpg')); $target = __DIR__.'/100x100.jpg'; - $original = __DIR__.'/../../resources/resize/100x100.jpg'; $resize->crop(100, 100); $resize->save($target, 'jpg', 100); - $this->assertEquals(is_readable($target), true); - $this->assertEquals(\md5(\file_get_contents($target)), \md5(\file_get_contents($original))); + $this->assertEquals(\is_readable($target), true); + $this->assertNotEmpty(\md5(\file_get_contents($target))); + + $image = new \Imagick($target); + $this->assertEquals(100, $image->getImageWidth()); + $this->assertEquals(100, $image->getImageHeight()); + $this->assertEquals('JPEG', $image->getImageFormat()); \unlink($target); } @@ -35,14 +39,18 @@ class ResizeTest extends TestCase { $resize = new Resize(\file_get_contents(__DIR__ . '/../../resources/disk-a/kitten-1.jpg')); $target = __DIR__.'/100x400.jpg'; - $original = __DIR__.'/../../resources/resize/100x400.jpg'; $resize->crop(100, 400); $resize->save($target, 'jpg', 100); - $this->assertEquals(is_readable($target), true); - $this->assertEquals(\md5(\file_get_contents($target)), \md5(\file_get_contents($original))); + $this->assertEquals(\is_readable($target), true); + $this->assertNotEmpty(\md5(\file_get_contents($target))); + + $image = new \Imagick($target); + $this->assertEquals(100, $image->getImageWidth()); + $this->assertEquals(400, $image->getImageHeight()); + $this->assertEquals('JPEG', $image->getImageFormat()); \unlink($target); } @@ -51,14 +59,18 @@ class ResizeTest extends TestCase { $resize = new Resize(\file_get_contents(__DIR__ . '/../../resources/disk-a/kitten-1.jpg')); $target = __DIR__.'/400x100.jpg'; - $original = __DIR__.'/../../resources/resize/400x100.jpg'; $resize->crop(400, 100); $resize->save($target, 'jpg', 100); - $this->assertEquals(is_readable($target), true); - $this->assertEquals(\md5(\file_get_contents($target)), \md5(\file_get_contents($original))); + $this->assertEquals(\is_readable($target), true); + $this->assertNotEmpty(\md5(\file_get_contents($target))); + + $image = new \Imagick($target); + $this->assertEquals(400, $image->getImageWidth()); + $this->assertEquals(100, $image->getImageHeight()); + $this->assertEquals('JPEG', $image->getImageFormat()); \unlink($target); } @@ -73,8 +85,14 @@ class ResizeTest extends TestCase $resize->save($target, 'webp', 100); - $this->assertEquals(is_readable($target), true); - $this->assertEquals(\md5(\file_get_contents($target)), \md5(\file_get_contents($original))); + $this->assertEquals(\is_readable($target), true); + $this->assertNotEmpty(\md5(\file_get_contents($target))); + + $image = new \Imagick($target); + + $this->assertEquals(100, $image->getImageWidth()); + $this->assertEquals(100, $image->getImageHeight()); + $this->assertTrue(in_array($image->getImageFormat(), ['PAM', 'WEBP'])); \unlink($target); } @@ -89,9 +107,16 @@ class ResizeTest extends TestCase $resize->save($target, 'png', 100); - $this->assertEquals(is_readable($target), true); - $this->assertEquals(\filesize($target), \filesize($original)); + $this->assertEquals(\is_readable($target), true); + $this->assertGreaterThan(15000, \filesize($target)); + $this->assertLessThan(30000, \filesize($target)); $this->assertEquals(\mime_content_type($target), \mime_content_type($original)); + $this->assertNotEmpty(\md5(\file_get_contents($target))); + + $image = new \Imagick($target); + $this->assertEquals(100, $image->getImageWidth()); + $this->assertEquals(100, $image->getImageHeight()); + $this->assertEquals('PNG', $image->getImageFormat()); \unlink($target); } @@ -106,9 +131,16 @@ class ResizeTest extends TestCase $resize->save($target, 'jpg', 10); - $this->assertEquals(is_readable($target), true); - $this->assertEquals(\filesize($target), \filesize($original)); + $this->assertEquals(\is_readable($target), true); + $this->assertGreaterThan(500, \filesize($target)); + $this->assertLessThan(2000, \filesize($target)); $this->assertEquals(\mime_content_type($target), \mime_content_type($original)); + $this->assertNotEmpty(\md5(\file_get_contents($target))); + + $image = new \Imagick($target); + $this->assertEquals(100, $image->getImageWidth()); + $this->assertEquals(100, $image->getImageHeight()); + $this->assertEquals('JPEG', $image->getImageFormat()); \unlink($target); } @@ -123,23 +155,16 @@ class ResizeTest extends TestCase $resize->save($target, 'gif', 100); - $this->assertEquals(is_readable($target), true); - $this->assertEquals(\filesize($target), \filesize($original)); + $this->assertEquals(\is_readable($target), true); + $this->assertGreaterThan(400000, \filesize($target)); + $this->assertLessThan(800000, \filesize($target)); $this->assertEquals(\mime_content_type($target), \mime_content_type($original)); + $this->assertNotEmpty(\md5(\file_get_contents($target))); + $image = new \Imagick($target); + $this->assertEquals(100, $image->getImageWidth()); + $this->assertEquals(100, $image->getImageHeight()); + $this->assertEquals('GIF', $image->getImageFormat()); \unlink($target); } - - public function testCrop100x100Output() - { - $resize = new Resize(\file_get_contents(__DIR__ . '/../../resources/disk-a/kitten-1.jpg')); - $original = __DIR__.'/../../resources/resize/100x100.jpg'; - - $resize->crop(100, 100); - - $result = $resize->output('jpg', 100); - - $this->assertEquals(!empty($result), true); - $this->assertEquals(\md5($result), \md5(\file_get_contents($original))); - } } \ No newline at end of file diff --git a/tests/unit/Storage/Device/LocalTest.php b/tests/unit/Storage/Device/LocalTest.php index c71b831604..3d00138fbe 100644 --- a/tests/unit/Storage/Device/LocalTest.php +++ b/tests/unit/Storage/Device/LocalTest.php @@ -33,14 +33,14 @@ class LocalTest extends TestCase public function testRoot() { - $this->assertEquals($this->object->getRoot(), '/usr/share/nginx/html/tests/resources/disk-a'); + $this->assertEquals($this->object->getRoot(), '/usr/src/code/tests/resources/disk-a'); } public function testPath() { - $this->assertEquals($this->object->getPath('image.png'), '/usr/share/nginx/html/tests/resources/disk-a/i/m/a/g/image.png'); - $this->assertEquals($this->object->getPath('x.png'), '/usr/share/nginx/html/tests/resources/disk-a/x/./p/n/x.png'); - $this->assertEquals($this->object->getPath('y'), '/usr/share/nginx/html/tests/resources/disk-a/y/x/x/x/y'); + $this->assertEquals($this->object->getPath('image.png'), '/usr/src/code/tests/resources/disk-a/i/m/a/g/image.png'); + $this->assertEquals($this->object->getPath('x.png'), '/usr/src/code/tests/resources/disk-a/x/./p/n/x.png'); + $this->assertEquals($this->object->getPath('y'), '/usr/src/code/tests/resources/disk-a/y/x/x/x/y'); } public function testWrite() diff --git a/tests/unit/Storage/StorageTest.php b/tests/unit/Storage/StorageTest.php index 3b3f095463..0355dfef74 100644 --- a/tests/unit/Storage/StorageTest.php +++ b/tests/unit/Storage/StorageTest.php @@ -7,8 +7,8 @@ use Appwrite\Storage\Storage; use Appwrite\Storage\Device\Local; use PHPUnit\Framework\TestCase; -Storage::addDevice('disk-a', new Local(__DIR__ . '/../../resources/disk-a')); -Storage::addDevice('disk-b', new Local(__DIR__ . '/../../resources/disk-b')); +Storage::setDevice('disk-a', new Local(__DIR__ . '/../../resources/disk-a')); +Storage::setDevice('disk-b', new Local(__DIR__ . '/../../resources/disk-b')); class StorageTest extends TestCase { diff --git a/tests/unit/Swoole/FilesTest.php b/tests/unit/Swoole/FilesTest.php new file mode 100644 index 0000000000..adcf4b4fc0 --- /dev/null +++ b/tests/unit/Swoole/FilesTest.php @@ -0,0 +1,48 @@ +assertGreaterThan(100, Files::getCount()); + $this->assertLessThan(250, Files::getCount()); + + $this->assertEquals(false, Files::isFileLoaded('/index.php')); + $this->assertEquals(false, Files::isFileLoaded('/unknown.jpg')); + $this->assertEquals(true, Files::isFileLoaded('/scripts/app.js')); + $this->assertEquals(true, Files::isFileLoaded('/styles/default.less')); + $this->assertEquals(true, Files::isFileLoaded('/dist/scripts/app.js')); + $this->assertEquals(true, Files::isFileLoaded('/dist/styles/default-ltr.css')); + $this->assertEquals(true, Files::isFileLoaded('/dist/styles/default-rtl.css')); + + $this->assertEquals('application/vnd.ms-fontobject', Files::getFileMimeType('/fonts/poppins-v9-latin-100.eot')); + $this->assertEquals('image/svg+xml', Files::getFileMimeType('/fonts/poppins-v9-latin-100.svg')); + $this->assertEquals('font/sfnt', Files::getFileMimeType('/fonts/poppins-v9-latin-100.ttf')); + $this->assertEquals('application/octet-stream', Files::getFileMimeType('/fonts/poppins-v9-latin-100.woff')); + $this->assertEquals('application/octet-stream', Files::getFileMimeType('/fonts/poppins-v9-latin-100.woff2')); + $this->assertEquals('image/png', Files::getFileMimeType('/images/apple.png')); + $this->assertEquals('text/javascript', Files::getFileMimeType('/dist/scripts/app.js')); + $this->assertEquals('text/javascript', Files::getFileMimeType('/dist/scripts/app.js')); + $this->assertEquals('text/css', Files::getFileMimeType('/dist/styles/default-ltr.css')); + $this->assertEquals('text/css', Files::getFileMimeType('/dist/styles/default-rtl.css')); + + $this->assertNotEmpty(Files::getFileContents('/fonts/poppins-v9-latin-100.eot')); + $this->assertNotEmpty(Files::getFileContents('/fonts/poppins-v9-latin-100.svg')); + $this->assertNotEmpty(Files::getFileContents('/fonts/poppins-v9-latin-100.ttf')); + $this->assertNotEmpty(Files::getFileContents('/fonts/poppins-v9-latin-100.woff')); + $this->assertNotEmpty(Files::getFileMimeType('/fonts/poppins-v9-latin-100.woff2')); + $this->assertNotEmpty(Files::getFileContents('/images/apple.png')); + $this->assertNotEmpty(Files::getFileContents('/dist/scripts/app.js')); + $this->assertNotEmpty(Files::getFileContents('/dist/scripts/app.js')); + $this->assertNotEmpty(Files::getFileContents('/dist/styles/default-ltr.css')); + $this->assertNotEmpty(Files::getFileContents('/dist/styles/default-rtl.css')); + + } +} \ No newline at end of file diff --git a/tests/unit/Template/TemplateTest.php b/tests/unit/Template/TemplateTest.php index b09fe67b2e..7404e6abff 100644 --- a/tests/unit/Template/TemplateTest.php +++ b/tests/unit/Template/TemplateTest.php @@ -53,4 +53,16 @@ class TemplateTest extends TestCase { $this->assertEquals($this->object->mergeQuery('key1=value1&key2=value2', ['key1' => 'value3', 'key4' => 'value4']), 'key1=value3&key2=value2&key4=value4'); } + + public function testFromCamelCaseToSnake() + { + $this->assertEquals('app_write', Template::fromCamelCaseToSnake('appWrite')); + $this->assertEquals('app_write', Template::fromCamelCaseToSnake('App Write')); + } + + public function testFromCamelCaseToDash() + { + $this->assertEquals('app-write', Template::fromCamelCaseToDash('appWrite')); + $this->assertEquals('app-write', Template::fromCamelCaseToDash('App Write')); + } } \ No newline at end of file