diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..e69de29bb2 diff --git a/CHANGES.md b/CHANGES.md index 9fea296376..4d255f717e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,16 +2,50 @@ ## Features +<<<<<<< HEAD - New route in Locale API to fetch a list of languages - 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 +======= +- New route in Locale API to fetch a list of languages (@TorstenDittmann) +- Added Google Fonts to Appwrite for offline availability +- Added a new route in the Avatars API to get user initials avatar +- Added option to delete team from the console +- Added option to view team members from the console +- Added option to join a user to any team from the console +- Added support for Brotli compression (@PedroCisnerosSantana, @Rohitub222) +- New UI micro-interactions and CSS fixes (@AnatoleLucet) +- UI performance & accessibility improvments (#406) +- Updated ClamAV conntainer to version 1.0.9 +- New Doctor CLI to debug the Appwrite server ([#415](https://github.com/appwrite/appwrite/issues/415)) +- All emails are now sent asynchronously for improved performance (@TorstenDittmann) +- Updated grid for OAuth2 providers list in the console +- Upgraded Redis Resque queue library to version 1.3.6 +- Added container names to docker-compose.yml (@drandell) +- Upgraded ClamAV container image to version 1.0.9 +>>>>>>> 0abfa5e5fc2ff2d43a43426fbdaf90b62315c8e9 ## Bug Fixes - Fixed output of /v1/health/queue/certificates returning wrong data +- Fixed bug where team members count was wrong in some cases +- Fixed network calculation for uploaded files +- Fixed a UI bug preventing float values in numeric fields +- Fixed scroll positioning when moving rules order up & down +- Fixed missing validation for database documents key length (32 chars) +- Grammer fix for pt-br email templates (@rubensdemelo) +- Fixed update form labels and tooltips for Flutter Android apps +- Fixed missing custom scopes param for OAuth2 session create API route +- Fixed wrong JSON validation when creating and updating database documnets + +## Security + +- Access to Health API now requires authentication with an API Key with access to `health.read` scope allowed +- Added option to force HTTPS connection to the Appwrite server (_APP_OPTIONS_FORCE_HTTPS) +- Now using your `_APP_SYSTEM_EMAIL_ADDRESS` as the email address for issuing and renewing SSL certificates # Version 0.6.2 (PRE-RELEASE) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83321193d9..d497b3652b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -161,6 +161,16 @@ To run tests manually, run phpunit from your command line: docker exec appwrite test ``` +## Code Maintenance + +We use some automation tools to help us keep a healthy code base. + +Improve PHP exeution time by using [fully-qualified function calls](https://veewee.github.io/blog/optimizing-php-performance-by-fq-function-calls/): + +```bash +php-cs-fixer fix src/ --rules=native_function_invocation --allow-risky=yes +``` + ## Tutorials From time to time, our team will add tutorials that will help contributors find their way in the Appwrite source code. Below is a list of currently available tutorials: diff --git a/Dockerfile b/Dockerfile index 2f157ae752..c4e7e72c82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ ENV TZ=Asia/Tel_Aviv \ RUN \ apt-get update && \ - apt-get install -y --no-install-recommends --no-install-suggests ca-certificates software-properties-common wget curl git openssl && \ + 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 && \ @@ -23,7 +23,13 @@ RUN \ ./configure && \ make && \ # Composer - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=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/ @@ -51,6 +57,7 @@ ENV TZ=Asia/Tel_Aviv \ _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=104857600 \ _APP_STORAGE_ANTIVIRUS=enabled \ @@ -74,31 +81,50 @@ ENV TZ=Asia/Tel_Aviv \ #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 curl ca-certificates software-properties-common openssl gnupg && \ + 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 htop supervisor 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 php$PHP_VERSION-yaml webp certbot && \ + 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 - echo "deb http://nginx.org/packages/mainline/ubuntu/ bionic nginx" >> /etc/apt/sources.list.d/nginx.list && \ - wget -q http://nginx.org/keys/nginx_signing.key && \ - apt-key add nginx_signing.key && \ - apt-get update && \ - apt-get install -y --no-install-recommends --no-install-suggests 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 software-properties-common gnupg curl && \ + 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) @@ -140,6 +166,7 @@ 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 diff --git a/README.md b/README.md index bc78047f0f..d0900ade4f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

-[![Discord](https://badgen.net/badge/discord/chat/green)](https://discord.gg/GSeTUeA) +[![Discord](https://img.shields.io/discord/564160730845151244?label=discord)](https://discord.gg/GSeTUeA) [![Docker Pulls](https://badgen.net/docker/pulls/appwrite/appwrite)](https://travis-ci.org/appwrite/appwrite) [![Travis CI](https://badgen.net/travis/appwrite/appwrite?label=build)](https://travis-ci.org/appwrite/appwrite) [![Twitter Account](https://badgen.net/twitter/follow/appwrite_io?label=twitter)](https://twitter.com/appwrite_io) diff --git a/app/app.php b/app/app.php index a72116a767..64a4967568 100644 --- a/app/app.php +++ b/app/app.php @@ -16,7 +16,7 @@ use Appwrite\Database\Database; use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; use Appwrite\Event\Event; -use Appwrite\Network\Validators\Origin; +use Appwrite\Network\Validator\Origin; /* * Configuration files @@ -27,15 +27,16 @@ $services = include __DIR__.'/config/services.php'; // List of services $webhook = new Event('v1-webhooks', 'WebhooksV1'); $audit = new Event('v1-audits', 'AuditsV1'); $usage = new Event('v1-usage', 'UsageV1'); +$mail = new Event('v1-mails', 'MailsV1'); $deletes = new Event('v1-deletes', 'DeletesV1'); /** * Get All verified client URLs for both console and current projects * + Filter for duplicated entries */ -$clientsConsole = array_map(function ($node) { +$clientsConsole = \array_map(function ($node) { return $node['hostname']; - }, array_filter($console->getAttribute('platforms', []), function ($node) { + }, \array_filter($console->getAttribute('platforms', []), function ($node) { if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { return true; } @@ -43,9 +44,9 @@ $clientsConsole = array_map(function ($node) { return false; })); -$clients = array_unique(array_merge($clientsConsole, array_map(function ($node) { +$clients = \array_unique(\array_merge($clientsConsole, \array_map(function ($node) { return $node['hostname']; - }, array_filter($project->getAttribute('platforms', []), function ($node) { + }, \array_filter($project->getAttribute('platforms', []), function ($node) { if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { return true; } @@ -53,7 +54,7 @@ $clients = array_unique(array_merge($clientsConsole, array_map(function ($node) return false; })))); -$utopia->init(function () use ($utopia, $request, $response, &$user, $project, $console, $roles, $webhook, $audit, $usage, $clients) { +$utopia->init(function () use ($utopia, $request, $response, &$user, $project, $console, $roles, $webhook, $mail, $audit, $usage, $clients) { $route = $utopia->match($request); @@ -62,11 +63,11 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ } $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); + $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); - $refDomain = $protocol.'://'.((in_array($origin, $clients)) + $refDomain = $protocol.'://'.((\in_array($origin, $clients)) ? $origin : 'localhost') . (!empty($port) ? ':'.$port : ''); $selfDomain = new Domain(Config::getParam('hostname')); @@ -82,9 +83,17 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ * 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')); + } + + $response->addHeader('Strict-Transport-Security', 'max-age='.(60 * 60 * 24 * 126)); // 126 days + } + $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->getServer('REQUEST_URI'))) //->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') @@ -100,10 +109,10 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ * Skip this check for non-web platforms which are not requiredto send an origin header */ $origin = $request->getServer('HTTP_ORIGIN', $request->getServer('HTTP_REFERER', '')); - $originValidator = new Origin(array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', []))); + $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]) + && \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', ''))) { throw new Exception($originValidator->getDescription(), 403); @@ -153,7 +162,7 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ ]); $role = Auth::USER_ROLE_APP; - $scopes = array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', [])); + $scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', [])); Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. } @@ -161,7 +170,7 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ Authorization::setRole('user:'.$user->getId()); Authorization::setRole('role:'.$role); - array_map(function ($node) { + \array_map(function ($node) { if (isset($node['teamId']) && isset($node['roles'])) { Authorization::setRole('team:'.$node['teamId']); @@ -173,12 +182,12 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ // TDOO Check if user is god - if (!in_array($scope, $scopes)) { + if (!\in_array($scope, $scopes)) { if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS !== $project->getCollection()) { // Check if permission is denied because project is missing throw new Exception('Project not found', 404); } - throw new Exception($user->getAttribute('email', 'Guest').' (role: '.strtolower($roles[$role]['label']).') missing scope ('.$scope.')', 401); + throw new Exception($user->getAttribute('email', 'User').' (role: '.\strtolower($roles[$role]['label']).') missing scope ('.$scope.')', 401); } if (Auth::USER_STATUS_BLOCKED == $user->getAttribute('status')) { // Account has not been activated @@ -236,12 +245,12 @@ $utopia->shutdown(function () use ($response, $request, $webhook, $audit, $usage } $route = $utopia->match($request); - + 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()) + ->setParam('request', $request->getSize() + $usage->getParam('storage')) ->setParam('response', $response->getSize()) ->trigger() ; @@ -283,7 +292,7 @@ $utopia->error(function ($error /* @var $error Exception */) use ($request, $res $_SERVER = []; // Reset before reporting to error log to avoid keys being compromised - $output = ((App::ENV_TYPE_DEVELOPMENT == $env)) ? [ + $output = ((App::MODE_TYPE_DEVELOPMENT == $env)) ? [ 'message' => $error->getMessage(), 'code' => $error->getCode(), 'file' => $error->getFile(), @@ -387,9 +396,9 @@ $utopia->get('/.well-known/acme-challenge') ->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); + $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); @@ -399,15 +408,15 @@ $utopia->get('/.well-known/acme-challenge') throw new Exception('Unknown path', 404); } - if(!substr($absolute, 0, strlen($base)) === $base) { + if(!\substr($absolute, 0, \strlen($base)) === $base) { throw new Exception('Invalid path', 401); } - if(!file_exists($absolute)) { + if(!\file_exists($absolute)) { throw new Exception('Unknown path', 404); } - $content = @file_get_contents($absolute); + $content = @\file_get_contents($absolute); if(!$content) { throw new Exception('Failed to get contents', 500); @@ -419,9 +428,9 @@ $utopia->get('/.well-known/acme-challenge') $name = APP_NAME; -if (array_key_exists($service, $services)) { /** @noinspection PhpIncludeInspection */ +if (\array_key_exists($service, $services)) { /** @noinspection PhpIncludeInspection */ include_once $services[$service]['controller']; - $name = APP_NAME.' '.ucfirst($services[$service]['name']); + $name = APP_NAME.' '.\ucfirst($services[$service]['name']); } else { /** @noinspection PhpIncludeInspection */ include_once $services['/']['controller']; diff --git a/app/config/collections.php b/app/config/collections.php index 3b1d167912..3141419c1b 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -41,7 +41,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_PLATFORMS, 'name' => 'Current Host', 'type' => 'web', - 'hostname' => parse_url('https://'.$request->getServer('HTTP_HOST'), PHP_URL_HOST), + 'hostname' => \parse_url('https://'.$request->getServer('HTTP_HOST'), PHP_URL_HOST), ], ], 'legalName' => '', @@ -50,9 +50,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($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)) : [], ], Database::SYSTEM_COLLECTION_COLLECTIONS => [ '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, @@ -1199,8 +1199,8 @@ foreach ($providers as $index => $provider) { $collections[Database::SYSTEM_COLLECTION_PROJECTS]['rules'][] = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, - 'label' => 'OAuth2 '.ucfirst($index).' ID', - 'key' => 'usersOauth2'.ucfirst($index).'Appid', + 'label' => 'OAuth2 '.\ucfirst($index).' ID', + 'key' => 'usersOauth2'.\ucfirst($index).'Appid', 'type' => 'text', 'default' => '', 'required' => false, @@ -1209,8 +1209,8 @@ foreach ($providers as $index => $provider) { $collections[Database::SYSTEM_COLLECTION_PROJECTS]['rules'][] = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, - 'label' => 'OAuth2 '.ucfirst($index).' Secret', - 'key' => 'usersOauth2'.ucfirst($index).'Secret', + 'label' => 'OAuth2 '.\ucfirst($index).' Secret', + 'key' => 'usersOauth2'.\ucfirst($index).'Secret', 'type' => 'text', 'default' => '', 'required' => false, @@ -1219,8 +1219,8 @@ foreach ($providers as $index => $provider) { $collections[Database::SYSTEM_COLLECTION_USERS]['rules'][] = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, - 'label' => 'OAuth2 '.ucfirst($index).' ID', - 'key' => 'oauth2'.ucfirst($index), + 'label' => 'OAuth2 '.\ucfirst($index).' ID', + 'key' => 'oauth2'.\ucfirst($index), 'type' => 'text', 'default' => '', 'required' => false, @@ -1229,8 +1229,8 @@ foreach ($providers as $index => $provider) { $collections[Database::SYSTEM_COLLECTION_USERS]['rules'][] = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, - 'label' => 'OAuth2 '.ucfirst($index).' Access Token', - 'key' => 'oauth2'.ucfirst($index).'AccessToken', + 'label' => 'OAuth2 '.\ucfirst($index).' Access Token', + 'key' => 'oauth2'.\ucfirst($index).'AccessToken', 'type' => 'text', 'default' => '', 'required' => false, diff --git a/app/config/eu.php b/app/config/eu.php index d63ebef707..fbefd23694 100644 --- a/app/config/eu.php +++ b/app/config/eu.php @@ -30,7 +30,7 @@ $list = [ 'SE', // Sweden ]; -if (time() < strtotime('2020-01-31')) { // @see https://en.wikipedia.org/wiki/Brexit +if (\time() < \strtotime('2020-01-31')) { // @see https://en.wikipedia.org/wiki/Brexit $list[] = 'GB'; // // United Kingdom } diff --git a/app/config/languages.php b/app/config/languages.php new file mode 100644 index 0000000000..5f0434b4d0 --- /dev/null +++ b/app/config/languages.php @@ -0,0 +1,936 @@ + "aa", + "name" => "Afar", + "nativeName" => "Afar" + ], + [ + "code" => "ab", + "name" => "Abkhazian", + "nativeName" => "Аҧсуа" + ], + [ + "code" => "af", + "name" => "Afrikaans", + "nativeName" => "Afrikaans" + ], + [ + "code" => "ak", + "name" => "Akan", + "nativeName" => "Akana" + ], + [ + "code" => "am", + "name" => "Amharic", + "nativeName" => "አማርኛ" + ], + [ + "code" => "an", + "name" => "Aragonese", + "nativeName" => "Aragonés" + ], + [ + "code" => "ar", + "name" => "Arabic", + "nativeName" => "العربية" + ], + [ + "code" => "as", + "name" => "Assamese", + "nativeName" => "অসমীয়া" + ], + [ + "code" => "av", + "name" => "Avar", + "nativeName" => "Авар" + ], + [ + "code" => "ay", + "name" => "Aymara", + "nativeName" => "Aymar" + ], + [ + "code" => "az", + "name" => "Azerbaijani", + "nativeName" => "Azərbaycanca / آذربايجان" + ], + [ + "code" => "ba", + "name" => "Bashkir", + "nativeName" => "Башҡорт" + ], + [ + "code" => "be", + "name" => "Belarusian", + "nativeName" => "Беларуская" + ], + [ + "code" => "bg", + "name" => "Bulgarian", + "nativeName" => "Български" + ], + [ + "code" => "bh", + "name" => "Bihari", + "nativeName" => "भोजपुरी" + ], + [ + "code" => "bi", + "name" => "Bislama", + "nativeName" => "Bislama" + ], + [ + "code" => "bm", + "name" => "Bambara", + "nativeName" => "Bamanankan" + ], + [ + "code" => "bn", + "name" => "Bengali", + "nativeName" => "বাংলা" + ], + [ + "code" => "bo", + "name" => "Tibetan", + "nativeName" => "བོད་ཡིག / Bod skad" + ], + [ + "code" => "br", + "name" => "Breton", + "nativeName" => "Brezhoneg" + ], + [ + "code" => "bs", + "name" => "Bosnian", + "nativeName" => "Bosanski" + ], + [ + "code" => "ca", + "name" => "Catalan", + "nativeName" => "Català" + ], + [ + "code" => "ce", + "name" => "Chechen", + "nativeName" => "Нохчийн" + ], + [ + "code" => "ch", + "name" => "Chamorro", + "nativeName" => "Chamoru" + ], + [ + "code" => "co", + "name" => "Corsican", + "nativeName" => "Corsu" + ], + [ + "code" => "cr", + "name" => "Cree", + "nativeName" => "Nehiyaw" + ], + [ + "code" => "cs", + "name" => "Czech", + "nativeName" => "Česky" + ], + [ + "code" => "cu", + "name" => "Old Church Slavonic / Old Bulgarian", + "nativeName" => "словѣньскъ / slověnĭskŭ" + ], + [ + "code" => "cv", + "name" => "Chuvash", + "nativeName" => "Чăваш" + ], + [ + "code" => "cy", + "name" => "Welsh", + "nativeName" => "Cymraeg" + ], + [ + "code" => "da", + "name" => "Danish", + "nativeName" => "Dansk" + ], + [ + "code" => "de", + "name" => "German", + "nativeName" => "Deutsch" + ], + [ + "code" => "dv", + "name" => "Divehi", + "nativeName" => "ދިވެހިބަސް" + ], + [ + "code" => "dz", + "name" => "Dzongkha", + "nativeName" => "ཇོང་ཁ" + ], + [ + "code" => "ee", + "name" => "Ewe", + "nativeName" => "Ɛʋɛ" + ], + [ + "code" => "el", + "name" => "Greek", + "nativeName" => "Ελληνικά" + ], + [ + "code" => "en", + "name" => "English", + "nativeName" => "English" + ], + [ + "code" => "eo", + "name" => "Esperanto", + "nativeName" => "Esperanto" + ], + [ + "code" => "es", + "name" => "Spanish", + "nativeName" => "Español" + ], + [ + "code" => "et", + "name" => "Estonian", + "nativeName" => "Eesti" + ], + [ + "code" => "eu", + "name" => "Basque", + "nativeName" => "Euskara" + ], + [ + "code" => "fa", + "name" => "Persian", + "nativeName" => "فارسی" + ], + [ + "code" => "ff", + "name" => "Peul", + "nativeName" => "Fulfulde" + ], + [ + "code" => "fi", + "name" => "Finnish", + "nativeName" => "Suomi" + ], + [ + "code" => "fj", + "name" => "Fijian", + "nativeName" => "Na Vosa Vakaviti" + ], + [ + "code" => "fo", + "name" => "Faroese", + "nativeName" => "Føroyskt" + ], + [ + "code" => "fr", + "name" => "French", + "nativeName" => "Français" + ], + [ + "code" => "fy", + "name" => "West Frisian", + "nativeName" => "Frysk" + ], + [ + "code" => "ga", + "name" => "Irish", + "nativeName" => "Gaeilge" + ], + [ + "code" => "gd", + "name" => "Scottish Gaelic", + "nativeName" => "Gàidhlig" + ], + [ + "code" => "gl", + "name" => "Galician", + "nativeName" => "Galego" + ], + [ + "code" => "gn", + "name" => "Guarani", + "nativeName" => "Avañe'ẽ" + ], + [ + "code" => "gu", + "name" => "Gujarati", + "nativeName" => "ગુજરાતી" + ], + [ + "code" => "gv", + "name" => "Manx", + "nativeName" => "Gaelg" + ], + [ + "code" => "ha", + "name" => "Hausa", + "nativeName" => "هَوُسَ" + ], + [ + "code" => "he", + "name" => "Hebrew", + "nativeName" => "עברית" + ], + [ + "code" => "hi", + "name" => "Hindi", + "nativeName" => "हिन्दी" + ], + [ + "code" => "ho", + "name" => "Hiri Motu", + "nativeName" => "Hiri Motu" + ], + [ + "code" => "hr", + "name" => "Croatian", + "nativeName" => "Hrvatski" + ], + [ + "code" => "ht", + "name" => "Haitian", + "nativeName" => "Krèyol ayisyen" + ], + [ + "code" => "hu", + "name" => "Hungarian", + "nativeName" => "Magyar" + ], + [ + "code" => "hy", + "name" => "Armenian", + "nativeName" => "Հայերեն" + ], + [ + "code" => "hz", + "name" => "Herero", + "nativeName" => "Otsiherero" + ], + [ + "code" => "ia", + "name" => "Interlingua", + "nativeName" => "Interlingua" + ], + [ + "code" => "id", + "name" => "Indonesian", + "nativeName" => "Bahasa Indonesia" + ], + [ + "code" => "ie", + "name" => "Interlingue", + "nativeName" => "Interlingue" + ], + [ + "code" => "ig", + "name" => "Igbo", + "nativeName" => "Igbo" + ], + [ + "code" => "ii", + "name" => "Sichuan Yi", + "nativeName" => "ꆇꉙ / 四川彝语" + ], + [ + "code" => "ik", + "name" => "Inupiak", + "nativeName" => "Iñupiak" + ], + [ + "code" => "io", + "name" => "Ido", + "nativeName" => "Ido" + ], + [ + "code" => "is", + "name" => "Icelandic", + "nativeName" => "Íslenska" + ], + [ + "code" => "it", + "name" => "Italian", + "nativeName" => "Italiano" + ], + [ + "code" => "iu", + "name" => "Inuktitut", + "nativeName" => "ᐃᓄᒃᑎᑐᑦ" + ], + [ + "code" => "ja", + "name" => "Japanese", + "nativeName" => "日本語" + ], + [ + "code" => "jv", + "name" => "Javanese", + "nativeName" => "Basa Jawa" + ], + [ + "code" => "ka", + "name" => "Georgian", + "nativeName" => "ქართული" + ], + [ + "code" => "kg", + "name" => "Kongo", + "nativeName" => "KiKongo" + ], + [ + "code" => "ki", + "name" => "Kikuyu", + "nativeName" => "Gĩkũyũ" + ], + [ + "code" => "kj", + "name" => "Kuanyama", + "nativeName" => "Kuanyama" + ], + [ + "code" => "kk", + "name" => "Kazakh", + "nativeName" => "Қазақша" + ], + [ + "code" => "kl", + "name" => "Greenlandic", + "nativeName" => "Kalaallisut" + ], + [ + "code" => "km", + "name" => "Cambodian", + "nativeName" => "ភាសាខ្មែរ" + ], + [ + "code" => "kn", + "name" => "Kannada", + "nativeName" => "ಕನ್ನಡ" + ], + [ + "code" => "ko", + "name" => "Korean", + "nativeName" => "한국어" + ], + [ + "code" => "kr", + "name" => "Kanuri", + "nativeName" => "Kanuri" + ], + [ + "code" => "ks", + "name" => "Kashmiri", + "nativeName" => "कश्मीरी / كشميري" + ], + [ + "code" => "ku", + "name" => "Kurdish", + "nativeName" => "Kurdî / كوردی" + ], + [ + "code" => "kv", + "name" => "Komi", + "nativeName" => "Коми" + ], + [ + "code" => "kw", + "name" => "Cornish", + "nativeName" => "Kernewek" + ], + [ + "code" => "ky", + "name" => "Kirghiz", + "nativeName" => "Kırgızca / Кыргызча" + ], + [ + "code" => "la", + "name" => "Latin", + "nativeName" => "Latina" + ], + [ + "code" => "lb", + "name" => "Luxembourgish", + "nativeName" => "Lëtzebuergesch" + ], + [ + "code" => "lg", + "name" => "Ganda", + "nativeName" => "Luganda" + ], + [ + "code" => "li", + "name" => "Limburgian", + "nativeName" => "Limburgs" + ], + [ + "code" => "ln", + "name" => "Lingala", + "nativeName" => "Lingála" + ], + [ + "code" => "lo", + "name" => "Laotian", + "nativeName" => "ລາວ / Pha xa lao" + ], + [ + "code" => "lt", + "name" => "Lithuanian", + "nativeName" => "Lietuvių" + ], + [ + "code" => "lu", + "name" => "Luba-Katanga", + "nativeName" => "Tshiluba" + ], + [ + "code" => "lv", + "name" => "Latvian", + "nativeName" => "Latviešu" + ], + [ + "code" => "mg", + "name" => "Malagasy", + "nativeName" => "Malagasy" + ], + [ + "code" => "mh", + "name" => "Marshallese", + "nativeName" => "Kajin Majel / Ebon" + ], + [ + "code" => "mi", + "name" => "Maori", + "nativeName" => "Māori" + ], + [ + "code" => "mk", + "name" => "Macedonian", + "nativeName" => "Македонски" + ], + [ + "code" => "ml", + "name" => "Malayalam", + "nativeName" => "മലയാളം" + ], + [ + "code" => "mn", + "name" => "Mongolian", + "nativeName" => "Монгол" + ], + [ + "code" => "mo", + "name" => "Moldovan", + "nativeName" => "Moldovenească" + ], + [ + "code" => "mr", + "name" => "Marathi", + "nativeName" => "मराठी" + ], + [ + "code" => "ms", + "name" => "Malay", + "nativeName" => "Bahasa Melayu" + ], + [ + "code" => "mt", + "name" => "Maltese", + "nativeName" => "bil-Malti" + ], + [ + "code" => "my", + "name" => "Burmese", + "nativeName" => "မြန်မာစာ" + ], + [ + "code" => "na", + "name" => "Nauruan", + "nativeName" => "Dorerin Naoero" + ], + [ + "code" => "nb", + "name" => "Norwegian Bokmål", + "nativeName" => "Norsk bokmål" + ], + [ + "code" => "nd", + "name" => "North Ndebele", + "nativeName" => "Sindebele" + ], + [ + "code" => "ne", + "name" => "Nepali", + "nativeName" => "नेपाली" + ], + [ + "code" => "ng", + "name" => "Ndonga", + "nativeName" => "Oshiwambo" + ], + [ + "code" => "nl", + "name" => "Dutch", + "nativeName" => "Nederlands" + ], + [ + "code" => "nn", + "name" => "Norwegian Nynorsk", + "nativeName" => "Norsk nynorsk" + ], + [ + "code" => "no", + "name" => "Norwegian", + "nativeName" => "Norsk" + ], + [ + "code" => "nr", + "name" => "South Ndebele", + "nativeName" => "isiNdebele" + ], + [ + "code" => "nv", + "name" => "Navajo", + "nativeName" => "Diné bizaad" + ], + [ + "code" => "ny", + "name" => "Chichewa", + "nativeName" => "Chi-Chewa" + ], + [ + "code" => "oc", + "name" => "Occitan", + "nativeName" => "Occitan" + ], + [ + "code" => "oj", + "name" => "Ojibwa", + "nativeName" => "ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin" + ], + [ + "code" => "om", + "name" => "Oromo", + "nativeName" => "Oromoo" + ], + [ + "code" => "or", + "name" => "Oriya", + "nativeName" => "ଓଡ଼ିଆ" + ], + [ + "code" => "os", + "name" => "Ossetian / Ossetic", + "nativeName" => "Иронау" + ], + [ + "code" => "pa", + "name" => "Panjabi / Punjabi", + "nativeName" => "ਪੰਜਾਬੀ / पंजाबी / پنجابي" + ], + [ + "code" => "pi", + "name" => "Pali", + "nativeName" => "Pāli / पाऴि" + ], + [ + "code" => "pl", + "name" => "Polish", + "nativeName" => "Polski" + ], + [ + "code" => "ps", + "name" => "Pashto", + "nativeName" => "پښتو" + ], + [ + "code" => "pt", + "name" => "Portuguese", + "nativeName" => "Português" + ], + [ + "code" => "qu", + "name" => "Quechua", + "nativeName" => "Runa Simi" + ], + [ + "code" => "rm", + "name" => "Raeto Romance", + "nativeName" => "Rumantsch" + ], + [ + "code" => "rn", + "name" => "Kirundi", + "nativeName" => "Kirundi" + ], + [ + "code" => "ro", + "name" => "Romanian", + "nativeName" => "Română" + ], + [ + "code" => "ru", + "name" => "Russian", + "nativeName" => "Русский" + ], + [ + "code" => "rw", + "name" => "Rwandi", + "nativeName" => "Kinyarwandi" + ], + [ + "code" => "sa", + "name" => "Sanskrit", + "nativeName" => "संस्कृतम्" + ], + [ + "code" => "sc", + "name" => "Sardinian", + "nativeName" => "Sardu" + ], + [ + "code" => "sd", + "name" => "Sindhi", + "nativeName" => "सिनधि" + ], + [ + "code" => "se", + "name" => "Northern Sami", + "nativeName" => "Sámegiella" + ], + [ + "code" => "sg", + "name" => "Sango", + "nativeName" => "Sängö" + ], + [ + "code" => "sh", + "name" => "Serbo-Croatian", + "nativeName" => "Srpskohrvatski / Српскохрватски" + ], + [ + "code" => "si", + "name" => "Sinhalese", + "nativeName" => "සිංහල" + ], + [ + "code" => "sk", + "name" => "Slovak", + "nativeName" => "Slovenčina" + ], + [ + "code" => "sl", + "name" => "Slovenian", + "nativeName" => "Slovenščina" + ], + [ + "code" => "sm", + "name" => "Samoan", + "nativeName" => "Gagana Samoa" + ], + [ + "code" => "sn", + "name" => "Shona", + "nativeName" => "chiShona" + ], + [ + "code" => "so", + "name" => "Somalia", + "nativeName" => "Soomaaliga" + ], + [ + "code" => "sq", + "name" => "Albanian", + "nativeName" => "Shqip" + ], + [ + "code" => "sr", + "name" => "Serbian", + "nativeName" => "Српски" + ], + [ + "code" => "ss", + "name" => "Swati", + "nativeName" => "SiSwati" + ], + [ + "code" => "st", + "name" => "Southern Sotho", + "nativeName" => "Sesotho" + ], + [ + "code" => "su", + "name" => "Sundanese", + "nativeName" => "Basa Sunda" + ], + [ + "code" => "sv", + "name" => "Swedish", + "nativeName" => "Svenska" + ], + [ + "code" => "sw", + "name" => "Swahili", + "nativeName" => "Kiswahili" + ], + [ + "code" => "ta", + "name" => "Tamil", + "nativeName" => "தமிழ்" + ], + [ + "code" => "te", + "name" => "Telugu", + "nativeName" => "తెలుగు" + ], + [ + "code" => "tg", + "name" => "Tajik", + "nativeName" => "Тоҷикӣ" + ], + [ + "code" => "th", + "name" => "Thai", + "nativeName" => "ไทย / Phasa Thai" + ], + [ + "code" => "ti", + "name" => "Tigrinya", + "nativeName" => "ትግርኛ" + ], + [ + "code" => "tk", + "name" => "Turkmen", + "nativeName" => "Туркмен / تركمن" + ], + [ + "code" => "tl", + "name" => "Tagalog / Filipino", + "nativeName" => "Tagalog" + ], + [ + "code" => "tn", + "name" => "Tswana", + "nativeName" => "Setswana" + ], + [ + "code" => "to", + "name" => "Tonga", + "nativeName" => "Lea Faka-Tonga" + ], + [ + "code" => "tr", + "name" => "Turkish", + "nativeName" => "Türkçe" + ], + [ + "code" => "ts", + "name" => "Tsonga", + "nativeName" => "Xitsonga" + ], + [ + "code" => "tt", + "name" => "Tatar", + "nativeName" => "Tatarça" + ], + [ + "code" => "tw", + "name" => "Twi", + "nativeName" => "Twi" + ], + [ + "code" => "ty", + "name" => "Tahitian", + "nativeName" => "Reo Mā`ohi" + ], + [ + "code" => "ug", + "name" => "Uyghur", + "nativeName" => "Uyƣurqə / ئۇيغۇرچە" + ], + [ + "code" => "uk", + "name" => "Ukrainian", + "nativeName" => "Українська" + ], + [ + "code" => "ur", + "name" => "Urdu", + "nativeName" => "اردو" + ], + [ + "code" => "uz", + "name" => "Uzbek", + "nativeName" => "Ўзбек" + ], + [ + "code" => "ve", + "name" => "Venda", + "nativeName" => "Tshivenḓa" + ], + [ + "code" => "vi", + "name" => "Vietnamese", + "nativeName" => "Tiếng Việt" + ], + [ + "code" => "vo", + "name" => "Volapük", + "nativeName" => "Volapük" + ], + [ + "code" => "wa", + "name" => "Walloon", + "nativeName" => "Walon" + ], + [ + "code" => "wo", + "name" => "Wolof", + "nativeName" => "Wollof" + ], + [ + "code" => "xh", + "name" => "Xhosa", + "nativeName" => "isiXhosa" + ], + [ + "code" => "yi", + "name" => "Yiddish", + "nativeName" => "ייִדיש" + ], + [ + "code" => "yo", + "name" => "Yoruba", + "nativeName" => "Yorùbá" + ], + [ + "code" => "za", + "name" => "Zhuang", + "nativeName" => "Cuengh / Tôô / 壮语" + ], + [ + "code" => "zh", + "name" => "Chinese", + "nativeName" => "中文" + ], + [ + "code" => "zu", + "name" => "Zulu", + "nativeName" => "isiZulu" + ] +]; diff --git a/app/config/locales/templates/_base.tpl b/app/config/locales/templates/_base.tpl new file mode 100644 index 0000000000..561ce73855 --- /dev/null +++ b/app/config/locales/templates/_base.tpl @@ -0,0 +1,238 @@ + + + + + + + {{title}} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/config/locales/templates/_cta.tpl b/app/config/locales/templates/_cta.tpl new file mode 100644 index 0000000000..634609a09e --- /dev/null +++ b/app/config/locales/templates/_cta.tpl @@ -0,0 +1,26 @@ + + + + + + + +

+ + + {{redirect}} + + +

\ No newline at end of file diff --git a/app/config/locales/templates/af.email.auth.confirm.tpl b/app/config/locales/templates/af.email.auth.confirm.tpl index e0a6c40356..a9dbb2af46 100644 --- a/app/config/locales/templates/af.email.auth.confirm.tpl +++ b/app/config/locales/templates/af.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

Om jou epos adres te verifieer, kliek op die web adres hier: -
- {{redirect}} -
-
+

+{{cta}} +

As jy nie aangevra het om jou epos adres te verifieer nie, kan jy die boodskap ignoreer. -
-
+

+

Baie dankie,
{{project}} span -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/af.email.auth.invitation.tpl b/app/config/locales/templates/af.email.auth.invitation.tpl index eed6cf69d1..0c1c72c13a 100644 --- a/app/config/locales/templates/af.email.auth.invitation.tpl +++ b/app/config/locales/templates/af.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hallo, -
-
+

+

Hierdie epos is vir jou gestuur omdat {{owner}} jou graag wou nooi om 'n spanlid te word van {{team}} by {{project}}. -
-
+

+

Om by die {{team}} span aan te sluit: -
- {{redirect}} -
-
+

+{{cta}} +

As jy nie belangstel nie, kan jy die boodskap ignoreer. -
-
+

+

Baie dankie,
{{project}} span -

+

diff --git a/app/config/locales/templates/af.email.auth.recovery.tpl b/app/config/locales/templates/af.email.auth.recovery.tpl index 598c4c1d96..925baaf290 100644 --- a/app/config/locales/templates/af.email.auth.recovery.tpl +++ b/app/config/locales/templates/af.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

Volg hierdie web adres om jou wagwoord te verander vir {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

As jy nie gevra het om you wagwoord te verander nie, kan jy hierdie boodskap ignoreer. -
-
+

+

Baie dankie,
{{project}} span -

+

diff --git a/app/config/locales/templates/alb.email.auth.confirm.tpl b/app/config/locales/templates/alb.email.auth.confirm.tpl index 549340eca9..f350c13fa9 100644 --- a/app/config/locales/templates/alb.email.auth.confirm.tpl +++ b/app/config/locales/templates/alb.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hej {{name}}, -
-
+

+

Ndiqni këtë lidhje për të konfirmuar adresën tuaj të emailit. -
- {{redirect}} -
-
+

+{{cta}} +

Nëse nuk keni kërkuar të konfirmoni këtë adresë, mund ta injoroni këtë mesazh. -
-
+

+

Faleminderit,
Ekipi i tij {{project}} -

+

diff --git a/app/config/locales/templates/alb.email.auth.invitation.tpl b/app/config/locales/templates/alb.email.auth.invitation.tpl index fc75e51825..b734162067 100644 --- a/app/config/locales/templates/alb.email.auth.invitation.tpl +++ b/app/config/locales/templates/alb.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Ηej, -
-
+

+

Ju e morët këtë email sepse {{owner}} ju ftoi të bashkoheni në ekip {{team}} në {{project}}. -
-
+

+

Ndiqni këtë lidhje për t'u bashkuar me grupin {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Nëse nuk jeni të interesuar, mund ta injoroni këtë mesazh. -
-
+

+

Faleminderit,
Ekipi i tij {{project}} -

+

diff --git a/app/config/locales/templates/alb.email.auth.recovery.tpl b/app/config/locales/templates/alb.email.auth.recovery.tpl index d8e0bf867c..044cb963ae 100644 --- a/app/config/locales/templates/alb.email.auth.recovery.tpl +++ b/app/config/locales/templates/alb.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hej {{name}}, -
-
+

+

Ndiqni këtë lidhje për të rivendosur fjalëkalimin tuaj ajo {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Nëse nuk keni kërkuar një ndryshim të fjalëkalimit, mund ta injoroni këtë mesazh. -
-
+

+

Faleminderit,
Ekipi i tij {{project}} -

+

diff --git a/app/config/locales/templates/ar.email.auth.confirm.tpl b/app/config/locales/templates/ar.email.auth.confirm.tpl index 8cd1929dd9..a924e75be8 100644 --- a/app/config/locales/templates/ar.email.auth.confirm.tpl +++ b/app/config/locales/templates/ar.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

مرحبا {{name}}, -
-
+

+

اتبع هذا الرابط للتحقق من عنوان بريدك الإلكتروني. -
- {{redirect}} -
-
+

+{{cta}} +

إذا لم تطلب التحقق من هذا العنوان، فيمكنك تجاهل هذه الرسالة. -
-
+

+

Thanks,
فريق {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ar.email.auth.invitation.tpl b/app/config/locales/templates/ar.email.auth.invitation.tpl index fba4a731e3..78a1321dab 100644 --- a/app/config/locales/templates/ar.email.auth.invitation.tpl +++ b/app/config/locales/templates/ar.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

مرحبا، -
-
+

+

تم إرسال هذه الرسالة إليك لأن {{owner}} أراد دعوتك لتصبح عضوًا في فريق {{team}} في {{project}}. -
-
+

+

اتبع هذا الرابط للانضمام إلى فريق {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

إذا لم تكن مهتمًا، يمكنك تجاهل هذه الرسالة. -
-
+

+

شكرا،
فريق {{project}} -

+

diff --git a/app/config/locales/templates/ar.email.auth.recovery.tpl b/app/config/locales/templates/ar.email.auth.recovery.tpl index 06ca47f35e..53c4537e3c 100644 --- a/app/config/locales/templates/ar.email.auth.recovery.tpl +++ b/app/config/locales/templates/ar.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

مرحبا {{name}}، -
-
+

+

اتبع هذا الرابط لإعادة تعيين كلمة مرور {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

إذا لم تطلب إعادة تعيين كلمة المرور الخاصة بك، فيمكنك تجاهل هذه الرسالة. -
-
+

+

شكرا،
فريق {{project}} -

+

diff --git a/app/config/locales/templates/bn.email.auth.confirm.tpl b/app/config/locales/templates/bn.email.auth.confirm.tpl index dabf4f1222..12be7cecdb 100644 --- a/app/config/locales/templates/bn.email.auth.confirm.tpl +++ b/app/config/locales/templates/bn.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

হ্যালো {{name}}, -
-
+

+

এই লিংকটি অনুসরণ করুন আপনার ইমেইল এড্রেস যাচাই করতে | -
- {{redirect}} -
-
+

+{{cta}} +

আপনি যদি আপনার ইমেইল এড্রেস যাচাই করতে অনুরোধ করেননি, আপনি এই মেসেজটি অগ্রাহ্য করতে পারেন | -
-
+

+

ধন্যবাদান্তে,
{{project}} টীম -

+

diff --git a/app/config/locales/templates/bn.email.auth.invitation.tpl b/app/config/locales/templates/bn.email.auth.invitation.tpl index f09c23adf9..b0e1395a5f 100644 --- a/app/config/locales/templates/bn.email.auth.invitation.tpl +++ b/app/config/locales/templates/bn.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hello, -
-
+

+

এই মেইলটি আপনাকে পাঠানো হয়েছে কারণ {{owner}} আমন্ত্রণ করেছেন আপনাকে {{team}} এই টীমর মেম্বার হতে যেটি {{project}} এই প্রজেক্টর অন্তর্গত | -
-
+

+

এই লিংকটি অনুসরণ করুন {{team}} এই টীম-এ যোগ দিতে: -
- {{redirect}} -
-
+

+{{cta}} +

আপনি যদি অনাগ্রহী হন, এই মেসেজটি অগ্রাহ্য করতে পারেন | -
-
+

+

ধন্যবাদান্তে,
{{project}} টীম -

+

diff --git a/app/config/locales/templates/bn.email.auth.recovery.tpl b/app/config/locales/templates/bn.email.auth.recovery.tpl index 4a356d9889..0dbe9bc643 100644 --- a/app/config/locales/templates/bn.email.auth.recovery.tpl +++ b/app/config/locales/templates/bn.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

হ্যালো {{name}}, -
-
+

+

এই লিংকটি অনুসরণ করুন আপনার {{project}} পাসওয়ার্ড রিসেট করতে | -
- {{redirect}} -
-
+

+{{cta}} +

আপনি যদি পাসওয়ার্ড রিসেট করতে অনুরোধ করেননি, আপনি এই মেসেজটি অগ্রাহ্য করতে পারেন | -
-
+

+

ধন্যবাদান্তে,
{{project}} টীম -

+

diff --git a/app/config/locales/templates/cat.email.auth.confirm.tpl b/app/config/locales/templates/cat.email.auth.confirm.tpl index 3761912010..c6987289eb 100644 --- a/app/config/locales/templates/cat.email.auth.confirm.tpl +++ b/app/config/locales/templates/cat.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hola {{name}}, -
-
+

+

Segueix aquest enllaç per verificar la teva direcció de correu: -
- {{redirect}} -
-
+

+{{cta}} +

Si no has solicitat verificar aquesta direcció, pots ignorar aquest missatge. -
-
+

+

Gràcies,
Equip {{project}} -

+

diff --git a/app/config/locales/templates/cat.email.auth.invitation.tpl b/app/config/locales/templates/cat.email.auth.invitation.tpl index 69687693a2..1a8b062531 100644 --- a/app/config/locales/templates/cat.email.auth.invitation.tpl +++ b/app/config/locales/templates/cat.email.auth.invitation.tpl @@ -1,28 +1,18 @@ - - -
+

Hola, -
-
- T'hem enviat aquest correu perquè {{owner}} et vol convidar a formar part - de l'equip {{team}} a {{project}}. -
-
+

+

+ T'hem enviat aquest correu perquè {{owner}} et vol convidar a formar part de l'equip {{team}} a {{project}}. +

+

Segueix aquest enllaç per unir-te a l'equip {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Si no estàs interessat, pots ignorar aquest missatge. -
-
+

+

Gràcies,
Equip {{project}} -

+

diff --git a/app/config/locales/templates/cat.email.auth.recovery.tpl b/app/config/locales/templates/cat.email.auth.recovery.tpl index 2c17989674..856b92c457 100644 --- a/app/config/locales/templates/cat.email.auth.recovery.tpl +++ b/app/config/locales/templates/cat.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hola {{name}}, -
-
+

+

Segueix aquest enllaç per restablir la teva contrasenya de {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Si no has demanat restablir la teva contrasenya, pots ignorar aquest missatge. -
-
+

+

Gràcies,
Equip {{project}} -

+

diff --git a/app/config/locales/templates/cz.email.auth.confirm.tpl b/app/config/locales/templates/cz.email.auth.confirm.tpl index 481b440ab9..e72b357a48 100644 --- a/app/config/locales/templates/cz.email.auth.confirm.tpl +++ b/app/config/locales/templates/cz.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Ahoj {{name}}, -
-
+

+

Kliknutím na tento odkaz ověřte svou e-mailovou adresu. -
- {{redirect}} -
-
+

+{{cta}} +

Pokud jste nepožádali o ověření této adresy, můžete tuto zprávu ignorovat. -
-
+

+

dík,
{{project}} tým -

+

diff --git a/app/config/locales/templates/cz.email.auth.invitation.tpl b/app/config/locales/templates/cz.email.auth.invitation.tpl index 8e8a8dc5ce..d5353e71cf 100644 --- a/app/config/locales/templates/cz.email.auth.invitation.tpl +++ b/app/config/locales/templates/cz.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Ahoj, -
-
+

+

Tento e-mail vám byl zaslán, protože vás {{owner}} chtěl pozvat, abyste se stali členem týmu v týmu {{team}} v {{project}}. -
-
+

+

Klepnutím na tento odkaz se připojíte k týmu {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Pokud vás nezajímá, můžete tuto zprávu ignorovat. -
-
+

+

Dík,
{{project}} tým -

+

diff --git a/app/config/locales/templates/cz.email.auth.recovery.tpl b/app/config/locales/templates/cz.email.auth.recovery.tpl index d8a3e6a2b1..04b947d7ad 100644 --- a/app/config/locales/templates/cz.email.auth.recovery.tpl +++ b/app/config/locales/templates/cz.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Ahoj {{name}}, -
-
+

+

Pomocí tohoto odkazu obnovte své heslo {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Pokud jste nepožádali o resetování hesla, můžete tuto zprávu ignorovat. -
-
+

+

Dík,
{{project}} tým -

+

diff --git a/app/config/locales/templates/de.email.auth.confirm.tpl b/app/config/locales/templates/de.email.auth.confirm.tpl index 2a3211bbab..2780869778 100644 --- a/app/config/locales/templates/de.email.auth.confirm.tpl +++ b/app/config/locales/templates/de.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

bitte folge diesem Link um deine E-Mail Adresse zu verifizieren. -
- {{redirect}} -
-
+

+{{cta}} +

Bitte ignoriere diese Nachricht, wenn du das Verifizieren deiner E-Mail Adresse nicht beantragt hast. -
-
+

+

Vielen Dank,
{{project}} Team -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/de.email.auth.invitation.tpl b/app/config/locales/templates/de.email.auth.invitation.tpl index c6d9162730..ae55630797 100644 --- a/app/config/locales/templates/de.email.auth.invitation.tpl +++ b/app/config/locales/templates/de.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hallo, -
-
+

+

diese E-Mail wurde dir geschickt, weil {{owner}} dich eingeladen hat Teammitglied im Team {{team}} bei {{project}} zu werden. -
-
+

+

Folge diesem Link um dem Team {{team}} beizutreten: -
- {{redirect}} -
-
+

+{{cta}} +

Wenn du daran nicht interessiert bist, kannst du diese Nachricht ignorieren. -
-
+

+

Vielen Dank,
{{project}} Team -

+

diff --git a/app/config/locales/templates/de.email.auth.recovery.tpl b/app/config/locales/templates/de.email.auth.recovery.tpl index 6d2ccd7bae..8ca8140aec 100644 --- a/app/config/locales/templates/de.email.auth.recovery.tpl +++ b/app/config/locales/templates/de.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

Folge diesem Link um dein Passwort für {{project}} zurückzusetzen. -
- {{redirect}} -
-
+

+{{cta}} +

Bitte ignoriere diese Nachricht, wenn du das Zurücksetzen deines Passworts nicht beantragt hast. -
-
+

+

Vielen Dank,
{{project}} Team -

+

diff --git a/app/config/locales/templates/en.email.auth.confirm.tpl b/app/config/locales/templates/en.email.auth.confirm.tpl index 92078114e3..799832da56 100644 --- a/app/config/locales/templates/en.email.auth.confirm.tpl +++ b/app/config/locales/templates/en.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hello {{name}}, -
-
+

+

Follow this link to verify your email address. -
- {{redirect}} -
-
+

+{{cta}} +

If you didn’t ask to verify this address, you can ignore this message. -
-
+

+

Thanks,
{{project}} team -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/en.email.auth.invitation.tpl b/app/config/locales/templates/en.email.auth.invitation.tpl index fda2bbcce0..70ea922c0c 100644 --- a/app/config/locales/templates/en.email.auth.invitation.tpl +++ b/app/config/locales/templates/en.email.auth.invitation.tpl @@ -1,27 +1,14 @@ - - -
- Hello, -
-
+

+ Hello, +

+

This mail was sent to you because {{owner}} wanted to invite you to become a team member at the {{team}} team over at {{project}}. -
-
- Follow this link to join the {{team}} team: -
- {{redirect}} -
-
- If you are not interested, you can ignore this message. -
-
+

+{{cta}} +

+ If you are not interested, you can ignore this message.

+

Thanks,
{{project}} team -

+

\ No newline at end of file diff --git a/app/config/locales/templates/en.email.auth.recovery.tpl b/app/config/locales/templates/en.email.auth.recovery.tpl index 82b365546a..a4a982f46f 100644 --- a/app/config/locales/templates/en.email.auth.recovery.tpl +++ b/app/config/locales/templates/en.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hello {{name}}, -
-
+

+

Follow this link to reset your {{project}} password. -
- {{redirect}} -
-
- If you didn't ask to reset your password, you can ignore this message. -
-
+

+{{cta}} +

+ If you didn’t ask to verify this address, you can ignore this message. +

+

Thanks,
{{project}} team -

+

\ No newline at end of file diff --git a/app/config/locales/templates/es.email.auth.confirm.tpl b/app/config/locales/templates/es.email.auth.confirm.tpl index a5b9235be8..2c728ddce1 100644 --- a/app/config/locales/templates/es.email.auth.confirm.tpl +++ b/app/config/locales/templates/es.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hola {{name}}, -
-
+

+

Sigue este enlace para verificar tu dirección de correo. -
- {{redirect}} -
-
+

+{{cta}} +

Si no has solicitado verificar esta dirección, puedes ignorar este mensaje. -
-
+

+

Gracias,
Equipo {{project}} -

+

diff --git a/app/config/locales/templates/es.email.auth.invitation.tpl b/app/config/locales/templates/es.email.auth.invitation.tpl index aa0469d9c6..d07b7ffb55 100644 --- a/app/config/locales/templates/es.email.auth.invitation.tpl +++ b/app/config/locales/templates/es.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hola, -
-
+

+

Te hemos enviamos este correo porque {{owner}} quiere invitarte a formar parte del equipo {{team}} en {{project}}. -
-
+

+

Sigue este enlace para unirte al equipo {{team}}: -
- {{redirect}} -
-
- Si no ests interesado, puedes ignorar este mensaje. -
-
+

+{{cta}} +

+ Si no estás interesado, puedes ignorar este mensaje. +

+

Gracias,
Equipo {{project}} -

+

diff --git a/app/config/locales/templates/es.email.auth.recovery.tpl b/app/config/locales/templates/es.email.auth.recovery.tpl index 6f78fffa7c..0f1cfaa434 100644 --- a/app/config/locales/templates/es.email.auth.recovery.tpl +++ b/app/config/locales/templates/es.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hola {{name}}, -
-
+

+

Sigue este enlace para reestablecer tu contraseña de {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Si no has pedido reestablecer tu contraseña, puedes ignorar este mensaje. -
-
+

+

Gracias,
Equipo {{project}} -

+

diff --git a/app/config/locales/templates/fi.email.auth.confirm.tpl b/app/config/locales/templates/fi.email.auth.confirm.tpl index 1354e7a5d6..d14b0e32b0 100644 --- a/app/config/locales/templates/fi.email.auth.confirm.tpl +++ b/app/config/locales/templates/fi.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hei {{name}}, -
-
+

+

Varmista sähköpostiosoite tästä linkistä. -
- {{redirect}} -
-
+

+{{cta}} +

Jos et kysynyt tämän sähköpostiosoitteen varmistamista, voit sivuuttaa tämän viestin. -
-
+

+

kiitos,
{{project}} tiimi -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/fi.email.auth.invitation.tpl b/app/config/locales/templates/fi.email.auth.invitation.tpl index 7e955d0216..6680259aa7 100644 --- a/app/config/locales/templates/fi.email.auth.invitation.tpl +++ b/app/config/locales/templates/fi.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hei, -
-
+

+

Sait tämän sähköpostin koska {{owner}} halusi kutsua sinut jäseneksi {{team}} tiimiin, täällä {{project}}. -
-
+

+

Käytä tätä linkkiä liittyäksesi {{team}} tiimiin: -
- {{redirect}} -
-
+

+{{cta}} +

Jos et ole kiinnostunut, voit sivuuttaa tämän viestin. -
-
+

+

kiitos,
{{project}} tiimi -

+

diff --git a/app/config/locales/templates/fi.email.auth.recovery.tpl b/app/config/locales/templates/fi.email.auth.recovery.tpl index b273c65617..34c3177418 100644 --- a/app/config/locales/templates/fi.email.auth.recovery.tpl +++ b/app/config/locales/templates/fi.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hei {{name}}, -
-
+

+

Resetoi {{project}} salasana tästä linkistä. -
- {{redirect}} -
-
+

+{{cta}} +

Jos et pyytänyt salasanan nollaamista, voit sivuuttaa tämän viestin- -
-
+

+

Kiitos,
{{project}} tiimi -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/fo.email.auth.confirm.tpl b/app/config/locales/templates/fo.email.auth.confirm.tpl index 9af52651b8..5400c2ab01 100644 --- a/app/config/locales/templates/fo.email.auth.confirm.tpl +++ b/app/config/locales/templates/fo.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Halló {{name}}, -
-
+

+

Fylg hesa lenka fyrið at verifisera teldupostur -
- {{redirect}} -
-
+

+{{cta}} +

Om tú ikke spurdi om at verifisera hesa teldupostur, kannst tú ignorera hesa boð. -
-
+

+

Takk,
{{project}} lið -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/fo.email.auth.invitation.tpl b/app/config/locales/templates/fo.email.auth.invitation.tpl index 586daac0e4..c264c7516f 100644 --- a/app/config/locales/templates/fo.email.auth.invitation.tpl +++ b/app/config/locales/templates/fo.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Halló, -
-
+

+

Hesa teldupost var send til tín tí {{owner}} vildi bjóða tær at vera eitt lið limur hjá {{team}} á {{project}} -
-
+

+

Fylg hettar lenka fyrið at bliva vi {{team}} lið: -
- {{redirect}} -
-
+

+{{cta}} +

Om tú ikke er áhugaður, kannst tú ignorera hesa boð -
-
+

+

Takk,
{{project}} lið -

+

diff --git a/app/config/locales/templates/fo.email.auth.recovery.tpl b/app/config/locales/templates/fo.email.auth.recovery.tpl index 8c916a723f..e2f17313ee 100644 --- a/app/config/locales/templates/fo.email.auth.recovery.tpl +++ b/app/config/locales/templates/fo.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Halló {{name}}, -
-
+

+

Fylg hettar lenka fyrið at resette títt {{project}} passord. -
- {{redirect}} -
-
+

+{{cta}} +

Om tú ikke spurdi om at verifisera hesa telduposturin, kannst du ignorera hesa boð. -
-
+

+

Takk,
{{project}} lið -

+

diff --git a/app/config/locales/templates/fr.email.auth.confirm.tpl b/app/config/locales/templates/fr.email.auth.confirm.tpl index 21621e237a..ad0d242740 100644 --- a/app/config/locales/templates/fr.email.auth.confirm.tpl +++ b/app/config/locales/templates/fr.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Bonjour {{name}}, -
-
+

+

Cliquez sur le lien suivant pour vérifier votre adresse mail. -
- {{redirect}} -
-
+

+{{cta}} +

Si vous n'avez pas demandé une vérification de cette adresse mail, vous pouvez ignorer ce message. -
-
+

+

Merci,
L'équipe {{project}} -

+

diff --git a/app/config/locales/templates/fr.email.auth.invitation.tpl b/app/config/locales/templates/fr.email.auth.invitation.tpl index 3b793bb25c..c2d8f8cd1b 100644 --- a/app/config/locales/templates/fr.email.auth.invitation.tpl +++ b/app/config/locales/templates/fr.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Bonjour, -
-
+

+

Ce mail vous a été envoyé car {{owner}} vous invite à devenir membre de l'équipe {{team}} sur le projet {{project}}. -
-
+

+

Cliquez sur le lien suivant pour rejoindre l'équipe {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Si vous n'êtes pas intéressé, vous pouvez ignorer ce message. -
-
+

+

Merci,
L'équipe {{project}} -

+

diff --git a/app/config/locales/templates/fr.email.auth.recovery.tpl b/app/config/locales/templates/fr.email.auth.recovery.tpl index d3ab2a305f..597b421bf3 100644 --- a/app/config/locales/templates/fr.email.auth.recovery.tpl +++ b/app/config/locales/templates/fr.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Bonjour {{name}}, -
-
+

+

Cliquez sur le lien suivant pour réinitialiser votre mot de passe pour le projet {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Si vous n'êtes pas à l'origine de cette demande de réinitialisation de mot de passe, vous pouvez ignorer ce message. -
-
+

+

Merci,
L'équipe {{project}} -

+

diff --git a/app/config/locales/templates/gr.email.auth.confirm.tpl b/app/config/locales/templates/gr.email.auth.confirm.tpl index a62445476b..e52209c9db 100644 --- a/app/config/locales/templates/gr.email.auth.confirm.tpl +++ b/app/config/locales/templates/gr.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Γεια σου {{name}}, -
-
+

+

Ακολούθησε αυτό τον σύνδεσμο για να επιβεβαιώσεις τη διεύθυνση email σου. -
- {{redirect}} -
-
+

+{{cta}} +

Αν δεν ζήτησες να επιβεβαιώσεις αυτή τη διεύθυνση, μπορείς να αγνοήσεις αυτό το μήνυμα. -
-
+

+

Ευχαριστούμε,
Η ομάδα του {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/gr.email.auth.invitation.tpl b/app/config/locales/templates/gr.email.auth.invitation.tpl index 101a1c673c..a7c0743ff6 100644 --- a/app/config/locales/templates/gr.email.auth.invitation.tpl +++ b/app/config/locales/templates/gr.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Γεια, -
-
+

+

Έλαβες αυτό το email επειδή ο {{owner}} σε προσκάλεσε να γίνεις μέλος της ομάδας {{team}} στο {{project}}. -
-
+

+

Ακολούθησε αυτό τον σύνδεσμο για να γίνεις μέλος της ομάδας {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Άν δεν ενδιαφέρεσαι, μπορείς να αγνοήσεις αυτό το μήνυμα. -
-
+

+

Ευχαριστούμε,
Η ομάδα του {{project}} -

+

diff --git a/app/config/locales/templates/gr.email.auth.recovery.tpl b/app/config/locales/templates/gr.email.auth.recovery.tpl index cae164df22..fb18c16ef3 100644 --- a/app/config/locales/templates/gr.email.auth.recovery.tpl +++ b/app/config/locales/templates/gr.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Γεια σου {{name}}, -
-
+

+

Ακολούθησε αυτό τον σύνδεσμο για να επαναφέρεις τον κωδικό πρόσβασής σου για το {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Άν δεν ζήτησες αλλαγή κωδικού πρόσβασης, μπορείς να αγνοήσεις αυτο το μήνυμα. -
-
+

+

Ευχαριστούμε,
Η ομάδα του {{project}} -

+

diff --git a/app/config/locales/templates/he.email.auth.confirm.tpl b/app/config/locales/templates/he.email.auth.confirm.tpl index bc9189a16f..0c1f2a36d3 100644 --- a/app/config/locales/templates/he.email.auth.confirm.tpl +++ b/app/config/locales/templates/he.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

שלום {{name}}, -
-
+

+

נא ללחוץ על הקישור שלהלן כדי לאמת את החשבון שלך. -
- {{redirect}} -
-
+

+{{cta}} +

אם לא ביקשת לאמת את כתובת הדוא״ל, ניתן להתעלם מההודעה זו. -
-
+

+

בברכה,
צוות {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/he.email.auth.invitation.tpl b/app/config/locales/templates/he.email.auth.invitation.tpl index a7514affda..9a9226a5ba 100644 --- a/app/config/locales/templates/he.email.auth.invitation.tpl +++ b/app/config/locales/templates/he.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

שלום, -
-
+

+

הודעת דוא״ל זו נשלחה אליך כי {{owner}} ביקש להזמינך להצטרף לצוות {{team}} ב־{{project}}. -
-
+

+

כדי להצטרף לצוות {{team}}, נא ללחוץ על הקישור: -
- {{redirect}} -
-
+

+{{cta}} +

אם איך לך עניין להצטרף לצוות, ניתן להתעלם מהודעת דוא״ל זו. -
-
+

+

בברכה,
צוות {{project}} -

+

diff --git a/app/config/locales/templates/he.email.auth.recovery.tpl b/app/config/locales/templates/he.email.auth.recovery.tpl index e349365f07..e46bf1990e 100644 --- a/app/config/locales/templates/he.email.auth.recovery.tpl +++ b/app/config/locales/templates/he.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

שלום {{name}}, -
-
+

+

נא ללחוץ על הקישור שלהלן כדי לאפס את הסיסמה שלך ב־{{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

אם לא ביקשת לאפס את סיסמתך, ניתן להתעלם מהודעת דוא״ל זו. -
-
+

+

בברכה,
צוות {{project}} -

+

diff --git a/app/config/locales/templates/hi.email.auth.confirm.tpl b/app/config/locales/templates/hi.email.auth.confirm.tpl index a8b2f10086..ec71210dce 100644 --- a/app/config/locales/templates/hi.email.auth.confirm.tpl +++ b/app/config/locales/templates/hi.email.auth.confirm.tpl @@ -1,25 +1,16 @@ - - -
+

नमस्ते {{name}}, -
-
+

+

अपना ईमेल पता वेरीफाई करने के लिए इस लिंक पर क्लिक करे। -
- {{redirect}} -
-
+

+{{cta}} +

यदि आपने इस ईमेल को वेरीफाई करने के लिए नहीं कहा है, तो आप इस संदेश को अनदेखा कर सकते हैं। -
-
+

+

धन्यवाद,
{{project}} टीम -

+

diff --git a/app/config/locales/templates/hi.email.auth.invitation.tpl b/app/config/locales/templates/hi.email.auth.invitation.tpl index 53eea9b70a..f4b5255038 100644 --- a/app/config/locales/templates/hi.email.auth.invitation.tpl +++ b/app/config/locales/templates/hi.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

नमस्ते, -
-
+

+

यह मेल आपको इसलिए भेजा गया था क्योंकि {{owner}} आपको {{project}} के लिए {{team}} टीम में टीम मेंबर बनने के लिए आमंत्रित करना चाहते थे। -
-
+

+

टीम {{team}} ज्वाइन करने के लिए इस लिंक पर क्लिक करे : -
- {{redirect}} -
-
+

+{{cta}} +

यदि आप रुचि नहीं रखते हैं, तो आप इस संदेश को अनदेखा कर सकते हैं। -
-
+

+

धन्यवाद,
{{project}} टीम -

+

diff --git a/app/config/locales/templates/hi.email.auth.recovery.tpl b/app/config/locales/templates/hi.email.auth.recovery.tpl index 697c839480..0dbeb3fd9a 100644 --- a/app/config/locales/templates/hi.email.auth.recovery.tpl +++ b/app/config/locales/templates/hi.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

नमस्ते {{name}}, -
-
+

+

{{project}} का पासवर्ड रिसेट करने के लिए इस लिंक पर क्लिक करे। -
- {{redirect}} -
-
+

+{{cta}} +

यदि आपने अपना पासवर्ड रीसेट करने के लिए नहीं कहा है, तो आप इस संदेश को अनदेखा कर सकते हैं। -
-
+

+

धन्यवाद,
{{project}} टीम -

+

diff --git a/app/config/locales/templates/hu.email.auth.confirm.tpl b/app/config/locales/templates/hu.email.auth.confirm.tpl index 680e0da2ec..259c7ceb9b 100644 --- a/app/config/locales/templates/hu.email.auth.confirm.tpl +++ b/app/config/locales/templates/hu.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Szia {{name}}, -
-
+

+

Kattints erre a linkre, hogy megerősítsd az e-mail címed. -
- {{redirect}} -
-
+

+{{cta}} +

Ha nem kérelmezted, hogy megerősítsük ezt a címet, ignoráld ezt a levelet. -
-
+

+

Köszönettel,
{{project}} csapat -

+

diff --git a/app/config/locales/templates/hu.email.auth.invitation.tpl b/app/config/locales/templates/hu.email.auth.invitation.tpl index 6dce55a639..5c8d90b86a 100644 --- a/app/config/locales/templates/hu.email.auth.invitation.tpl +++ b/app/config/locales/templates/hu.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Szia, -
-
+

+

Azért küldtük ezt az e-mailt {{owner}} mert meg szeretnénk hívni a {{team}} csapatba a következő projektre {{project}}. -
-
+

+

Kattints erre a linkre, hogy a {{team}} csapat tagja legyél: -
- {{redirect}} -
-
+

+{{cta}} +

Ha nem vegy ebben érdekelt ignoráld ezt az üzenetet. -
-
+

+

Köszönettel,
{{project}} csapat -

+

diff --git a/app/config/locales/templates/hu.email.auth.recovery.tpl b/app/config/locales/templates/hu.email.auth.recovery.tpl index 564d55bd0b..b5e915516a 100644 --- a/app/config/locales/templates/hu.email.auth.recovery.tpl +++ b/app/config/locales/templates/hu.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Szia {{name}}, -
-
+

+

Kattints erre a linkre, hogy visszaállítsuk a {{project}} jelszavad. -
- {{redirect}} -
-
+

+{{cta}} +

Ha nem kérvényezted, hogy visszaállítsuk a jelszavad ignoráld ezt a levelet. -
-
+

+

Köszönettel,
{{project}} csapat -

+

diff --git a/app/config/locales/templates/hy.email.auth.confirm.tpl b/app/config/locales/templates/hy.email.auth.confirm.tpl index f8c8f99670..dd8666c3a3 100644 --- a/app/config/locales/templates/hy.email.auth.confirm.tpl +++ b/app/config/locales/templates/hy.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Ողջույն, {{name}}, -
-
+

+

Անցեք հղումով, որպեսզի հաստատեք Ձեր էլեկտրոնային հասցեն։ -
- {{redirect}} -
-
+

+{{cta}} +

Եթե չեք պահանջել էլեկտրոնային հասցեի հաստատում, պարզապես արհամարհեք այս նամակը։ -
-
+

+

Շնորհակալություն,
{{project}} թիմ -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/hy.email.auth.invitation.tpl b/app/config/locales/templates/hy.email.auth.invitation.tpl index e471c81b63..2d0f125403 100644 --- a/app/config/locales/templates/hy.email.auth.invitation.tpl +++ b/app/config/locales/templates/hy.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Ողջույն, -
-
+

+

Դուք ստացել եք այս նամակը, քանի որ {{owner}}-ը հրավիրում է Ձեզ {{team}} խումբ, {{project}} պրոեկտում։ -
-
+

+

Անցեք հղումով, որ միանաք {{team}} թիմին՝ -
- {{redirect}} -
-
+

+{{cta}} +

Եթե հետաքրքրված չեք դրանով, պարզապես արհամարհեք այս նամակը։ -
-
+

+

Շնորհակալություն,
{{project}} թիմ -

+

diff --git a/app/config/locales/templates/hy.email.auth.recovery.tpl b/app/config/locales/templates/hy.email.auth.recovery.tpl index d08068efab..f51dcc96fd 100644 --- a/app/config/locales/templates/hy.email.auth.recovery.tpl +++ b/app/config/locales/templates/hy.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Ողջույն, {{name}}, -
-
+

+

Անցեք հղումով, որպեսզի փոխեք գաղտնաբառը {{project}} պրոեկտի համար։ -
- {{redirect}} -
-
+

+{{cta}} +

Եթե չեք պահանջել գաղտնաբառի փոփոխություն, արհամարհեք այս նամակը։ -
-
+

+

Շնորհակալություն
{{project}} թիմ -

+

diff --git a/app/config/locales/templates/id.email.auth.confirm.tpl b/app/config/locales/templates/id.email.auth.confirm.tpl index a6049cb8f6..a5f3427c35 100644 --- a/app/config/locales/templates/id.email.auth.confirm.tpl +++ b/app/config/locales/templates/id.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Halo {{name}}, -
-
+

+

Ikuti link ini untuk memverifikasi alamat email Anda. -
- {{redirect}} -
-
+

+{{cta}} +

Jika Anda tidak meminta untuk memverifikasi alamat ini, Anda dapat mengabaikan pesan ini. -
-
+

+

Terima kasih,
Tim {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/id.email.auth.invitation.tpl b/app/config/locales/templates/id.email.auth.invitation.tpl index 7ab49b2002..d81a4a5d83 100644 --- a/app/config/locales/templates/id.email.auth.invitation.tpl +++ b/app/config/locales/templates/id.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Halo, -
-
+

+

Email ini dikirimkan kepada Anda karena {{owner}} ingin mengundang Anda untuk menjadi anggota tim {{team}} di {{project}}. -
-
+

+

Ikuti link ini untuk bergabung dengan tim {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Jika Anda tidak tertarik, Anda dapat mengabaikan pesan ini. -
-
+

+

Terima kasih,
Tim {{project}} -

+

diff --git a/app/config/locales/templates/id.email.auth.recovery.tpl b/app/config/locales/templates/id.email.auth.recovery.tpl index e0b953f224..6a7d2596c6 100644 --- a/app/config/locales/templates/id.email.auth.recovery.tpl +++ b/app/config/locales/templates/id.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Halo {{name}}, -
-
+

+

Ikuti link ini untuk mereset kata sandi {{project}} Anda. -
- {{redirect}} -
-
+

+{{cta}} +

Jika Anda tidak meminta untuk mereset kata sandi Anda, Anda dapat mengabaikan pesan ini. -
-
+

+

Terima kasih,
Tim {{project}} -

+

diff --git a/app/config/locales/templates/is.email.auth.confirm.tpl b/app/config/locales/templates/is.email.auth.confirm.tpl index e9733cd47d..c6b9778a45 100644 --- a/app/config/locales/templates/is.email.auth.confirm.tpl +++ b/app/config/locales/templates/is.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Halló {{name}}, -
-
+

+

Fylgdu þessum tengli til að staðfesta netfangið þitt. -
- {{redirect}} -
-
+

+{{cta}} +

Ef þú baðst ekki um að staðfesta þetta netfang geturðu hunsað þessi skilaboð. -
-
+

+

Takk,
{{project}} Teymi -

+

diff --git a/app/config/locales/templates/is.email.auth.invitation.tpl b/app/config/locales/templates/is.email.auth.invitation.tpl index 2738fc2025..83e1e5696d 100644 --- a/app/config/locales/templates/is.email.auth.invitation.tpl +++ b/app/config/locales/templates/is.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Halló, -
-
+

+

Þessi póstur var sendur til þín vegna þess að {{owner}} vildi bjóða þér að gerast liðsmaður í {{team}} teymi í {{project}}. -
-
+

+

Fylgdu þessum hlekk til að ganga í {{team}} liðið: -
- {{redirect}} -
-
+

+{{cta}} +

Ef þú hefur ekki áhuga geturðu hunsað þessi skilaboð. -
-
+

+

Takk,
{{project}} Teymi -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/is.email.auth.recovery.tpl b/app/config/locales/templates/is.email.auth.recovery.tpl index fc3bc57722..edddba1ffb 100644 --- a/app/config/locales/templates/is.email.auth.recovery.tpl +++ b/app/config/locales/templates/is.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Halló {{name}}, -
-
+

+

Fylgdu þessum tengli til að núllstilla lykilorð {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Ef þú baðst ekki um að endurstilla lykilorðið þitt geturðu hunsað þessi skilaboð. -
-
+

+

Takk,
{{project}} Teymi -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/it.email.auth.confirm.tpl b/app/config/locales/templates/it.email.auth.confirm.tpl index 93ee5e006f..ec7837ec3d 100644 --- a/app/config/locales/templates/it.email.auth.confirm.tpl +++ b/app/config/locales/templates/it.email.auth.confirm.tpl @@ -1,25 +1,16 @@ - - -
+

Ciao {{name}}, -
-
+

+

Segui questo link per verificare il tuo indirizzo email. -
- {{redirect}} -
-
+

+{{cta}} +

Se non hai chiesto di verificare questo indirizzo, puoi ignorare questo messaggio. -
-
+

+

Grazie,
Il team di {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/it.email.auth.invitation.tpl b/app/config/locales/templates/it.email.auth.invitation.tpl index e6eddb5d1b..f4b8e4f93b 100644 --- a/app/config/locales/templates/it.email.auth.invitation.tpl +++ b/app/config/locales/templates/it.email.auth.invitation.tpl @@ -1,28 +1,19 @@ - - -
+

Ciao, -
-
+

+

Questa mail ti è stata inviata perchè {{owner}} vuole invitarti a diventare un membro del team {{team}} del progetto {{project}}. -
-
+

+

Segui questo link per unirti al team {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Se non sei interessato, puoi ignorare questo messaggio. -
-
+

+

Grazie,
Il team di {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/it.email.auth.recovery.tpl b/app/config/locales/templates/it.email.auth.recovery.tpl index 64e865fe76..870b3ab0f5 100644 --- a/app/config/locales/templates/it.email.auth.recovery.tpl +++ b/app/config/locales/templates/it.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Ciao {{name}}, -
-
+

+

Segui questo link per reimpostare la tua password per {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Se non hai chiesto di reimpostare la password, puoi ignorare questo messaggio. -
-
+

+

Grazie,
Il team di {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ja.email.auth.confirm.tpl b/app/config/locales/templates/ja.email.auth.confirm.tpl index 6c7a89f355..a06f96cd83 100644 --- a/app/config/locales/templates/ja.email.auth.confirm.tpl +++ b/app/config/locales/templates/ja.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

{{name}}さん こんにちは。 -
-
+

+

下記のリンクからメールアドレスを認証してください。 -
- {{redirect}} -
-
+

+{{cta}} +

お手数ですが、心当たりがない場合このメールを破棄してください。 -
-
+

+

ありがとうございます。
{{project}} チーム -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ja.email.auth.invitation.tpl b/app/config/locales/templates/ja.email.auth.invitation.tpl index ed59caff07..26d5de981b 100644 --- a/app/config/locales/templates/ja.email.auth.invitation.tpl +++ b/app/config/locales/templates/ja.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

こんにちは。 -
-
+

+

{{owner}} さんから {{project}} プロジェクトの {{team}} チームへの参加招待が届きました。 -
-
+

+

下記のリンクから {{team}} へ参加してください。 -
- {{redirect}} -
-
+

+{{cta}} +

お手数ですが、心当たりがない場合このメールを破棄してください。 -
-
+

+

ありがとうございます。
{{project}} チーム -

+

diff --git a/app/config/locales/templates/ja.email.auth.recovery.tpl b/app/config/locales/templates/ja.email.auth.recovery.tpl index 108c7aebee..0b0260c586 100644 --- a/app/config/locales/templates/ja.email.auth.recovery.tpl +++ b/app/config/locales/templates/ja.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

{{name}}さん こんにちは。 -
-
+

+

下記のリンクから {{project}} プロジェクトのパスワードを再設定してください。 -
- {{redirect}} -
-
+

+{{cta}} +

お手数ですが、心当たりがない場合このメールを破棄してください。 -
-
+

+

ありがとうございます。
{{project}} チーム -

+

diff --git a/app/config/locales/templates/jv.email.auth.confirm.tpl b/app/config/locales/templates/jv.email.auth.confirm.tpl index c1afdc69a9..71279a3c89 100644 --- a/app/config/locales/templates/jv.email.auth.confirm.tpl +++ b/app/config/locales/templates/jv.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Halo, {{name}}, -
-
+

+

Tindakake tautan iki kanggo verifikasi alamat email sampeyan. -
- {{redirect}} -
-
+

+{{cta}} +

Yen sampeyan ora njaluk verifikasi alamat iki, sampeyan bisa nglalekake pesen iki. -
-
+

+

Matur suwun,
tim {{project}} -

+

diff --git a/app/config/locales/templates/jv.email.auth.invitation.tpl b/app/config/locales/templates/jv.email.auth.invitation.tpl index 8c5de0f419..807cc3f478 100644 --- a/app/config/locales/templates/jv.email.auth.invitation.tpl +++ b/app/config/locales/templates/jv.email.auth.invitation.tpl @@ -1,28 +1,19 @@ - - -
+

Halo, -
-
+

+

Email iki dikirim menyang sampeyan amarga {{owner}} pengin ngajak sampeyan dadi anggota tim ing {{team}} tim ing {{project}}. -
-
+

+

Tindakake link iki kanggo gabung ing {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Yen sampeyan ora kasengsem, sampeyan bisa nglirwakake pesen iki. -
-
+

+

Matur suwun,
tim {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/jv.email.auth.recovery.tpl b/app/config/locales/templates/jv.email.auth.recovery.tpl index ed6c879aeb..1efd86bc77 100644 --- a/app/config/locales/templates/jv.email.auth.recovery.tpl +++ b/app/config/locales/templates/jv.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Halo {{name}}, -
-
+

+

Tindakake link iki kanggo ngreset {{project}} sandhi. -
- {{redirect}} -
-
+

+{{cta}} +

Yen sampeyan ora njaluk ngreset sandhi, sampeyan bisa nglalekake pesen iki. -
-
+

+

Matur suwun,
tim {{project}} -

+

diff --git a/app/config/locales/templates/km.email.auth.confirm.tpl b/app/config/locales/templates/km.email.auth.confirm.tpl index c2c94d08c0..193508c9bd 100644 --- a/app/config/locales/templates/km.email.auth.confirm.tpl +++ b/app/config/locales/templates/km.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

សួស្ដី {{name}}, -
-
+

+

តាមតំណនេះដើម្បីផ្ទៀងផ្ទាត់អាសយដ្ឋានសារអេឡិចត្រូនិចរបស់អ្នក។ -
- {{redirect}} -
-
+

+{{cta}} +

ប្រសិនបើអ្នកពុំបានស្នើសុំផ្ទៀងផ្ទាត់អាសយដ្ឋាននេះទេ អ្នកអាចមិនអើពើនឹងសារនេះបាន។ -
-
+

+

អរគុណ,
ក្រុម {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/km.email.auth.invitation.tpl b/app/config/locales/templates/km.email.auth.invitation.tpl index 910a14451f..a366ed73e9 100644 --- a/app/config/locales/templates/km.email.auth.invitation.tpl +++ b/app/config/locales/templates/km.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

សួស្ដី, -
-
+

+

សារអេឡិចត្រូនិចនេះត្រូវបានផ្ញើទៅអ្នកដោយសារ {{owner}} ចង់អញ្ជើញអ្នកឲ្យក្លាយជាសមាជិកនៅក្រុម {{team}} លើ {{project}}។ -
-
+

+

តាមតំណនេះដើម្បីចូលរួមក្នុងក្រុម {{team}}៖ -
- {{redirect}} -
-
+

+{{cta}} +

ប្រសិនបើអ្នកមិនចាប់អារម្មណ៍ អ្នកអាចមិនអើពើនឹងសារនេះបាន។ -
-
+

+

អរគុណ,
ក្រុម {{project}} -

+

diff --git a/app/config/locales/templates/km.email.auth.recovery.tpl b/app/config/locales/templates/km.email.auth.recovery.tpl index 138997623b..578fe15d14 100644 --- a/app/config/locales/templates/km.email.auth.recovery.tpl +++ b/app/config/locales/templates/km.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

សួស្ដី {{name}}, -
-
+

+

តាមតំណនេះដើម្បីកំណត់ពាក្យសម្ងាត់នៃ {{project}} របស់អ្នកឡើងវិញ។ -
- {{redirect}} -
-
+

+{{cta}} +

ប្រសិនបើអ្នកពុំបានស្នើសុំកំណត់ពាក្យសម្ងាត់របស់អ្នកឡើងវិញទេ អ្នកអាចមិនអើពើនឹងសារនេះបាន។ -
-
+

+

អរគុណ,
ក្រុម {{project}} -

+

diff --git a/app/config/locales/templates/ko.email.auth.confirm.tpl b/app/config/locales/templates/ko.email.auth.confirm.tpl index e3c6070d55..bf9bfd1c08 100644 --- a/app/config/locales/templates/ko.email.auth.confirm.tpl +++ b/app/config/locales/templates/ko.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

안녕하세요 {{name}}님, -
-
+

+

회원님 이메일을 인증하러 아래 링크를 클릭하세요. -
- {{redirect}} -
-
+

+{{cta}} +

만약 인증 요청하지 않으셨다면 이 이메일을 무시하세요. -
-
+

+

감사합니다!
{{project}}팀 드림 -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ko.email.auth.invitation.tpl b/app/config/locales/templates/ko.email.auth.invitation.tpl index 4fc62b8516..21ed74de68 100644 --- a/app/config/locales/templates/ko.email.auth.invitation.tpl +++ b/app/config/locales/templates/ko.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

안녕하세요, -
-
+

+

{{owner}}님이 {{project}}프로젝트의 {{team}}팀에 초대했습니다. -
-
+

+

아래 링크를 통하여 {{team}}팀에 합류해주시면 됩니다. -
- {{redirect}} -
-
+

+{{cta}} +

만약 합류에 관심 없으시면 이 이메일을 무시하세요. -
-
+

+

감사합니다!
{{project}}팀 드림 -

+

diff --git a/app/config/locales/templates/ko.email.auth.recovery.tpl b/app/config/locales/templates/ko.email.auth.recovery.tpl index bf73a93058..5445ef6a3e 100644 --- a/app/config/locales/templates/ko.email.auth.recovery.tpl +++ b/app/config/locales/templates/ko.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

안녕하세요 {{name}}님, -
-
+

+

{{project}} 비밀번호 재설정하러 아래 링크를 클릭하세요. -
- {{redirect}} -
-
+

+{{cta}} +

만약 회원님이 비밀번호 재설정 요청하지 않으셨다면 이 이메일을 무시하세요. -
-
+

+

감사합니다!
{{project}}팀 드림 -

+

diff --git a/app/config/locales/templates/lt.email.auth.confirm.tpl b/app/config/locales/templates/lt.email.auth.confirm.tpl index f1bc439b11..d7514cc25b 100644 --- a/app/config/locales/templates/lt.email.auth.confirm.tpl +++ b/app/config/locales/templates/lt.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Sveiki, {{name}}, -
-
+

+

Paspauskite nuorodą, kad patvirtinti savo el. paštą. -
- {{redirect}} -
-
+

+{{cta}} +

Jei neprašėte el. pašto patvirtinimo, ignoruokite šį laišką -
-
+

+

Ačiū,
komanda {{project}} -

+

diff --git a/app/config/locales/templates/lt.email.auth.invitation.tpl b/app/config/locales/templates/lt.email.auth.invitation.tpl index 0c40a3cbc3..2eb630b5bd 100644 --- a/app/config/locales/templates/lt.email.auth.invitation.tpl +++ b/app/config/locales/templates/lt.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Sveiki, -
-
+

+

Šis laiškas buvo išsiųstas dėl to, kad {{owner}} kviečia tapti komandos {{team}} nariu projekte {{project}}. -
-
+

+

Paspauskite nuorodą, kad prisijungti prie komandos {{team}} : -
- {{redirect}} -
-
+

+{{cta}} +

Jei Jums neįdomu, ignoruokite šį laišką. -
-
+

+

Ačiū,
komanda {{project}} -

+

diff --git a/app/config/locales/templates/lt.email.auth.recovery.tpl b/app/config/locales/templates/lt.email.auth.recovery.tpl index 7aba4cd2ce..0ed8354918 100644 --- a/app/config/locales/templates/lt.email.auth.recovery.tpl +++ b/app/config/locales/templates/lt.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Sveiki, {{name}}, -
-
+

+

Paspauskite nuorodą, kad pakeisti slaptožodį projektui {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Jei neprašėte atkurti slaptažodžio, ignoruokite šį laišką. -
-
+

+

Ačiū,
komanda {{project}} -

+

diff --git a/app/config/locales/templates/ml.email.auth.confirm.tpl b/app/config/locales/templates/ml.email.auth.confirm.tpl index 14746f04a5..b4f0724d70 100644 --- a/app/config/locales/templates/ml.email.auth.confirm.tpl +++ b/app/config/locales/templates/ml.email.auth.confirm.tpl @@ -1,28 +1,19 @@ - - -
+

നമസ്കാരം {{name}}, -
-
+

+

താങ്കളുടെ ഇമെയിൽ ഐഡി വെരിഫൈ ചെയ്യുന്നതിന് താഴെ കാണുന്ന ലിങ്കിൽ ക്ലിക്ക് ചെയ്യൂ -
- {{redirect}} -
-
+

+{{cta}} +

ഇമെയിൽ ഐഡി വെരിഫൈ ചെയ്യേണ്ടെങ്കിൽ ഈ മെസ്സേജ് ഇഗ്നോർ ചെയ്യാം -
-
+

+

നന്ദി
{{project}} ടീം. -

+

diff --git a/app/config/locales/templates/ml.email.auth.invitation.tpl b/app/config/locales/templates/ml.email.auth.invitation.tpl index 5f16374dcd..06b5bdf08e 100644 --- a/app/config/locales/templates/ml.email.auth.invitation.tpl +++ b/app/config/locales/templates/ml.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

നമസ്കാരം, -
-
+

+

{{owner}} താങ്കളെ {{project}} പ്രോജക്ടിലെ {{team}} ടീമിൽ അംഗമാവാൻ ക്ഷണിച്ചിരിക്കുന്നു. -
-
+

+

{{team}} ടീമിൽ ചേരുവാൻ താഴെ കാണുന്ന ലിങ്കിൽ ക്ലിക്ക് ചെയ്യൂ. -
- {{redirect}} -
-
+

+{{cta}} +

താൽപര്യമില്ലെങ്കിൽ ഈ മെസ്സേജ് ഇഗ്നോർ ചെയ്യാം -
-
+

+

നന്ദി,
{{project}} ടീം. -

+

diff --git a/app/config/locales/templates/ml.email.auth.recovery.tpl b/app/config/locales/templates/ml.email.auth.recovery.tpl index ee1e73a52b..cf82e5c489 100644 --- a/app/config/locales/templates/ml.email.auth.recovery.tpl +++ b/app/config/locales/templates/ml.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

നമസ്കാരം {{name}}, -
-
+

+

താങ്കളുടെ {{project}} പാസ്‌വേഡ് റീസെറ്റ് ചെയ്യാൻ താഴെ കാണുന്ന ലിങ്കിൽ ക്ലിക്ക് ചെയ്യുക -
- {{redirect}} -
-
+

+{{cta}} +

പാസ്‌വേഡ് റീസെറ്റ് ചെയ്യാൻ താങ്കൾ റിക്വസ്റ്റ് ചെയ്തിട്ടില്ലെങ്കിൽ ദയവായി ഈ മെസ്സേജ് ഇഗ്നോർ ചെയ്യുക -
-
+

+

നന്ദി,
{{project}} ടീം -

+

diff --git a/app/config/locales/templates/my.email.auth.confirm.tpl b/app/config/locales/templates/my.email.auth.confirm.tpl index 65a224c51f..174b100e02 100644 --- a/app/config/locales/templates/my.email.auth.confirm.tpl +++ b/app/config/locales/templates/my.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hello {{name}}, -
-
+

+

Ikuti pautan ini untuk mengesahkan alamat e-mel anda. -
- {{redirect}} -
-
+

+{{cta}} +

Jika anda tidak meminta untuk mengesahkan alamat ini, anda boleh mengabaikan mesej ini. -
-
+

+

Terima kasih,
Kumpulan {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/my.email.auth.invitation.tpl b/app/config/locales/templates/my.email.auth.invitation.tpl index 891e1eb4de..66e38bb608 100644 --- a/app/config/locales/templates/my.email.auth.invitation.tpl +++ b/app/config/locales/templates/my.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hello, -
-
+

+

E-mel ini dihantar kepada anda kerana {{owner}}ingin mengundang Anda untuk menjadi anggota kumpulan {{team}} di dalam {{project}}. -
-
+

+

Ikuti pautan ini untuk menyertai kumpulan {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Sekiranya anda tidak berminat,anda boleh mengabaikan mesej ini. -
-
+

+

Terima Kasih,
Kumpulan {{project}} -

+

diff --git a/app/config/locales/templates/my.email.auth.recovery.tpl b/app/config/locales/templates/my.email.auth.recovery.tpl index 25f81408d7..a14c78cb59 100644 --- a/app/config/locales/templates/my.email.auth.recovery.tpl +++ b/app/config/locales/templates/my.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hello {{name}}, -
-
+

+

Ikuti pautan ini sekiranya anda mahu menetapkan semula kata laluan {{project}} anda. -
- {{redirect}} -
-
+

+{{cta}} +

Sekiranya anda tidak meminta untuk menetapkan semula kata laluan anda,anda boleh sahaja mengabaikan mesej ini. -
-
+

+

Terima Kasih,
Kumpulan {{project}} -

+

diff --git a/app/config/locales/templates/nl.email.auth.confirm.tpl b/app/config/locales/templates/nl.email.auth.confirm.tpl index 513593bfc6..04f6a14ccf 100644 --- a/app/config/locales/templates/nl.email.auth.confirm.tpl +++ b/app/config/locales/templates/nl.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

Klink op deze link om uw emailadres te valideren. -
- {{redirect}} -
-
+

+{{cta}} +

Als u niet heeft gevraagd om dit adres te verifiëren, kunt u dit bericht negeren. -
-
+

+

Bedankt,
{{project}} team -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/nl.email.auth.invitation.tpl b/app/config/locales/templates/nl.email.auth.invitation.tpl index 252e104aff..75ac6f4510 100644 --- a/app/config/locales/templates/nl.email.auth.invitation.tpl +++ b/app/config/locales/templates/nl.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hallo, -
-
+

+

Deze mail is naar je gestuurd omdat {{owner}} wilde dat u een lid zou worden in het {{team}} team voor {{project}}. -
-
+

+

Volg deze link om het team te joinen {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

ALs u niet geïnteresseerd bent, kunt u dit bericht negeren. -
-
+

+

Bedankt,
{{project}} team -

+

diff --git a/app/config/locales/templates/nl.email.auth.recovery.tpl b/app/config/locales/templates/nl.email.auth.recovery.tpl index 2c4d5952a2..4c7ac87961 100644 --- a/app/config/locales/templates/nl.email.auth.recovery.tpl +++ b/app/config/locales/templates/nl.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

Klik op deze link om uw {{project}} wachtwoord te resetten. -
- {{redirect}} -
-
+

+{{cta}} +

Als u niet gevraagd heeft om uw wachtwoord te resetten, kunt u dit bericht negeren. -
-
+

+

Bedankt,
{{project}} team -

+

diff --git a/app/config/locales/templates/no.email.auth.confirm.tpl b/app/config/locales/templates/no.email.auth.confirm.tpl index abe17ff65f..63c906cdf5 100644 --- a/app/config/locales/templates/no.email.auth.confirm.tpl +++ b/app/config/locales/templates/no.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hei {{name}}, -
-
+

+

Følg denne lenken for å verifisere din e-postadresse. -
- {{redirect}} -
-
+

+{{cta}} +

Hvis du ikke har spurt om å verifisere din e-post, kan du ignorere denne meldingen. -
-
+

+

Hilsen,
{{project}}-teamet -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/no.email.auth.invitation.tpl b/app/config/locales/templates/no.email.auth.invitation.tpl index c56e913d3d..00cd9defe5 100644 --- a/app/config/locales/templates/no.email.auth.invitation.tpl +++ b/app/config/locales/templates/no.email.auth.invitation.tpl @@ -1,28 +1,19 @@ - - -
+

Hei, -
-
+

+

Denne mailen ble sendt til deg fordi {{owner}} har invitert deg til å bli medlem av {{team}}-teamet på {{project}}. -
-
+

+

Follow this link to join the {{team}} team: Følg denne lenken for å bli med på {{team}}-teamet: -
- {{redirect}} -
-
+

+{{cta}} +

Hvis du ikke er interresert kan du ignorere denne meldingen. -
-
+

+

Hilsen,
{{project}}-teamet -

+

diff --git a/app/config/locales/templates/no.email.auth.recovery.tpl b/app/config/locales/templates/no.email.auth.recovery.tpl index 75b8be0e96..472674be31 100644 --- a/app/config/locales/templates/no.email.auth.recovery.tpl +++ b/app/config/locales/templates/no.email.auth.recovery.tpl @@ -1,25 +1,16 @@ - - -
+

Hei {{name}}, -
-
+

+

Follow this link to reset your {{project}} password. Følg denne lenken for å tilbakestille ditt {{project}}-passord. -
- {{redirect}} -
-
+

+{{cta}} +

Hvis du ikke har spurt om å tilbakestille passordet ditt, kan du ignorere denne meldingen. -
-
+

+

Hilsen,
{{project}}-teamet -

+

diff --git a/app/config/locales/templates/ph.email.auth.confirm.tpl b/app/config/locales/templates/ph.email.auth.confirm.tpl index 08a9b6d639..63e5db58db 100644 --- a/app/config/locales/templates/ph.email.auth.confirm.tpl +++ b/app/config/locales/templates/ph.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Kamusta {{name}}, -
-
+

+

Sundun ang link na ito upang ma-verify ang iyong email address. -
- {{redirect}} -
-
+

+{{cta}} +

Kung hindi mo hiniling na i-verify ang address na ito, maaari mong balewalain ang mensaheng ito. -
-
+

+

Salamat,
Pangkat ng {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ph.email.auth.invitation.tpl b/app/config/locales/templates/ph.email.auth.invitation.tpl index f8a8668e50..23916efb0c 100644 --- a/app/config/locales/templates/ph.email.auth.invitation.tpl +++ b/app/config/locales/templates/ph.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Kamusta, -
-
+

+

Ipinadala ang email na ito sa iyo dahil nais kang anyayahan ni {{ owner }} upang maging isang kasapi ng pangkat ng {{ team }} sa {{ project }}. -
-
+

+

Sundan ang link na ito upang sumali sa pangkat ng {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Kung hindi ka interesado, maari mong balewalain ang mensahing ito. -
-
+

+

Salamat,
Pangkat ng {{project}} -

+

diff --git a/app/config/locales/templates/ph.email.auth.recovery.tpl b/app/config/locales/templates/ph.email.auth.recovery.tpl index abf1b780ef..c9096076a9 100644 --- a/app/config/locales/templates/ph.email.auth.recovery.tpl +++ b/app/config/locales/templates/ph.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Kamusta {{name}}, -
-
+

+

Sundan ang link na ito updang i-reset ang iyong password sa {{ project }}. -
- {{redirect}} -
-
+

+{{cta}} +

Kung hindi mo hiniling na i-reset ang iyong password, maaari mong balewalain ang mensahe na ito. -
-
+

+

Salamat,
Pangkat ng {{project}} -

+

diff --git a/app/config/locales/templates/pl.email.auth.confirm.tpl b/app/config/locales/templates/pl.email.auth.confirm.tpl index e46187999f..5b797d331b 100644 --- a/app/config/locales/templates/pl.email.auth.confirm.tpl +++ b/app/config/locales/templates/pl.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Witaj {{name}}, -
-
+

+

Kliknij ten link, aby zweryfikować swój adres e-mail. -
- {{redirect}} -
-
+

+{{cta}} +

Jeśli nie prosiłeś o weryfikację tego adresu, możesz zignorować tę wiadomość. -
-
+

+

Dziękujemy,
zespół {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/pl.email.auth.invitation.tpl b/app/config/locales/templates/pl.email.auth.invitation.tpl index d11586de75..387b2e4ae8 100644 --- a/app/config/locales/templates/pl.email.auth.invitation.tpl +++ b/app/config/locales/templates/pl.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Witaj, -
-
+

+

Ta wiadomość została do Ciebie wysłana, ponieważ {{owner}} chciałby zaprosić Cię do dołączenia do zespołu {{team}} w {{project}}. -
-
+

+

Kliknij ten link, aby dołączyć do zespołu {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Jeśli nie jesteś zainteresowany, możesz zignorować tę wiadomość. -
-
+

+

Dziękujemy,
zaspół {{project}} -

+

diff --git a/app/config/locales/templates/pl.email.auth.recovery.tpl b/app/config/locales/templates/pl.email.auth.recovery.tpl index 78066ee064..5b79846ae6 100644 --- a/app/config/locales/templates/pl.email.auth.recovery.tpl +++ b/app/config/locales/templates/pl.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Witaj {{name}}, -
-
+

+

Kliknij ten link, aby zresetować hasło do {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Jeśli nie prosiłeś o zresetowanie hasła, możesz zignorować tę wiadomość. -
-
+

+

Dziękujemy,
zaspół {{project}} -

+

diff --git a/app/config/locales/templates/pn.email.auth.confirm.tpl b/app/config/locales/templates/pn.email.auth.confirm.tpl index 3b31381808..c96d27dee1 100644 --- a/app/config/locales/templates/pn.email.auth.confirm.tpl +++ b/app/config/locales/templates/pn.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

ਹੈਲੋ {{name}}, -
-
+

+

ਆਪਣੇ ਈਮੇਲ ਪਤੇ ਦੀ ਪੁਸ਼ਟੀ ਕਰਨ ਲਈ ਇਸ ਲਿੰਕ ਦਾ ਪਾਲਣ ਕਰੋ: -
- {{redirect}} -
-
+

+{{cta}} +

ਜੇ ਤੁਸੀਂ ਇਸ ਪਤੇ ਨੂੰ ਪ੍ਰਮਾਣਿਤ ਕਰਨ ਲਈ ਨਹੀਂ ਕਿਹਾ, ਤਾਂ ਤੁਸੀਂ ਇਸ ਸੁਨੇਹੇ ਨੂੰ ਨਜ਼ਰ ਅੰਦਾਜ਼ ਕਰ ਸਕਦੇ ਹੋ. -
-
+

+

ਧੰਨਵਾਦ,
{{project}} ਟੀਮ -

+

diff --git a/app/config/locales/templates/pn.email.auth.invitation.tpl b/app/config/locales/templates/pn.email.auth.invitation.tpl index 2b0a5fd279..6280d951a1 100644 --- a/app/config/locales/templates/pn.email.auth.invitation.tpl +++ b/app/config/locales/templates/pn.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

ਸਤ ਸ੍ਰੀ ਅਕਾਲ, -
-
+

+

ਇਹ ਮੇਲ ਤੁਹਾਨੂੰ ਇਸ ਲਈ ਭੇਜਿਆ ਗਿਆ ਸੀ ਕਿਉਂਕਿ {{owner}} ਤੁਹਾਨੂੰ {{team} at ਟੀਮ {{project}} 'ਤੇ ਟੀਮ ਦੇ ਮੈਂਬਰ ਬਣਨ ਲਈ ਸੱਦਾ ਦੇਣਾ ਚਾਹੁੰਦਾ ਸੀ. -
-
+

+

{{team}} ਟੀਮ ਵਿੱਚ ਸ਼ਾਮਲ ਹੋਣ ਲਈ ਇਸ ਲਿੰਕ ਦਾ ਪਾਲਣ ਕਰੋ: -
- {{redirect}} -
-
+

+{{cta}} +

ਜੇ ਤੁਸੀਂ ਦਿਲਚਸਪੀ ਨਹੀਂ ਰੱਖਦੇ, ਤਾਂ ਤੁਸੀਂ ਇਸ ਸੰਦੇਸ਼ ਨੂੰ ਨਜ਼ਰ ਅੰਦਾਜ਼ ਕਰ ਸਕਦੇ ਹੋ. -
-
+

+

ਧੰਨਵਾਦ,
{{project}} ਟੀਮ -

+

diff --git a/app/config/locales/templates/pn.email.auth.recovery.tpl b/app/config/locales/templates/pn.email.auth.recovery.tpl index e85026a203..cdb4fd069d 100644 --- a/app/config/locales/templates/pn.email.auth.recovery.tpl +++ b/app/config/locales/templates/pn.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

ਸਤ ਸ੍ਰੀ ਅਕਾਲ {{name}}, -
-
+

+

ਆਪਣੇ {{project}} ਪਾਸਵਰਡ ਨੂੰ ਰੀਸੈਟ ਕਰਨ ਲਈ ਇਸ ਲਿੰਕ ਦਾ ਪਾਲਣ ਕਰੋ. -
- {{redirect}} -
-
+

+{{cta}} +

ਜੇ ਤੁਸੀਂ ਆਪਣਾ ਪਾਸਵਰਡ ਰੀਸੈਟ ਕਰਨ ਲਈ ਨਹੀਂ ਕਿਹਾ, ਤਾਂ ਤੁਸੀਂ ਇਸ ਸੁਨੇਹੇ ਨੂੰ ਨਜ਼ਰ ਅੰਦਾਜ਼ ਕਰ ਸਕਦੇ ਹੋ. -
-
+

+

ਧੰਨਵਾਦ,
{{project}} ਟੀਮ -

+

diff --git a/app/config/locales/templates/pt-br.email.auth.confirm.tpl b/app/config/locales/templates/pt-br.email.auth.confirm.tpl index 7cb959914e..f7ffdd8c38 100644 --- a/app/config/locales/templates/pt-br.email.auth.confirm.tpl +++ b/app/config/locales/templates/pt-br.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Olá {{name}}, -
-
+

+

Por favor, confirme o seu email acessando o link abaixo. -
- {{redirect}} -
-
+

+{{cta}} +

Caso a confirmação de email não foi solicitada por você, ignore esta mensagem. -
-
+

+

Atenciosamente,
Equipe {{project}} -

+

diff --git a/app/config/locales/templates/pt-br.email.auth.invitation.tpl b/app/config/locales/templates/pt-br.email.auth.invitation.tpl index f4255ffafa..b0de23b3fd 100644 --- a/app/config/locales/templates/pt-br.email.auth.invitation.tpl +++ b/app/config/locales/templates/pt-br.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Olá, -
-
+

+

Este email foi enviado a você porque
{{owner}} deseja convidá-lo para se tornar membro da equipe {{team}} no {{project}}. -
-
+

+

Entre no link abaixo para se juntar a equipe {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Caso não estiver interessado, por favor ignore esta mensagem. -
-
+

+

Atenciosamente,
Equipe {{project}} -

+

diff --git a/app/config/locales/templates/pt-br.email.auth.recovery.tpl b/app/config/locales/templates/pt-br.email.auth.recovery.tpl index e2d710f38f..5393316918 100644 --- a/app/config/locales/templates/pt-br.email.auth.recovery.tpl +++ b/app/config/locales/templates/pt-br.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Olá {{name}}, -
-
+

+

Acesse o link abaixo para redefinir sua senha do {{project}}. -
- {{redirect}} -
-
- Caso você não solicitou a redefinição de senha, por favor ignore esta mensagem. -
-
+

+{{cta}} +

+ Caso não tenha solicitado a redefinição de senha, por favor ignore esta mensagem. +

+

Atenciosamente,
Equipe {{project}} -

+

diff --git a/app/config/locales/templates/pt-pt.email.auth.confirm.tpl b/app/config/locales/templates/pt-pt.email.auth.confirm.tpl index d2209883cc..a2d8998f3c 100644 --- a/app/config/locales/templates/pt-pt.email.auth.confirm.tpl +++ b/app/config/locales/templates/pt-pt.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Olá {{name}}, -
-
+

+

Por favor, confirme o seu email através do link abaixo. -
- {{redirect}} -
-
+

+{{cta}} +

Se não solicitou a confirmação de email, por favor ignore esta mensagem. -
-
+

+

Com os melhores cumprimentos,
Equipa {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/pt-pt.email.auth.invitation.tpl b/app/config/locales/templates/pt-pt.email.auth.invitation.tpl index 429a30be9a..c0bdfa4136 100644 --- a/app/config/locales/templates/pt-pt.email.auth.invitation.tpl +++ b/app/config/locales/templates/pt-pt.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Olá, -
-
+

+

Recebeu este email porque
{{owner}} deseja convidá-lo a tornar-se membro da equipa {{team}} no {{project}}. -
-
+

+

Use este link para se juntar à equipa {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Se não estiver interessado, por favor ignore esta mensagem. -
-
+

+

Com os melhores cumprimentos,
Equipa {{project}} -

+

diff --git a/app/config/locales/templates/pt-pt.email.auth.recovery.tpl b/app/config/locales/templates/pt-pt.email.auth.recovery.tpl index 7c89f1b729..ae09ac0998 100644 --- a/app/config/locales/templates/pt-pt.email.auth.recovery.tpl +++ b/app/config/locales/templates/pt-pt.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Olá {{name}}, -
-
+

+

Use este link para repor a sua palavra-passe {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Se não solicitou a reposição da sua palavra-passe, por favor ignore esta mensagem. -
-
+

+

Com os melhores cumprimentos,
Equipa {{project}} -

+

diff --git a/app/config/locales/templates/ro.email.auth.confirm.tpl b/app/config/locales/templates/ro.email.auth.confirm.tpl index e5672f6d9a..133fe2a189 100644 --- a/app/config/locales/templates/ro.email.auth.confirm.tpl +++ b/app/config/locales/templates/ro.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Salut {{name}}, -
-
+

+

Accesează link-ul următor pentru a-ți confirma mail-ul. -
- {{redirect}} -
-
+

+{{cta}} +

Dacă nu ai solicitat acest mail, te rugăm frumos să îl ignori. -
-
+

+

Mulțumim,
Echipa {{project}} -

+

diff --git a/app/config/locales/templates/ro.email.auth.invitation.tpl b/app/config/locales/templates/ro.email.auth.invitation.tpl index 477ecfe84b..04cdf9e0bd 100644 --- a/app/config/locales/templates/ro.email.auth.invitation.tpl +++ b/app/config/locales/templates/ro.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Salut, -
-
+

+

Ai primit acest mail deoarece {{owner}} te-a invitat să faci parte din echipa {{team}} în proiectul {{project}}. -
-
+

+

Accesează link-ul următor pentru a accepta invitația în echipa {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Dacă nu ești interesat de această invitație, te rugăm frumos să o ignori. -
-
+

+

Cu drag,
Echipa {{project}} -

+

diff --git a/app/config/locales/templates/ro.email.auth.recovery.tpl b/app/config/locales/templates/ro.email.auth.recovery.tpl index 2e934194bb..9e5f24cbea 100644 --- a/app/config/locales/templates/ro.email.auth.recovery.tpl +++ b/app/config/locales/templates/ro.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Salut {{name}}, -
-
+

+

Accesează link-ul următor pentru a-ți reseta parola din proiectul {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Dacă nu ai solicitat acest mail, te rugăm frumos să îl ignori. -
-
+

+

Cu drag,
Echipa {{project}} -

+

diff --git a/app/config/locales/templates/ru.email.auth.confirm.tpl b/app/config/locales/templates/ru.email.auth.confirm.tpl index 29993e988f..7e1c976c63 100644 --- a/app/config/locales/templates/ru.email.auth.confirm.tpl +++ b/app/config/locales/templates/ru.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Здравствуйте, {{name}}, -
-
+

+

Перейдите по ссылке, чтобы подтвердить свой адрес электронной почты. -
- {{redirect}} -
-
+

+{{cta}} +

Если вы не запрашивали подтверждение этого адреса, проигнорируйте это сообщение. -
-
+

+

Спасибо,
команда {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ru.email.auth.invitation.tpl b/app/config/locales/templates/ru.email.auth.invitation.tpl index 41de21d09f..7dc637a3de 100644 --- a/app/config/locales/templates/ru.email.auth.invitation.tpl +++ b/app/config/locales/templates/ru.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Здравствуйте, -
-
+

+

Это письмо отправлено вам, потому что {{owner}} приглашает стать членом команды {{team}} в проекте {{project}}. -
-
+

+

Перейдите по ссылке, чтобы присоединиться к команде {{team}} : -
- {{redirect}} -
-
+

+{{cta}} +

Если вы не заинтересованы, проигнорируйте это сообщение. -
-
+

+

Спасибо,
команда {{project}} -

+

diff --git a/app/config/locales/templates/ru.email.auth.recovery.tpl b/app/config/locales/templates/ru.email.auth.recovery.tpl index 680278e42c..cdcc25ac05 100644 --- a/app/config/locales/templates/ru.email.auth.recovery.tpl +++ b/app/config/locales/templates/ru.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Здравствуйте, {{name}}, -
-
+

+

Перейдите по ссылке, чтобы сбросить пароль для проекта {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Если вы не запрашивали сброс пароля, проигнорируйте это сообщение. -
-
+

+

Спасибо,
команда {{project}} -

+

diff --git a/app/config/locales/templates/si.email.auth.confirm.tpl b/app/config/locales/templates/si.email.auth.confirm.tpl index c7be6ec681..aa1bc4f273 100644 --- a/app/config/locales/templates/si.email.auth.confirm.tpl +++ b/app/config/locales/templates/si.email.auth.confirm.tpl @@ -1,25 +1,16 @@ - - -
+

ආයුබෝවන් {{name}}, -
-
+

+

ඔබගේ විද්‍යුත් තැපැල් ලිපිනය සත්‍යාපනය කිරීමට මෙම සබැඳිය අනුගමනය කරන්න. -
- {{redirect}} -
-
+

+{{cta}} +

ඔබ මෙම ලිපිනය සත්‍යාපනය කිරීමට ඉල්ලා නොසිටියේ නම්, ඔබට මෙම පණිවිඩය නොසලකා හැරිය හැක. -
-
+

+

ස්තූතියි,
{{project}} කණ්ඩායම -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/si.email.auth.invitation.tpl b/app/config/locales/templates/si.email.auth.invitation.tpl index 1a0842d2e8..2fa979751e 100644 --- a/app/config/locales/templates/si.email.auth.invitation.tpl +++ b/app/config/locales/templates/si.email.auth.invitation.tpl @@ -1,31 +1,22 @@ - - -
+

ආයුබෝවන්, -
-
+

+

මෙම තැපෑල ඔබ වෙත එවනු ලැබුවේ {{owner}}ට ඔබව , {{project}} ව්‍යාපෘතියෙහි {{team}} කණ්ඩායමේ කණ්ඩායම් සාමාජිකයෙකු වීමට ආරාධනා කිරීමට අවශ්‍ය වූ නිසාය. -
-
+

+

{{team}} කණ්ඩායමට එක්වීමට මෙම සබැඳිය අනුගමනය කරන්න: -
- {{redirect}} -
-
+

+{{cta}} +

ඔබ උනන්දුවක් නොදක්වන්නේ නම්, ඔබට මෙම පණිවිඩය නොසලකා හැරිය හැකිය. -
-
+

+

ස්තූතියි,
{{project}} කණ්ඩායම. -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/si.email.auth.recovery.tpl b/app/config/locales/templates/si.email.auth.recovery.tpl index bcf32b61ba..4df6902070 100644 --- a/app/config/locales/templates/si.email.auth.recovery.tpl +++ b/app/config/locales/templates/si.email.auth.recovery.tpl @@ -1,25 +1,15 @@ - - -
+

ආයුබෝවන් {{name}}, -
-
+

+

ඔබගේ {{project}} ව්‍යාපෘතියෙහි මුරපදය නැවත සැකසීමට මෙම සබැඳිය අනුගමනය කරන්න - -
- {{redirect}} -
-
+

+{{cta}} +

ඔබගේ මුරපදය නැවත සැකසීමට ඔබ ඉල්ලා නොසිටියේ නම්, ඔබට මෙම පණිවිඩය නොසලකා හැරිය හැක. -
-
+

+

ස්තූතියි,
{{project}} කණ්ඩායම -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/sl.email.auth.confirm.tpl b/app/config/locales/templates/sl.email.auth.confirm.tpl index e7f15272da..fb6c4a59d1 100644 --- a/app/config/locales/templates/sl.email.auth.confirm.tpl +++ b/app/config/locales/templates/sl.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Pozdravljeni {{name}}, -
-
+

+

Sledite tej povezavi za potrditev vašega email naslova. -
- {{redirect}} -
-
+

+{{cta}} +

Če niste zahtevali potrditve tega naslova, lahko to sporočilo prezrete. -
-
+

+

Hvala,
{{project}} ekipa -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/sl.email.auth.invitation.tpl b/app/config/locales/templates/sl.email.auth.invitation.tpl index 13467789a3..0dc64594b0 100644 --- a/app/config/locales/templates/sl.email.auth.invitation.tpl +++ b/app/config/locales/templates/sl.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Pozdravljeni, -
-
+

+

To sporočilo vam je bilo posredovano, ker vas je {{owner}} povabil/a, da postanete član {{team}} ekipe za {{project}}. -
-
+

+

Sledite tej povezavi, da se pridružite {{team}} ekipi: -
- {{redirect}} -
-
+

+{{cta}} +

Če vas ne zanima, lahko to sporočilo prezrete. -
-
+

+

Hvala,
{{project}} ekipa -

+

diff --git a/app/config/locales/templates/sl.email.auth.recovery.tpl b/app/config/locales/templates/sl.email.auth.recovery.tpl index 701805e12e..46b0e4ee5e 100644 --- a/app/config/locales/templates/sl.email.auth.recovery.tpl +++ b/app/config/locales/templates/sl.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Pozdravljeni {{name}}, -
-
+

+

Za ponastavitev vašega {{project}} gesla sledite tej povezavi. -
- {{redirect}} -
-
+

+{{cta}} +

Če niste zahtevali ponastavitve gesla, lahko to sporočilo prezrete. -
-
+

+

Hvala,
{{project}} ekipa -

+

diff --git a/app/config/locales/templates/sv.email.auth.confirm.tpl b/app/config/locales/templates/sv.email.auth.confirm.tpl index 0b1e4451ff..dc406e1857 100644 --- a/app/config/locales/templates/sv.email.auth.confirm.tpl +++ b/app/config/locales/templates/sv.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hej {{name}}, -
-
+

+

Vänligen följ länken nedan för att verifiera din epostadress. -
- {{redirect}} -
-
+

+{{cta}} +

Om du inte vill verifiera din epostadress så kan du ignorera detta meddelande. -
-
+

+

Tack,
{{project}}-teamet -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/sv.email.auth.invitation.tpl b/app/config/locales/templates/sv.email.auth.invitation.tpl index 604e94227d..12f4ddf858 100644 --- a/app/config/locales/templates/sv.email.auth.invitation.tpl +++ b/app/config/locales/templates/sv.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hej, -
-
+

+

{{owner}} vill bjuda in dig att bli del av {{team}}-teamet inom {{project}}. -
-
+

+

Följ denna länk för att bli del av {{team}}-teamet: -
- {{redirect}} -
-
+

+{{cta}} +

Om du inte är intresserad så kan du ignorera detta meddelande. -
-
+

+

Tack,
{{project}}-teamet -

+

diff --git a/app/config/locales/templates/sv.email.auth.recovery.tpl b/app/config/locales/templates/sv.email.auth.recovery.tpl index 673d914bd1..d781ed1075 100644 --- a/app/config/locales/templates/sv.email.auth.recovery.tpl +++ b/app/config/locales/templates/sv.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hej {{name}}, -
-
+

+

Följ denna länk för att ändra ditt {{project}} lösenord. -
- {{redirect}} -
-
+

+{{cta}} +

Om du inte bett om att ändra ditt lösenord så kan du ignorera detta meddelande. -
-
+

+

Tack,
{{project}}-teamet -

+

diff --git a/app/config/locales/templates/ta.email.auth.confirm.tpl b/app/config/locales/templates/ta.email.auth.confirm.tpl index 25a34d067c..843ce248e1 100644 --- a/app/config/locales/templates/ta.email.auth.confirm.tpl +++ b/app/config/locales/templates/ta.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

வணக்கம் {{name}}, -
-
+

+

இந்த இணைப்பைப் பின்தொடர்ந்து உங்கள் மின்னஞ்சல் முகவரியை சரிபார்க்கவும். -
- {{redirect}} -
-
+

+{{cta}} +

இந்த முகவரியை சரிபார்க்க நீங்கள் கேட்கவில்லை என்றால், இந்த செய்தியை நீங்கள் புறக்கணிக்கலாம். -
-
+

+

நன்றி,
{{project}} குழு -

+

diff --git a/app/config/locales/templates/ta.email.auth.invitation.tpl b/app/config/locales/templates/ta.email.auth.invitation.tpl index b9c417c269..00ec55d08a 100644 --- a/app/config/locales/templates/ta.email.auth.invitation.tpl +++ b/app/config/locales/templates/ta.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

வணக்கம், -
-
+

+

இந்த மின்னஞ்சல் உங்களுக்கு அனுப்பப்பட்டது, ஏனெனில் {{owner}} உங்களை {{team}} குழுவின் {{project}} திட்டத்தில் உறுப்பினராக அழைக்க விரும்பினார்கள். -
-
+

+

இந்த இணைப்பைப் பின்தொடர்ந்து {{team}} குழுவில் சேரவும்: -
- {{redirect}} -
-
+

+{{cta}} +

இந்த முகவரியை சரிபார்க்க நீங்கள் கேட்கவில்லை என்றால், இந்த செய்தியை நீங்கள் புறக்கணிக்கலாம். -
-
+

+

நன்றி,
{{project}} குழு -

+

diff --git a/app/config/locales/templates/ta.email.auth.recovery.tpl b/app/config/locales/templates/ta.email.auth.recovery.tpl index 455a6cf25b..36e9707f8d 100644 --- a/app/config/locales/templates/ta.email.auth.recovery.tpl +++ b/app/config/locales/templates/ta.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

வணக்கம் {{name}}, -
-
+

+

இந்த இணைப்பைப் பின்தொடர்ந்து உங்கள் {{project}} திட்டத்தின் கடவுச்சொல்லை மீட்கவும். -
- {{redirect}} -
-
+

+{{cta}} +

உங்கள் கடவுச்சொல்லை மீட்டமைக்க நீங்கள் கேட்கவில்லை என்றால், இந்த செய்தியை நீங்கள் புறக்கணிக்கலாம். -
-
+

+

நன்றி,
{{project}} குழு -

+

diff --git a/app/config/locales/templates/th.email.auth.confirm.tpl b/app/config/locales/templates/th.email.auth.confirm.tpl index 99a6268ea7..c45fbf7d5d 100644 --- a/app/config/locales/templates/th.email.auth.confirm.tpl +++ b/app/config/locales/templates/th.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

สวัสดี {{name}}, -
-
+

+

ตามไปยังลิงค์นี้เพื่อยืนยันที่อยู่อีเมลของคุณ -
- {{redirect}} -
-
+

+{{cta}} +

หากคุณไม่ได้ขอให้ยืนยันที่อยู่นี้คุณสามารถเพิกเฉยต่อข้อความนี้ได้ -
-
+

+

ขอบคุณ,
ทีม {{project}} -

+

diff --git a/app/config/locales/templates/th.email.auth.invitation.tpl b/app/config/locales/templates/th.email.auth.invitation.tpl index 399e47c686..918b28cc8f 100644 --- a/app/config/locales/templates/th.email.auth.invitation.tpl +++ b/app/config/locales/templates/th.email.auth.invitation.tpl @@ -1,28 +1,18 @@ - - -
+

สวัสดี, -
-
- +

+

เมลนี้ส่งถึงคุณเพราะ {{owner}} ต้องการเชิญคุณเป็นสมาชิกในทีม {{team}} ของ {{project}}. -
-
+

+

ตามลิงค์นี้เพื่อเข้าร่วม {{team}} ทีม: -
- {{redirect}} -
-
+

+{{cta}} +

หากคุณไม่สนใจคุณสามารถละเว้นข้อความนี้ -
-
+

+

ขอบคุณ,
{{project}} ทีม -

+

diff --git a/app/config/locales/templates/th.email.auth.recovery.tpl b/app/config/locales/templates/th.email.auth.recovery.tpl index d835481dc4..220a193e19 100644 --- a/app/config/locales/templates/th.email.auth.recovery.tpl +++ b/app/config/locales/templates/th.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

สวัสดี {{name}}, -
-
+

+

ตามไปยังลิงค์นี้เพื่อรีเซ็ตรหัสผ่าน {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

หากคุณไม่ได้ขอให้ตั้งรหัสผ่านใหม่คุณสามารถเพิกเฉยต่อข้อความนี้ได้ -
-
+

+

ขอบคุณ,
ทีม {{project}} -

+

diff --git a/app/config/locales/templates/tr.email.auth.confirm.tpl b/app/config/locales/templates/tr.email.auth.confirm.tpl index d83bda1e67..0ed2fa54c4 100644 --- a/app/config/locales/templates/tr.email.auth.confirm.tpl +++ b/app/config/locales/templates/tr.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Merhaba {{name}}, -
-
+

+

Aşağıdaki bağlantıyı takip ederek email hesabınızı doğrulayın. -
- {{redirect}} -
-
+

+{{cta}} +

Bu adresi doğrulamayı istemediyseniz, bu mesajı yok sayabilirsiniz. -
-
+

+

Teşekkürler,
{{project}} takımı -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/tr.email.auth.invitation.tpl b/app/config/locales/templates/tr.email.auth.invitation.tpl index 07e8f06db4..c3c35b6bbd 100644 --- a/app/config/locales/templates/tr.email.auth.invitation.tpl +++ b/app/config/locales/templates/tr.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Merhaba, -
-
+

+

Bu posta size gönderildi, çünkü {{owner}} sizi {{project}} için {{team}} ekibinde takım üyesi olmaya davet etmek istedi. -
-
+

+

Aşağıdaki bağlantıyı takip ederek {{team}} takımına takılın: -
- {{redirect}} -
-
+

+{{cta}} +

Eğer ilgilenmiyorsanız, bu mesajı yok sayabilirsiniz. -
-
+

+

Teşekkürler,
{{project}} takımı -

+

diff --git a/app/config/locales/templates/tr.email.auth.recovery.tpl b/app/config/locales/templates/tr.email.auth.recovery.tpl index d04ffce5ee..4e7ce73d8e 100644 --- a/app/config/locales/templates/tr.email.auth.recovery.tpl +++ b/app/config/locales/templates/tr.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Merhaba {{name}}, -
-
+

+

Aşağıdaki bağlantıyı takip ederek {{project}} şifresini değiştirin. -
- {{redirect}} -
-
+

+{{cta}} +

Eğer şifrenizi sıfırlamayı istemediyseniz, bu mesajı yok sayabilirsiniz. -
-
+

+

Teşekkürler,
{{project}} takımı -

+

diff --git a/app/config/locales/templates/ua.email.auth.confirm.tpl b/app/config/locales/templates/ua.email.auth.confirm.tpl index 8d135457f0..ea628111cd 100644 --- a/app/config/locales/templates/ua.email.auth.confirm.tpl +++ b/app/config/locales/templates/ua.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Вітаємо {{name}}, -
-
+

+

Перейдіть за цим посиланням, та підтвердіть свою електронну адресу -
- {{redirect}} -
-
+

+{{cta}} +

Якщо ви не запитували підтвердження цієї адреси, проігноруйте це повідомлення. -
-
+

+

Дякуємо,
команда {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ua.email.auth.invitation.tpl b/app/config/locales/templates/ua.email.auth.invitation.tpl index 8daaecbaaf..dc4d3aed02 100644 --- a/app/config/locales/templates/ua.email.auth.invitation.tpl +++ b/app/config/locales/templates/ua.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Вітаємо, -
-
+

+

Цей лист був надісланий вам тому що {{owner}} хоче запросити вас стати членом команди {{team}} у {{project}}. -
-
+

+

Перейдіть за цим посиланням щоб приєднатись до команди {{team}} : -
- {{redirect}} -
-
+

+{{cta}} +

Якщо ви не зацікавлені, проігноруйте це повідомлення. -
-
+

+

Дякуємо,
Команда {{project}} -

+

diff --git a/app/config/locales/templates/ua.email.auth.recovery.tpl b/app/config/locales/templates/ua.email.auth.recovery.tpl index b833a6575a..0e1431bad9 100644 --- a/app/config/locales/templates/ua.email.auth.recovery.tpl +++ b/app/config/locales/templates/ua.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Вітаємо, {{name}}, -
-
+

+

Перейдіть за цим посиланням для того щоб скинути свій пароль для {{project}} . -
- {{redirect}} -
-
+

+{{cta}} +

Якщо ви не запитували скидання паролю, проігноруйте це повідомлення. -
-
+

+

Дякуємо,
команда {{project}} -

+

diff --git a/app/config/locales/templates/vi.email.auth.confirm.tpl b/app/config/locales/templates/vi.email.auth.confirm.tpl index 40c7d496e1..463d26b1f3 100644 --- a/app/config/locales/templates/vi.email.auth.confirm.tpl +++ b/app/config/locales/templates/vi.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Xin chào {{name}}, -
-
+

+

Hãy vào liên kết này để xác nhận địa chỉ email của bạn. -
- {{redirect}} -
-
+

+{{cta}} +

Xin hãy bỏ qua email này nếu bạn không yêu cầu xác nhận địa chỉ này. -
-
+

+

Xin cảm ơn,
{{project}} team -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/vi.email.auth.invitation.tpl b/app/config/locales/templates/vi.email.auth.invitation.tpl index 387c537f55..8d222ea8c7 100644 --- a/app/config/locales/templates/vi.email.auth.invitation.tpl +++ b/app/config/locales/templates/vi.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Xin chào, -
-
+

+

Bạn nhận được email này vì {{owner}} muốn mời bạn tham gia {{project}} cùng với {{team}} team. -
-
+

+

Hãy theo liên kết này để tham gia vào {{team}} team: -
- {{redirect}} -
-
+

+{{cta}} +

Nếu bạn không thích tham gia, hãy bỏ qua lời nhắn này. -
-
+

+

Xin cảm ơn,
{{project}} team -

+

diff --git a/app/config/locales/templates/vi.email.auth.recovery.tpl b/app/config/locales/templates/vi.email.auth.recovery.tpl index 57c704f0cd..9c7feb4900 100644 --- a/app/config/locales/templates/vi.email.auth.recovery.tpl +++ b/app/config/locales/templates/vi.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Xin chào {{name}}, -
-
+

+

Hãy theo liên kết này để khôi phục mật khẩu của bạn ở {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Hãy bỏ qua lời nhắn này nếu bạn không yêu cầu khôi phục mật khẩu. -
-
+

+

Xin cảm ơn,
{{project}} team -

+

diff --git a/app/config/locales/templates/zh-cn.email.auth.confirm.tpl b/app/config/locales/templates/zh-cn.email.auth.confirm.tpl index a557d451c7..9a0d7a3c8c 100644 --- a/app/config/locales/templates/zh-cn.email.auth.confirm.tpl +++ b/app/config/locales/templates/zh-cn.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

{{name}} 你好, -
-
+

+

请点击下方的链接验证你的电子邮箱地址。 -
- {{redirect}} -
-
+

+{{cta}} +

如果你没有请求验证本邮箱,请忽略这份邮件。 -
-
+

+

谢谢。
来自 {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/zh-cn.email.auth.invitation.tpl b/app/config/locales/templates/zh-cn.email.auth.invitation.tpl index 7ee56a401a..4ef8caba5c 100644 --- a/app/config/locales/templates/zh-cn.email.auth.invitation.tpl +++ b/app/config/locales/templates/zh-cn.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

你好, -
-
+

+

{{owner}}邀请您加入{{project}}项目的{{team}}小组。 -
-
+

+

请点击下方的链接加入{{team}}小组: -
- {{redirect}} -
-
+

+{{cta}} +

如果您没有加入该小组的计划,请忽略本邮件。 -
-
+

+

谢谢。
来自 {{project}} -

+

diff --git a/app/config/locales/templates/zh-cn.email.auth.recovery.tpl b/app/config/locales/templates/zh-cn.email.auth.recovery.tpl index 8a473b5537..1144b36b3c 100644 --- a/app/config/locales/templates/zh-cn.email.auth.recovery.tpl +++ b/app/config/locales/templates/zh-cn.email.auth.recovery.tpl @@ -1,25 +1,16 @@ - - -
+

{{name}} 你好, -
-
+

+

请点击下方的链接重新设置{{project}}的密码。 Follow this link to reset your {{project}} password. -
- {{redirect}} -
-
+

+{{cta}} +

如果您未曾申请重设密码,请忽略本邮件。 -
-
+

+

谢谢。
来自 {{project}} -

+

diff --git a/app/config/locales/templates/zh-tw.email.auth.confirm.tpl b/app/config/locales/templates/zh-tw.email.auth.confirm.tpl index 9ba081bb91..7a19636dbb 100644 --- a/app/config/locales/templates/zh-tw.email.auth.confirm.tpl +++ b/app/config/locales/templates/zh-tw.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

{{name}} 你好, -
-
+

+

請點擊下方的鏈接驗證你的電子郵箱地址。 -
- {{redirect}} -
-
+

+{{cta}} +

如果你沒有請求驗證本郵箱,請忽略這份郵件。 -
-
+

+

謝謝。
來自 {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/zh-tw.email.auth.invitation.tpl b/app/config/locales/templates/zh-tw.email.auth.invitation.tpl index 08fb921fd7..b5e6cc8225 100644 --- a/app/config/locales/templates/zh-tw.email.auth.invitation.tpl +++ b/app/config/locales/templates/zh-tw.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

你好, -
-
+

+

{{owner}}邀請您加入{{project}}項目的{{team}}小組。 -
-
+

+

請點擊下方的鏈接加入{{team}}小組: -
- {{redirect}} -
-
+

+{{cta}} +

如果您沒有加入該小組的計劃,請忽略本郵件。 -
-
+

+

謝謝。
來自 {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/zh-tw.email.auth.recovery.tpl b/app/config/locales/templates/zh-tw.email.auth.recovery.tpl index 8a473b5537..1144b36b3c 100644 --- a/app/config/locales/templates/zh-tw.email.auth.recovery.tpl +++ b/app/config/locales/templates/zh-tw.email.auth.recovery.tpl @@ -1,25 +1,16 @@ - - -
+

{{name}} 你好, -
-
+

+

请点击下方的链接重新设置{{project}}的密码。 Follow this link to reset your {{project}} password. -
- {{redirect}} -
-
+

+{{cta}} +

如果您未曾申请重设密码,请忽略本邮件。 -
-
+

+

谢谢。
来自 {{project}} -

+

diff --git a/app/config/platforms.php b/app/config/platforms.php index f05dcde39b..a0b4955a8e 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -21,7 +21,7 @@ return [ 'beta' => false, 'family' => APP_PLATFORM_CLIENT, 'prism' => 'javascript', - 'source' => realpath(__DIR__ . '/../sdks/client-web'), + 'source' => \realpath(__DIR__ . '/../sdks/client-web'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-js.git', 'gitRepoName' => 'sdk-for-js', 'gitUserName' => 'appwrite', @@ -35,7 +35,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_CLIENT, 'prism' => 'dart', - 'source' => realpath(__DIR__ . '/../sdks/client-flutter'), + 'source' => \realpath(__DIR__ . '/../sdks/client-flutter'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-flutter.git', 'gitRepoName' => 'sdk-for-flutter', 'gitUserName' => 'appwrite', @@ -110,7 +110,7 @@ return [ 'beta' => false, 'family' => APP_PLATFORM_CONSOLE, 'prism' => 'console', - 'source' => realpath(__DIR__ . '/../sdks/console-web'), + 'source' => \realpath(__DIR__ . '/../sdks/console-web'), 'gitUrl' => null, 'gitRepoName' => 'sdk-for-console', 'gitUserName' => 'appwrite', @@ -134,7 +134,7 @@ return [ 'beta' => false, 'family' => APP_PLATFORM_SERVER, 'prism' => 'javascript', - 'source' => realpath(__DIR__ . '/../sdks/server-nodejs'), + 'source' => \realpath(__DIR__ . '/../sdks/server-nodejs'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-node.git', 'gitRepoName' => 'sdk-for-node', 'gitUserName' => 'appwrite', @@ -148,7 +148,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'typescript', - 'source' => realpath(__DIR__ . '/../sdks/server-deno'), + 'source' => \realpath(__DIR__ . '/../sdks/server-deno'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-deno.git', 'gitRepoName' => 'sdk-for-deno', 'gitUserName' => 'appwrite', @@ -162,7 +162,7 @@ return [ 'beta' => false, 'family' => APP_PLATFORM_SERVER, 'prism' => 'php', - 'source' => realpath(__DIR__ . '/../sdks/server-php'), + 'source' => \realpath(__DIR__ . '/../sdks/server-php'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-php.git', 'gitRepoName' => 'sdk-for-php', 'gitUserName' => 'appwrite', @@ -176,7 +176,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'python', - 'source' => realpath(__DIR__ . '/../sdks/server-python'), + 'source' => \realpath(__DIR__ . '/../sdks/server-python'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-python.git', 'gitRepoName' => 'sdk-for-python', 'gitUserName' => 'appwrite', @@ -190,7 +190,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'ruby', - 'source' => realpath(__DIR__ . '/../sdks/server-ruby'), + 'source' => \realpath(__DIR__ . '/../sdks/server-ruby'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-ruby.git', 'gitRepoName' => 'sdk-for-ruby', 'gitUserName' => 'appwrite', @@ -204,7 +204,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'go', - 'source' => realpath(__DIR__ . '/../sdks/server-go'), + 'source' => \realpath(__DIR__ . '/../sdks/server-go'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-go.git', 'gitRepoName' => 'sdk-for-go', 'gitUserName' => 'appwrite', @@ -218,7 +218,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'java', - 'source' => realpath(__DIR__ . '/../sdks/server-java'), + 'source' => \realpath(__DIR__ . '/../sdks/server-java'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-java.git', 'gitRepoName' => 'sdk-for-java', 'gitUserName' => 'appwrite', @@ -232,7 +232,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'java', - 'source' => realpath(__DIR__ . '/../sdks/server-dart'), + 'source' => \realpath(__DIR__ . '/../sdks/server-dart'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-dart.git', 'gitRepoName' => 'sdk-for-dart', 'gitUserName' => 'appwrite', diff --git a/app/config/providers.php b/app/config/providers.php index 7eb5113d96..505c2898c4 100644 --- a/app/config/providers.php +++ b/app/config/providers.php @@ -1,6 +1,22 @@ [ + 'developers' => 'https://developer.amazon.com/apps-and-games/services-and-apis', + 'icon' => 'icon-amazon', + 'enabled' => true, + 'form' => false, + 'beta' => false, + 'mock' => false, + ], + 'apple' => [ + 'developers' => 'https://developer.apple.com/', + 'icon' => 'icon-apple', + 'enabled' => true, + 'form' => 'apple.phtml', // Perperation for adding ability to customized OAuth UI forms, currently handled hardcoded. + 'beta' => true, + 'mock' => false, + ], 'bitbucket' => [ 'developers' => 'https://developer.atlassian.com/bitbucket', 'icon' => 'icon-bitbucket', @@ -9,6 +25,30 @@ return [ 'beta' => false, 'mock' => false, ], + 'bitly' => [ + 'developers' => 'https://dev.bitly.com/v4_documentation.html', + 'icon' => 'icon-bitly', + 'enabled' => true, + 'form' => false, + 'beta' => false, + 'mock' => false + ], + 'discord' => [ + 'developers' => 'https://discordapp.com/developers/docs/topics/oauth2', + 'icon' => 'icon-discord', + 'enabled' => true, + 'form' => false, + 'beta' => false, + 'mock' => false, + ], + 'dropbox' => [ + 'developers' => 'https://www.dropbox.com/developers/documentation', + 'icon' => 'icon-dropbox', + 'enabled' => true, + 'form' => false, + 'beta' => false, + 'mock' => false, + ], 'facebook' => [ 'developers' => 'https://developers.facebook.com/', 'icon' => 'icon-facebook', @@ -34,20 +74,21 @@ return [ 'mock' => false, ], 'google' => [ - 'developers' => 'https://developers.google.com/', + 'developers' => 'https://support.google.com/googleapi/answer/6158849', 'icon' => 'icon-google', 'enabled' => true, 'form' => false, 'beta' => false, 'mock' => false, ], - // 'instagram' => [ - // 'developers' => 'https://www.instagram.com/developer/', - // 'icon' => 'icon-instagram', - // 'enabled' => false, - // 'beta' => false, - // 'mock' => false, - // ], + 'linkedin' => [ + 'developers' => 'https://developer.linkedin.com/', + 'icon' => 'icon-linkedin', + 'enabled' => true, + 'form' => false, + 'beta' => false, + 'mock' => false, + ], 'microsoft' => [ 'developers' => 'https://developer.microsoft.com/en-us/', 'icon' => 'icon-windows', @@ -56,16 +97,17 @@ return [ 'beta' => false, 'mock' => false, ], - // 'twitter' => [ - // 'developers' => 'https://developer.twitter.com/', - // 'icon' => 'icon-twitter', - // 'enabled' => false, - // 'beta' => false, - // 'mock' => false, - // ], - 'linkedin' => [ - 'developers' => 'https://developer.linkedin.com/', - 'icon' => 'icon-linkedin', + 'paypal' => [ + 'developers' => 'https://developer.paypal.com/docs/api/overview/', + 'icon' => 'icon-paypal', + 'enabled' => true, + 'form' => false, + 'beta' => false, + 'mock' => false + ], + 'salesforce' => [ + 'developers' => 'https://developer.salesforce.com/docs/', + 'icon' => 'icon-salesforce', 'enabled' => true, 'form' => false, 'beta' => false, @@ -79,49 +121,9 @@ return [ 'beta' => false, 'mock' => false, ], - 'dropbox' => [ - 'developers' => 'https://www.dropbox.com/developers/documentation', - 'icon' => 'icon-dropbox', - 'enabled' => true, - 'form' => false, - 'beta' => false, - 'mock' => false, - ], - 'salesforce' => [ - 'developers' => 'https://developer.salesforce.com/docs/', - 'icon' => 'icon-salesforce', - 'enabled' => true, - 'form' => false, - 'beta' => false, - 'mock' => false, - ], - 'apple' => [ - 'developers' => 'https://developer.apple.com/', - 'icon' => 'icon-apple', - 'enabled' => true, - 'form' => 'apple.phtml', // Perperation for adding ability to customized OAuth UI forms, currently handled hardcoded. - 'beta' => true, - 'mock' => false, - ], - 'amazon' => [ - 'developers' => 'https://developer.amazon.com/apps-and-games/services-and-apis', - 'icon' => 'icon-amazon', - 'enabled' => true, - 'form' => false, - 'beta' => false, - 'mock' => false, - ], - 'vk' => [ - 'developers' => 'https://vk.com/dev', - 'icon' => 'icon-vk', - 'enabled' => true, - 'form' => false, - 'beta' => false, - 'mock' => false, - ], - 'discord' => [ - 'developers' => 'https://discordapp.com/developers/docs/topics/oauth2', - 'icon' => 'icon-discord', + 'spotify' => [ + 'developers' => 'https://developer.spotify.com/documentation/general/guides/authorization-guide/', + 'icon' => 'icon-spotify', 'enabled' => true, 'form' => false, 'beta' => false, @@ -135,9 +137,9 @@ return [ 'beta' => false, 'mock' => false, ], - 'spotify' => [ - 'developers' => 'https://developer.spotify.com/documentation/general/guides/authorization-guide/', - 'icon' => 'icon-spotify', + 'vk' => [ + 'developers' => 'https://vk.com/dev', + 'icon' => 'icon-vk', 'enabled' => true, 'form' => false, 'beta' => false, @@ -159,30 +161,20 @@ return [ 'beta' => false, 'mock' => false, ], - 'twitter' => [ - 'developers' => 'https://developer.twitter.com/', - 'icon' => 'icon-twitter', - 'enabled' => false, - 'form' => false, - 'beta' => false, - 'mock' => false - ], - 'paypal' => [ - 'developers' => 'https://developer.paypal.com/docs/api/overview/', - 'icon' => 'icon-paypal', - 'enabled' => true, - 'form' => false, - 'beta' => false, - 'mock' => false - ], - 'bitly' => [ - 'developers' => 'https://dev.bitly.com/v4_documentation.html', - 'icon' => 'icon-bitly', - 'enabled' => true, - 'form' => false, - 'beta' => false, - 'mock' => false - ], + // 'instagram' => [ + // 'developers' => 'https://www.instagram.com/developer/', + // 'icon' => 'icon-instagram', + // 'enabled' => false, + // 'beta' => false, + // 'mock' => false, + // ], + // 'twitter' => [ + // 'developers' => 'https://developer.twitter.com/', + // 'icon' => 'icon-twitter', + // 'enabled' => false, + // 'beta' => false, + // 'mock' => false, + // ], // Keep Last 'mock' => [ 'developers' => 'https://appwrite.io', diff --git a/app/config/roles.php b/app/config/roles.php index f462305d97..4d2420ec58 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -24,7 +24,6 @@ $logged = [ 'projects.write', 'locale.read', 'avatars.read', - 'health.read', ]; $admins = [ @@ -62,24 +61,23 @@ return [ 'files.read', 'locale.read', 'avatars.read', - 'health.read', ], ], ROLE_MEMBER => [ 'label' => 'Member', - 'scopes' => array_merge($logged, []), + 'scopes' => \array_merge($logged, []), ], ROLE_ADMIN => [ 'label' => 'Admin', - 'scopes' => array_merge($admins, []), + 'scopes' => \array_merge($admins, []), ], ROLE_DEVELOPER => [ 'label' => 'Developer', - 'scopes' => array_merge($admins, []), + 'scopes' => \array_merge($admins, []), ], ROLE_OWNER => [ 'label' => 'Owner', - 'scopes' => array_merge($logged, $admins, []), + 'scopes' => \array_merge($logged, $admins, []), ], ROLE_APP => [ 'label' => 'Application', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index dd78f84dab..c2c2c642ac 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1,7 +1,7 @@ init(function() use (&$oauth2Keys) { continue; } - $oauth2Keys[] = 'oauth2'.ucfirst($key); - $oauth2Keys[] = 'oauth2'.ucfirst($key).'AccessToken'; + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } }); @@ -59,21 +60,21 @@ $utopia->post('/v1/account') ->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 ($register, $request, $response, $audit, $projectDB, $project, $webhook, $oauth2Keys) { + 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'); - if (!empty($whitlistEmails) && !in_array($email, $whitlistEmails)) { + 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 (!empty($whitlistIPs) && !in_array($request->getIP(), $whitlistIPs)) { + 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)) { + 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); } } @@ -104,8 +105,8 @@ $utopia->post('/v1/account') 'emailVerification' => false, 'status' => Auth::USER_STATUS_UNACTIVATED, 'password' => Auth::passwordHash($password), - 'password-update' => time(), - 'registration' => time(), + 'password-update' => \time(), + 'registration' => \time(), 'reset' => false, 'name' => $name, ], ['email' => $email]); @@ -170,7 +171,7 @@ $utopia->post('/v1/account/sessions') throw new Exception('Invalid credentials', 401); // Wrong password or username } - $expiry = time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; $secret = Auth::tokenGenerator(); $session = new Document([ '$collection' => Database::SYSTEM_COLLECTION_TOKENS, @@ -213,7 +214,7 @@ $utopia->post('/v1/account/sessions') if(!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([Auth::$cookieName => Auth::encodeSession($profile->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($profile->getId(), $secret)])) ; } @@ -240,34 +241,35 @@ $utopia->get('/v1/account/sessions/oauth2/:provider') ->label('sdk.methodType', 'webAuth') ->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('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('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) use ($response, $request, $project) { + 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', '{}'); + $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'])); + $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); } 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); + $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); - if (!class_exists($classname)) { + if (!\class_exists($classname)) { throw new Exception('Provider is not supported', 501); } - $oauth2 = new $classname($appId, $appSecret, $callback, ['success' => $success, 'failure' => $failure]); + $oauth2 = new $classname($appId, $appSecret, $callback, ['success' => $success, 'failure' => $failure], $scopes); $response ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') @@ -282,7 +284,7 @@ $utopia->get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->label('scope', 'public') ->label('docs', false) ->param('projectId', '', function () { return new Text(1024); }, 'Project unique ID.') - ->param('provider', '', function () { return new WhiteList(array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') + ->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( @@ -294,7 +296,7 @@ $utopia->get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->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])); + .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); } ); @@ -305,7 +307,7 @@ $utopia->post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->label('origin', '*') ->label('docs', false) ->param('projectId', '', function () { return new Text(1024); }, 'Project unique ID.') - ->param('provider', '', function () { return new WhiteList(array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') + ->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( @@ -317,7 +319,7 @@ $utopia->post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->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])); + .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); } ); @@ -329,7 +331,7 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') ->label('docs', false) - ->param('provider', '', function () { return new WhiteList(array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') + ->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( @@ -339,19 +341,19 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') $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'])); + $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); } - $classname = 'Appwrite\\Auth\\OAuth2\\'.ucfirst($provider); + $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); - if (!class_exists($classname)) { + if (!\class_exists($classname)) { throw new Exception('Provider is not supported', 501); } @@ -359,7 +361,7 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') if (!empty($state)) { try { - $state = array_merge($defaultState, $oauth2->parseState($state)); + $state = \array_merge($defaultState, $oauth2->parseState($state)); } catch (\Exception $exception) { throw new Exception('Failed to parse login state params as passed from OAuth2 provider'); } @@ -407,7 +409,7 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') 'first' => true, 'filters' => [ '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'oauth2'.ucfirst($provider).'='.$oauth2ID, + 'oauth2'.\ucfirst($provider).'='.$oauth2ID, ], ]) : $user; @@ -435,8 +437,8 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') '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(), + 'password-update' => \time(), + 'registration' => \time(), 'reset' => false, 'name' => $name, ], ['email' => $email]); @@ -455,7 +457,7 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') // Create session token, verify user account and update OAuth2 ID and Access Token $secret = Auth::tokenGenerator(); - $expiry = time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; $session = new Document([ '$collection' => Database::SYSTEM_COLLECTION_TOKENS, '$permissions' => ['read' => ['user:'.$user['$id']], 'write' => ['user:'.$user['$id']]], @@ -467,8 +469,8 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') ]); $user - ->setAttribute('oauth2'.ucfirst($provider), $oauth2ID) - ->setAttribute('oauth2'.ucfirst($provider).'AccessToken', $accessToken) + ->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID) + ->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken) ->setAttribute('status', Auth::USER_STATUS_ACTIVATED) ->setAttribute('tokens', $session, Document::SET_TYPE_APPEND) ; @@ -490,7 +492,7 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') if(!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) ; } @@ -541,7 +543,7 @@ $utopia->get('/v1/account/prefs') $prefs = $user->getAttribute('prefs', '{}'); try { - $prefs = json_decode($prefs, true); + $prefs = \json_decode($prefs, true); $prefs = ($prefs) ? $prefs : []; } catch (\Exception $error) { throw new Exception('Failed to parse prefs', 500); @@ -595,7 +597,7 @@ $utopia->get('/v1/account/sessions') try { $record = $reader->country($token->getAttribute('ip', '')); - $sessions[$index]['geo']['isoCode'] = strtolower($record->country->isoCode); + $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'] = '--'; @@ -657,7 +659,7 @@ $utopia->get('/v1/account/logs') $output[$i] = [ 'event' => $log['event'], 'ip' => $log['ip'], - 'time' => strtotime($log['time']), + 'time' => \strtotime($log['time']), 'OS' => $dd->getOs(), 'client' => $dd->getClient(), 'device' => $dd->getDevice(), @@ -668,7 +670,7 @@ $utopia->get('/v1/account/logs') try { $record = $reader->country($log['ip']); - $output[$i]['geo']['isoCode'] = strtolower($record->country->isoCode); + $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) { @@ -692,7 +694,7 @@ $utopia->patch('/v1/account/name') ->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(), [ + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ 'name' => $name, ])); @@ -706,7 +708,7 @@ $utopia->patch('/v1/account/name') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(array_merge($user->getArrayCopy(array_merge( + $response->json(\array_merge($user->getArrayCopy(\array_merge( [ '$id', 'email', @@ -734,7 +736,7 @@ $utopia->patch('/v1/account/password') throw new Exception('Invalid credentials', 401); } - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ 'password' => Auth::passwordHash($password), ])); @@ -748,7 +750,7 @@ $utopia->patch('/v1/account/password') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(array_merge($user->getArrayCopy(array_merge( + $response->json(\array_merge($user->getArrayCopy(\array_merge( [ '$id', 'email', @@ -791,7 +793,7 @@ $utopia->patch('/v1/account/email') // TODO after this user needs to confirm mail again - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ 'email' => $email, 'emailVerification' => false, ])); @@ -806,7 +808,7 @@ $utopia->patch('/v1/account/email') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(array_merge($user->getArrayCopy(array_merge( + $response->json(\array_merge($user->getArrayCopy(\array_merge( [ '$id', 'email', @@ -829,11 +831,11 @@ $utopia->patch('/v1/account/prefs') ->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 = \json_decode($user->getAttribute('prefs', '{}'), true); $old = ($old) ? $old : []; - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ - 'prefs' => json_encode(array_merge($old, $prefs)), + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'prefs' => \json_encode(\array_merge($old, $prefs)), ])); if (false === $user) { @@ -848,7 +850,7 @@ $utopia->patch('/v1/account/prefs') $prefs = $user->getAttribute('prefs', '{}'); try { - $prefs = json_decode($prefs, true); + $prefs = \json_decode($prefs, true); $prefs = ($prefs) ? $prefs : []; } catch (\Exception $error) { throw new Exception('Failed to parse prefs', 500); @@ -869,7 +871,7 @@ $utopia->delete('/v1/account') ->action( function () use ($response, $user, $projectDB, $audit, $webhook) { $protocol = Config::getParam('protocol'); - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ 'status' => Auth::USER_STATUS_BLOCKED, ])); @@ -901,13 +903,13 @@ $utopia->delete('/v1/account') if(!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([])) + ->addHeader('X-Fallback-Cookies', \json_encode([])) ; } $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, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) ->noContent() ; } @@ -953,14 +955,14 @@ $utopia->delete('/v1/account/sessions/:sessionId') if(!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([])) + ->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) + ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) ; } @@ -1006,14 +1008,14 @@ $utopia->delete('/v1/account/sessions') if(!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([])) + ->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) + ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) ; } } @@ -1034,7 +1036,7 @@ $utopia->post('/v1/account/recovery') ->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, $register, $audit, $project) { + function ($email, $url) use ($request, $response, $projectDB, $mail, $audit, $project) { $profile = $projectDB->getCollection([ // Get user by email address 'limit' => 1, 'first' => true, @@ -1054,7 +1056,7 @@ $utopia->post('/v1/account/recovery') '$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, + 'expire' => \time() + Auth::TOKEN_EXPIRATION_RECOVERY, 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), 'ip' => $request->getIP(), ]); @@ -1079,27 +1081,34 @@ $utopia->post('/v1/account/recovery') $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/'.Locale::getText('account.emails.recovery.body')); + $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 = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ - - $mail->addAddress($profile->getAttribute('email', ''), $profile->getAttribute('name', '')); - - $mail->Subject = Locale::getText('account.emails.recovery.title'); - $mail->Body = $body->render(); - $mail->AltBody = strip_tags($body->render()); - - try { - $mail->send(); - } catch (\Exception $error) { - throw new Exception('Error sending mail: ' . $error->getMessage(), 500); - } + $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()) @@ -1154,9 +1163,9 @@ $utopia->put('/v1/account/recovery') Authorization::setRole('user:'.$profile->getId()); - $profile = $projectDB->updateDocument(array_merge($profile->getArrayCopy(), [ + $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ 'password' => Auth::passwordHash($password), - 'password-update' => time(), + 'password-update' => \time(), 'emailVerification' => true, ])); @@ -1195,7 +1204,7 @@ $utopia->post('/v1/account/verification') ->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, $register, $user, $project, $projectDB, $audit) { + function ($url) use ($request, $response, $mail, $user, $project, $projectDB, $audit) { $verificationSecret = Auth::tokenGenerator(); $verification = new Document([ @@ -1203,7 +1212,7 @@ $utopia->post('/v1/account/verification') '$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, + 'expire' => \time() + Auth::TOKEN_EXPIRATION_CONFIRM, 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), 'ip' => $request->getIP(), ]); @@ -1228,27 +1237,34 @@ $utopia->post('/v1/account/verification') $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/'.Locale::getText('account.emails.verification.body')); + $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 = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ - - $mail->addAddress($user->getAttribute('email'), $user->getAttribute('name')); - - $mail->Subject = Locale::getText('account.emails.verification.title'); - $mail->Body = $body->render(); - $mail->AltBody = strip_tags($body->render()); - - try { - $mail->send(); - } catch (\Exception $error) { - throw new Exception('Problem sending mail: ' . $error->getMessage(), 500); - } + $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()) @@ -1297,7 +1313,7 @@ $utopia->put('/v1/account/verification') Authorization::setRole('user:'.$profile->getId()); - $profile = $projectDB->updateDocument(array_merge($profile->getArrayCopy(), [ + $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ 'emailVerification' => true, ])); diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index bc493f8cc0..4bff678c5d 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -16,6 +16,7 @@ use BaconQrCode\Renderer\Image\ImagickImageBackEnd; use BaconQrCode\Renderer\RendererStyle\RendererStyle; use BaconQrCode\Writer; use Utopia\Config\Config; +use Utopia\Validator\HexColor; include_once __DIR__ . '/../shared/api.php'; @@ -26,28 +27,28 @@ $types = [ ]; $avatarCallback = function ($type, $code, $width, $height, $quality) use ($types, $response, $request) { - $code = strtolower($code); - $type = strtolower($type); + $code = \strtolower($code); + $type = \strtolower($type); - if (!array_key_exists($type, $types)) { + if (!\array_key_exists($type, $types)) { throw new Exception('Avatar set not found', 404); } - if (!array_key_exists($code, $types[$type])) { + if (!\array_key_exists($code, $types[$type])) { throw new Exception('Avatar not found', 404); } - if (!extension_loaded('imagick')) { + if (!\extension_loaded('imagick')) { throw new Exception('Imagick extension is missing', 500); } $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); + $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]; $type = 'png'; - if (!is_readable($path)) { + if (!\is_readable($path)) { throw new Exception('File not readable in '.$path, 500); } @@ -65,7 +66,7 @@ $avatarCallback = function ($type, $code, $width, $height, $quality) use ($types ; } - $resize = new Resize(file_get_contents($path)); + $resize = new Resize(\file_get_contents($path)); $resize->crop((int) $width, (int) $height); @@ -91,7 +92,7 @@ $avatarCallback = function ($type, $code, $width, $height, $quality) use ($types $utopia->get('/v1/avatars/credit-cards/:code') ->desc('Get Credit Card Icon') - ->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('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) @@ -106,7 +107,7 @@ $utopia->get('/v1/avatars/credit-cards/:code') $utopia->get('/v1/avatars/browsers/:code') ->desc('Get Browser Icon') - ->param('code', '', function () use ($types) { return new WhiteList(array_keys($types['browsers'])); }, 'Browser Code.') + ->param('code', '', function () use ($types) { return new WhiteList(\array_keys($types['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) @@ -121,7 +122,7 @@ $utopia->get('/v1/avatars/browsers/:code') $utopia->get('/v1/avatars/flags/:code') ->desc('Get Country Flag') - ->param('code', '', function () use ($types) { return new WhiteList(array_keys($types['flags'])); }, 'Country Code. ISO Alpha-2 country code format.') + ->param('code', '', function () use ($types) { return new WhiteList(\array_keys($types['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) @@ -149,8 +150,8 @@ $utopia->get('/v1/avatars/image') 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); + $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 */); @@ -164,11 +165,11 @@ $utopia->get('/v1/avatars/image') ; } - if (!extension_loaded('imagick')) { + if (!\extension_loaded('imagick')) { throw new Exception('Imagick extension is missing', 500); } - $fetch = @file_get_contents($url, false); + $fetch = @\file_get_contents($url, false); if (!$fetch) { throw new Exception('Image not found', 404); @@ -218,8 +219,8 @@ $utopia->get('/v1/avatars/favicon') $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); + $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 */); @@ -233,26 +234,26 @@ $utopia->get('/v1/avatars/favicon') ; } - if (!extension_loaded('imagick')) { + if (!\extension_loaded('imagick')) { throw new Exception('Imagick extension is missing', 500); } - $curl = curl_init(); + $curl = \curl_init(); - curl_setopt_array($curl, [ + \curl_setopt_array($curl, [ CURLOPT_RETURNTRANSFER => 1, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_URL => $url, - CURLOPT_USERAGENT => sprintf(APP_USERAGENT, + CURLOPT_USERAGENT => \sprintf(APP_USERAGENT, Config::getParam('version'), $request->getServer('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) ), ]); - $html = curl_exec($curl); + $html = \curl_exec($curl); - curl_close($curl); + \curl_close($curl); if (!$html) { throw new Exception('Failed to fetch remote URL', 404); @@ -271,20 +272,20 @@ $utopia->get('/v1/avatars/favicon') $href = $link->getAttribute('href'); $rel = $link->getAttribute('rel'); $sizes = $link->getAttribute('sizes'); - $absolute = URLParse::unparse(array_merge(parse_url($url), parse_url($href))); + $absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href))); - switch (strtolower($rel)) { + switch (\strtolower($rel)) { case 'icon': case 'shortcut icon': //case 'apple-touch-icon': - $ext = pathinfo(parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); + $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)); + $size = \explode('x', \strtolower($sizes)); $sizeWidth = (isset($size[0])) ? (int) $size[0] : 0; $sizeHeight = (isset($size[1])) ? (int) $size[1] : 0; @@ -303,16 +304,16 @@ $utopia->get('/v1/avatars/favicon') } if (empty($outputHref) || empty($outputExt)) { - $default = parse_url($url); + $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); + $data = @\file_get_contents($outputHref, false); - if (empty($data) || (mb_substr($data, 0, 5) === 'get('/v1/avatars/favicon') ; } - $fetch = @file_get_contents($outputHref, false); + $fetch = @\file_get_contents($outputHref, false); if (!$fetch) { throw new Exception('Icon not found', 404); @@ -383,9 +384,85 @@ $utopia->get('/v1/avatars/qr') } $response - ->addHeader('Expires', date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->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)) + ->send($writer->writeString($text)) + ; + } + ); + +$utopia->get('/v1/avatars/initials') + ->desc('Get User Initials') + ->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 + ]; + + $rand = \rand(0, \count($themes)-1); + + $name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', '')); + $words = \explode(' ', \strtoupper($name)); + $initials = null; + $code = 0; + + 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 diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 69d01e9dc7..d8ecedab84 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -9,6 +9,7 @@ use Utopia\Validator\Range; use Utopia\Validator\WhiteList; use Utopia\Validator\Text; use Utopia\Validator\ArrayList; +use Utopia\Validator\JSON; use Utopia\Locale\Locale; use Utopia\Audit\Audit; use Utopia\Audit\Adapters\MySQL as AuditAdapter; @@ -26,7 +27,7 @@ use GeoIp2\Database\Reader; include_once __DIR__ . '/../shared/api.php'; -$isDev = (App::ENV_TYPE_PRODUCTION !== $utopia->getEnv()); +$isDev = (App::MODE_TYPE_PRODUCTION !== $utopia->getMode()); $utopia->post('/v1/database/collections') ->desc('Create Collection') @@ -45,7 +46,7 @@ $utopia->post('/v1/database/collections') $parsedRules = []; foreach ($rules as &$rule) { - $parsedRules[] = array_merge([ + $parsedRules[] = \array_merge([ '$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => [ 'read' => $read, @@ -58,8 +59,8 @@ $utopia->post('/v1/database/collections') $data = $projectDB->createDocument([ '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, 'name' => $name, - 'dateCreated' => time(), - 'dateUpdated' => time(), + 'dateCreated' => \time(), + 'dateUpdated' => \time(), 'structure' => true, '$permissions' => [ 'read' => $read, @@ -169,69 +170,69 @@ $utopia->get('/v1/database/collections/:collectionId') } ); -$utopia->get('/v1/database/collections/:collectionId/logs') - ->desc('Get Collection Logs') - ->label('scope', 'collections.read') - ->label('sdk.platform', [APP_PLATFORM_SERVER]) - ->label('sdk.namespace', 'database') - ->label('sdk.method', 'getCollectionLogs') - ->label('sdk.description', '/docs/references/database/get-collection-logs.md') - ->param('collectionId', '', function () { return new UID(); }, 'Collection unique ID.') - ->action( - function ($collectionId) use ($response, $register, $projectDB, $project) { - $collection = $projectDB->getDocument($collectionId); +// $utopia->get('/v1/database/collections/:collectionId/logs') +// ->desc('Get Collection Logs') +// ->label('scope', 'collections.read') +// ->label('sdk.platform', [APP_PLATFORM_SERVER]) +// ->label('sdk.namespace', 'database') +// ->label('sdk.method', 'getCollectionLogs') +// ->label('sdk.description', '/docs/references/database/get-collection-logs.md') +// ->param('collectionId', '', function () { return new UID(); }, 'Collection unique ID.') +// ->action( +// function ($collectionId) use ($response, $register, $projectDB, $project) { +// $collection = $projectDB->getDocument($collectionId); - if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } +// if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { +// throw new Exception('Collection not found', 404); +// } - $adapter = new AuditAdapter($register->get('db')); - $adapter->setNamespace('app_'.$project->getId()); +// $adapter = new AuditAdapter($register->get('db')); +// $adapter->setNamespace('app_'.$project->getId()); - $audit = new Audit($adapter); +// $audit = new Audit($adapter); - $countries = Locale::getText('countries'); +// $countries = Locale::getText('countries'); - $logs = $audit->getLogsByResource('database/collection/'.$collection->getId()); +// $logs = $audit->getLogsByResource('database/collection/'.$collection->getId()); - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $output = []; +// $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'; +// foreach ($logs as $i => &$log) { +// $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; - $dd = new DeviceDetector($log['userAgent']); +// $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->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - $dd->parse(); +// $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' => [], - ]; +// $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'); - } - } +// 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); - } - ); +// $response->json($output); +// } +// ); $utopia->put('/v1/database/collections/:collectionId') ->desc('Update Collection') @@ -257,7 +258,7 @@ $utopia->put('/v1/database/collections/:collectionId') $parsedRules = []; foreach ($rules as &$rule) { - $parsedRules[] = array_merge([ + $parsedRules[] = \array_merge([ '$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => [ 'read' => $read, @@ -267,10 +268,10 @@ $utopia->put('/v1/database/collections/:collectionId') } try { - $collection = $projectDB->updateDocument(array_merge($collection->getArrayCopy(), [ + $collection = $projectDB->updateDocument(\array_merge($collection->getArrayCopy(), [ 'name' => $name, 'structure' => true, - 'dateUpdated' => time(), + 'dateUpdated' => \time(), '$permissions' => [ 'read' => $read, 'write' => $write, @@ -350,8 +351,8 @@ $utopia->post('/v1/database/collections/:collectionId/documents') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.method', 'createDocument') ->label('sdk.description', '/docs/references/database/create-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/database?platform=server#createCollection).') - ->param('data', [], function () { return new \Utopia\Validator\Mock(); }, 'Document data as JSON object.') + ->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('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.') ->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) @@ -359,8 +360,8 @@ $utopia->post('/v1/database/collections/:collectionId/documents') ->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) && $result = json_decode($data, true)) ? $result : $data; // Cast to JSON array - + $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array + if (empty($data)) { throw new Exception('Missing payload', 400); } @@ -371,7 +372,7 @@ $utopia->post('/v1/database/collections/:collectionId/documents') $collection = $projectDB->getDocument($collectionId/*, $isDev*/); - if (is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { throw new Exception('Collection not found', 404); } @@ -468,7 +469,7 @@ $utopia->get('/v1/database/collections/:collectionId/documents') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.method', 'listDocuments') ->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/database?platform=server#createCollection).') + ->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('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) @@ -482,7 +483,7 @@ $utopia->get('/v1/database/collections/:collectionId/documents') function ($collectionId, $filters, $offset, $limit, $orderField, $orderType, $orderCast, $search, $first, $last) use ($response, $projectDB, $isDev) { $collection = $projectDB->getDocument($collectionId, $isDev); - if (is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { throw new Exception('Collection not found', 404); } @@ -495,7 +496,7 @@ $utopia->get('/v1/database/collections/:collectionId/documents') 'search' => $search, 'first' => (bool) $first, 'last' => (bool) $last, - 'filters' => array_merge($filters, [ + 'filters' => \array_merge($filters, [ '$collection='.$collectionId, ]), ]); @@ -535,7 +536,7 @@ $utopia->get('/v1/database/collections/:collectionId/documents/:documentId') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.method', 'getDocument') ->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/database?platform=server#createCollection).') + ->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, $isDev) { @@ -548,20 +549,20 @@ $utopia->get('/v1/database/collections/:collectionId/documents/:documentId') $output = $document->getArrayCopy(); - $paths = explode('/', $request->getParam('q', '')); - $paths = array_slice($paths, 7, count($paths)); + $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)); + 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))); + $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)) { + if (!\is_array($output)) { throw new Exception('No document found', 404); } } @@ -581,9 +582,9 @@ $utopia->patch('/v1/database/collections/:collectionId/documents/:documentId') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.method', 'updateDocument') ->label('sdk.description', '/docs/references/database/update-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/database?platform=server#createCollection).') + ->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.') - ->param('data', [], function () { return new \Utopia\Validator\Mock(); }, 'Document data as JSON object.') + ->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( @@ -591,13 +592,13 @@ $utopia->patch('/v1/database/collections/:collectionId/documents/:documentId') $collection = $projectDB->getDocument($collectionId/*, $isDev*/); $document = $projectDB->getDocument($documentId, $isDev); - $data = (is_string($data) && $result = json_decode($data, true)) ? $result : $data; // Cast to JSON array + $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array - if (!is_array($data)) { + if (!\is_array($data)) { throw new Exception('Data param should be a valid JSON', 400); } - if (is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { throw new Exception('Collection not found', 404); } @@ -615,7 +616,7 @@ $utopia->patch('/v1/database/collections/:collectionId/documents/:documentId') $data['$permissions']['write'] = $read; } - $data = array_merge($document->getArrayCopy(), $data); + $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 @@ -660,7 +661,7 @@ $utopia->delete('/v1/database/collections/:collectionId/documents/:documentId') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.method', 'deleteDocument') ->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/database?platform=server#createCollection).') + ->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, $isDev) { @@ -671,7 +672,7 @@ $utopia->delete('/v1/database/collections/:collectionId/documents/:documentId') throw new Exception('No document found', 404); } - if (is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { throw new Exception('Collection not found', 404); } diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index e2b3c3776f..94c99bc82b 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -3,7 +3,7 @@ global $utopia, $request, $response, $register, $project; use Utopia\Exception; -use Appwrite\Storage\Devices\Local; +use Appwrite\Storage\Device\Local; use Appwrite\Storage\Storage; use Appwrite\ClamAV\Network; @@ -20,6 +20,15 @@ $utopia->get('/v1/health') } ); +$utopia->get('/v1/health/version') + ->desc('Get Version') + ->label('scope', 'public') + ->action( + function () use ($response) { + $response->json(['version' => APP_VERSION_STABLE]); + } + ); + $utopia->get('/v1/health/db') ->desc('Get DB') ->label('scope', 'health.read') @@ -66,34 +75,34 @@ $utopia->get('/v1/health/time') $gap = 60; // Allow [X] seconds gap /* Create a socket and connect to NTP server */ - $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + $sock = \socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); - socket_connect($sock, $host, 123); + \socket_connect($sock, $host, 123); /* Send request */ - $msg = "\010".str_repeat("\0", 47); + $msg = "\010".\str_repeat("\0", 47); - socket_send($sock, $msg, strlen($msg), 0); + \socket_send($sock, $msg, \strlen($msg), 0); /* Receive response and close socket */ - socket_recv($sock, $recv, 48, MSG_WAITALL); - socket_close($sock); + \socket_recv($sock, $recv, 48, MSG_WAITALL); + \socket_close($sock); /* Interpret response */ - $data = unpack('N12', $recv); - $timestamp = sprintf('%u', $data[9]); + $data = \unpack('N12', $recv); + $timestamp = \sprintf('%u', $data[9]); /* 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; - $diff = ($timestamp - time()); + $diff = ($timestamp - \time()); if ($diff > $gap || $diff < ($gap * -1)) { throw new Exception('Server time gaps detected'); } - $response->json(['remote' => $timestamp, 'local' => time(), 'diff' => $diff]); + $response->json(['remote' => $timestamp, 'local' => \time(), 'diff' => $diff]); } ); @@ -124,7 +133,7 @@ $utopia->get('/v1/health/queue/tasks') ); $utopia->get('/v1/health/queue/logs') -->desc('Get Logs Queue') + ->desc('Get Logs Queue') ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -184,26 +193,22 @@ $utopia->get('/v1/health/storage/local') ->label('sdk.description', '/docs/references/health/get-storage-local.md') ->action( function () use ($response) { - $device = new Local(APP_STORAGE_UPLOADS.'/'); - if (!is_readable($device->getRoot().'/..')) { - throw new Exception('Device is not readable'); - } - - if (!is_writable($device->getRoot().'/../uploads')) { - throw new Exception('Device uploads dir is not writable'); - } - - if (!is_writable($device->getRoot().'/../cache')) { - throw new Exception('Device cache dir is not writable'); - } + 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().'/../config')) { - throw new Exception('Device config dir is not writable'); - } + if (!\is_readable($device->getRoot())) { + throw new Exception('Device '.$key.' dir is not readable'); + } - if (!is_writable($device->getRoot().'/../certificates')) { - throw new Exception('Device certificates dir is not writable'); + if (!\is_writable($device->getRoot())) { + throw new Exception('Device '.$key.' dir is not writable'); + } } $response->json(['status' => 'OK']); @@ -250,7 +255,7 @@ $utopia->get('/v1/health/stats') // Currently only used internally ->json([ 'server' => [ 'name' => 'nginx', - 'version' => shell_exec('nginx -v 2>&1'), + 'version' => \shell_exec('nginx -v 2>&1'), ], 'storage' => [ 'used' => Storage::human($device->getDirectorySize($device->getRoot().'/')), diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php index 8f4f2e3272..30a1211e91 100644 --- a/app/controllers/api/locale.php +++ b/app/controllers/api/locale.php @@ -26,7 +26,7 @@ $utopia->get('/v1/locale') $countries = Locale::getText('countries'); $continents = Locale::getText('continents'); - if (App::ENV_TYPE_PRODUCTION !== $utopia->getEnv()) { + if (App::MODE_TYPE_PRODUCTION !== $utopia->getMode()) { $ip = '79.177.241.94'; } @@ -41,10 +41,10 @@ $utopia->get('/v1/locale') //$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; + $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'])) { + if (isset($element['locations']) && isset($element['code']) && \in_array($record->country->isoCode, $element['locations'])) { $currency = $element['code']; } } @@ -61,7 +61,7 @@ $utopia->get('/v1/locale') $response ->addHeader('Cache-Control', 'public, max-age='.$time) - ->addHeader('Expires', date('D, d M Y H:i:s', time() + $time).' GMT') // 45 days cache + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache ->json($output); } ); @@ -77,7 +77,7 @@ $utopia->get('/v1/locale/countries') function () use ($response) { $list = Locale::getText('countries'); /* @var $list array */ - asort($list); + \asort($list); $response->json($list); } @@ -97,12 +97,12 @@ $utopia->get('/v1/locale/countries/eu') $list = []; foreach ($eu as $code) { - if (array_key_exists($code, $countries)) { + if (\array_key_exists($code, $countries)) { $list[$code] = $countries[$code]; } } - asort($list); + \asort($list); $response->json($list); } @@ -122,12 +122,12 @@ $utopia->get('/v1/locale/countries/phones') $countries = Locale::getText('countries'); /* @var $countries array */ foreach ($list as $code => $name) { - if (array_key_exists($code, $countries)) { + if (\array_key_exists($code, $countries)) { $list[$code] = '+'.$list[$code]; } } - asort($list); + \asort($list); $response->json($list); } @@ -144,7 +144,7 @@ $utopia->get('/v1/locale/continents') function () use ($response) { $list = Locale::getText('continents'); /* @var $list array */ - asort($list); + \asort($list); $response->json($list); } @@ -165,3 +165,19 @@ $utopia->get('/v1/locale/currencies') $response->json($currencies); } ); + + +$utopia->get('/v1/locale/languages') + ->desc('List Languages') + ->label('scope', 'locale.read') + ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) + ->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'; + + $response->json($languages); + } + ); \ No newline at end of file diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 2ffc3a0233..4511c34807 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -18,7 +18,7 @@ use Appwrite\Database\Database; use Appwrite\Database\Document; use Appwrite\Database\Validator\UID; use Appwrite\OpenSSL\OpenSSL; -use Appwrite\Network\Validators\CNAME; +use Appwrite\Network\Validator\CNAME; use Cron\CronExpression; include_once __DIR__ . '/../shared/api.php'; @@ -107,11 +107,11 @@ $utopia->get('/v1/projects') foreach ($results as $project) { foreach (Config::getParam('providers') as $provider => $node) { - $secret = json_decode($project->getAttribute('usersOauth2'.ucfirst($provider).'Secret', '{}'), true); + $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']))); + $project->setAttribute('usersOauth2'.\ucfirst($provider).'Secret', OpenSSL::decrypt($secret['data'], $secret['method'], $key, 0, \hex2bin($secret['iv']), \hex2bin($secret['tag']))); } } } @@ -135,11 +135,11 @@ $utopia->get('/v1/projects/:projectId') } foreach (Config::getParam('providers') as $provider => $node) { - $secret = json_decode($project->getAttribute('usersOauth2'.ucfirst($provider).'Secret', '{}'), true); + $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']))); + $project->setAttribute('usersOauth2'.\ucfirst($provider).'Secret', OpenSSL::decrypt($secret['data'], $secret['method'], $key, 0, \hex2bin($secret['iv']), \hex2bin($secret['tag']))); } } @@ -164,23 +164,23 @@ $utopia->get('/v1/projects/:projectId/usage') $period = [ 'daily' => [ - 'start' => DateTime::createFromFormat('U', strtotime('today')), - 'end' => DateTime::createFromFormat('U', strtotime('tomorrow')), + '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')), + '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')), + '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')), + 'start' => DateTime::createFromFormat('U', \strtotime('-90 days')), + 'end' => DateTime::createFromFormat('U', \strtotime('today')), 'group' => '1d', ], // 'yearly' => [ @@ -207,7 +207,7 @@ $utopia->get('/v1/projects/:projectId/usage') foreach ($points as $point) { $requests[] = [ 'value' => (!empty($point['value'])) ? $point['value'] : 0, - 'date' => strtotime($point['time']), + 'date' => \strtotime($point['time']), ]; } @@ -218,7 +218,7 @@ $utopia->get('/v1/projects/:projectId/usage') foreach ($points as $point) { $network[] = [ 'value' => (!empty($point['value'])) ? $point['value'] : 0, - 'date' => strtotime($point['time']), + 'date' => \strtotime($point['time']), ]; } } @@ -262,18 +262,18 @@ $utopia->get('/v1/projects/:projectId/usage') } // Tasks - $tasksTotal = count($project->getAttribute('tasks', [])); + $tasksTotal = \count($project->getAttribute('tasks', [])); $response->json([ 'requests' => [ 'data' => $requests, - 'total' => array_sum(array_map(function ($item) { + 'total' => \array_sum(\array_map(function ($item) { return $item['value']; }, $requests)), ], 'network' => [ 'data' => $network, - 'total' => array_sum(array_map(function ($item) { + 'total' => \array_sum(\array_map(function ($item) { return $item['value']; }, $network)), ], @@ -283,7 +283,7 @@ $utopia->get('/v1/projects/:projectId/usage') ], 'documents' => [ 'data' => $documents, - 'total' => array_sum(array_map(function ($item) { + 'total' => \array_sum(\array_map(function ($item) { return $item['total']; }, $documents)), ], @@ -332,7 +332,7 @@ $utopia->patch('/v1/projects/:projectId') throw new Exception('Project not found', 404); } - $project = $consoleDB->updateDocument(array_merge($project->getArrayCopy(), [ + $project = $consoleDB->updateDocument(\array_merge($project->getArrayCopy(), [ 'name' => $name, 'description' => $description, 'logo' => $logo, @@ -359,7 +359,7 @@ $utopia->patch('/v1/projects/:projectId/oauth2') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'updateOAuth2') ->param('projectId', '', function () { return new UID(); }, 'Project unique ID.') - ->param('provider', '', function () { return new WhiteList(array_keys(Config::getParam('providers'))); }, 'Provider Name', false) + ->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( @@ -373,17 +373,17 @@ $utopia->patch('/v1/projects/:projectId/oauth2') $key = $request->getServer('_APP_OPENSSL_KEY_V1'); $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); $tag = null; - $secret = json_encode([ + $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), + '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, + $project = $consoleDB->updateDocument(\array_merge($project->getArrayCopy(), [ + 'usersOauth2'.\ucfirst($provider).'Appid' => $appId, + 'usersOauth2'.\ucfirst($provider).'Secret' => $secret, ])); if (false === $project) { @@ -424,6 +424,10 @@ $utopia->delete('/v1/projects/:projectId') } } } + + 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); @@ -458,11 +462,11 @@ $utopia->post('/v1/projects/:projectId/webhooks') $key = $request->getServer('_APP_OPENSSL_KEY_V1'); $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); $tag = null; - $httpPass = json_encode([ + $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), + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), 'version' => '1', ]); @@ -516,7 +520,7 @@ $utopia->get('/v1/projects/:projectId/webhooks') $webhooks = $project->getAttribute('webhooks', []); foreach ($webhooks as $webhook) { /* @var $webhook Document */ - $httpPass = json_decode($webhook->getAttribute('httpPass', '{}'), true); + $httpPass = \json_decode($webhook->getAttribute('httpPass', '{}'), true); if (empty($httpPass) || !isset($httpPass['version'])) { continue; @@ -524,7 +528,7 @@ $utopia->get('/v1/projects/:projectId/webhooks') $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']))); + $webhook->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); } $response->json($webhooks); @@ -552,11 +556,11 @@ $utopia->get('/v1/projects/:projectId/webhooks/:webhookId') throw new Exception('Webhook not found', 404); } - $httpPass = json_decode($webhook->getAttribute('httpPass', '{}'), true); + $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']))); + $webhook->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); } $response->json($webhook->getArrayCopy()); @@ -588,11 +592,11 @@ $utopia->put('/v1/projects/:projectId/webhooks/:webhookId') $key = $request->getServer('_APP_OPENSSL_KEY_V1'); $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); $tag = null; - $httpPass = json_encode([ + $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), + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), 'version' => '1', ]); @@ -674,7 +678,7 @@ $utopia->post('/v1/projects/:projectId/keys') ], 'name' => $name, 'scopes' => $scopes, - 'secret' => bin2hex(random_bytes(128)), + 'secret' => \bin2hex(\random_bytes(128)), ]); if (false === $key) { @@ -835,11 +839,11 @@ $utopia->post('/v1/projects/:projectId/tasks') $key = $request->getServer('_APP_OPENSSL_KEY_V1'); $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); $tag = null; - $httpPass = json_encode([ + $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), + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), 'version' => '1', ]); @@ -852,7 +856,7 @@ $utopia->post('/v1/projects/:projectId/tasks') 'name' => $name, 'status' => $status, 'schedule' => $schedule, - 'updated' => time(), + 'updated' => \time(), 'previous' => null, 'next' => $next, 'security' => (int) $security, @@ -905,7 +909,7 @@ $utopia->get('/v1/projects/:projectId/tasks') $tasks = $project->getAttribute('tasks', []); foreach ($tasks as $task) { /* @var $task Document */ - $httpPass = json_decode($task->getAttribute('httpPass', '{}'), true); + $httpPass = \json_decode($task->getAttribute('httpPass', '{}'), true); if (empty($httpPass) || !isset($httpPass['version'])) { continue; @@ -913,7 +917,7 @@ $utopia->get('/v1/projects/:projectId/tasks') $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']))); + $task->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); } $response->json($tasks); @@ -941,11 +945,11 @@ $utopia->get('/v1/projects/:projectId/tasks/:taskId') throw new Exception('Task not found', 404); } - $httpPass = json_decode($task->getAttribute('httpPass', '{}'), true); + $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']))); + $task->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); } $response->json($task->getArrayCopy()); @@ -988,11 +992,11 @@ $utopia->put('/v1/projects/:projectId/tasks/:taskId') $key = $request->getServer('_APP_OPENSSL_KEY_V1'); $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); $tag = null; - $httpPass = json_encode([ + $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), + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), 'version' => '1', ]); @@ -1000,7 +1004,7 @@ $utopia->put('/v1/projects/:projectId/tasks/:taskId') ->setAttribute('name', $name) ->setAttribute('status', $status) ->setAttribute('schedule', $schedule) - ->setAttribute('updated', time()) + ->setAttribute('updated', \time()) ->setAttribute('next', $next) ->setAttribute('security', (int) $security) ->setAttribute('httpMethod', $httpMethod) @@ -1083,8 +1087,8 @@ $utopia->post('/v1/projects/:projectId/platforms') 'key' => $key, 'store' => $store, 'hostname' => $hostname, - 'dateCreated' => time(), - 'dateUpdated' => time(), + 'dateCreated' => \time(), + 'dateUpdated' => \time(), ]); if (false === $platform) { @@ -1178,7 +1182,7 @@ $utopia->put('/v1/projects/:projectId/platforms/:platformId') $platform ->setAttribute('name', $name) - ->setAttribute('dateUpdated', time()) + ->setAttribute('dateUpdated', \time()) ->setAttribute('key', $key) ->setAttribute('store', $store) ->setAttribute('hostname', $hostname) @@ -1258,7 +1262,7 @@ $utopia->post('/v1/projects/:projectId/domains') 'read' => ['team:'.$project->getAttribute('teamId', null)], 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], ], - 'updated' => time(), + 'updated' => \time(), 'domain' => $domain->get(), 'tld' => $domain->getSuffix(), 'registerable' => $domain->getRegisterable(), diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 18a8c98c26..6ffcfbf985 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -15,10 +15,10 @@ use Appwrite\ClamAV\Network; use Appwrite\Database\Database; use Appwrite\Database\Validator\UID; use Appwrite\Storage\Storage; -use Appwrite\Storage\Devices\Local; -use Appwrite\Storage\Validators\File; -use Appwrite\Storage\Validators\FileSize; -use Appwrite\Storage\Validators\Upload; +use Appwrite\Storage\Device\Local; +use Appwrite\Storage\Validator\File; +use Appwrite\Storage\Validator\FileSize; +use Appwrite\Storage\Validator\Upload; use Appwrite\Storage\Compression\Algorithms\GZIP; use Appwrite\Resize\Resize; use Appwrite\OpenSSL\OpenSSL; @@ -165,9 +165,9 @@ $utopia->post('/v1/storage/files') } // 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']; + $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'])) { @@ -190,7 +190,7 @@ $utopia->post('/v1/storage/files') // Save to storage $size = $device->getFileSize($file['tmp_name']); - $path = $device->getPath(uniqid().'.'.pathinfo($file['name'], PATHINFO_EXTENSION)); + $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); @@ -228,7 +228,7 @@ $utopia->post('/v1/storage/files') 'read' => $read, 'write' => $write, ], - 'dateCreated' => time(), + 'dateCreated' => \time(), 'folderId' => $folderId, 'name' => $file['name'], 'path' => $path, @@ -237,12 +237,12 @@ $utopia->post('/v1/storage/files') 'sizeOriginal' => $size, 'sizeActual' => $sizeActual, 'algorithm' => $compressor->getName(), - 'token' => bin2hex(random_bytes(64)), + 'token' => \bin2hex(\random_bytes(64)), 'comment' => '', 'fileOpenSSLVersion' => '1', 'fileOpenSSLCipher' => OpenSSL::CIPHER_AES_128_GCM, - 'fileOpenSSLTag' => bin2hex($tag), - 'fileOpenSSLIV' => bin2hex($iv), + 'fileOpenSSLTag' => \bin2hex($tag), + 'fileOpenSSLIV' => \bin2hex($iv), ]); if (false === $file) { @@ -294,7 +294,7 @@ $utopia->get('/v1/storage/files') ], ]); - $results = array_map(function ($value) { /* @var $value \Database\Document */ + $results = \array_map(function ($value) { /* @var $value \Database\Document */ return $value->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal']); }, $results); @@ -336,14 +336,14 @@ $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) + ->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) //->param('storage', 'local', function () {return new WhiteList(array('local'));}, 'Selected storage device. defaults to local') //->param('token', '', function () {return new Text(128);}, 'Preview token', true) ->action( function ($fileId, $width, $height, $quality, $background, $output) use ($request, $response, $projectDB, $project, $inputs, $outputs, $fileLogos) { $storage = 'local'; - if (!extension_loaded('imagick')) { + if (!\extension_loaded('imagick')) { throw new Exception('Imagick extension is missing', 500); } @@ -351,12 +351,12 @@ $utopia->get('/v1/storage/files/:fileId/preview') throw new Exception('No such storage device', 400); } - if ((strpos($request->getServer('HTTP_ACCEPT'), 'image/webp') === false) && ('webp' == $output)) { // Fallback webp to jpeg when no browser support + if ((\strpos($request->getServer('HTTP_ACCEPT'), 'image/webp') === false) && ('webp' == $output)) { // Fallback webp to jpeg when no browser support $output = 'jpg'; } - $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); + $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); $file = $projectDB->getDocument($fileId); @@ -365,24 +365,24 @@ $utopia->get('/v1/storage/files/:fileId/preview') } $path = $file->getAttribute('path'); - $type = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + $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']; + 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); + $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); + $key = \md5($path.$width.$height.$quality.$background.$storage.$output); } $compressor = new GZIP(); $device = Storage::getDevice('local'); - if (!file_exists($path)) { + if (!\file_exists($path)) { throw new Exception('File not found', 404); } @@ -393,7 +393,7 @@ $utopia->get('/v1/storage/files/:fileId/preview') $output = (empty($output)) ? $type : $output; $response - ->setContentType((in_array($output, $outputs)) ? $outputs[$output] : $outputs['jpg']) + ->setContentType((\in_array($output, $outputs)) ? $outputs[$output] : $outputs['jpg']) ->addHeader('Expires', $date) ->addHeader('X-Appwrite-Cache', 'hit') ->send($data) @@ -410,8 +410,8 @@ $utopia->get('/v1/storage/files/:fileId/preview') $file->getAttribute('fileOpenSSLCipher'), $request->getServer('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), 0, - hex2bin($file->getAttribute('fileOpenSSLIV')), - hex2bin($file->getAttribute('fileOpenSSLTag')) + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) ); } @@ -466,7 +466,7 @@ $utopia->get('/v1/storage/files/:fileId/download') $path = $file->getAttribute('path', ''); - if (!file_exists($path)) { + if (!\file_exists($path)) { throw new Exception('File not found in '.$path, 404); } @@ -481,8 +481,8 @@ $utopia->get('/v1/storage/files/:fileId/download') $file->getAttribute('fileOpenSSLCipher'), $request->getServer('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), 0, - hex2bin($file->getAttribute('fileOpenSSLIV')), - hex2bin($file->getAttribute('fileOpenSSLTag')) + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) ); } @@ -492,8 +492,8 @@ $utopia->get('/v1/storage/files/:fileId/download') $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()) + ->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) ; } @@ -520,7 +520,7 @@ $utopia->get('/v1/storage/files/:fileId/view') $path = $file->getAttribute('path', ''); - if (!file_exists($path)) { + if (!\file_exists($path)) { throw new Exception('File not found in '.$path, 404); } @@ -529,7 +529,7 @@ $utopia->get('/v1/storage/files/:fileId/view') $contentType = 'text/plain'; - if (in_array($file->getAttribute('mimeType'), $mimes)) { + if (\in_array($file->getAttribute('mimeType'), $mimes)) { $contentType = $file->getAttribute('mimeType'); } @@ -541,8 +541,8 @@ $utopia->get('/v1/storage/files/:fileId/view') $file->getAttribute('fileOpenSSLCipher'), $request->getServer('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), 0, - hex2bin($file->getAttribute('fileOpenSSLIV')), - hex2bin($file->getAttribute('fileOpenSSLTag')) + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) ); } @@ -554,7 +554,7 @@ $utopia->get('/v1/storage/files/:fileId/view') 'text' => 'text/plain', ]; - $contentType = (array_key_exists($as, $contentTypes)) ? $contentTypes[$as] : $contentType; + $contentType = (\array_key_exists($as, $contentTypes)) ? $contentTypes[$as] : $contentType; // Response $response @@ -562,8 +562,8 @@ $utopia->get('/v1/storage/files/:fileId/view') ->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()) + ->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) ; } @@ -589,7 +589,7 @@ $utopia->put('/v1/storage/files/:fileId') throw new Exception('File not found', 404); } - $file = $projectDB->updateDocument(array_merge($file->getArrayCopy(), [ + $file = $projectDB->updateDocument(\array_merge($file->getArrayCopy(), [ '$permissions' => [ 'read' => $read, 'write' => $write, diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index fad09b695e..b8e0bf1194 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,6 +1,6 @@ post('/v1/teams') ], 'name' => $name, 'sum' => ($mode !== APP_MODE_ADMIN && $user->getId()) ? 1 : 0, - 'dateCreated' => time(), + 'dateCreated' => \time(), ]); Authorization::reset(); @@ -62,8 +62,8 @@ $utopia->post('/v1/teams') 'userId' => $user->getId(), 'teamId' => $team->getId(), 'roles' => $roles, - 'invited' => time(), - 'joined' => time(), + 'invited' => \time(), + 'joined' => \time(), 'confirm' => true, 'secret' => '', ]); @@ -151,7 +151,7 @@ $utopia->put('/v1/teams/:teamId') throw new Exception('Team not found', 404); } - $team = $projectDB->updateDocument(array_merge($team->getArrayCopy(), [ + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ 'name' => $name, ])); @@ -215,7 +215,7 @@ $utopia->post('/v1/teams/:teamId/memberships') ->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, $register, $project, $user, $audit, $projectDB) { + function ($teamId, $email, $name, $roles, $url) use ($response, $mail, $project, $user, $audit, $projectDB, $mode) { $name = (empty($name)) ? $email : $name; $team = $projectDB->getDocument($teamId); @@ -256,8 +256,8 @@ $utopia->post('/v1/teams/:teamId/memberships') 'emailVerification' => false, 'status' => Auth::USER_STATUS_UNACTIVATED, 'password' => Auth::passwordHash(Auth::passwordGenerator()), - 'password-update' => time(), - 'registration' => time(), + 'password-update' => \time(), + 'registration' => \time(), 'reset' => false, 'name' => $name, 'tokens' => [], @@ -280,12 +280,12 @@ $utopia->post('/v1/teams/:teamId/memberships') 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', []))) { + if ($member->getAttribute('userId') == $user->getId() && \in_array('owner', $member->getAttribute('roles', []))) { $isOwner = true; } } - if (!$isOwner) { + if (!$isOwner && (APP_MODE_ADMIN !== $mode)) { throw new Exception('User is not allowed to send invitations for this team', 401); } @@ -300,13 +300,20 @@ $utopia->post('/v1/teams/:teamId/memberships') 'userId' => $invitee->getId(), 'teamId' => $team->getId(), 'roles' => $roles, - 'invited' => time(), + 'invited' => \time(), 'joined' => 0, - 'confirm' => false, + 'confirm' => (APP_MODE_ADMIN === $mode), 'secret' => Auth::hash($secret), ]); - $membership = $projectDB->createDocument($membership->getArrayCopy()); + if(APP_MODE_ADMIN === $mode) { // Allow admin to create membership + Authorization::disable(); + $membership = $projectDB->createDocument($membership->getArrayCopy()); + Authorization::reset(); + } + else { + $membership = $projectDB->createDocument($membership->getArrayCopy()); + } if (false === $membership) { throw new Exception('Failed saving membership to DB', 500); @@ -316,27 +323,36 @@ $utopia->post('/v1/teams/:teamId/memberships') $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/'.Locale::getText('account.emails.invitation.body')); + $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') ; - $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ - - $mail->addAddress($email, $name); - - $mail->Subject = sprintf(Locale::getText('account.emails.invitation.title'), $team->getAttribute('name', '[TEAM-NAME]'), $project->getAttribute('name', ['[APP-NAME]'])); - $mail->Body = $body->render(); - $mail->AltBody = strip_tags($body->render()); - - try { - $mail->send(); - } catch (\Exception $error) { - throw new Exception('Error sending mail: ' . $error->getMessage(), 500); + if(APP_MODE_ADMIN !== $mode) { // No need in comfirmation when in admin 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 @@ -347,7 +363,7 @@ $utopia->post('/v1/teams/:teamId/memberships') $response ->setStatusCode(Response::STATUS_CODE_CREATED) // TODO change response of this endpoint - ->json(array_merge($membership->getArrayCopy([ + ->json(\array_merge($membership->getArrayCopy([ '$id', 'userId', 'teamId', @@ -371,8 +387,12 @@ $utopia->get('/v1/teams/:teamId/memberships') ->label('sdk.method', 'getMemberships') ->label('sdk.description', '/docs/references/teams/get-team-members.md') ->param('teamId', '', function () { return new UID(); }, 'Team 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 ($teamId) use ($response, $projectDB) { + function ($teamId, $search, $limit, $offset, $orderType) use ($response, $projectDB) { $team = $projectDB->getDocument($teamId); if (empty($team->getId()) || Database::SYSTEM_COLLECTION_TEAMS != $team->getCollection()) { @@ -380,8 +400,12 @@ $utopia->get('/v1/teams/:teamId/memberships') } $memberships = $projectDB->getCollection([ - 'limit' => 50, - 'offset' => 0, + 'limit' => $limit, + 'offset' => $offset, + 'orderField' => 'joined', + 'orderType' => $orderType, + 'orderCast' => 'int', + 'search' => $search, 'filters' => [ '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, 'teamId='.$teamId, @@ -397,7 +421,7 @@ $utopia->get('/v1/teams/:teamId/memberships') $temp = $projectDB->getDocument($membership->getAttribute('userId', null))->getArrayCopy(['email', 'name']); - $users[] = array_merge($temp, $membership->getArrayCopy([ + $users[] = \array_merge($temp, $membership->getArrayCopy([ '$id', 'userId', 'teamId', @@ -408,15 +432,8 @@ $utopia->get('/v1/teams/:teamId/memberships') ])); } - usort($users, function ($a, $b) { - if ($a['joined'] === 0 || $b['joined'] === 0) { - return $b['joined'] - $a['joined']; - } + $response->json(['sum' => $projectDB->getSum(), 'memberships' => $users]); - return $a['joined'] - $b['joined']; - }); - - $response->json($users); } ); @@ -478,7 +495,7 @@ $utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') } $membership // Attach user to team - ->setAttribute('joined', time()) + ->setAttribute('joined', \time()) ->setAttribute('confirm', true) ; @@ -488,7 +505,7 @@ $utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') ; // Log user in - $expiry = time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; $secret = Auth::tokenGenerator(); $user->setAttribute('tokens', new Document([ @@ -511,7 +528,7 @@ $utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') Authorization::disable(); - $team = $projectDB->updateDocument(array_merge($team->getArrayCopy(), [ + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ 'sum' => $team->getAttribute('sum', 0) + 1, ])); @@ -529,14 +546,14 @@ $utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') if(!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) ; } $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([ + ->json(\array_merge($membership->getArrayCopy([ '$id', 'userId', 'teamId', @@ -583,9 +600,11 @@ $utopia->delete('/v1/teams/:teamId/memberships/:inviteId') throw new Exception('Failed to remove membership from DB', 500); } - $team = $projectDB->updateDocument(array_merge($team->getArrayCopy(), [ - 'sum' => $team->getAttribute('sum', 0) - 1, - ])); + 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); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 12090ecc66..d032cef947 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -59,8 +59,8 @@ $utopia->post('/v1/users') 'emailVerification' => false, 'status' => Auth::USER_STATUS_UNACTIVATED, 'password' => Auth::passwordHash($password), - 'password-update' => time(), - 'registration' => time(), + 'password-update' => \time(), + 'registration' => \time(), 'reset' => false, 'name' => $name, ], ['email' => $email]); @@ -75,13 +75,13 @@ $utopia->post('/v1/users') continue; } - $oauth2Keys[] = 'oauth2'.ucfirst($key); - $oauth2Keys[] = 'oauth2'.ucfirst($key).'AccessToken'; + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } $response ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json(array_merge($user->getArrayCopy(array_merge([ + ->json(\array_merge($user->getArrayCopy(\array_merge([ '$id', 'status', 'email', @@ -124,12 +124,12 @@ $utopia->get('/v1/users') continue; } - $oauth2Keys[] = 'oauth2'.ucfirst($key); - $oauth2Keys[] = 'oauth2'.ucfirst($key).'AccessToken'; + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } - $results = array_map(function ($value) use ($oauth2Keys) { /* @var $value \Database\Document */ - return $value->getArrayCopy(array_merge( + $results = \array_map(function ($value) use ($oauth2Keys) { /* @var $value \Database\Document */ + return $value->getArrayCopy(\array_merge( [ '$id', 'status', @@ -169,11 +169,11 @@ $utopia->get('/v1/users/:userId') continue; } - $oauth2Keys[] = 'oauth2'.ucfirst($key); - $oauth2Keys[] = 'oauth2'.ucfirst($key).'AccessToken'; + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } - $response->json(array_merge($user->getArrayCopy(array_merge( + $response->json(\array_merge($user->getArrayCopy(\array_merge( [ '$id', 'status', @@ -206,7 +206,7 @@ $utopia->get('/v1/users/:userId/prefs') $prefs = $user->getAttribute('prefs', ''); try { - $prefs = json_decode($prefs, true); + $prefs = \json_decode($prefs, true); $prefs = ($prefs) ? $prefs : []; } catch (\Exception $error) { throw new Exception('Failed to parse prefs', 500); @@ -265,7 +265,7 @@ $utopia->get('/v1/users/:userId/sessions') try { $record = $reader->country($token->getAttribute('ip', '')); - $sessions[$index]['geo']['isoCode'] = strtolower($record->country->isoCode); + $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'] = '--'; @@ -335,7 +335,7 @@ $utopia->get('/v1/users/:userId/logs') $output[$i] = [ 'event' => $log['event'], 'ip' => $log['ip'], - 'time' => strtotime($log['time']), + 'time' => \strtotime($log['time']), 'OS' => $dd->getOs(), 'client' => $dd->getClient(), 'device' => $dd->getDevice(), @@ -346,7 +346,7 @@ $utopia->get('/v1/users/:userId/logs') try { $record = $reader->country($log['ip']); - $output[$i]['geo']['isoCode'] = strtolower($record->country->isoCode); + $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) { @@ -376,7 +376,7 @@ $utopia->patch('/v1/users/:userId/status') throw new Exception('User not found', 404); } - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ 'status' => (int)$status, ])); @@ -391,12 +391,12 @@ $utopia->patch('/v1/users/:userId/status') continue; } - $oauth2Keys[] = 'oauth2'.ucfirst($key); - $oauth2Keys[] = 'oauth2'.ucfirst($key).'AccessToken'; + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } $response - ->json(array_merge($user->getArrayCopy(array_merge([ + ->json(\array_merge($user->getArrayCopy(\array_merge([ '$id', 'status', 'email', @@ -424,11 +424,11 @@ $utopia->patch('/v1/users/:userId/prefs') throw new Exception('User not found', 404); } - $old = json_decode($user->getAttribute('prefs', '{}'), true); + $old = \json_decode($user->getAttribute('prefs', '{}'), true); $old = ($old) ? $old : []; - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ - 'prefs' => json_encode(array_merge($old, $prefs)), + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'prefs' => \json_encode(\array_merge($old, $prefs)), ])); if (false === $user) { @@ -438,7 +438,7 @@ $utopia->patch('/v1/users/:userId/prefs') $prefs = $user->getAttribute('prefs', ''); try { - $prefs = json_decode($prefs, true); + $prefs = \json_decode($prefs, true); $prefs = ($prefs) ? $prefs : []; } catch (\Exception $error) { throw new Exception('Failed to parse prefs', 500); diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 0a75d85d73..67e0776a90 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -7,7 +7,7 @@ use Utopia\Validator\Text; use Utopia\Validator\ArrayList; use Utopia\Response; use Utopia\Validator\Host; -use Appwrite\Storage\Validators\File; +use Appwrite\Storage\Validator\File; $result = []; @@ -175,9 +175,9 @@ $utopia->post('/v1/mock/tests/general/upload') ->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']]; + $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') { @@ -192,7 +192,7 @@ $utopia->post('/v1/mock/tests/general/upload') } foreach ($file['tmp_name'] as $i => $tmpName) { - if(md5(file_get_contents($tmpName)) !== 'd80e7e6999a3eb2ae0d631a96fe135a4') { + if(\md5(\file_get_contents($tmpName)) !== 'd80e7e6999a3eb2ae0d631a96fe135a4') { throw new Exception('Wrong file uploaded', 400); } } @@ -230,7 +230,7 @@ $utopia->get('/v1/mock/tests/general/set-cookie') ->label('sdk.description', 'Mock a set cookie request for SDK tests') ->action( function () use ($response) { - $response->addCookie('cookieName', 'cookieValue', time() + 31536000, '/', 'localhost', true, true); + $response->addCookie('cookieName', 'cookieValue', \time() + 31536000, '/', 'localhost', true, true); } ); @@ -271,7 +271,7 @@ $utopia->get('/v1/mock/tests/general/oauth2') ->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])); + $response->redirect($redirectURI.'?'.\http_build_query(['code' => 'abcdef', 'state' => $state])); } ); @@ -348,17 +348,17 @@ $utopia->shutdown(function() use ($response, $request, &$result, $utopia) { $route = $utopia->match($request); $path = APP_STORAGE_CACHE.'/tests.json'; - $tests = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; + $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; - if(!is_array($tests)) { + if(!\is_array($tests)) { throw new Exception('Failed to read results', 500); } $result[$route->getMethod() . ':' . $route->getURL()] = true; - $tests = array_merge($tests, $result); + $tests = \array_merge($tests, $result); - if(!file_put_contents($path, json_encode($tests), LOCK_EX)) { + if(!\file_put_contents($path, \json_encode($tests), LOCK_EX)) { throw new Exception('Failed to save resutls', 500); } diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 0430212be7..97da51031d 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -10,6 +10,10 @@ $utopia->init(function () use ($utopia, $request, $response, $register, $user, $ $route = $utopia->match($request); + if(empty($project->getId()) && $route->getLabel('abuse-limit', 0) > 0) { // Abuse limit requires an active project scope + throw new Exception('Missing or unknown project ID', 400); + } + /* * Abuse Check */ @@ -27,12 +31,13 @@ $utopia->init(function () use ($utopia, $request, $response, $register, $user, $ //TODO make sure we get array here foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys - $timeLimit->setParam('{param-'.$key.'}', (is_array($value)) ? json_encode($value) : $value); + $timeLimit->setParam('{param-'.$key.'}', (\is_array($value)) ? \json_encode($value) : $value); } $abuse = new Abuse($timeLimit); if ($timeLimit->limit()) { + $response ->addHeader('X-RateLimit-Limit', $timeLimit->limit()) ->addHeader('X-RateLimit-Remaining', $timeLimit->remaining()) diff --git a/app/controllers/shared/web.php b/app/controllers/shared/web.php index 42afb283e6..fced2de4db 100644 --- a/app/controllers/shared/web.php +++ b/app/controllers/shared/web.php @@ -28,16 +28,16 @@ $layout ->setParam('class', 'unknown') ->setParam('icon', '/images/favicon.png') ->setParam('roles', $roles) - ->setParam('env', $utopia->getEnv()) + ->setParam('env', $utopia->getMode()) ; $utopia->init(function () use ($utopia, $response, $request, $layout) { $time = (60 * 60 * 24 * 45); // 45 days cache - $isDev = (\Utopia\App::ENV_TYPE_DEVELOPMENT == Config::getParam('env')); + $isDev = (\Utopia\App::MODE_TYPE_DEVELOPMENT == Config::getParam('env')); $response ->addHeader('Cache-Control', 'public, max-age='.$time) - ->addHeader('Expires', date('D, d M Y H:i:s', time() + $time).' GMT') // 45 days cache + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache ->addHeader('X-UA-Compatible', 'IE=Edge'); // Deny IE browsers from going into quirks mode $route = $utopia->match($request); diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index 480c4ff9ba..ff59b03072 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -14,6 +14,7 @@ use Appwrite\Storage\Storage; $utopia->init(function () use ($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') ; }); @@ -277,14 +278,26 @@ $utopia->get('/console/users') ->setParam('body', $page); }); -$utopia->get('/console/users/view') +$utopia->get('/console/users/user') ->desc('Platform console project user') ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { - $page = new View(__DIR__.'/../../views/console/users/view.phtml'); + $page = new View(__DIR__.'/../../views/console/users/user.phtml'); $layout - ->setParam('title', APP_NAME.' - View User') + ->setParam('title', APP_NAME.' - User') + ->setParam('body', $page); + }); + +$utopia->get('/console/users/teams/team') + ->desc('Platform console project team') + ->label('permission', 'public') + ->label('scope', 'console') + ->action(function () use ($layout) { + $page = new View(__DIR__.'/../../views/console/users/team.phtml'); + + $layout + ->setParam('title', APP_NAME.' - Team') ->setParam('body', $page); }); diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index f3e4fd72c8..cfec346b91 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -168,18 +168,18 @@ $utopia->get('/open-api-2.json') function ($platform, $extensions, $tests) use ($response, $request, $utopia, $services) { function fromCamelCase($input) { - preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); + \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); + $match = $match == \strtoupper($match) ? \strtolower($match) : \lcfirst($match); } - return implode('_', $ret); + return \implode('_', $ret); } function fromCamelCaseToDash($input) { - return str_replace([' ', '_'], '-', strtolower(preg_replace('/([a-zA-Z])(?=[A-Z])/', '$1-', $input))); + return \str_replace([' ', '_'], '-', \strtolower(\preg_replace('/([a-zA-Z])(?=[A-Z])/', '$1-', $input))); } foreach ($services as $service) { /* @noinspection PhpIncludeInspection */ @@ -196,7 +196,7 @@ $utopia->get('/open-api-2.json') } /** @noinspection PhpIncludeInspection */ - include_once realpath(__DIR__.'/../../'.$service['controller']); + include_once \realpath(__DIR__.'/../../'.$service['controller']); } $security = [ @@ -295,7 +295,7 @@ $utopia->get('/open-api-2.json') 'url' => 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', ], ], - 'host' => parse_url($request->getServer('_APP_HOME', Config::getParam('domain')), PHP_URL_HOST), + 'host' => \parse_url($request->getServer('_APP_HOME', Config::getParam('domain')), PHP_URL_HOST), 'basePath' => '/v1', 'schemes' => ['https'], 'consumes' => ['application/json', 'multipart/form-data'], @@ -374,11 +374,11 @@ $utopia->get('/open-api-2.json') continue; } - if($platform !== APP_PLATFORM_CONSOLE && !in_array($platforms[$platform], $route->getLabel('sdk.platform', []))) { + if($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $route->getLabel('sdk.platform', []))) { continue; } - $url = str_replace('/v1', '', $route->getURL()); + $url = \str_replace('/v1', '', $route->getURL()); $scope = $route->getLabel('scope', ''); $hide = $route->getLabel('sdk.hide', false); $consumes = ['application/json']; @@ -387,14 +387,14 @@ $utopia->get('/open-api-2.json') continue; } - $desc = (!empty($route->getLabel('sdk.description', ''))) ? realpath('../'.$route->getLabel('sdk.description', '')) : null; + $desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath('../'.$route->getLabel('sdk.description', '')) : null; $temp = [ 'summary' => $route->getDesc(), - 'operationId' => $route->getLabel('sdk.method', uniqid()), + 'operationId' => $route->getLabel('sdk.method', \uniqid()), 'consumes' => [], 'tags' => [$route->getLabel('sdk.namespace', 'default')], - 'description' => ($desc) ? file_get_contents($desc) : '', + 'description' => ($desc) ? \file_get_contents($desc) : '', // 'responses' => [ // 200 => [ @@ -417,6 +417,7 @@ $utopia->get('/open-api-2.json') '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, ]; @@ -439,7 +440,7 @@ $utopia->get('/open-api-2.json') ]; foreach ($route->getParams() as $name => $param) { - $validator = (is_callable($param['validator'])) ? $param['validator']() : $param['validator']; /* @var $validator \Utopia\Validator */ + $validator = (\is_callable($param['validator'])) ? $param['validator']() : $param['validator']; /* @var $validator \Utopia\Validator */ $node = [ 'name' => $name, @@ -447,14 +448,14 @@ $utopia->get('/open-api-2.json') 'required' => !$param['optional'], ]; - switch ((!empty($validator)) ? get_class($validator) : '') { + switch ((!empty($validator)) ? \get_class($validator) : '') { case 'Utopia\Validator\Text': $node['type'] = 'string'; - $node['x-example'] = '['.strtoupper(fromCamelCase($node['name'])).']'; + $node['x-example'] = '['.\strtoupper(fromCamelCase($node['name'])).']'; break; case 'Appwrite\Database\Validator\UID': $node['type'] = 'string'; - $node['x-example'] = '['.strtoupper(fromCamelCase($node['name'])).']'; + $node['x-example'] = '['.\strtoupper(fromCamelCase($node['name'])).']'; break; case 'Utopia\Validator\Email': $node['type'] = 'string'; @@ -474,7 +475,7 @@ $utopia->get('/open-api-2.json') $node['x-example'] = '{}'; //$node['format'] = 'json'; break; - case 'Appwrite\Storage\Validators\File': + case 'Appwrite\Storage\Validator\File': $consumes = ['multipart/form-data']; $node['type'] = 'file'; break; @@ -516,11 +517,11 @@ $utopia->get('/open-api-2.json') break; } - if ($param['optional'] && !is_null($param['default'])) { // Param has default value + 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 + if (false !== \strpos($url, ':'.$name)) { // Param is in URL path $node['in'] = 'path'; $temp['parameters'][] = $node; } elseif ($key == 'GET') { // Param is in query @@ -536,12 +537,12 @@ $utopia->get('/open-api-2.json') } } - $url = str_replace(':'.$name, '{'.$name.'}', $url); + $url = \str_replace(':'.$name, '{'.$name.'}', $url); } $temp['consumes'] = $consumes; - $output['paths'][$url][strtolower($route->getMethod())] = $temp; + $output['paths'][$url][\strtolower($route->getMethod())] = $temp; } } @@ -549,7 +550,7 @@ $utopia->get('/open-api-2.json') var_dump($mock['name']); }*/ - ksort($output['paths']); + \ksort($output['paths']); $response ->json($output); diff --git a/app/init.php b/app/init.php index e66e48303e..69795f56cf 100644 --- a/app/init.php +++ b/app/init.php @@ -7,7 +7,7 @@ * Set configuration, framework resources, app constants * */ -if (file_exists(__DIR__.'/../vendor/autoload.php')) { +if (\file_exists(__DIR__.'/../vendor/autoload.php')) { require_once __DIR__.'/../vendor/autoload.php'; } @@ -50,6 +50,9 @@ const APP_SOCIAL_DEV = 'https://dev.to/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)); /* * ENV vars @@ -60,29 +63,27 @@ 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::setParam('env', $request->getServer('_APP_ENV', App::ENV_TYPE_PRODUCTION)); +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)); - -$utopia = new App('Asia/Tel_Aviv', Config::getParam('env')); +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($request->getServer('_APP_REDIS_HOST', '') .':'.$request->getServer('_APP_REDIS_PORT', '')); -define('COOKIE_DOMAIN', +\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) + (\filter_var(Config::getParam('hostname'), FILTER_VALIDATE_IP) !== false) ) ? null : '.'.Config::getParam('hostname') ); -define('COOKIE_SAMESITE', Response::COOKIE_SAMESITE_NONE); +\define('COOKIE_SAMESITE', Response::COOKIE_SAMESITE_NONE); /* * Registry @@ -149,8 +150,9 @@ $register->set('smtp', function () use ($request) { $mail->Password = $password; $mail->SMTPSecure = $request->getServer('_APP_SMTP_SECURE', false); $mail->SMTPAutoTLS = false; + $mail->CharSet = 'UTF-8'; - $from = urldecode($request->getServer('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')); + $from = \urldecode($request->getServer('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')); $email = $request->getServer('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); $mail->setFrom($email, $from); @@ -164,7 +166,7 @@ $register->set('smtp', function () use ($request) { /* * Localization */ -$locale = $request->getParam('locale', $request->getHeader('X-Appwrite-Locale', null)); +$locale = $request->getParam('locale', $request->getHeader('X-Appwrite-Locale', '')); Locale::$exceptions = false; @@ -217,14 +219,14 @@ Locale::setLanguage('zh-tw', include __DIR__.'/config/locales/zh-tw.php'); Locale::setDefault('en'); -if (in_array($locale, Config::getParam('locales'))) { +if (\in_array($locale, Config::getParam('locales'))) { Locale::setDefault($locale); } -stream_context_set_default([ // Set global user agent and http settings +\stream_context_set_default([ // Set global user agent and http settings 'http' => [ 'method' => 'GET', - 'user_agent' => sprintf(APP_USERAGENT, + 'user_agent' => \sprintf(APP_USERAGENT, Config::getParam('version'), $request->getServer('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)), 'timeout' => 2, @@ -241,7 +243,7 @@ $consoleDB->setNamespace('app_console'); // Should be replaced with param if we $consoleDB->setMocks(Config::getParam('collections', [])); Authorization::disable(); -$project = $consoleDB->getDocument($request->getParam('project', $request->getHeader('X-Appwrite-Project', null))); +$project = $consoleDB->getDocument($request->getParam('project', $request->getHeader('X-Appwrite-Project', ''))); Authorization::enable(); @@ -265,8 +267,8 @@ $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', null); - $fallback = json_decode($fallback, true); + $fallback = $request->getHeader('X-Fallback-Cookies', ''); + $fallback = \json_decode($fallback, true); $session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : '')); } @@ -278,9 +280,10 @@ $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)) $projectDB->setNamespace('app_'.$project->getId()); $projectDB->setMocks(Config::getParam('collections', [])); -$user = $projectDB->getDocument(Auth::$unique); - -if (APP_MODE_ADMIN === $mode) { +if (APP_MODE_ADMIN !== $mode) { + $user = $projectDB->getDocument(Auth::$unique); +} +else { $user = $consoleDB->getDocument(Auth::$unique); $user @@ -307,7 +310,7 @@ $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') + ? \urldecode($request->getServer('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')) + : \sprintf(Locale::getText('account.emails.team'), $project->getAttribute('name') ) ); diff --git a/app/sdks/client-flutter/README.md b/app/sdks/client-flutter/README.md index b0c261c115..9a5c8c5c1a 100644 --- a/app/sdks/client-flutter/README.md +++ b/app/sdks/client-flutter/README.md @@ -2,9 +2,9 @@ [![pub package](https://img.shields.io/pub/v/appwrite.svg)](https://pub.dartlang.org/packages/appwrite) ![License](https://img.shields.io/github/license/appwrite/sdk-for-flutter.svg?v=1) -![Version](https://img.shields.io/badge/api%20version-0.6.1-blue.svg?v=1) +![Version](https://img.shields.io/badge/api%20version-0.6.2-blue.svg?v=1) -**This SDK is compatible with Appwrite server version 0.6.1. For older versions, please check previous releases.** +**This SDK is compatible with Appwrite server version 0.6.2. For older versions, please check previous releases.** Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Flutter SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. diff --git a/app/sdks/client-flutter/docs/examples/account/create-o-auth2session.md b/app/sdks/client-flutter/docs/examples/account/create-o-auth2session.md index c617c34d4d..0e26dd27af 100644 --- a/app/sdks/client-flutter/docs/examples/account/create-o-auth2session.md +++ b/app/sdks/client-flutter/docs/examples/account/create-o-auth2session.md @@ -10,7 +10,7 @@ void main() { // Init SDK ; Future result = account.createOAuth2Session( - provider: 'bitbucket', + provider: 'amazon', ); result diff --git a/app/sdks/client-flutter/docs/examples/avatars/get-initials.md b/app/sdks/client-flutter/docs/examples/avatars/get-initials.md new file mode 100644 index 0000000000..b2e70788d6 --- /dev/null +++ b/app/sdks/client-flutter/docs/examples/avatars/get-initials.md @@ -0,0 +1,16 @@ +import 'package:appwrite/appwrite.dart'; + +void main() { // Init SDK + Client client = Client(); + Avatars avatars = Avatars(client); + + client + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + ; + + String result = avatars.getInitials( + ); + + print(result); // Resource URL string +} \ No newline at end of file diff --git a/app/sdks/client-flutter/docs/examples/locale/get-languages.md b/app/sdks/client-flutter/docs/examples/locale/get-languages.md new file mode 100644 index 0000000000..a4124d3cbd --- /dev/null +++ b/app/sdks/client-flutter/docs/examples/locale/get-languages.md @@ -0,0 +1,20 @@ +import 'package:appwrite/appwrite.dart'; + +void main() { // Init SDK + Client client = Client(); + Locale locale = Locale(client); + + client + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + ; + + Future result = locale.getLanguages( ); + + result + .then((response) { + print(response); + }).catchError((error) { + print(error.response); + }); +} \ No newline at end of file diff --git a/app/sdks/client-flutter/lib/services/account.dart b/app/sdks/client-flutter/lib/services/account.dart index 8cc237d104..c2083da526 100644 --- a/app/sdks/client-flutter/lib/services/account.dart +++ b/app/sdks/client-flutter/lib/services/account.dart @@ -33,10 +33,10 @@ class Account extends Service { /// /// Use this endpoint to allow a new user to register a new account in your /// project. After the user registration completes successfully, you can use - /// the [/account/verfication](/docs/account#createVerification) route to start - /// verifying the user email address. To allow your new user to login to his - /// new account, you need to create a new [account - /// session](/docs/account#createSession). + /// the [/account/verfication](/docs/client/account#createVerification) route + /// to start verifying the user email address. To allow your new user to login + /// to his new account, you need to create a new [account + /// session](/docs/client/account#createSession). /// Future create({@required String email, @required String password, String name = ''}) { final String path = '/account'; @@ -195,7 +195,7 @@ class Account extends Service { /// When the user clicks the confirmation link he is redirected back to your /// app password reset URL with the secret key and email address values /// attached to the URL query string. Use the query string params to submit a - /// request to the [PUT /account/recovery](/docs/account#updateRecovery) + /// request to the [PUT /account/recovery](/docs/client/account#updateRecovery) /// endpoint to complete the process. /// Future createRecovery({@required String email, @required String url}) { @@ -218,7 +218,7 @@ class Account extends Service { /// Use this endpoint to complete the user account password reset. Both the /// **userId** and **secret** arguments will be passed as query parameters to /// the redirect URL you have provided when sending your request to the [POST - /// /account/recovery](/docs/account#createRecovery) endpoint. + /// /account/recovery](/docs/client/account#createRecovery) endpoint. /// /// 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) @@ -305,21 +305,36 @@ class Account extends Service { /// first. Use the success and failure arguments to provide a redirect URL's /// back to your app when login is completed. /// - Future createOAuth2Session({@required String provider, String success = 'https://appwrite.io/auth/oauth2/success', String failure = 'https://appwrite.io/auth/oauth2/failure'}) { + Future createOAuth2Session({@required String provider, String success = 'https://appwrite.io/auth/oauth2/success', String failure = 'https://appwrite.io/auth/oauth2/failure', List scopes = const []}) { final String path = '/account/sessions/oauth2/{provider}'.replaceAll(RegExp('{provider}'), provider); final Map params = { 'success': success, 'failure': failure, + 'scopes': scopes, 'project': client.config['project'], }; + + final List query = []; + + params.forEach((key, value) { + if (value is List) { + for (var item in value) { + query.add(Uri.encodeComponent(key + '[]') + '=' + Uri.encodeComponent(item)); + } + } + else { + query.add(Uri.encodeComponent(key) + '=' + Uri.encodeComponent(value)); + } + }); + Uri endpoint = Uri.parse(client.endPoint); Uri url = new Uri(scheme: endpoint.scheme, host: endpoint.host, port: endpoint.port, path: endpoint.path + path, - queryParameters:params, + query: query.join('&') ); return FlutterWebAuth.authenticate( @@ -361,7 +376,7 @@ class Account extends Service { /// should redirect the user back for 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/account#updateAccountVerification). + /// 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) diff --git a/app/sdks/client-flutter/lib/services/avatars.dart b/app/sdks/client-flutter/lib/services/avatars.dart index fc9c403d90..16c7ce59ff 100644 --- a/app/sdks/client-flutter/lib/services/avatars.dart +++ b/app/sdks/client-flutter/lib/services/avatars.dart @@ -145,6 +145,42 @@ class Avatars extends Service { return location.toString(); } + /// Get User Initials + /// + /// Use this endpoint to show your user initials avatar icon on your website or + /// app. By default, this route will try to print your logged-in user name or + /// email initials. You can also overwrite the user name if you pass the 'name' + /// parameter. If no name is given and no user is logged, an empty avatar will + /// be returned. + /// + /// You can use the color and background params to change the avatar colors. By + /// default, a random theme will be selected. The random theme will persist for + /// the user's initials when reloading the same theme will always return for + /// the same initials. + /// + String getInitials({String name = '', int width = 500, int height = 500, String color = '', String background = ''}) { + final String path = '/avatars/initials'; + + final Map params = { + 'name': name, + 'width': width, + 'height': height, + 'color': color, + 'background': background, + 'project': client.config['project'], + }; + + Uri endpoint = Uri.parse(client.endPoint); + Uri location = new Uri(scheme: endpoint.scheme, + host: endpoint.host, + port: endpoint.port, + path: endpoint.path + path, + queryParameters:params, + ); + + return location.toString(); + } + /// Get QR Code /// /// Converts a given plain text to a QR code image. You can use the query diff --git a/app/sdks/client-flutter/lib/services/database.dart b/app/sdks/client-flutter/lib/services/database.dart index 1163c9dbb5..88082feef7 100644 --- a/app/sdks/client-flutter/lib/services/database.dart +++ b/app/sdks/client-flutter/lib/services/database.dart @@ -41,7 +41,10 @@ class Database extends Service { /// Create Document /// - /// Create a new Document. + /// Create a new Document. Before using this route, you should create a new + /// collection resource using either a [server + /// integration](/docs/server/database?sdk=nodejs#createCollection) API or + /// directly from your database console. /// Future createDocument({@required String collectionId, @required dynamic data, @required List read, @required List write, String parentDocument = '', String parentProperty = '', String parentPropertyType = 'assign'}) { final String path = '/database/collections/{collectionId}/documents'.replaceAll(RegExp('{collectionId}'), collectionId); diff --git a/app/sdks/client-flutter/lib/services/locale.dart b/app/sdks/client-flutter/lib/services/locale.dart index 91ea1567ef..095af4792c 100644 --- a/app/sdks/client-flutter/lib/services/locale.dart +++ b/app/sdks/client-flutter/lib/services/locale.dart @@ -106,9 +106,9 @@ class Locale extends Service { /// List Currencies /// - /// List of all currencies, including currency symol, name, plural, and decimal - /// digits for all major and minor currencies. You can use the locale header to - /// get the data in a supported language. + /// List of all currencies, including currency symbol, name, plural, and + /// decimal digits for all major and minor currencies. You can use the locale + /// header to get the data in a supported language. /// Future getCurrencies() { final String path = '/locale/currencies'; @@ -120,6 +120,24 @@ class Locale extends Service { 'content-type': 'application/json', }; + return client.call(HttpMethod.get, path: path, params: params, headers: headers); + } + + /// List Languages + /// + /// List of all languages classified by ISO 639-1 including 2-letter code, name + /// in English, and name in the respective language. + /// + Future getLanguages() { + final String path = '/locale/languages'; + + final Map params = { + }; + + final Map headers = { + 'content-type': 'application/json', + }; + return client.call(HttpMethod.get, path: path, params: params, headers: headers); } } \ No newline at end of file diff --git a/app/sdks/client-flutter/lib/services/teams.dart b/app/sdks/client-flutter/lib/services/teams.dart index 56e0731687..b9205da455 100644 --- a/app/sdks/client-flutter/lib/services/teams.dart +++ b/app/sdks/client-flutter/lib/services/teams.dart @@ -115,10 +115,14 @@ class Teams extends Service { /// Get team members by the team unique ID. All team members have read access /// for this list of resources. /// - Future getMemberships({@required String teamId}) { + Future getMemberships({@required String teamId, String search = '', int limit = 25, int offset = 0, OrderType orderType = OrderType.asc}) { final String path = '/teams/{teamId}/memberships'.replaceAll(RegExp('{teamId}'), teamId); final Map params = { + 'search': search, + 'limit': limit, + 'offset': offset, + 'orderType': orderType.name(), }; final Map headers = { @@ -136,8 +140,8 @@ class Teams extends Service { /// /// Use the 'URL' parameter to redirect the user from the invitation email back /// to your app. When the user is redirected, use the [Update Team Membership - /// Status](/docs/teams#updateMembershipStatus) endpoint to allow the user to - /// accept the invitation to the team. + /// Status](/docs/client/teams#updateMembershipStatus) endpoint to allow the + /// user to accept the invitation to the team. /// /// Please note that in order to avoid a [Redirect /// Attacks](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) diff --git a/app/sdks/client-web/README.md b/app/sdks/client-web/README.md index e59480b4a9..093783ab41 100644 --- a/app/sdks/client-web/README.md +++ b/app/sdks/client-web/README.md @@ -1,7 +1,7 @@ # Appwrite Web SDK ![License](https://img.shields.io/github/license/appwrite/sdk-for-js.svg?v=1) -![Version](https://img.shields.io/badge/api%20version-0.6.1-blue.svg?v=1) +![Version](https://img.shields.io/badge/api%20version-0.6.2-blue.svg?v=1) Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Web SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. diff --git a/app/sdks/client-web/docs/examples/account/create-o-auth2session.md b/app/sdks/client-web/docs/examples/account/create-o-auth2session.md index 5cc825a273..25b02e632e 100644 --- a/app/sdks/client-web/docs/examples/account/create-o-auth2session.md +++ b/app/sdks/client-web/docs/examples/account/create-o-auth2session.md @@ -5,10 +5,6 @@ sdk .setProject('5df5acd0d48c2') // Your project ID ; -let promise = sdk.account.createOAuth2Session('bitbucket'); +// Go to OAuth provider login page +sdk.account.createOAuth2Session('amazon'); -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/client-web/docs/examples/avatars/get-browser.md b/app/sdks/client-web/docs/examples/avatars/get-browser.md index 86d0df4a23..0855209f58 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-browser.md +++ b/app/sdks/client-web/docs/examples/avatars/get-browser.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getBrowser('aa'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-credit-card.md b/app/sdks/client-web/docs/examples/avatars/get-credit-card.md index 83a1381adf..1aa595bd65 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-credit-card.md +++ b/app/sdks/client-web/docs/examples/avatars/get-credit-card.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getCreditCard('amex'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-favicon.md b/app/sdks/client-web/docs/examples/avatars/get-favicon.md index c0ff93307a..a945e50bb2 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-favicon.md +++ b/app/sdks/client-web/docs/examples/avatars/get-favicon.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getFavicon('https://example.com'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-flag.md b/app/sdks/client-web/docs/examples/avatars/get-flag.md index 762aeb348e..222b388afc 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-flag.md +++ b/app/sdks/client-web/docs/examples/avatars/get-flag.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getFlag('af'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-image.md b/app/sdks/client-web/docs/examples/avatars/get-image.md index 58f0eb8077..3f15981c71 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-image.md +++ b/app/sdks/client-web/docs/examples/avatars/get-image.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getImage('https://example.com'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-initials.md b/app/sdks/client-web/docs/examples/avatars/get-initials.md new file mode 100644 index 0000000000..ef04988e2d --- /dev/null +++ b/app/sdks/client-web/docs/examples/avatars/get-initials.md @@ -0,0 +1,10 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID +; + +let result = sdk.avatars.getInitials(); + +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-q-r.md b/app/sdks/client-web/docs/examples/avatars/get-q-r.md index fb17032949..9d9ee3fea2 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-q-r.md +++ b/app/sdks/client-web/docs/examples/avatars/get-q-r.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getQR('[TEXT]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/locale/get-languages.md b/app/sdks/client-web/docs/examples/locale/get-languages.md new file mode 100644 index 0000000000..04e3063ee7 --- /dev/null +++ b/app/sdks/client-web/docs/examples/locale/get-languages.md @@ -0,0 +1,14 @@ +let sdk = new Appwrite(); + +sdk + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID +; + +let promise = sdk.locale.getLanguages(); + +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/client-web/docs/examples/storage/get-file-download.md b/app/sdks/client-web/docs/examples/storage/get-file-download.md index ae3a643529..1323b8b8df 100644 --- a/app/sdks/client-web/docs/examples/storage/get-file-download.md +++ b/app/sdks/client-web/docs/examples/storage/get-file-download.md @@ -7,4 +7,4 @@ sdk let result = sdk.storage.getFileDownload('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/storage/get-file-preview.md b/app/sdks/client-web/docs/examples/storage/get-file-preview.md index 30461e4d9a..5ede9fdb93 100644 --- a/app/sdks/client-web/docs/examples/storage/get-file-preview.md +++ b/app/sdks/client-web/docs/examples/storage/get-file-preview.md @@ -7,4 +7,4 @@ sdk let result = sdk.storage.getFilePreview('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/storage/get-file-view.md b/app/sdks/client-web/docs/examples/storage/get-file-view.md index 341dd502de..53877ae352 100644 --- a/app/sdks/client-web/docs/examples/storage/get-file-view.md +++ b/app/sdks/client-web/docs/examples/storage/get-file-view.md @@ -7,4 +7,4 @@ sdk let result = sdk.storage.getFileView('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ 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 0d7a7deaab..5c98ea89b3 100644 --- a/app/sdks/client-web/src/sdk.js +++ b/app/sdks/client-web/src/sdk.js @@ -190,28 +190,27 @@ } request.onload = function () { + let data = request.response; + let contentType = this.getResponseHeader('content-type') || ''; + contentType = contentType.substring(0, contentType.indexOf(';')); + + switch (contentType) { + case 'application/json': + data = JSON.parse(data); + break; + } + + let cookieFallback = this.getResponseHeader('X-Fallback-Cookies') || ''; + + if(window.localStorage && cookieFallback) { + window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); + window.localStorage.setItem('cookieFallback', cookieFallback); + } + if (4 === request.readyState && 399 >= request.status) { - let data = request.response; - let contentType = this.getResponseHeader('content-type') || ''; - contentType = contentType.substring(0, contentType.indexOf(';')); - - switch (contentType) { - case 'application/json': - data = JSON.parse(data); - break; - } - - let cookieFallback = this.getResponseHeader('X-Fallback-Cookies') || ''; - - if(window.localStorage && cookieFallback) { - window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); - window.localStorage.setItem('cookieFallback', cookieFallback); - } - resolve(data); - } else { - reject(new Error(request.statusText)); + reject(data); } }; @@ -276,10 +275,10 @@ * * Use this endpoint to allow a new user to register a new account in your * project. After the user registration completes successfully, you can use - * the [/account/verfication](/docs/account#createVerification) route to start - * verifying the user email address. To allow your new user to login to his - * new account, you need to create a new [account - * session](/docs/account#createSession). + * the [/account/verfication](/docs/client/account#createVerification) route + * to start verifying the user email address. To allow your new user to login + * to his new account, you need to create a new [account + * session](/docs/client/account#createSession). * * @param {string} email * @param {string} password @@ -522,7 +521,7 @@ * When the user clicks the confirmation link he is redirected back to your * app password reset URL with the secret key and email address values * attached to the URL query string. Use the query string params to submit a - * request to the [PUT /account/recovery](/docs/account#updateRecovery) + * request to the [PUT /account/recovery](/docs/client/account#updateRecovery) * endpoint to complete the process. * * @param {string} email @@ -563,7 +562,7 @@ * Use this endpoint to complete the user account password reset. Both the * **userId** and **secret** arguments will be passed as query parameters to * the redirect URL you have provided when sending your request to the [POST - * /account/recovery](/docs/account#createRecovery) endpoint. + * /account/recovery](/docs/client/account#createRecovery) endpoint. * * 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) @@ -709,10 +708,11 @@ * @param {string} provider * @param {string} success * @param {string} failure + * @param {string[]} scopes * @throws {Error} * @return {Promise} */ - createOAuth2Session: function(provider, success = 'https://appwrite.io/auth/oauth2/success', failure = 'https://appwrite.io/auth/oauth2/failure') { + createOAuth2Session: function(provider, success = 'https://appwrite.io/auth/oauth2/success', failure = 'https://appwrite.io/auth/oauth2/failure', scopes = []) { if(provider === undefined) { throw new Error('Missing required parameter: "provider"'); } @@ -729,9 +729,28 @@ payload['failure'] = failure; } + if(scopes) { + payload['scopes'] = scopes; + } + payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); window.location = config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -772,7 +791,7 @@ * should redirect the user back for 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/account#updateAccountVerification). + * 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) @@ -883,7 +902,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -926,7 +960,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -956,7 +1005,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -998,7 +1062,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1040,7 +1119,91 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); + + return config.endpoint + path + ((query) ? '?' + query : ''); + }, + + /** + * Get User Initials + * + * Use this endpoint to show your user initials avatar icon on your website or + * app. By default, this route will try to print your logged-in user name or + * email initials. You can also overwrite the user name if you pass the 'name' + * parameter. If no name is given and no user is logged, an empty avatar will + * be returned. + * + * You can use the color and background params to change the avatar colors. By + * default, a random theme will be selected. The random theme will persist for + * the user's initials when reloading the same theme will always return for + * the same initials. + * + * @param {string} name + * @param {number} width + * @param {number} height + * @param {string} color + * @param {string} background + * @throws {Error} + * @return {string} + */ + getInitials: function(name = '', width = 500, height = 500, color = '', background = '') { + let path = '/avatars/initials'; + + let payload = {}; + + if(name) { + payload['name'] = name; + } + + if(width) { + payload['width'] = width; + } + + 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 < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1085,7 +1248,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); } @@ -1168,7 +1346,10 @@ /** * Create Document * - * Create a new Document. + * Create a new Document. Before using this route, you should create a new + * collection resource using either a [server + * integration](/docs/server/database?sdk=nodejs#createCollection) API or + * directly from your database console. * * @param {string} collectionId * @param {object} data @@ -1457,9 +1638,9 @@ /** * List Currencies * - * List of all currencies, including currency symol, name, plural, and decimal - * digits for all major and minor currencies. You can use the locale header to - * get the data in a supported language. + * List of all currencies, including currency symbol, name, plural, and + * decimal digits for all major and minor currencies. You can use the locale + * header to get the data in a supported language. * * @throws {Error} * @return {Promise} @@ -1469,6 +1650,26 @@ let payload = {}; + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * List Languages + * + * List of all languages classified by ISO 639-1 including 2-letter code, name + * in English, and name in the respective language. + * + * @throws {Error} + * @return {Promise} + */ + getLanguages: function() { + let path = '/locale/languages'; + + let payload = {}; + return http .get(path, { 'content-type': 'application/json', @@ -1682,7 +1883,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1735,7 +1951,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1766,7 +1997,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); } @@ -1942,10 +2188,14 @@ * for this list of resources. * * @param {string} teamId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType * @throws {Error} * @return {Promise} */ - getMemberships: function(teamId) { + getMemberships: function(teamId, search = '', limit = 25, offset = 0, orderType = 'ASC') { if(teamId === undefined) { throw new Error('Missing required parameter: "teamId"'); } @@ -1954,6 +2204,22 @@ 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', @@ -1969,8 +2235,8 @@ * * Use the 'URL' parameter to redirect the user from the invitation email back * to your app. When the user is redirected, use the [Update Team Membership - * Status](/docs/teams#updateMembershipStatus) endpoint to allow the user to - * accept the invitation to the team. + * Status](/docs/client/teams#updateMembershipStatus) endpoint to allow the + * user to accept the invitation to the team. * * Please note that in order to avoid a [Redirect * Attacks](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) diff --git a/app/sdks/client-web/src/sdk.min.js b/app/sdks/client-web/src/sdk.min.js index 113a3402b8..7b470dc380 100644 --- a/app/sdks/client-web/src/sdk.min.js +++ b/app/sdks/client-web/src/sdk.min.js @@ -9,9 +9,9 @@ if(method==='GET'){for(let param in params){if(param.hasOwnProperty(key)){path=a switch(headers['content-type']){case 'application/json':params=JSON.stringify(params);break;case 'multipart/form-data':let formData=new FormData();Object.keys(params).forEach(function(key){let param=params[key];formData.append(key+(Array.isArray(param)?'[]':''),param)});params=formData;break} return new Promise(function(resolve,reject){let request=new XMLHttpRequest(),key;request.withCredentials=!0;request.open(method,path,!0);for(key in headers){if(headers.hasOwnProperty(key)){if(key==='content-type'&&headers[key]==='multipart/form-data'){continue} request.setRequestHeader(key,headers[key])}} -request.onload=function(){if(4===request.readyState&&399>=request.status){let data=request.response;let contentType=this.getResponseHeader('content-type')||'';contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case 'application/json':data=JSON.parse(data);break} +request.onload=function(){let data=request.response;let contentType=this.getResponseHeader('content-type')||'';contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case 'application/json':data=JSON.parse(data);break} let cookieFallback=this.getResponseHeader('X-Fallback-Cookies')||'';if(window.localStorage&&cookieFallback){window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.');window.localStorage.setItem('cookieFallback',cookieFallback)} -resolve(data)}else{reject(new Error(request.statusText))}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,!1)} +if(4===request.readyState&&399>=request.status){resolve(data)}else{reject(data)}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,!1)} request.onerror=function(){reject(new Error("Network Error"))};request.send(params)})};return{'get':function(path,headers={},params={}){return call('GET',path+((Object.keys(params).length>0)?'?'+buildQuery(params):''),headers,{})},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress)},'put':function(path,headers={},params={},progress=null){return call('PUT',path,headers,params,progress)},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress)},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress)},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let account={get:function(){let path='/account';let payload={};return http.get(path,{'content-type':'application/json',},payload)},create:function(email,password,name=''){if(email===undefined){throw new Error('Missing required parameter: "email"')} if(password===undefined){throw new Error('Missing required parameter: "password"')} let path='/account';let payload={};if(email){payload.email=email} @@ -45,10 +45,12 @@ return http.put(path,{'content-type':'application/json',},payload)},getSessions: if(password===undefined){throw new Error('Missing required parameter: "password"')} let path='/account/sessions';let payload={};if(email){payload.email=email} if(password){payload.password=password} -return http.post(path,{'content-type':'application/json',},payload)},deleteSessions:function(){let path='/account/sessions';let payload={};return http.delete(path,{'content-type':'application/json',},payload)},createOAuth2Session:function(provider,success='https://appwrite.io/auth/oauth2/success',failure='https://appwrite.io/auth/oauth2/failure'){if(provider===undefined){throw new Error('Missing required parameter: "provider"')} +return http.post(path,{'content-type':'application/json',},payload)},deleteSessions:function(){let path='/account/sessions';let payload={};return http.delete(path,{'content-type':'application/json',},payload)},createOAuth2Session:function(provider,success='https://appwrite.io/auth/oauth2/success',failure='https://appwrite.io/auth/oauth2/failure',scopes=[]){if(provider===undefined){throw new Error('Missing required parameter: "provider"')} let path='/account/sessions/oauth2/{provider}'.replace(new RegExp('{provider}','g'),provider);let payload={};if(success){payload.success=success} if(failure){payload.failure=failure} -payload.project=config.project;let query=Object.keys(payload).map(key=>key+'='+encodeURIComponent(payload[key])).join('&');window.location=config.endpoint+path+((query)?'?'+query:'')},deleteSession:function(sessionId){if(sessionId===undefined){throw new Error('Missing required parameter: "sessionId"')} +if(scopes){payload.scopes=scopes} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getCreditCard:function(code,width=100,height=100,quality=100){if(code===undefined){throw new Error('Missing required parameter: "code"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFavicon:function(url){if(url===undefined){throw new Error('Missing required parameter: "url"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFlag:function(code,width=100,height=100,quality=100){if(code===undefined){throw new Error('Missing required parameter: "code"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getImage:function(url,width=400,height=400){if(url===undefined){throw new Error('Missing required parameter: "url"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getQR:function(text,size=400,margin=1,download=0){if(text===undefined){throw new Error('Missing required parameter: "text"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')}};let database={listDocuments:function(collectionId,filters=[],offset=0,limit=50,orderField='$id',orderType='ASC',orderCast='string',search='',first=0,last=0){if(collectionId===undefined){throw new Error('Missing required parameter: "collectionId"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFilePreview:function(fileId,width=0,height=0,quality=100,background='',output=''){if(fileId===undefined){throw new Error('Missing required parameter: "fileId"')} +let path='/storage/files/{fileId}/download'.replace(new RegExp('{fileId}','g'),fileId);let payload={};payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFileView:function(fileId,as=''){if(fileId===undefined){throw new Error('Missing required parameter: "fileId"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')}};let teams={list:function(search='',limit=25,offset=0,orderType='ASC'){let path='/teams';let payload={};if(search){payload.search=search} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;index; + createOAuth2Session(provider: string, success: string, failure: string, scopes: string[]): Promise; /** * Delete Account Session @@ -276,7 +277,7 @@ declare namespace Appwrite { * should redirect the user back for 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/account#updateAccountVerification). + * 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) @@ -386,6 +387,30 @@ declare namespace Appwrite { */ getImage(url: string, width: number, height: number): string; + /** + * Get User Initials + * + * Use this endpoint to show your user initials avatar icon on your website or + * app. By default, this route will try to print your logged-in user name or + * email initials. You can also overwrite the user name if you pass the 'name' + * parameter. If no name is given and no user is logged, an empty avatar will + * be returned. + * + * You can use the color and background params to change the avatar colors. By + * default, a random theme will be selected. The random theme will persist for + * the user's initials when reloading the same theme will always return for + * the same initials. + * + * @param {string} name + * @param {number} width + * @param {number} height + * @param {string} color + * @param {string} background + * @throws {Error} + * @return {string} + */ + getInitials(name: string, width: number, height: number, color: string, background: string): string; + /** * Get QR Code * @@ -431,7 +456,10 @@ declare namespace Appwrite { /** * Create Document * - * Create a new Document. + * Create a new Document. Before using this route, you should create a new + * collection resource using either a [server + * integration](/docs/server/database?sdk=nodejs#createCollection) API or + * directly from your database console. * * @param {string} collectionId * @param {object} data @@ -552,15 +580,26 @@ declare namespace Appwrite { /** * List Currencies * - * List of all currencies, including currency symol, name, plural, and decimal - * digits for all major and minor currencies. You can use the locale header to - * get the data in a supported language. + * List of all currencies, including currency symbol, name, plural, and + * decimal digits for all major and minor currencies. You can use the locale + * header to get the data in a supported language. * * @throws {Error} * @return {Promise} */ getCurrencies(): Promise; + /** + * List Languages + * + * List of all languages classified by ISO 639-1 including 2-letter code, name + * in English, and name in the respective language. + * + * @throws {Error} + * @return {Promise} + */ + getLanguages(): Promise; + } export interface Storage { @@ -758,10 +797,14 @@ declare namespace Appwrite { * for this list of resources. * * @param {string} teamId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType * @throws {Error} * @return {Promise} */ - getMemberships(teamId: string): Promise; + getMemberships(teamId: string, search: string, limit: number, offset: number, orderType: string): Promise; /** * Create Team Membership @@ -772,8 +815,8 @@ declare namespace Appwrite { * * Use the 'URL' parameter to redirect the user from the invitation email back * to your app. When the user is redirected, use the [Update Team Membership - * Status](/docs/teams#updateMembershipStatus) endpoint to allow the user to - * accept the invitation to the team. + * Status](/docs/client/teams#updateMembershipStatus) endpoint to allow the + * user to accept the invitation to the team. * * Please note that in order to avoid a [Redirect * Attacks](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) diff --git a/app/sdks/console-web/README.md b/app/sdks/console-web/README.md index 8d44e2769b..68b7e8ff9c 100644 --- a/app/sdks/console-web/README.md +++ b/app/sdks/console-web/README.md @@ -1,7 +1,7 @@ # Appwrite Console SDK ![License](https://img.shields.io/github/license/appwrite/sdk-for-console.svg?v=1) -![Version](https://img.shields.io/badge/api%20version-0.6.1-blue.svg?v=1) +![Version](https://img.shields.io/badge/api%20version-0.6.2-blue.svg?v=1) Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Console SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. diff --git a/app/sdks/console-web/docs/examples/account/create-o-auth2session.md b/app/sdks/console-web/docs/examples/account/create-o-auth2session.md index bea25a368f..3a2dcba651 100644 --- a/app/sdks/console-web/docs/examples/account/create-o-auth2session.md +++ b/app/sdks/console-web/docs/examples/account/create-o-auth2session.md @@ -6,10 +6,6 @@ sdk .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key ; -let promise = sdk.account.createOAuth2Session('bitbucket'); +// Go to OAuth provider login page +sdk.account.createOAuth2Session('amazon'); -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/avatars/get-browser.md b/app/sdks/console-web/docs/examples/avatars/get-browser.md index cd89107b91..bbbdb9738f 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-browser.md +++ b/app/sdks/console-web/docs/examples/avatars/get-browser.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getBrowser('aa'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-credit-card.md b/app/sdks/console-web/docs/examples/avatars/get-credit-card.md index 4906a4127c..e1fab01cf2 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-credit-card.md +++ b/app/sdks/console-web/docs/examples/avatars/get-credit-card.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getCreditCard('amex'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-favicon.md b/app/sdks/console-web/docs/examples/avatars/get-favicon.md index ec53212364..397986b11a 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-favicon.md +++ b/app/sdks/console-web/docs/examples/avatars/get-favicon.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getFavicon('https://example.com'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-flag.md b/app/sdks/console-web/docs/examples/avatars/get-flag.md index 87122bddb3..6e90b72feb 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-flag.md +++ b/app/sdks/console-web/docs/examples/avatars/get-flag.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getFlag('af'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-image.md b/app/sdks/console-web/docs/examples/avatars/get-image.md index d6789db732..afd1a17cb5 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-image.md +++ b/app/sdks/console-web/docs/examples/avatars/get-image.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getImage('https://example.com'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-initials.md b/app/sdks/console-web/docs/examples/avatars/get-initials.md new file mode 100644 index 0000000000..dab6417ca5 --- /dev/null +++ b/app/sdks/console-web/docs/examples/avatars/get-initials.md @@ -0,0 +1,11 @@ +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 result = sdk.avatars.getInitials(); + +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-q-r.md b/app/sdks/console-web/docs/examples/avatars/get-q-r.md index 93d3438746..31f490cedd 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-q-r.md +++ b/app/sdks/console-web/docs/examples/avatars/get-q-r.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getQR('[TEXT]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/locale/get-languages.md b/app/sdks/console-web/docs/examples/locale/get-languages.md new file mode 100644 index 0000000000..2d1f713eeb --- /dev/null +++ b/app/sdks/console-web/docs/examples/locale/get-languages.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.locale.getLanguages(); + +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/update-o-auth2.md b/app/sdks/console-web/docs/examples/projects/update-o-auth2.md index 4a82548a6a..479c5e9d30 100644 --- a/app/sdks/console-web/docs/examples/projects/update-o-auth2.md +++ b/app/sdks/console-web/docs/examples/projects/update-o-auth2.md @@ -6,7 +6,7 @@ sdk .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key ; -let promise = sdk.projects.updateOAuth2('[PROJECT_ID]', 'bitbucket'); +let promise = sdk.projects.updateOAuth2('[PROJECT_ID]', 'amazon'); promise.then(function (response) { console.log(response); // Success diff --git a/app/sdks/console-web/docs/examples/storage/get-file-download.md b/app/sdks/console-web/docs/examples/storage/get-file-download.md index 5be3f81694..0a4677fe80 100644 --- a/app/sdks/console-web/docs/examples/storage/get-file-download.md +++ b/app/sdks/console-web/docs/examples/storage/get-file-download.md @@ -8,4 +8,4 @@ sdk let result = sdk.storage.getFileDownload('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/storage/get-file-preview.md b/app/sdks/console-web/docs/examples/storage/get-file-preview.md index 737ead95c3..a44cb6479c 100644 --- a/app/sdks/console-web/docs/examples/storage/get-file-preview.md +++ b/app/sdks/console-web/docs/examples/storage/get-file-preview.md @@ -8,4 +8,4 @@ sdk let result = sdk.storage.getFilePreview('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/storage/get-file-view.md b/app/sdks/console-web/docs/examples/storage/get-file-view.md index c9f73b7d28..adfb6320c3 100644 --- a/app/sdks/console-web/docs/examples/storage/get-file-view.md +++ b/app/sdks/console-web/docs/examples/storage/get-file-view.md @@ -8,4 +8,4 @@ sdk let result = sdk.storage.getFileView('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/src/sdk.js b/app/sdks/console-web/src/sdk.js index 86e219cde5..27eb23b742 100644 --- a/app/sdks/console-web/src/sdk.js +++ b/app/sdks/console-web/src/sdk.js @@ -226,28 +226,27 @@ } request.onload = function () { + let data = request.response; + let contentType = this.getResponseHeader('content-type') || ''; + contentType = contentType.substring(0, contentType.indexOf(';')); + + switch (contentType) { + case 'application/json': + data = JSON.parse(data); + break; + } + + let cookieFallback = this.getResponseHeader('X-Fallback-Cookies') || ''; + + if(window.localStorage && cookieFallback) { + window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); + window.localStorage.setItem('cookieFallback', cookieFallback); + } + if (4 === request.readyState && 399 >= request.status) { - let data = request.response; - let contentType = this.getResponseHeader('content-type') || ''; - contentType = contentType.substring(0, contentType.indexOf(';')); - - switch (contentType) { - case 'application/json': - data = JSON.parse(data); - break; - } - - let cookieFallback = this.getResponseHeader('X-Fallback-Cookies') || ''; - - if(window.localStorage && cookieFallback) { - window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); - window.localStorage.setItem('cookieFallback', cookieFallback); - } - resolve(data); - } else { - reject(new Error(request.statusText)); + reject(data); } }; @@ -312,10 +311,10 @@ * * Use this endpoint to allow a new user to register a new account in your * project. After the user registration completes successfully, you can use - * the [/account/verfication](/docs/account#createVerification) route to start - * verifying the user email address. To allow your new user to login to his - * new account, you need to create a new [account - * session](/docs/account#createSession). + * the [/account/verfication](/docs/client/account#createVerification) route + * to start verifying the user email address. To allow your new user to login + * to his new account, you need to create a new [account + * session](/docs/client/account#createSession). * * @param {string} email * @param {string} password @@ -558,7 +557,7 @@ * When the user clicks the confirmation link he is redirected back to your * app password reset URL with the secret key and email address values * attached to the URL query string. Use the query string params to submit a - * request to the [PUT /account/recovery](/docs/account#updateRecovery) + * request to the [PUT /account/recovery](/docs/client/account#updateRecovery) * endpoint to complete the process. * * @param {string} email @@ -599,7 +598,7 @@ * Use this endpoint to complete the user account password reset. Both the * **userId** and **secret** arguments will be passed as query parameters to * the redirect URL you have provided when sending your request to the [POST - * /account/recovery](/docs/account#createRecovery) endpoint. + * /account/recovery](/docs/client/account#createRecovery) endpoint. * * 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) @@ -745,10 +744,11 @@ * @param {string} provider * @param {string} success * @param {string} failure + * @param {string[]} scopes * @throws {Error} * @return {Promise} */ - createOAuth2Session: function(provider, success = 'https://appwrite.io/auth/oauth2/success', failure = 'https://appwrite.io/auth/oauth2/failure') { + createOAuth2Session: function(provider, success = 'https://appwrite.io/auth/oauth2/success', failure = 'https://appwrite.io/auth/oauth2/failure', scopes = []) { if(provider === undefined) { throw new Error('Missing required parameter: "provider"'); } @@ -765,11 +765,30 @@ payload['failure'] = failure; } + if(scopes) { + payload['scopes'] = scopes; + } + payload['project'] = config.project; payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); window.location = config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -810,7 +829,7 @@ * should redirect the user back for 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/account#updateAccountVerification). + * 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) @@ -923,7 +942,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -968,7 +1002,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1000,7 +1049,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1044,7 +1108,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1088,7 +1167,93 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); + + return config.endpoint + path + ((query) ? '?' + query : ''); + }, + + /** + * Get User Initials + * + * Use this endpoint to show your user initials avatar icon on your website or + * app. By default, this route will try to print your logged-in user name or + * email initials. You can also overwrite the user name if you pass the 'name' + * parameter. If no name is given and no user is logged, an empty avatar will + * be returned. + * + * You can use the color and background params to change the avatar colors. By + * default, a random theme will be selected. The random theme will persist for + * the user's initials when reloading the same theme will always return for + * the same initials. + * + * @param {string} name + * @param {number} width + * @param {number} height + * @param {string} color + * @param {string} background + * @throws {Error} + * @return {string} + */ + getInitials: function(name = '', width = 500, height = 500, color = '', background = '') { + let path = '/avatars/initials'; + + let payload = {}; + + if(name) { + payload['name'] = name; + } + + if(width) { + payload['width'] = width; + } + + 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 < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1135,7 +1300,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); } @@ -1421,7 +1601,10 @@ /** * Create Document * - * Create a new Document. + * Create a new Document. Before using this route, you should create a new + * collection resource using either a [server + * integration](/docs/server/database?sdk=nodejs#createCollection) API or + * directly from your database console. * * @param {string} collectionId * @param {object} data @@ -1976,9 +2159,9 @@ /** * List Currencies * - * List of all currencies, including currency symol, name, plural, and decimal - * digits for all major and minor currencies. You can use the locale header to - * get the data in a supported language. + * List of all currencies, including currency symbol, name, plural, and + * decimal digits for all major and minor currencies. You can use the locale + * header to get the data in a supported language. * * @throws {Error} * @return {Promise} @@ -1988,6 +2171,26 @@ let payload = {}; + return http + .get(path, { + 'content-type': 'application/json', + }, payload); + }, + + /** + * List Languages + * + * List of all languages classified by ISO 639-1 including 2-letter code, name + * in English, and name in the respective language. + * + * @throws {Error} + * @return {Promise} + */ + getLanguages: function() { + let path = '/locale/languages'; + + let payload = {}; + return http .get(path, { 'content-type': 'application/json', @@ -3489,7 +3692,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -3544,7 +3762,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -3577,7 +3810,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); } @@ -3753,10 +4001,14 @@ * for this list of resources. * * @param {string} teamId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType * @throws {Error} * @return {Promise} */ - getMemberships: function(teamId) { + getMemberships: function(teamId, search = '', limit = 25, offset = 0, orderType = 'ASC') { if(teamId === undefined) { throw new Error('Missing required parameter: "teamId"'); } @@ -3765,6 +4017,22 @@ 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', @@ -3780,8 +4048,8 @@ * * Use the 'URL' parameter to redirect the user from the invitation email back * to your app. When the user is redirected, use the [Update Team Membership - * Status](/docs/teams#updateMembershipStatus) endpoint to allow the user to - * accept the invitation to the team. + * Status](/docs/client/teams#updateMembershipStatus) endpoint to allow the + * user to accept the invitation to the team. * * Please note that in order to avoid a [Redirect * Attacks](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) diff --git a/app/sdks/console-web/src/sdk.min.js b/app/sdks/console-web/src/sdk.min.js index b43f612ff4..f6a2781f21 100644 --- a/app/sdks/console-web/src/sdk.min.js +++ b/app/sdks/console-web/src/sdk.min.js @@ -9,9 +9,9 @@ if(method==='GET'){for(let param in params){if(param.hasOwnProperty(key)){path=a switch(headers['content-type']){case 'application/json':params=JSON.stringify(params);break;case 'multipart/form-data':let formData=new FormData();Object.keys(params).forEach(function(key){let param=params[key];formData.append(key+(Array.isArray(param)?'[]':''),param)});params=formData;break} return new Promise(function(resolve,reject){let request=new XMLHttpRequest(),key;request.withCredentials=!0;request.open(method,path,!0);for(key in headers){if(headers.hasOwnProperty(key)){if(key==='content-type'&&headers[key]==='multipart/form-data'){continue} request.setRequestHeader(key,headers[key])}} -request.onload=function(){if(4===request.readyState&&399>=request.status){let data=request.response;let contentType=this.getResponseHeader('content-type')||'';contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case 'application/json':data=JSON.parse(data);break} +request.onload=function(){let data=request.response;let contentType=this.getResponseHeader('content-type')||'';contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case 'application/json':data=JSON.parse(data);break} let cookieFallback=this.getResponseHeader('X-Fallback-Cookies')||'';if(window.localStorage&&cookieFallback){window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.');window.localStorage.setItem('cookieFallback',cookieFallback)} -resolve(data)}else{reject(new Error(request.statusText))}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,!1)} +if(4===request.readyState&&399>=request.status){resolve(data)}else{reject(data)}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,!1)} request.onerror=function(){reject(new Error("Network Error"))};request.send(params)})};return{'get':function(path,headers={},params={}){return call('GET',path+((Object.keys(params).length>0)?'?'+buildQuery(params):''),headers,{})},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress)},'put':function(path,headers={},params={},progress=null){return call('PUT',path,headers,params,progress)},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress)},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress)},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let account={get:function(){let path='/account';let payload={};return http.get(path,{'content-type':'application/json',},payload)},create:function(email,password,name=''){if(email===undefined){throw new Error('Missing required parameter: "email"')} if(password===undefined){throw new Error('Missing required parameter: "password"')} let path='/account';let payload={};if(email){payload.email=email} @@ -45,10 +45,12 @@ return http.put(path,{'content-type':'application/json',},payload)},getSessions: if(password===undefined){throw new Error('Missing required parameter: "password"')} let path='/account/sessions';let payload={};if(email){payload.email=email} if(password){payload.password=password} -return http.post(path,{'content-type':'application/json',},payload)},deleteSessions:function(){let path='/account/sessions';let payload={};return http.delete(path,{'content-type':'application/json',},payload)},createOAuth2Session:function(provider,success='https://appwrite.io/auth/oauth2/success',failure='https://appwrite.io/auth/oauth2/failure'){if(provider===undefined){throw new Error('Missing required parameter: "provider"')} +return http.post(path,{'content-type':'application/json',},payload)},deleteSessions:function(){let path='/account/sessions';let payload={};return http.delete(path,{'content-type':'application/json',},payload)},createOAuth2Session:function(provider,success='https://appwrite.io/auth/oauth2/success',failure='https://appwrite.io/auth/oauth2/failure',scopes=[]){if(provider===undefined){throw new Error('Missing required parameter: "provider"')} let path='/account/sessions/oauth2/{provider}'.replace(new RegExp('{provider}','g'),provider);let payload={};if(success){payload.success=success} if(failure){payload.failure=failure} -payload.project=config.project;payload.key=config.key;let query=Object.keys(payload).map(key=>key+'='+encodeURIComponent(payload[key])).join('&');window.location=config.endpoint+path+((query)?'?'+query:'')},deleteSession:function(sessionId){if(sessionId===undefined){throw new Error('Missing required parameter: "sessionId"')} +if(scopes){payload.scopes=scopes} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getCreditCard:function(code,width=100,height=100,quality=100){if(code===undefined){throw new Error('Missing required parameter: "code"')} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFavicon:function(url){if(url===undefined){throw new Error('Missing required parameter: "url"')} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFlag:function(code,width=100,height=100,quality=100){if(code===undefined){throw new Error('Missing required parameter: "code"')} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getImage:function(url,width=400,height=400){if(url===undefined){throw new Error('Missing required parameter: "url"')} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getQR:function(text,size=400,margin=1,download=0){if(text===undefined){throw new Error('Missing required parameter: "text"')} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')}};let database={listCollections:function(search='',limit=25,offset=0,orderType='ASC'){let path='/database/collections';let payload={};if(search){payload.search=search} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFilePreview:function(fileId,width=0,height=0,quality=100,background='',output=''){if(fileId===undefined){throw new Error('Missing required parameter: "fileId"')} +let path='/storage/files/{fileId}/download'.replace(new RegExp('{fileId}','g'),fileId);let payload={};payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFileView:function(fileId,as=''){if(fileId===undefined){throw new Error('Missing required parameter: "fileId"')} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')}};let teams={list:function(search='',limit=25,offset=0,orderType='ASC'){let path='/teams';let payload={};if(search){payload.search=search} +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; + createOAuth2Session(provider: string, success: string, failure: string, scopes: string[]): Promise; /** * Delete Account Session @@ -297,7 +298,7 @@ declare namespace Appwrite { * should redirect the user back for 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/account#updateAccountVerification). + * 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) @@ -407,6 +408,30 @@ declare namespace Appwrite { */ getImage(url: string, width: number, height: number): string; + /** + * Get User Initials + * + * Use this endpoint to show your user initials avatar icon on your website or + * app. By default, this route will try to print your logged-in user name or + * email initials. You can also overwrite the user name if you pass the 'name' + * parameter. If no name is given and no user is logged, an empty avatar will + * be returned. + * + * You can use the color and background params to change the avatar colors. By + * default, a random theme will be selected. The random theme will persist for + * the user's initials when reloading the same theme will always return for + * the same initials. + * + * @param {string} name + * @param {number} width + * @param {number} height + * @param {string} color + * @param {string} background + * @throws {Error} + * @return {string} + */ + getInitials(name: string, width: number, height: number, color: string, background: string): string; + /** * Get QR Code * @@ -522,7 +547,10 @@ declare namespace Appwrite { /** * Create Document * - * Create a new Document. + * Create a new Document. Before using this route, you should create a new + * collection resource using either a [server + * integration](/docs/server/database?sdk=nodejs#createCollection) API or + * directly from your database console. * * @param {string} collectionId * @param {object} data @@ -789,15 +817,26 @@ declare namespace Appwrite { /** * List Currencies * - * List of all currencies, including currency symol, name, plural, and decimal - * digits for all major and minor currencies. You can use the locale header to - * get the data in a supported language. + * List of all currencies, including currency symbol, name, plural, and + * decimal digits for all major and minor currencies. You can use the locale + * header to get the data in a supported language. * * @throws {Error} * @return {Promise} */ getCurrencies(): Promise; + /** + * List Languages + * + * List of all languages classified by ISO 639-1 including 2-letter code, name + * in English, and name in the respective language. + * + * @throws {Error} + * @return {Promise} + */ + getLanguages(): Promise; + } export interface Projects { @@ -1402,10 +1441,14 @@ declare namespace Appwrite { * for this list of resources. * * @param {string} teamId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType * @throws {Error} * @return {Promise} */ - getMemberships(teamId: string): Promise; + getMemberships(teamId: string, search: string, limit: number, offset: number, orderType: string): Promise; /** * Create Team Membership @@ -1416,8 +1459,8 @@ declare namespace Appwrite { * * Use the 'URL' parameter to redirect the user from the invitation email back * to your app. When the user is redirected, use the [Update Team Membership - * Status](/docs/teams#updateMembershipStatus) endpoint to allow the user to - * accept the invitation to the team. + * Status](/docs/client/teams#updateMembershipStatus) endpoint to allow the + * user to accept the invitation to the team. * * Please note that in order to avoid a [Redirect * Attacks](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) diff --git a/app/tasks/init.php b/app/tasks/init.php index 71619381d0..a6e6af101d 100644 --- a/app/tasks/init.php +++ b/app/tasks/init.php @@ -5,8 +5,12 @@ require_once __DIR__.'/../init.php'; global $request; +use Appwrite\ClamAV\Network; +use Appwrite\Storage\Device\Local; +use Appwrite\Storage\Storage; use Utopia\CLI\CLI; use Utopia\CLI\Console; +use Utopia\Domains\Domain; $cli = new CLI(); @@ -18,7 +22,7 @@ $cli Console::log('Issue a TLS certificate for master domain ('.$domain.')'); - ResqueScheduler::enqueueAt(time() + 30, 'v1-certificates', 'CertificatesV1', [ + ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ 'document' => [], 'domain' => $domain, 'validateTarget' => false, @@ -26,4 +30,230 @@ $cli ]); }); +$cli + ->task('doctor') + ->desc('Validate server health') + ->action(function () use ($request, $register) { + Console::log(" __ ____ ____ _ _ ____ __ ____ ____ __ __ + / _\ ( _ \( _ \/ )( \( _ \( )(_ _)( __) ( )/ \ +/ \ ) __/ ) __/\ /\ / ) / )( )( ) _) _ )(( O ) +\_/\_/(__) (__) (_/\_)(__\_)(__) (__) (____)(_)(__)\__/ "); + + Console::log("\n".'👩‍⚕️ Running '.APP_NAME.' Doctor for version '.$request->getServer('_APP_VERSION', 'UNKNOWN').' ...'."\n"); + + Console::log('Checking for production best practices...'); + + $domain = new Domain($request->getServer('_APP_DOMAIN')); + + if(!$domain->isKnown() || $domain->isTest()) { + Console::log('🔴 Hostname has a public suffix'); + } + else { + Console::log('🟢 Hostname has a public suffix'); + } + + $domain = new Domain($request->getServer('_APP_DOMAIN_TARGET')); + + if(!$domain->isKnown() || $domain->isTest()) { + Console::log('🔴 CNAME target has a public suffix'); + } + else { + Console::log('🟢 CNAME target has a public suffix'); + } + + if($request->getServer('_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') { + 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') { + 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); + + if(empty($authWhitelistEmails) + && empty($authWhitelistDomains) + && empty($authWhitelistIPs) + ) { + Console::log('🔴 Console access limits are disabled'); + } + else { + Console::log('🟢 Console access limits are enabled'); + } + + if(empty($request->getServer('_APP_OPTIONS_FORCE_HTTPS', null))) { + Console::log('🔴 HTTP force option is disabled'); + } + else { + Console::log('🟢 HTTP force option is enabled'); + } + + \sleep(0.2); + + try { + Console::log("\n".'Checking connectivity...'); + } catch (\Throwable $th) { + //throw $th; + } + + try { + $register->get('db'); /* @var $db PDO */ + Console::success('Database............connected 👍'); + } catch (\Throwable $th) { + Console::error('Database.........disconnected 👎'); + } + + try { + $register->get('cache'); + Console::success('Queue...............connected 👍'); + } catch (\Throwable $th) { + Console::error('Queue............disconnected 👎'); + } + + try { + $register->get('cache'); + Console::success('Cache...............connected 👍'); + } catch (\Throwable $th) { + Console::error('Cache............disconnected 👎'); + } + + if($request->getServer('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled + try { + $antiVirus = new Network('clamav', 3310); + + if((@$antiVirus->ping())) { + Console::success('AntiVirus...........connected 👍'); + } + else { + Console::error('AntiVirus........disconnected 👎'); + } + } catch (\Throwable $th) { + Console::error('AntiVirus........disconnected 👎'); + } + } + + try { + $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ + + $mail->addAddress('demo@example.com', 'Example.com'); + $mail->Subject = 'Test SMTP Connection'; + $mail->Body = 'Hello World'; + $mail->AltBody = 'Hello World'; + + $mail->send(); + Console::success('SMTP................connected 👍'); + } catch (\Throwable $th) { + Console::error('SMTP.............disconnected 👎'); + } + + $host = $request->getServer('_APP_STATSD_HOST', 'telegraf'); + $port = $request->getServer('_APP_STATSD_PORT', 8125); + + if($fp = @\fsockopen('udp://'.$host, $port, $errCode, $errStr, 2)){ + Console::success('StatsD..............connected 👍'); + \fclose($fp); + } else { + Console::error('StatsD...........disconnected 👎'); + } + + $host = $request->getServer('_APP_INFLUXDB_HOST', ''); + $port = $request->getServer('_APP_INFLUXDB_PORT', ''); + + if($fp = @\fsockopen($host, $port, $errCode, $errStr, 2)){ + Console::success('InfluxDB............connected 👍'); + \fclose($fp); + } else { + Console::error('InfluxDB.........disconnected 👎'); + } + + \sleep(0.2); + + Console::log(''); + Console::log('Checking volumes...'); + + 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_readable($device->getRoot())) { + Console::success('🟢 '.$key.' Volume is readable'); + } + else { + Console::error('🔴 '.$key.' Volume is unreadable'); + } + + if (\is_writable($device->getRoot())) { + Console::success('🟢 '.$key.' Volume is writeable'); + } + else { + Console::error('🔴 '.$key.' Volume is unwriteable'); + } + } + + \sleep(0.2); + + Console::log(''); + Console::log('Checking disk space usage...'); + + foreach ([ + 'Uploads' => APP_STORAGE_UPLOADS, + 'Cache' => APP_STORAGE_CACHE, + 'Config' => APP_STORAGE_CONFIG, + 'Certs' => APP_STORAGE_CERTIFICATES + ] as $key => $volume) { + $device = new Local($volume); + + $percentage = (($device->getPartitionTotalSpace() - $device->getPartitionFreeSpace()) + / $device->getPartitionTotalSpace()) * 100; + + $message = $key.' Volume has '.Storage::human($device->getPartitionFreeSpace()) . ' free space ('.\round($percentage, 2).'% used)'; + + if ($percentage < 80) { + Console::success('🟢 ' . $message); + } + else { + Console::error('🔴 ' . $message); + } + } + + + try { + Console::log(''); + $version = \json_decode(@\file_get_contents($request->getServer('_APP_HOME', 'http://localhost').'/v1/health/version'), true); + + if($version && isset($version['version'])) { + if(\version_compare($version['version'], $request->getServer('_APP_VERSION', 'UNKNOWN')) === 0) { + Console::info('You are running the latest version of '.APP_NAME.'! 🥳'); + } + else { + Console::info('A new version ('.$version['version'].') is available! 🥳'."\n"); + } + } + else { + Console::error('Failed to check for a newer version'."\n"); + } + } catch (\Throwable $th) { + Console::error('Failed to check for a newer version'."\n"); + } + }); + $cli->run(); diff --git a/app/tasks/migrate.php b/app/tasks/migrate.php index e05d1bc0ff..fe833e27f6 100644 --- a/app/tasks/migrate.php +++ b/app/tasks/migrate.php @@ -38,7 +38,7 @@ $callbacks = [ 'orderCast' => 'string', ]); - $sum = count($all); + $sum = \count($all); Console::log('Migrating: '.$offset.' / '.$projectDB->getSum()); @@ -97,17 +97,17 @@ function fixDocument(Document $document) { if($document->getAttribute('$collection') === Database::SYSTEM_COLLECTION_PROJECTS){ foreach($providers as $key => $provider) { - if(!empty($document->getAttribute('usersOauth'.ucfirst($key).'Appid'))) { + if(!empty($document->getAttribute('usersOauth'.\ucfirst($key).'Appid'))) { $document - ->setAttribute('usersOauth2'.ucfirst($key).'Appid', $document->getAttribute('usersOauth'.ucfirst($key).'Appid', '')) - ->removeAttribute('usersOauth'.ucfirst($key).'Appid') + ->setAttribute('usersOauth2'.\ucfirst($key).'Appid', $document->getAttribute('usersOauth'.\ucfirst($key).'Appid', '')) + ->removeAttribute('usersOauth'.\ucfirst($key).'Appid') ; } - if(!empty($document->getAttribute('usersOauth'.ucfirst($key).'Secret'))) { + if(!empty($document->getAttribute('usersOauth'.\ucfirst($key).'Secret'))) { $document - ->setAttribute('usersOauth2'.ucfirst($key).'Secret', $document->getAttribute('usersOauth'.ucfirst($key).'Secret', '')) - ->removeAttribute('usersOauth'.ucfirst($key).'Secret') + ->setAttribute('usersOauth2'.\ucfirst($key).'Secret', $document->getAttribute('usersOauth'.\ucfirst($key).'Secret', '')) + ->removeAttribute('usersOauth'.\ucfirst($key).'Secret') ; } } @@ -115,17 +115,17 @@ function fixDocument(Document $document) { if($document->getAttribute('$collection') === Database::SYSTEM_COLLECTION_USERS) { foreach($providers as $key => $provider) { - if(!empty($document->getAttribute('oauth'.ucfirst($key)))) { + if(!empty($document->getAttribute('oauth'.\ucfirst($key)))) { $document - ->setAttribute('oauth2'.ucfirst($key), $document->getAttribute('oauth'.ucfirst($key), '')) - ->removeAttribute('oauth'.ucfirst($key)) + ->setAttribute('oauth2'.\ucfirst($key), $document->getAttribute('oauth'.\ucfirst($key), '')) + ->removeAttribute('oauth'.\ucfirst($key)) ; } - if(!empty($document->getAttribute('oauth'.ucfirst($key).'AccessToken'))) { + if(!empty($document->getAttribute('oauth'.\ucfirst($key).'AccessToken'))) { $document - ->setAttribute('oauth2'.ucfirst($key).'AccessToken', $document->getAttribute('oauth'.ucfirst($key).'AccessToken', '')) - ->removeAttribute('oauth'.ucfirst($key).'AccessToken') + ->setAttribute('oauth2'.\ucfirst($key).'AccessToken', $document->getAttribute('oauth'.\ucfirst($key).'AccessToken', '')) + ->removeAttribute('oauth'.\ucfirst($key).'AccessToken') ; } } @@ -141,7 +141,7 @@ function fixDocument(Document $document) { if($document->getAttribute('$collection') === Database::SYSTEM_COLLECTION_PLATFORMS) { if($document->getAttribute('url', null) !== null) { $document - ->setAttribute('hostname', parse_url($document->getAttribute('url', $document->getAttribute('hostname', '')), PHP_URL_HOST)) + ->setAttribute('hostname', \parse_url($document->getAttribute('url', $document->getAttribute('hostname', '')), PHP_URL_HOST)) ->removeAttribute('url') ; } @@ -159,7 +159,7 @@ function fixDocument(Document $document) { $attr = fixDocument($attr); } - if(is_array($attr)) { + if(\is_array($attr)) { foreach($attr as &$child) { if($child instanceof Document) { $child = fixDocument($child); @@ -207,7 +207,7 @@ $cli ], ]); - $sum = count($projects); + $sum = \count($projects); $offset = $offset + $limit; Console::log('Fetched '.$sum.' projects...'); diff --git a/app/tasks/sdks.php b/app/tasks/sdks.php index 2843443924..0753c4dfef 100644 --- a/app/tasks/sdks.php +++ b/app/tasks/sdks.php @@ -29,20 +29,20 @@ $cli ->action(function () use ($warning, $version) { function getSSLPage($url) { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $result = curl_exec($ch); - curl_close($ch); + $ch = \curl_init(); + \curl_setopt($ch, CURLOPT_HEADER, false); + \curl_setopt($ch, CURLOPT_URL, $url); + \curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + \curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $result = \curl_exec($ch); + \curl_close($ch); return $result; } $platforms = Config::getParam('platforms'); - $selected = strtolower(Console::confirm('Choose SDK ("*" for all):')); + $selected = \strtolower(Console::confirm('Choose SDK ("*" for all):')); $message = Console::confirm('Please enter your commit message:'); $production = (Console::confirm('Type "Appwrite" to deploy for production') == 'Appwrite'); @@ -63,14 +63,14 @@ $cli $spec = getSSLPage('https://appwrite.io/v1/open-api-2.json?extensions=1&platform='.$language['family']); $spec = getSSLPage('https://localhost/v1/open-api-2.json?extensions=1&platform='.$language['family']); - $result = realpath(__DIR__.'/..').'/sdks/'.$key.'-'.$language['key']; - $target = realpath(__DIR__.'/..').'/sdks/git/'.$language['key'].'/'; - $readme = realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/README.md'); - $readme = ($readme) ? file_get_contents($readme) : ''; - $examples = realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/EXAMPLES.md'); - $examples = ($examples) ? file_get_contents($examples) : ''; - $changelog = realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/CHANGELOG.md'); - $changelog = ($changelog) ? file_get_contents($changelog) : '# Change Log'; + $result = \realpath(__DIR__.'/..').'/sdks/'.$key.'-'.$language['key']; + $target = \realpath(__DIR__.'/..').'/sdks/git/'.$language['key'].'/'; + $readme = \realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/README.md'); + $readme = ($readme) ? \file_get_contents($readme) : ''; + $examples = \realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/EXAMPLES.md'); + $examples = ($examples) ? \file_get_contents($examples) : ''; + $changelog = \realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/CHANGELOG.md'); + $changelog = ($changelog) ? \file_get_contents($changelog) : '# Change Log'; $warning = ($language['beta']) ? '**This SDK is compatible with Appwrite server version ' . $version . '. For older versions, please check previous releases.**' : ''; $license = 'BSD-3-Clause'; $licenseContent = 'Copyright (c) 2019 Appwrite (https://appwrite.io) and individual contributors. @@ -186,7 +186,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $gitUrl = 'git@github.com:aw-tests/'.$language['gitRepoName'].'.git'; } - exec('rm -rf '.$target.' && \ + \exec('rm -rf '.$target.' && \ mkdir -p '.$target.' && \ cd '.$target.' && \ git init && \ @@ -201,7 +201,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); - exec('rm -rf '.$target); + \exec('rm -rf '.$target); Console::success("Remove temp directory '{$target}' for {$language['name']} SDK"); } diff --git a/app/views/console/account/index.phtml b/app/views/console/account/index.phtml index 37abdb5ddc..da40715f19 100644 --- a/app/views/console/account/index.phtml +++ b/app/views/console/account/index.phtml @@ -1,6 +1,6 @@

- Home + Home
Your Account @@ -22,15 +22,7 @@
- User Avatar - -
- - Upload - -
- - (via gravatar.com ) + User Avatar
@@ -57,7 +49,7 @@
- +
@@ -85,7 +77,7 @@

- diff --git a/app/views/console/database/collection.phtml b/app/views/console/database/collection.phtml index f7e561d2bf..cc90964848 100644 --- a/app/views/console/database/collection.phtml +++ b/app/views/console/database/collection.phtml @@ -1,6 +1,7 @@ getParam('collection', []); $rules = $collection->getAttribute('rules', []); +$maxCells = 10; ?>
getAttribute('rules', []);

- Database + Database
@@ -87,7 +88,10 @@ $rules = $collection->getAttribute('rules', []); - $rule): + if($i > $maxCells) { + break; + } $label = (isset($rule['label'])) ? $rule['label'] : ''; ?> @@ -96,7 +100,10 @@ $rules = $collection->getAttribute('rules', []); - $rule): + if($i > $maxCells) { + break; + } $label = (isset($rule['label'])) ? $rule['label'] : ''; $key = (isset($rule['key'])) ? $rule['key'] : ''; $type = (isset($rule['type'])) ? $rule['type'] : ''; @@ -251,7 +258,7 @@ $rules = $collection->getAttribute('rules', []);
- +
@@ -301,17 +308,17 @@ $rules = $collection->getAttribute('rules', []); -
+

Permissions

- +
Add * for wildcard access
- +
Add * for wildcard access
@@ -404,7 +411,7 @@ $rules = $collection->getAttribute('rules', []);
- +
@@ -457,7 +464,7 @@ $rules = $collection->getAttribute('rules', []); @@ -516,4 +523,4 @@ $rules = $collection->getAttribute('rules', []); data-event="load,database.createCollection,database.updateCollection,database.deleteCollection" data-scope="sdk" data-name="project-collections"> - \ No newline at end of file + diff --git a/app/views/console/database/document.phtml b/app/views/console/database/document.phtml index ed12ff38be..e8f760f686 100644 --- a/app/views/console/database/document.phtml +++ b/app/views/console/database/document.phtml @@ -20,7 +20,7 @@ $collections = []; $key = (isset($rule['key'])) ? $rule['key'] : ''; $label = (isset($rule['label'])) ? $rule['label'] : ''; $type = (isset($rule['type'])) ? $rule['type'] : ''; - $list = (isset($rule['list'])) ? $rule['list'] : []; + $list = (isset($rule['list']) && !empty($list)) ? $rule['list'] : []; ?> getId()) { @@ -51,7 +51,7 @@ $collections = []; $key = (isset($rule['key'])) ? $rule['key'] : ''; $label = (isset($rule['label'])) ? $rule['label'] : ''; $type = (isset($rule['type'])) ? $rule['type'] : ''; - $list = (isset($rule['list'])) ? $rule['list'] : false; + $list = (isset($rule['list'])) ? $rule['list'] : []; $array = (isset($rule['array'])) ? $rule['array'] : false; ?> @@ -163,7 +163,7 @@ $collections = [];

- escape($name); ?> + escape($name); ?>
@@ -246,17 +246,17 @@ $collections = []; echo $comp->render(); ?> -
+

Permissions

- +
Add * for wildcard access
- +
Add * for wildcard access
@@ -315,4 +315,4 @@ $collections = []; -->
-

\ No newline at end of file + diff --git a/app/views/console/database/index.phtml b/app/views/console/database/index.phtml index 5e83290fb6..306f46de7a 100644 --- a/app/views/console/database/index.phtml +++ b/app/views/console/database/index.phtml @@ -1,6 +1,6 @@

- Home + Home
Database @@ -109,4 +109,4 @@ -->

- \ No newline at end of file + diff --git a/app/views/console/database/rules/array.phtml b/app/views/console/database/rules/array.phtml index ae2600c6cb..8cc93b3c47 100644 --- a/app/views/console/database/rules/array.phtml +++ b/app/views/console/database/rules/array.phtml @@ -20,7 +20,7 @@ if($type === 'document') {
  • -
    +
    • @@ -41,7 +41,7 @@ if($type === 'document') {
    -
    +
      getAttribute('name', '') : ''; diff --git a/app/views/console/database/rules/numeric.phtml b/app/views/console/database/rules/numeric.phtml index 2f74331cf9..1a7774f427 100644 --- a/app/views/console/database/rules/numeric.phtml +++ b/app/views/console/database/rules/numeric.phtml @@ -4,4 +4,4 @@ $required = $this->getParam('required', ''); $namespace = $this->getParam('namespace', ''); ?> - required class="margin-bottom-no" /> \ No newline at end of file + required class="margin-bottom-no" step=any /> \ No newline at end of file diff --git a/app/views/console/home/index.phtml b/app/views/console/home/index.phtml index b9b59cec93..2141eb8ee8 100644 --- a/app/views/console/home/index.phtml +++ b/app/views/console/home/index.phtml @@ -14,10 +14,10 @@ $graph = $this->getParam('graph', false);
    @@ -157,22 +157,22 @@ $graph = $this->getParam('graph', false); Manage Your Server API Keys
    -
    +
    • - +
    • - +
    • - +
    • - +
    • - +
    @@ -201,7 +201,7 @@ $graph = $this->getParam('graph', false); - + @@ -209,7 +209,7 @@ $graph = $this->getParam('graph', false);
    Next Steps
    -

    After adding your new website, install our JS SDK to integrate with your code and read our getting started tutorial.

    +

    After adding your new website, install our JS SDK to integrate with your code and read our getting started tutorial.

    npm install appwrite
    @@ -277,10 +277,10 @@ $graph = $this->getParam('graph', false); - + - +
    @@ -309,10 +309,10 @@ $graph = $this->getParam('graph', false); - + - +
    @@ -341,7 +341,7 @@ $graph = $this->getParam('graph', false); - + @@ -371,7 +371,7 @@ $graph = $this->getParam('graph', false); - + @@ -387,7 +387,7 @@ $graph = $this->getParam('graph', false);
escape($label); ?>